[Java]Byte型からInt型への変換を本気で考える

5月 09 2011 Published by たけし under Java

whyJavaでByte型変数をInt型にキャストする際には、気を付けなければならないことがあります。

前回のエントリーで、Byte型配列を16進数文字列に変換するメソッドを作りましたが、その時にByte型をInt型へ変換するには、ただ単にキャスト変換するのではなく、「0xFF」でANDビット演算しなければ値がおかしくなるという事を書きました。


どういう事かというと…

byte a = (byte)0xC8 //10進数では200のはず
int b = (int)a;
Sytem.out.println(b);

この実行結果は、「200」ではなく、「-56」が表示されます。

それを、

byte a = (byte)0xC8 //10進数では200のはず
int b = a & 0xff;
Sytem.out.println(b);

とすると、実行結果は「200」になってくれます。

しかし、なぜそのままInt型にキャスト変換するだけじゃダメなのか? なぜ0xFFでAND演算をしたら、期待した数値になるのか?そのあたりが僕の中でもやもやしてました。

「なんだかよく分からないけど、Byte型で作った16進数をInt型に変換する時には、0xFFでAND演算するオマジナイをしなきゃならない」

初級プログラマな僕は、そんな感じで全然理解出来てなかったんですよね。 でも、このオマジナイが僕にとってはブラックボックスで気持ち悪い; 自分が理解出来てないものを、プログラムの中で扱うのなんて落ち着かないったらありゃしません。

そこで、なぜByte型をInt型へそのままキャストしてはいけないのか。なぜ0xFFでAND演算をするとうまく変換されるのか。 そのオマジナイの中身を調べてみることにしました。

 


Java言語仕様を調べる

とりあえずは、Java言語でキャスト変換する時に何が起こっているのか? それを調べるために、Javaの言語仕様を調べることにしました。

最新のJava言語仕様は、Oracle(Sun)のHPに掲載されています。

(公式) The Java Language Specification
http://java.sun.com/docs/books/jls/index.html

ただし、英語・・・English。。。ジャパニーズな自分には、ちょっとしんどい。。。 読めるには読めるでしょうけど、辞書引きながらですごい時間かかってしまうでしょうね。

…ので、日本語訳が欲しいところ。でもOracleのHPには、日本語訳は無いんですよ。JDKは日本語訳を用意してくれてるのに。ちくしょぅ。

※いや、でもエンジニアならば英語は書いたり話したりは出来なくても、すらすら読めるようになるべきだとは思います。一次情報はほとんどが英語ですから。 むしろこれからのエンジニアは、英語もスキルの一つとして積極的に身につけて行くべきでしょうし。

 

JLS

あとは、日本語訳された書籍ならば出版されているようです。

Java言語仕様 第3版 (The Java Series)

ただ、値も張りますし、何より今すぐ読みたい。なうです。

 

 

 

出来ればネットで親切な人が、日本語訳して公開しててくれたら嬉しいんだけどなー。。。 と探してみたらありました。 第2版の日本語訳なので、最新版(第3版)ではありませんが、ありがたやありがたや(^▽^)

http://www.y-adagio.com/public/standards/tr_javalang2/jTOC.doc.html

 

というわけで、このJava言語仕様・第2版の日本語訳で、まずはキャスト変換について調べてみることにしました。

 

キャスト変換で何が起こるのか?

まずは、16進数値を入れたByte型変数をInt型へキャストした時に、期待した数値にならなかった理由を知りたい。 なので、キャスト変換をした時に何が起こっているのか?それを調べてみます。

Java言語仕様の目次を見ていくと、キャスト変換の項目がありました。以下、転載です。

5.5 キャスト変換

