JavaのCharset
Javaは内部的にはUTF-16で文字を扱っており、UTF-16とその他の文字コードを変換するための機能を持っています。この記事では「Charsetを作る」、つまり独自の名前の文字コード・コンバーターの作り方をまとめます。
Charsetの作り方の概要
基本的には、以下の4つのクラスおよび1つの設定ファイル(プロバイダー構成ファイル)を用意して、クラスパスに通します。
- java.nio.charset.Charset を拡張したクラス
- java.nio.charset.CharsetDecoder を拡張したクラス
- java.nio.charset.CharsetEncoder を拡張したクラス
- java.nio.charset.spi.CharsetProvider を拡張したクラス
- META-INF/services/java.nio.charset.spi.CharsetProvider
レベル0:まずは肩ならし
まずは最小限の用意で新しい名前のCharsetを作ってみましょう。
- XMS932Xという名前のCharset
- 変換はMS932と全く同じ
ここでは、実際の変換を既存のMS932コンバーターに丸投げするので、Decoder/Encoderを作る必要もありません。必要なのはCharsetProviderとプロバイダー構成ファイルの2つです。(さきほどのリストの最後の2つです)
まずは下記のようにCharsetProviderを拡張したクラスを作成します。コードをよく見ていただくと、MS932 Charsetオブジェクトを作っているのがわかると思います。
package mycode ;
import java.nio.charset.* ;
import java.nio.charset.spi.CharsetProvider ;
import java.util.* ;
public class XMS932XProvider extends CharsetProvider {
private ArrayList<Charset> list = null ;
private Iterator<Charset> getIter() {
if (list == null) {
list = new ArrayList(1) ;
list.add(Charset.forName("MS932")) ;
}
return list.iterator() ;
}
public Iterator<Charset> charsets() {
return getIter() ;
}
public Charset charsetForName(String charsetName) {
if ("XMS932X".equals(charsetName)
|| "MS932".equals(charsetName)) return (Charset)list.get(0) ;
return null ;
}
}
もうひとつ、プロバイダー構成ファイルを作成します。以下のファイル名で、作成したCharsetProviderの名前を含みます。
mycode.XMS932XProvider
上記 javaプログラムをコンパイルした後、jarにパッケージします。
javac mycode/XMS932XProvider.java
jar cvf xms932x.jar META-INF/services/java.nio.charset.spi.CharsetProvider mycode/XMS932XProvider.class
この作成した xms932x.jar をクラスパスに含めれば、無事 XMS932X という Charsetが使えるようになります。当然ですが、MS932と全く同じ文字コード変換を行います。
レベル1:とりあえず4つのクラスを実装してみよう
次に、XMS932XXというCharsetを作ります。上記レベル0と同じく、MS932変換をそのまま行うというのは変わりません。
しかし、ここでは実際に文字列のデータを変換する本体である、CharsetDecode/CharsetEncoder の実装を用意して、その中でMS932変換を呼び出してみます。
まずは、以下のようにCharsetEncoderを拡張したクラスを用意します。実装するのは encoderLoop
メソッドのみです。encodeLoop
の引数を見ていただくと、CharBuffer から文字を読み込んで、ByteBuffer
にバイト列として出力するメソッドだというのがわかると思います。今回はMS932のEncoderを呼び出してそのまま使っています。
package mycode ;
import java.nio.charset.* ;
import java.nio.* ;
public class XMS932XXEncoder extends CharsetEncoder {
protected XMS932XXEncoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte) {
super(cs, averageCharsPerByte,maxCharsPerByte) ;
}
protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) {
Charset charset = Charset.forName("MS932");
CharsetEncoder ce = charset.newEncoder() ;
ce.reset() ;
return ce.encode(in, out, false) ;
}
}
同様に、CharsetDecoderを拡張したクラスを用意します。decodeLoop
を見ていただくと、先ほどとは逆に、バイト列を読み込んでCharとして出力するメソッドだということがわかります。
package mycode ;
import java.nio.charset.* ;
import java.nio.* ;
public class XMS932XXDecoder extends CharsetDecoder {
protected XMS932XXDecoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte) {
super(cs, averageCharsPerByte,maxCharsPerByte) ;
}
protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out){
Charset charset = Charset.forName("MS932") ;
CharsetDecoder cd = charset.newDecoder() ;
cd.reset() ;
return cd.decode(in, out, false) ;
}
}
次に、Charsetを拡張したクラスを作成します。この中の、newDecoder/newEncoderメソッドでそれぞれのDecoder/Encoderのインスタンスを作成して返します。コンストラクターの2つの引数 (float)2
は、バイト列(ネイティブコード)に変換したときの平均バイト数と最大バイト数を表します。MS932はSJISですから一文字は最大2バイトとなるため、2を指定しています。平均のほうはもっと小さくてもいいかもしれません。
contains
メソッドは、指定されたCharsetの変換を包含していれば true
を返します。自分自身はもちろん含んでいる必要があります。このサンプルの場合はMS932も含んでもいいかもしれません。
package mycode ;
import java.nio.charset.* ;
import java.nio.* ;
public class XMS932XX extends Charset {
public XMS932XX(String s, String[] ss) {
super(s, ss) ;
}
public boolean contains(Charset cs) {
if (cs instanceof XMS932XX) return true ;
return false ;
}
public CharsetDecoder newDecoder() {
return new XMS932XXDecoder(this, (float)2, (float)2) ;
}
public CharsetEncoder newEncoder() {
return new XMS932XXEncoder(this, (float)2, (float)2) ;
}
}
以下はCharsetProviderを拡張したクラスです。
レベル0のときは、Charsetとして "MS932"のインスタンスを返していましたが、ここでは XMS932XXのインスタンスを返すようにしています。
package mycode ;
import java.nio.charset.* ;
import java.nio.charset.spi.CharsetProvider ;
import java.util.* ;
public class XMS932XXProvider extends CharsetProvider {
private ArrayList<Charset> list = null ;
private Iterator ite = getIter() ;
private Iterator<Charset> getIter() {
if (list == null) {
list = new ArrayList(1) ;
list.add(new XMS932XX("XMS932XX", new String[]{"XMS932XX"})) ;
}
return list.iterator() ;
}
public Iterator<Charset> charsets() {
return getIter() ;
}
public Charset charsetForName(String charsetName) {
if ("XMS932XX".equals(charsetName)) return (Charset)list.get(0) ;
return null ;
}
}
最後にプロバイダー構成ファイルを用意します。
mycode.XMS932XXProvider
あとは各Javaファイルをコンパイルした後、Jarに固めます。
javac mycode/XMS932XX*.java
jar cvf xms932xx.jar META-INF/services/java.nio.charset.spi.CharsetProvider mycode/XMS932XX*.class
この作成した xms932xx.jar をクラスパスに含めれば、XMS932XX という Charsetが使えるようになります。これも実際の変換はMS932と全く同じです。しかし、Decoder/Encoderを用意したことで、なんとなく実装の仕方を見えたのではないでしょうか。
レベル2:独自の(ちょっとした)変換を実装!
仕組みがわかったところで、実際に独自の変換を行うCharsetを作ってみましょう。
ここでは、ネイティブコード(MS932)からUnicodeへの変換について、半角カタカナを全角カタカナに変換することを考えてみます。
一からすべてのファイルを用意するのは面倒なので、レベル1で作成したXMS932XXを改造しましょう。どれを改造すればいいかというと、Decoderですね。
package mycode ;
import java.nio.charset.* ;
import java.nio.charset.spi.CharsetProvider ;
import java.nio.* ;
import java.io.* ;
public class XMS932XXDecoder extends CharsetDecoder {
static final String hankana = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン゚゙" ;
static final char[] zenkana = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン゛゜".toCharArray() ;
protected XMS932XXDecoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte) {
super(cs, averageCharsPerByte,maxCharsPerByte) ;
}
protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) {
Charset charset = Charset.forName("MS932") ;
CharsetDecoder cd = charset.newDecoder() ;
CharBuffer tmp = CharBuffer.allocate(out.remaining()) ;//一旦ここに変換
//このループでできるだけinをtmpにMS932変換する
while (in.hasRemaining()) {
if (in.remaining() == 1 &&
((int)(in.get(in.position()))&0xff)>0x80) break ;//SJIS1バイト目だけ残った場合
CoderResult result = cd.decode(in, tmp, false);
if (result.isError()) {
return result;
} else if (result.isOverflow()) {
break;
}
}
tmp.flip();
int pos = -1 ;
//半角カナを見つけたら全角カナに置き換えて出力
while (tmp.hasRemaining()) {
char c = tmp.get();
if ((pos = hankana.indexOf(c)) >= 0) {
c = zenkana[pos] ;
}
out.put(c) ;
}
//outがいっぱいのときはOVERFLOW、inの残りが無いときはUNDERFLOW
if (out.remaining()==0) return CoderResult.OVERFLOW ;
else return CoderResult.UNDERFLOW;
}
}
ちょっと複雑なので、decodeLoop
について解説します。
まず、最初のwhile
ブロックで、与えられたバイト列 in
を、MS932コンバーターを使用して CharBuffer tmp
に変換しています。
注意が必要なのが while
の次の行の if
文で、これがないと無限ループに陥る可能性があります。このwhileブロックの終了条件は、
-
in
から読み込むデータがなくなるか、 -
tmp
がいっぱいになるか、 - エラーになるか
のいずれかなのですが、in
のデータがSJISの2バイト文字の一文字目で終わっている場合、MS932でデコードが完了しないため in
のデータが一文字残り、かつtmp
もいっぱいではなく、エラーでもないという状況になり、終了条件を満たさないためです。この if
文で、1バイト残っていて、かつ2バイト文字の1バイト目(0x80より大きい)であると判断した場合while
から抜けるようにしています。
2つ目のwhile
は、MS932で変換した文字列を一文字ずつチェックし、半角カタカナの場合は対応する全角カタカナを out
に出力し、それ以外の場合はその文字をそのままout
に出力しています。
このクラスをコンパイルして、さきほどと同様 xms932xx.jarとしてパッケージすることで、半角カナを全角カナに変換するXMS932XX Charsetが使えるようになります。
SJISで半角カタカナを含むテキストファイルを用意し、XMS932XXで変換すると全角カタカナに変換されるのが確認できます。
アイウエオアイウエオ
import java.io.* ;
public class Test{
public static void main(String[] args) throws Exception {
String enc = ( args.length > 0 ? args[0] : "XMS932XX") ;
String line = null ;
BufferedReader r = new BufferedReader(new InputStreamReader(System.in, enc)) ;
while((line = r.readLine()) != null) {
System.out.println("line = " + line) ;
}
}
}
実行結果
$ java -cp xms932xx.jar:. Test XMS932XX < input-sjis.txt
line = アイウエオアイウエオ
おわりに
以前はネットを検索するとJavaのCharsetを作るための情報がちらほらあったと思うのですが、最近はあまり無いようでググっても見つからなかったので書いてみました。何かのご参考にどうぞ。