はじめに
ラピッドプロトタイピングに便利な開発環境として Processing を多用している。
https://processing.org/
Processing はjavaに便利なライブラリを付けて、皮をかぶせたものだ。
javaで直接書くこともできる。
Processingの情報は日本では必ずしも多くないが、javaのリソースは世の中に山程あって、困っても大概問題が解決するのもありがたい。先人の知恵の偉大なることよ。
今回はjavaの byte, char, int, String の違いに関して私の勘違いがあってバグを取るのに苦労したのだが、先人の知恵を得て問題解決した、という話を書く。
なお、以下のサンプルコードはProcessingを前提としている。素のjavaでも考え方は同じ。
byte に16進数が代入できない
気づきの最初は、以下のコードがエラーになって、あれ、と思ったところからだった。
byte b = 0x8D;
cannot convert from int to byte
というエラーメッセージが出る。
一方で、以下はOKだ。
char c = 0x8D;
また、当然ながら以下もOKだ。
int i = 0x8D;
実は byte は signed の1バイト整数で、-128から127の値をとる。
だから、0x8D=141は代入できない。なんと。
そのため、以下はOKだったりする。
byte b = 0x7F;
0x7F = 127なので、範囲内になり、代入できる。
byteに0x80から0xFFの間の値を代入するにはキャストを使う。
byte b = (byte)0x8D;
では、数値としては何として表現されているのかというと、以下のようになる。
byte b1 = (byte)0x8D;
byte b2 = (byte)0x7F;
println(b1); // -115
println(b2); // 127
Processingにはhex()という便利な関数があるので、16進数表示してみると、以下のようになる。
byte b = (byte)0x8D;
println(hex(b)); // 8D
ではcharやintはどうなっているのか
では、charやintはどうなっているのかというと、それぞれ2バイト、4バイトで表現されている。
byte b = (byte)0x8D;
char c = 0x8D;
int i = 0x8D;
println(hex(b)); // 8D
println(hex(c)); // 008D
println(hex(i)); // 0000008D
この内部表現の違いを理解していなかったが為に、値を変換する際にトラブルを起こしていたのが、私のミスだった。
ちなみにbyteをcharにキャストすると、足りない部分がFFで補われる。
(shiracamusさんに教えてもらったが、マイナスの場合はFFで、プラスの場合は00で補われる。0x7Fは0x007Fに、0x80だと0xFF80になる。)
byte b = (byte)0x8D;
char c = (char)b;
println(hex(c)); // FF8D
簡単な間違いの例
例えば、以下のプログラムでは、一見、OKが出てきそうに思えるが、実は条件文が成立せず、何も表示されない。
byte b = (byte)0x8D;
if (b == 0x8D) println("OK");
何故かは0x8Dを16進数表示させてみるとわかる。
println(hex(0x8D)); // 0000008D
プログラム中の 0x8Dは intと判断されるため、一旦0x0000008Dとして表現され、byteに代入する時に、キャストして最後の1バイトだけが入っているため、異なる値である。比較を成立させるには、以下のように比較相手もbyteにキャストする。
byte b = (byte)0x8D;
if (b == (byte)0x8D) println("OK");
もう一つ簡単な間違いの例を示す。
byte b = (byte)0x8D;
char c1 = (char)b;
char c2 = 0x8D;
if (c1 == c2) println("OK");
これも一見、OKが出力されそうだが、出力されない。理由は上述したようにcharにキャストしたc1は0xFF8Dになっているのに対して、直接代入したc2は0x008Dになっているからだ。
Stringとの関係
実際に私がトラブったプログラムを単純化すると、以下のようになる。
byte [] bb = { (byte)0x8D };
println(hex(bb[0])); // 8D
String s = new String(bb);
byte b = (byte)s.charAt(0);
println(hex(b)); // FD
byteの配列を用意して、bb[0]に0x8Dを代入する。よって、最初のprintlnでは8Dが出力される。
この配列を文字列の初期値として与えて、文字列sを作成する。0番目の文字をbyteとして取り出すと、何故かFDに変わっている。
これはStringを生成している3行目が間違っている。何も引数を指定せずに生成しているのでUTF-16だと思って無理やり変換している。(何故,FFFDになるのか不明。未解明)
最も簡単な解決策はHIBYTEを指定して、以下のように、new String(bb, 0);とすることだが、最新のjavaでは推奨されていない。
byte [] bb = { (byte)0x8D };
println(hex(bb[0])); // 8D
String s = new String(bb,0);
byte b = (byte)s.charAt(0);
println(hex(b)); // 8D
安全策としては、以下のようにcharの配列に入れてHIBYTEを確定してから、文字列に変換する。
byte [] bb = { (byte)0x8D };
println(hex(bb[0])); // 8D
char [] cc = new char [bb.length];
for (int i = 0; i < cc.length; i++) cc[i] = (char)bb[i];
String s = new String(cc);
byte b = (byte)s.charAt(0);
println(hex(b)); // 8D
byteが1バイトで、charやStringは2バイトなので、上位バイトはFFになっているが、それを理解して使うという考え方だ。ただ、1バイトの配列のようなつもりでStringを使うと、私のように痛い目にあう。
charとStringで使っている間は齟齬が生じないので、あまり気にしていなかったが、byteを中に混ぜると危険である。
バイト列を文字列に格納する方法としては、valueOfも使える。
byte [] bb = { (byte)0x8D };
println(hex(bb[0])); // 8D
String s = String.valueOf(bb);
byte b = (byte)s.charAt(0);
println(hex(b)); // 5B
ところが、これも上記のように変換されて、もとの値は保持されない。
byte怖いね。byteだけで完結できるならByteBufferを使うべきだろう。
(ただByteBufferは使い方に癖があるので、ちゃんと勉強して使おう)
おわりに
- byte型変数は符号付き1バイトである。(-128から127)
- char型変数やString内の各文字は符号なし2バイトである。
- short型変数は符号付き2バイトである。
- int型変数は符号付き4バイトである。
- キャストする時は副作用(符号ビットの扱い)に気をつけて。
- String(byte[])は思わぬ結果を生む。
- String.valueOf(byte[])も同様。
- Processingは偉大だ。