Java | Java 標準ライブラリ:auto-unboxing

Java Java
スポンサーリンク

auto-unboxing は「ラッパークラスから中身を自動で取り出す仕組み」

まず前提から整理します。

Java には、値そのものの 基本型(プリミティブ型)
それをオブジェクトとして包んだ ラッパークラス のペアがあります。

intInteger
longLong
doubleDouble
booleanBoolean
… という関係です。

「auto-unboxing(オート・アンボクシング)」は、この

Integer などのラッパークラス → int などの基本型

への変換を、コンパイラが自動でやってくれる仕組みのことです。

人間の感覚としては「Integer を int に代入しているだけ」に見えますが、
裏側ではちゃんと「中身を取り出すメソッド呼び出し」が差し込まれています。


具体例で動きをイメージする(代入編)

見た目はただの代入、裏ではメソッド呼び出し

次のコードを見てください。

Integer obj = Integer.valueOf(10);  // ラッパークラス
int x = obj;                        // ここで auto-unboxing
Java

これだけ見ると「objx に代入しているだけ」に見えますが、
コンパイラは内部的にだいたいこう変換します。

int x = obj.intValue();
Java

intValue() は、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();
Java

obj は null なので、これは実質 null.intValue() を呼んでいるのと同じです。
当然ここで NullPointerException が発生します。

「ラッパークラスは null を持てる」という性質とセットで危険

IntegerLong などのラッパークラスは「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 同士の比較
Java

auto-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() 呼び出しでやってくれる仕組み」

です。

そのおかげで、intInteger の境界をあまり意識せずに書けますが、
逆に

ラッパークラスが null のときに unboxing して NPE が起きる
比較や計算で「今どっちを扱っているのか」が分かりにくくなる

といった罠も生まれています。

初心者としてまず意識してほしいのは、

「ラッパークラス → 基本型 に代入したり渡したりしているところでは、裏で .xxxValue() が呼ばれている」

「そのラッパーは null にはならないか?」

この2点です。

タイトルとURLをコピーしました