信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
型変換とキャストの仕組み — byte+byteがintになる理由と情報損失の正体
Javaの暗黙的型変換(widening)と明示的キャスト(narrowing)の仕組み。なぜbyte+byteはintになるのか、long→intでビットが切り捨てられる様子、リテラルの型を根本から解説。
一言結論
Javaは情報が失われない変換は自動(widening)、失われる可能性がある変換はキャスト必須(narrowing)。byte+byteがintになるのはCPUが32bitの整数演算をするから。
型変換には2種類ある
Javaの変数には型がある。型が違う値を代入したり演算したりするとき、型変換が起きる。
型変換には自動で行われるものと、明示的に指示が必要なものがある。
int x = 10;
long y = x; // ✅ 自動変換(int → long)
long a = 10L;
int b = a; // ❌ コンパイルエラー(long → int は明示的なキャストが必要)
int c = (int) a; // ✅ キャストを明示
なぜ一方は自動で、もう一方は明示が必要なのか。
Widening:情報が増える方向は自動
数値型の「大きさ」の順番がある:
byte(8bit) → short(16bit) → int(32bit) → long(64bit) → float(32bit) → double(64bit)
char(16bit) ┘
小さい型から大きい型への変換を Widening Conversion(拡大変換) という。「情報が増える方向」なので自動でできる。
byte b = 100;
short s = b; // byte → short(自動)
int i = s; // short → int(自動)
long l = i; // int → long(自動)
float f = l; // long → float(自動)
double d = f; // float → double(自動)
直接の一気変換も可能:
byte b = 42;
double d = b; // byte → double(自動)
注意点:long→floatは精度が落ちることがある
long big = 1_000_000_000_000_000L; // 1000兆
float f = big; // 自動変換されるが...
System.out.println(big); // 1000000000000000
System.out.println(f); // 1.0E15(精度が落ちている)
longは64bit整数で正確な値を持てるが、floatは32bitの浮動小数点なので大きな整数を正確に表現できない。「情報が失われる可能性がある」にもかかわらずWideningとして自動変換されるのがJavaのここだけの例外的な挙動だ。
Narrowing:情報が減る方向はキャスト必須
大きい型から小さい型への変換を Narrowing Conversion(縮小変換) という。情報が失われる可能性があるので、キャストを明示的に書かなければならない:
int i = 300;
byte b = (byte) i; // ← キャストを明示
System.out.println(b); // 44(!)
なぜ300が44になるのか。ビットレベルで見ると:
int 300: 00000000 00000000 00000001 00101100
↓(下位8bitだけ取り出す)
byte: 00101100 = 44
intの下位8ビットだけが残る。上位24ビットは切り捨てられる。情報が「欠ける」のだ。
double→intの場合は小数点以下が切り捨てられる(四捨五入ではない):
double d = 9.99;
int i = (int) d;
System.out.println(i); // 9(小数点以下は切り捨て)
double d2 = -9.99;
int i2 = (int) d2;
System.out.println(i2); // -9(「ゼロ方向」に切り捨て)
マイナスの場合は「ゼロ方向」への切り捨てだ。Math.floor(-9.99) = -10 とは異なる。
リテラルの型
コード上に書いた数値リテラルにも型がある:
| リテラル | 型 |
|---|---|
100 | int |
100L または 100l | long |
3.14 | double |
3.14f または 3.14F | float |
'A' | char |
long l = 10000000000; // ❌ エラー:10000000000 はintリテラルだが、intの範囲を超えている
long l = 10000000000L; // ✅ longリテラル
float f = 3.14; // ❌ エラー:3.14 はdoubleリテラル。double→floatはnarrowing
float f = 3.14f; // ✅ floatリテラル
byteやshortへの代入は特例がある:
byte b = 127; // ✅ リテラル 127 はintだが、byteの範囲内ならコンパイラが許可する
byte b = 128; // ❌ エラー:128 はbyteの範囲(-128〜127)を超える
コンパイル時に値が確定しているリテラルの場合、コンパイラが範囲チェックをしてOKなら暗黙変換してくれる特例だ。
byte + byte がintになる理由
これが試験でよく問われる:
byte b1 = 10;
byte b2 = 20;
byte b3 = b1 + b2; // ❌ コンパイルエラー!
int i3 = b1 + b2; // ✅
b1もb2もbyteなのに、足した結果がintになる。なぜか?
CPUのアーキテクチャが理由だ。
現代のCPUは「32bitや64bitのレジスタ」で演算する。byte(8bit)をそのまま演算しようとすると、内部でCPUは32bitのレジスタに載せて計算する。これをJVM仕様が「整数の格上げ(Integer Promotion)」として明示的に定義している。
byte b1 = 10, b2 = 20;
演算前の格上げ:
b1 → int(10)
b2 → int(20)
演算:
int(10) + int(20) = int(30)
結果の型: int
byte, short, charの演算は、すべて演算前にintに格上げされる。演算結果はint型だ。
byte b1 = 10, b2 = 20;
byte b3 = (byte)(b1 + b2); // ✅ 明示的にbyteにキャスト
混合演算の型の決まり方
異なる型が混在した演算では、「より大きい型」に揃えられる:
// ルール: byte/short/char → int → long → float → double の順で格上げ
int i = 10;
long l = 20L;
int x = i + l; // ❌ int + long = long → intに代入不可
long y = i + l; // ✅ long
long a = 10L;
float f = 2.0f;
long b = a + f; // ❌ long + float = float → longに代入不可
float g = a + f; // ✅ float
float f2 = 1.0f;
double d = 2.0;
float h = f2 + d; // ❌ float + double = double
double dd = f2 + d; // ✅ double
混合演算では 左辺でも右辺でもなく「より大きい型」に統一されてから演算する。
よくある試験問題パターン
// これはOKか?
byte b = 10;
b = b + 1; // ❌ b + 1 は int(bがintに格上げされる)→ intをbyteに代入できない
b += 1; // ✅ 複合代入演算子(+=)は暗黙的にキャストする
b++; // ✅ インクリメント演算子も暗黙的にキャストする
b = b + 1 がエラーで b += 1 がOKというのは混乱しやすい。+=演算子は内部で b = (byte)(b + 1) と同等の処理をする。
まとめ
Widening(自動変換) = 小さい型 → 大きい型。情報が増える方向。(byte→short→int→long→float→double)
Narrowing(キャスト)= 大きい型 → 小さい型。情報が失われる可能性。(int)value と明示する
リテラルの型 = 整数はint、Lつきはlong、小数はdouble、fつきはfloat
整数の格上げ = byte/short/char同士の演算 → 演算前にintに格上げされる(結果はint)
混合演算の型 = より大きい型に揃えてから演算(int+long=long、long+double=double)
+= の特例 = 複合代入演算子は暗黙的にキャストを含む(b += 1 は b = (byte)(b+1))