キャスト変換 (casting conversion) は,キャスト演算子(15.16)のオペランドに適用する。このときオペランド式の型は,キャスト演算子で明示的に名前を与えた型に変換しなければならない。キャストの文脈では,恒等変換(5.1.1),プリミティブ型の拡大変換(5.1.2),プリミティブ型の縮小変換(5.1.3),参照型の拡大変換(5.1.4),又は,参照型の縮小変換(5.1.5)の利用を可能とする。従って,キャスト変換は,代入変換又は,メソッド呼出し変換よりも包括的とする。つまり,キャストは文字列変換以外の任意の変換が許される。

値集合変換(5.1.8)は,型変換の後に適用される。

コンパイル時に不正と証明されるキャストもある。このようなキャストは,コンパイル時エラーを発生させる。

プリミティブ型の値は,型が同じならば恒等変換によって,そうでないときにはプリミティブ型の拡大変換又はプリミティブ型の縮小変換によって,他のプリミティブ型にキャストできる。

 

重要なのは、赤字の部分です。

Byte型はプリミティブ型なので、キャストには恒等変換、プリミティブ型の拡大変換、プリミティブ型の縮小変換の3パターンの変換が発生し得るみたいですね。

じゃあ、今回のようにByte型からInt型へキャスト変換する場合には、この3パターンの内のどれが適用されるんでしょう?

この3パターンの変換の詳細を、1つずつ見て行ってみたいと思います。まずは恒等変換から。。。(5.1.1の章を参照)

5.1.1 恒等変換

ある型からそれと同じ型への変換は,いかなる型に対しても許される。

これは,二つの実用的な効果をもつ。第一に,すべての式は変換を受ける,と規則を簡潔に言明できる。第二に,明確化のために,プログラムが冗長なキャスト演算子を含むことを許可する。

型booleanを含む唯一許される変換は,booleanからbooleanへの恒等変換とする。

 

これは同じ型への変換のキャスト変換の場合に適用されるみたいですね。つまりは、Byte型からByte型へ変換する場合には、この恒等変換が適用されるみたいです。(あんま意味なさそうですけど。)

今回はByte型からInt型への変換なので、ちょっと違いますね。じゃあ、次はプリミティブ型の拡大変換を見てみます。

5.1.2 プリミティブ型の拡大変換

プリミティブ型における次の19個の変換をプリミティブ型の拡大変換(widening primitive conversion) と呼ぶ。

    ・byteからshort,int,long,float,又はdoubleへの変換。
    ・shortからint,long,float,又はdoubleへの変換。
    ・charからint,long,float,又はdoubleへの変換。
    ・intからlong,float,又はdoubleへの変換。
    ・longからfloat又はdoubleへの変換。
    ・floatからdoubleへの変換。

プリミティブ型の拡大変換は,数値の大きさについての情報を失わない。実際,整数の型から他の整数の型への拡大変換及び型floatから型doubleへの拡大変換は,いかなる情報も失うことはない。つまり,数値を正確に保存する。 同じく,strictfp式での型floatから型doubleへの拡大変換も,数値を正確に保存する。しかし,strictfpでない変換は,変換後の数値の大きさについての情報を失うかもしれない。

int若しくはlongの値からfloatへの変換,又はlongの値からdoubleへの変換は,精度の損失(loss of precision),すなわち値の最小位の数ビットを失うことがある。この場合,浮動小数点の結果値は,IEEE 754直近へのまるめモード(4.2.4)を利用して,正しく丸めた整数値とする。

符号付き整数値の整数型 T への拡大変換は,単により長い桁を埋めるために整数値の2の補数表現について符号拡張をするだけとする。文字から整数型 T への拡大変換は,より長い桁を埋めるために文字値の表現をゼロ拡張する。

精度の損失が発生する可能性があるという事実にもかかわらず,プリミティブ型間の拡大変換は,実行時例外(11.)を生じない。

 

最初の赤字部分を読んでの通り、Byte型からInt型への変換は、プリミティブ型の拡大変換に含まれるようです。 基本的には、サイズの小さいプリミティブ型から、サイズの大きいプリミティブ型への変換は、拡大変換になるようですね。

