- auto-unboxing は「ラッパークラスから中身を自動で取り出す仕組み」
- 具体例で動きをイメージする(代入編)
- メソッド呼び出しでも auto-unboxing は普通に起きる
- 計算式の中でも auto-unboxing はひっそり動いている
- auto-unboxing 最大の落とし穴:null で発生する NullPointerException
- null を含む可能性がある auto-unboxing をどう防ぐか
- auto-unboxing と比較演算:== と equals の関係
- auto-unboxing とパフォーマンスの話(少しだけ)
- まとめ:auto-unboxing をどう意識して書けばいいか
auto-unboxing は「ラッパークラスから中身を自動で取り出す仕組み」
まず前提から整理します。
Java には、値そのものの 基本型(プリミティブ型) と
それをオブジェクトとして包んだ ラッパークラス のペアがあります。
int ↔ Integerlong ↔ Longdouble ↔ Doubleboolean ↔ Boolean
… という関係です。
「auto-unboxing(オート・アンボクシング)」は、この
Integer などのラッパークラス → int などの基本型
への変換を、コンパイラが自動でやってくれる仕組みのことです。
人間の感覚としては「Integer を int に代入しているだけ」に見えますが、
裏側ではちゃんと「中身を取り出すメソッド呼び出し」が差し込まれています。
具体例で動きをイメージする(代入編)
見た目はただの代入、裏ではメソッド呼び出し
次のコードを見てください。
Integer obj = Integer.valueOf(10); // ラッパークラス
int x = obj; // ここで auto-unboxing
Javaこれだけ見ると「obj を x に代入しているだけ」に見えますが、
コンパイラは内部的にだいたいこう変換します。
int x = obj.intValue();
JavaintValue() は、Integer が持っている「中の int を取り出すメソッド」です。
つまり、auto-unboxing とは
「代入や計算のときに、ラッパークラスの xxxValue() を勝手に呼んでくれる機能」
だと考えると分かりやすいです。
メソッド呼び出しでも auto-unboxing は普通に起きる
引数に基本型が求められていると自動で変換される
例えば、基本型 int を受け取るメソッドがあるとします。
void printInt(int x) {
System.out.println("x = " + x);
}
Javaここに Integer を渡してもコンパイルが通ります。
Integer obj = Integer.valueOf(10);
printInt(obj); // ここで auto-unboxing
Java裏側では、だいたいこう書いたのと同じです。
printInt(obj.intValue());
Java「引数の型が int だから、Integer から int を取り出して渡してあげよう」と
コンパイラが気を利かせてくれているわけです。
戻り値側でも起きる
逆向きに、戻り値が Integer のメソッドから int 変数に代入するときも同じです。
Integer getInteger() {
return Integer.valueOf(20);
}
int x = getInteger(); // ここで auto-unboxing
Javaコンパイラはこう解釈します。
int x = getInteger().intValue();
Java「ラッパークラス → 基本型」の橋渡しを、常に自動でやってくれているイメージです。
計算式の中でも auto-unboxing はひっそり動いている
ラッパークラス同士の加算の中で unboxing → 計算 → boxing
次のコードを見てください。
Integer a = 10;
Integer b = 20;
Integer c = a + b;
Java一見、「オブジェクト同士を + している」ように見えますが、
実際には
a.intValue() + b.intValue() を計算して
その結果を Integer.valueOf(...) で包んでいます。
コンパイラが展開すると、だいたいこういう形です。
Integer c = Integer.valueOf(a.intValue() + b.intValue());
Javaつまり、「計算するときは一旦基本型まで降りる → 計算結果をもう一度ラップ」
という流れになります。
この「勝手に int として扱ってくれる」便利さのおかげで、
ラッパークラスと基本型を一緒に使っても、直感的なコードが書けるようになっています。
auto-unboxing 最大の落とし穴:null で発生する NullPointerException
「null から中身を取り出そうとしたら落ちる」のは当然
ここがいちばん重要なポイントです。
次のコードを見てください。
Integer obj = null;
int x = obj; // ここで何が起こる?
Java見た目は「null を int に入れようとしている」ように見えますが、
コンパイラはこう解釈します。
int x = obj.intValue();
Javaobj は null なので、これは実質 null.intValue() を呼んでいるのと同じです。
当然ここで NullPointerException が発生します。
「ラッパークラスは null を持てる」という性質とセットで危険
Integer や Long などのラッパークラスは「null を代入できる」ので、
DB や設定値の読み込みなどで
「値がない」→ null
という状態になることがよくあります。
その後、何も考えずに基本型に落とすと、
この「null アンボクシング NPE」が起きます。
具体的には、こんなコードが危険です。
Integer score = getScoreFromDb(userId); // DB に値がなければ null かも
int s = score; // ここで NPE の可能性
Javaこの手のバグは、「実行しているデータ次第」で出たり出なかったりするので、
テストでは通って、実運用で突然落ちる、という厄介な形をとりがちです。
null を含む可能性がある auto-unboxing をどう防ぐか
三項演算子で「null のときの値」をはっきり決める
ラッパークラスを基本型にする前に、
「null のときはこの値にする」というルールをはっきり決めておくと安全です。
Integer score = getScoreFromDb(userId); // null かも
int s = (score != null) ? score : 0; // null なら 0 にする
Javaこのように書けば、
score が null → 0
score が 100 → 100
という風に、「null のときの扱い」がはっきりします。
Optional と組み合わせる
Optional を使う設計にしていれば、orElse などと組み合わせて書くこともできます。
Optional<Integer> optScore = findScore(userId); // Optional<Integer>
int s = optScore.orElse(0);
Javaあるいは、Integer から直接 Optional を作ることもできます。
Integer score = getScoreFromDb(userId); // null かも
int s = Optional.ofNullable(score).orElse(0);
Javaいずれにせよ、「null のときどう扱うか」を決めずに auto-unboxing に任せる、
というのが危険なスタイルだと押さえておいてください。
auto-unboxing と比較演算:== と equals の関係
ラッパークラス同士を int として比べる場合
Integer 同士を == で比較すると、「参照比較」になってしまう話がありますが、
auto-unboxing を使うと、実は「int 比較」にできたりします。
Integer a = 100;
Integer b = 100;
boolean same = (a == b); // これは危険(参照比較)
boolean sameValue = (a.intValue() == b.intValue()); // int 同士の比較
Javaauto-unboxing を使うなら、こう書けます。
boolean sameValue = (a == b); // ここで unboxing が起きるように見える?
Javaただし、ここは微妙な罠ポイントなので、
あまり「unboxing 任せの ==」に頼らないほうが安全です。
おすすめは次のどちらかです。
a != null && a.equals(b)
またはint ai = (a != null) ? a : 0; int bi = (b != null) ? b : 0; ai == bi;
といったように、「自分の手で値として比較している」ことが
コードからはっきり読み取れる形が良いです。
auto-unboxing が絡むと、「これ int 比較だっけ?オブジェクト比較だっけ?」と
読み手が混乱しがちなので、敢えて明示的に unboxing してから比べるのも立派な選択です。
auto-unboxing とパフォーマンスの話(少しだけ)
何度も unboxing するときは「一度だけ取り出す」意識
unboxing 自体は単なるメソッド呼び出しですが、
何度も繰り返すと、それなりにコストになります。
例えば:
Integer n = getNumber();
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += n; // 毎回 auto-unboxing
}
Javaこういう場合は、一度だけ unboxing してから使うほうが自然です。
Integer nObj = getNumber();
int n = (nObj != null) ? nObj : 0; // ここで一度だけ unboxing
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += n; // ここは int のまま
}
Java初心者のうちはそこまで細かく気にしなくても大丈夫ですが、
「同じラッパー変数を何度も基本型として使うなら、一度だけ取り出す」という
感覚を持っておくと、パフォーマンスを意識し始めたときに役立ちます。
まとめ:auto-unboxing をどう意識して書けばいいか
auto-unboxing を一言で言うと、
「ラッパークラスから基本型への変換を、コンパイラが勝手に xxxValue() 呼び出しでやってくれる仕組み」
です。
そのおかげで、int と Integer の境界をあまり意識せずに書けますが、
逆に
ラッパークラスが null のときに unboxing して NPE が起きる
比較や計算で「今どっちを扱っているのか」が分かりにくくなる
といった罠も生まれています。
初心者としてまず意識してほしいのは、
「ラッパークラス → 基本型 に代入したり渡したりしているところでは、裏で .xxxValue() が呼ばれている」
「そのラッパーは null にはならないか?」
この2点です。
