t100のプログラミング脱出作戦

自分のプログラミング脳をプログラムにして、いつかプログラミングから脱出してやるぞっ!とか夢見ながら、日々プログラム作っていく 百野 貴博 の日記です!今は、屋号『百蔵。』として、Silverlight・WPFを追跡中です! (2007/09/30)
スポンサーサイト
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
【--/--/-- --:--】 | スポンサー広告 | トラックバック(-) | コメント(-) top↑
Javaのシフト演算の最小サイズが4バイト(int)だった件
いやー、ハマりました。。。
Javaのシフト演算、、、


それは、仕事でJavaでFlashのswfファイルを編集する為に
もんどり打ちながらバイナリファイルを読んでいた時に起こりました。

その作業の中で、私は2バイト(16ビット)のデータから
上位10ビットを”データ種別”、下位6ビットを”データ長”として
取り出さないといけないという課題に挑戦していました。

「フフン♪そんなの、シフト演算とビット演算でチョチョイのチョイやで。」


その直後、ハマリはやって来たのでした。
これは、そんなハマリ発生の背景から解決までの涙の一部始終です。
*■事件発生

とりあえず私は、ファイルから読み込んだ2バイト(byte[2]) を数値に変換してから
シフト演算とビット演算を使って、データを取り出すことにしました。

以下のような感じです。(簡易版です)

>InputStream in = new FileInputStream("meke.swf");
>byte[] data = new byte[2];
>
>in.read(data);
>
>//2バイトデータを数値データに変換
>int dataValue = new BigInteger(data).intValue();
>
>//6ビットシフトして、上位10ビットを取り出し。
>int dataType = dataValue >>> 6;
>//And演算で上位10ビットを0にして、下位6ビットを取り出し。
>int dataLength = dataValue & 0x3f;
>

これで、いけそうな気がするじゃないですか。

読むデータは、"11111111 00111111"(FF3F) です。

期待としては、
上位10ビットは 11111111 00 ⇒ 1020
下位 6ビットは 111111 ⇒ 63
が、取れるはず、、、


って、dataType(上位10ビット)に 67108860 が入ってるーー!!!きたー!!
00000011 11111111 11111111 11111100 かYO!!

ぐはーー!!でかい!!32ビット!!
なぜっ?!


#下位6ビットは、正しく63が取れますた。


*■符号無し数値が扱えないJava

今回読んだ2バイトデータ("11111111 00111111")は、Javaの中では以下のようになっていました。
(Eclipseの”表示”機能で確認)

●byte[2]の時の値
[-1, 63]

1バイト目がマイナスになってるのが、既に嫌な感じ・・・。

●new BigInteger(data).intValue();の値
-193

うお。
2バイトデータを数値にしたときに、65343になって欲しかったのに -193になってる!!

想像 ⇒65343 : 00000000 00000000 11111111 00111111
現実 ⇒ -193 : 11111111 11111111 11111111 00111111

ギャオス!

2バイトデータを4バイトデータ(intは4バイト)に変換する際の上位2バイトが1で埋められてるっ!!
元の2バイトデータの先頭のビットが1だったから、”負の数”と勝手に判断したなっ!!
ムキーー!!


そりゃ、この状態で6ビットシフトしたら
00000011 11111111 11111111 11111100 ⇒ 67108860 になるわけだよ・・・。


*■Javaのシフト演算は最小単位がintだった。

そもそも、2バイトデータなのにBigInteger使ってるのが悪いんじゃ?ということで
byte データのままシフト演算して検証してみることにしました。

以下の式を、Eclipseの”表示”機能で見てみると、、、
>data[0] >>> 6
> (int) 67108863
>
>((byte)-1) >>> 6
> (int) 67108863


ギャオス!!
やっぱりデカイ!!
ていうか、結果がintになってるっ!!



今回の事件まで知らなかったのですが、実はJavaのシフト演算の最小単位はintだったようです。
http://www.y-adagio.com/public/standards/tr_javalang/15.doc.htm#5121


>シフト演算子のオぺランドの型は,プリミティブ整数の型でなければならない。そうでないときにはコンパイル時エラーが発生する。各オペランドに対して2項数値昇格(5.6.2) は実行しないが,単項数値昇格(5.6.1) を各オぺランドに対し別々に実行する。 シフト演算式の型は,左辺オぺランドの昇格した型とする。

とあり、演算子の左の数字は”単項数値昇格”するとあります。
単項昇格ってなんじゃらほい?と思って調べてみると、、、

http://www.y-adagio.com/public/standards/tr_javalang/5.doc.htm#170952

>オペランドがコンパイル時の型 byte,short 又は char をもてば,単項数値昇格はこれを拡大変換(5.1.2)によって型 int の値に昇格する。

となっていました。
むおー。まーぢかーー。



つまり、自分としては1バイトのデータを6ビットシフトしているつもりだったのですが
6ビットシフトの演算前に、byteデータからintへの昇格が発生しており、

>11111111(-1) ⇒ 11111111 11111111 11111111 11111111(-1) 
>となり、その後6ビットシフトが行われ、、、
>00000011 11111111 11111111 11111111(67108863)
>と、計算されていたのでした。


*■解決策は、自分で4バイトにしちゃうこと。

果たして、この問題をどのように解決すればよいのか・・・。

問題は、2バイトデータを4バイトデータに変換する際の補完の仕方がおかしいからなのですが
Javaは符号無し数値が扱えない為、仕方の無い制約と言えます。

となると、変な補完をされる前に自分で補完しておく。
先に、2バイトデータを4バイトデータとして扱えばいいはずです。

修正後のコードは以下のようになりました。


>InputStream in = new FileInputStream("meke.swf");
>//4バイト分のサイズを確保
>byte[] data = new byte[4];
>
>下位2バイトにデータを読み込む。
>in.read(data, 2, 2);
>
>//4バイトデータを数値データに変換
>int dataValue = new BigInteger(data).intValue();
>
>//6ビットシフトして、上位10ビットを取り出し。
>int dataType = dataValue >>> 6;
>//And演算で上位10ビットを0にして、下位6ビットを取り出し。
>int dataLength = dataValue & 0x3f;
>

データを読む配列を4バイトにし、上位2バイトを0で埋まるようにしています。
これで、無事期待していた数値を取ることが出来ました。

ふぃぃぃ~~~


*■まとめ

まぁ、まとめと言うほどまとめは無いです。。。
シフト演算をするときは気をつけましょうってことかー。

この他にも、リトルエンディアンを扱うあたりで、若干ハマったりしたんですが
(これは自分の無知のせい)そのことは、またオイオイということで、、、

Javaで符号無し整数扱えるようにならないかな。
いやいや、その前にプロパティの導入お願いします。ほんと。
#あと、C#のデリゲートも欲しいなー。














管理者にだけ表示を許可する


トラックバックURL:
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。