ちなみに、プリミティブ型の縮小変換も調べてみましたが、拡大変換とは逆に、サイズの小さい型への変換時に行われる処理のようですので、今回は関係ありませんでした。

そして、後半の赤字部分に注目すると、Byte型をInt型へただ単にキャストしちゃいけない理由が分かります。

 

16進数値のByte型からInt型へキャストしてはイケない理由

まず、最初に認識しておかなければならないことは、「Byte型では-128~127までの整数しか表現できない」という事です。

つまり、「0xC8」という16進数表記は、普通に考えると10進数に変換すると「200」になるわけですが、Byte型ではこれが200になりません。-56という値になります。

これは何故かというと、「0xC8」を8ビットの2進数で表すと「11001000」となります。 JavaのByte型は符号付きの整数型ですので、先頭の1桁目が1だった場合、その数をマイナスの数として認識します。

マイナスの数。。。2の補数表現で考えた場合、「11001000」という数値は、「-56」というマイナスの10進数になります。

16進数 2進数 10進数
Byte型
(8ビット)
0xC8 
(10進数で200を想定)
11001000 -56

 

ここで、先ほどの…

符号付き整数値の整数型 T への拡大変換は,単により長い桁を埋めるために整数値の2の補数表現について符号拡張をするだけとする。

ということを踏まえると、Byte型(8ビット)からInt型(32ビット)に拡張する時に、足りない部分の24ビット分は、プラスマイナスの符号を変えないように埋めるということになります。

つまり、もともとの先頭1ビットが「0」だったらば、足りない部分は「0」で埋める。もともとの先頭1ビットが「1」だったらば、足りない部分は「1」で埋めるということになります。

そうすると、Byte型からInt型へキャスト変換をすると、こんな感じになります。

16進数 2進数 10進数
Byte型
(変換前)
0xC8 
(10進数で200を想定)
11001000 -56
Int型
(変換後)
0xFFFFFFFFFFFFFFC8 11111111 11111111 11111111 11001000 -56

 

10進数で考えると何も変わっていないですが、符号なし16進数で考えるとだいぶ変わってしまっていますね。本当はInt型へ変換された後の16進数も「0xC8」のままになって、10進数で「200」となって欲しかったところなのですが。。。

Byte型では元々16進数で考えた数値を、10進数で表現できない以上、Int型へキャストしたとしても、期待通りの10進数数値にはならないのですね;

 

なぜビット演算(AND)すると大丈夫なのか?

では、なぜこれが「0xFF」でAND演算をすると、期待通りの値になるのでしょうか?

まずは、AND演算について調べてみたいと思います。

15.22.1 整数値ビット単位演算子 &,^及び|

演算子 &,^ 又は | の両オペランドがプリミティブ整数型であるとき,最初にオペランドに二項数値昇格(5.6.2)を実行する。 ビット単位の演算子式の型は,そのオペランドの昇格された型とする。

 

オペランドというのは、演算対象のことです。 例えば、「1+2」という式があったとしたら、「+」はオペレーター(演算子)で、「1」と「2」がオペランドです。

Byte型はプリミティブ整数型ですし、0xFFも16進数表記のInt型として扱われますので、最初に各オペランドに、二項数昇格なるものが実行されるようです。

ちなみに、16進表記の数値がInt型として扱われるということは、以下に記載されていました。

3.10.1 整数リテラル

整数型及びその値の詳細は,4.2.1を参照のこと。

整数リテラル (integer literal)は,10進数(基数 10),16進数(基数 16),又は8進数(基数 8)で表現できる。

   IntegerLiteral:
        DecimalIntegerLiteral
        HexIntegerLiteral
        OctalIntegerLiteral

    DecimalIntegerLiteral:
        DecimalNumeral IntegerTypeSuffixopt

    HexIntegerLiteral:
        HexNumeral IntegerTypeSuffixopt

    OctalIntegerLiteral:
        OctalNumeral IntegerTypeSuffixopt

    IntegerTypeSuffix: one of
        l L

 

じゃあ、二項数値昇格ってなんぞや? と思いましたが、こんなことのようです。

5.6.2 二項数値昇格

ある演算子が 二項数値昇格(binary numeric promotion) をオペランドの対に適用するときは,個々のオペランドが,数値型の値を表さなければならないが,必要に応じてオベランドを変換するための拡大変換(5.1.2)を用いて,順番に次の規則を適用する。

・ 一方のオペランドが型 double ならば,他方を double に変換する。
・ そうでないときには,一方のオペランドが型 float ならば,他方を float に変換する。
・ そうでないときには,一方のオペランドが型 long ならば,他方を long に変換する。
そうでないときには,両オペランドを型 int に変換する。

二項数値昇格は,特定の演算子のオペランドに関して実行する。

・ 乗法系の演算子,*,/ 及び % (15.16)。
・ 数値型加算演算子+ 及び減算演算子 -(15.17.2)。
・ 数値比較演算子 <,<=,> 及び >= (15.19.1)。
・ 数値等価演算子 == 及び != (15.20.1)。
整数のビット単位の演算子 &,^ 及び | (15.21.1)。
・ 特定の場合における条件演算子 ? : (15.24)。

どうやら、各オペランドのサイズを合わせるために、大きい方のオペランドの型に合わせて変換することのようですね。

今回は、Byte型とInt型のAND演算なので、両方のオペランドがInt型になります。 つまり、Byte型変数がInt型へ変更されるようです。

先ほどの、ただ単にキャスト変換した時と結果は同じになります。

16進数 2進数 10進数
Byte型
(変換前)
0xC8 
(10進数で200を想定)
11001000 -56
Int型
(変換後)
0xFFFFFFFFFFFFFFC8 11111111 11111111 11111111 11001000 -56

 

上記のようにInt型へ変換された値と、「0xFF」をAND演算します。 すると、以下のようになります。

  16進数 2進数 10進数
Byte型からInt型へ変換された値 0xC8 
(10進数で200を想定)
11111111 11111111 11111111 11001000 -56
0xFF 0xFF 00000000 00000000 00000000 11111111 255
AND演算の結果 0xC8 00000000 00000000 00000000 11001000 200

 

Int型に変換した時に、拡大された最初の3バイト(24bit)が全て0になってくれれば、2進数でのマイナス表記にはならないので、正常に「0xC8」(200)になってくれます。

その状況を作りだすために、最後の8ビットだけ1に設定した値「0xFF」でAND演算をするのですね。

※AND演算ではお互いに1の部分だけが抜き出されることになるので。

 

ここまでの操作で、やっと16進数表記のByte型を、0xFFとAND演算することによって、期待通りの数値でInt型へ変換する。ということが出来たわけです。

※ちなみにC言語であれば、符号なしのInt型…unsigned int型がありますので、それにキャストするだけで期待通りの値に変換されるはずです。

 

終わりに

いやー、長かったけどスッキリしました! これでようやく、訳の分からないオマジナイではなく、ちゃんとした仕組みを理解したうえで使うことが出来ます!

実際には、概念やルール的な部分が多かったので、その辺りをちゃんと理解している人であれば当たり前の事なんでしょうけど、まだまだ初級プログラマな自分には勉強になりました。

やっぱりエンジニアとしては、正体不明な部分はなるべく無くしておきたいんですからね。 理解しないまま使うというのは極力避けたい。

ライブラリとかは、中身の具体的な処理を知らなくても、それが何をしているのかさえ理解していればそのまま使って良いと思いますが、やっぱりこうした基礎的な言語レベルの部分では、詳細を理解しておかないと、応用が利かなくなりそうで怖いです。

まだまだJava言語で理解出来ていないところも多いので、なるべく調べてアウトプットしておければと思います。

No responses yet

コメントをどうぞ