🔖

オートボクシングについて

2024/10/29に公開

オートボクシングとは

プリミティブ型(int、double、booleanなど)を対応するラッパークラス(Integer、Double、Booleanなど)に自動的に変換する機能のこと。
逆に、ラッパークラスをプリミティブ型に自動変換することをアンボクシング(unboxing)と呼ぶ。
オートボクシングはコードの可読性を向上させるために使ったりするが、
パフォーマンス面で注意が必要。

パフォーマンスへの影響

色々調べた感じだと以下の点でパフォーマンスへの影響がありそう。

追加のオブジェクト生成

オートボクシングではプリミティブ型をラッパークラスに変換する際、
追加のオブジェクトが生成される模様。

例えば、intIntegerに変換されると、
新しいIntegerオブジェクトが作られる場合がある。[1]
もちろんオブジェクト生成にはメモリと処理時間がかかる。
頻繁にボクシングが発生する(特に大量のデータを処理する)場合、
GC(ガーベッジコレクション)の負荷が増え、パフォーマンスに影響しそう。

アンボクシングのパフォーマンスコスト

オートボクシングが絡むと、アンボクシングも発生することになる。
例えば、Integerintに変換する際には、
ラッパークラスからプリミティブ型に値を取り出す処理が必要になる。
上記処理も追加のコストを生むため、パフォーマンスに影響を与えることになる。

特にループ内でボクシングしている場合や
コレクション(List<Integer>Map<Integer, Integer>)のように
ラッパークラスで定義を使用している場合、
頻繁にボクシングとアンボクシングが発生する可能性が高くなるのでパフォーマンスコストがかかる。

実際に確認してみた

どのくらい違うのか気になったので確認してみた。
openjdk version "18.0.2.1" 2022-08-18

Main
public static void main(String[] args) {
    final int EXECUTE_COUNT = 10;
    for (int i = 0; i < EXECUTE_COUNT; i++) {
        System.out.println("Execute count: " + i);
        execute();
    }
}

private static void execute(){
    final int LOOP_COUNT = 100_000_000;

    // オートボクシングを使わないケース
    long startTime = System.nanoTime();
    long sumPrimitive = 0;
    for (int i = 0; i < LOOP_COUNT; i++) {
        sumPrimitive += i;  // プリミティブ型のみ
    }
    long endTime = System.nanoTime();
    System.out.println("Without Autoboxing: " + (endTime - startTime) / 1_000_000 + " ms");

    // オートボクシングを使ったケース
    startTime = System.nanoTime();
    Long sumBoxed = 0L;  // ラッパークラス(Long)
    for (int i = 0; i < LOOP_COUNT; i++) {
        sumBoxed += i;  // オートボクシング
    }
    endTime = System.nanoTime();
    System.out.println("With Autoboxing: " + (endTime - startTime) / 1_000_000 + " ms");
}
Execute count: 0
Without Autoboxing: 41 ms
With Autoboxing: 355 ms
Execute count: 1
Without Autoboxing: 42 ms
With Autoboxing: 347 ms
Execute count: 2
Without Autoboxing: 41 ms
With Autoboxing: 350 ms
Execute count: 3
Without Autoboxing: 40 ms
With Autoboxing: 351 ms
Execute count: 4
Without Autoboxing: 40 ms
With Autoboxing: 352 ms
Execute count: 5
Without Autoboxing: 42 ms
With Autoboxing: 346 ms
Execute count: 6
Without Autoboxing: 42 ms
With Autoboxing: 348 ms
Execute count: 7
Without Autoboxing: 41 ms
With Autoboxing: 355 ms
Execute count: 8
Without Autoboxing: 40 ms
With Autoboxing: 350 ms
Execute count: 9
Without Autoboxing: 40 ms
With Autoboxing: 356 ms

差はかなりありそうだが、
そもそも気にするほどパフォーマンスに問題でるような時間はかからなさそう。
ただし、ちりつもで遅くなることはありそうな気配。

パフォーマンスを良くするためには

プリミティブ型の使用

そもそも可能であれば、不必要なラッパークラスを使わないことでオートボクシングを発生させない。
例えば、Integerを使わずにintを使うことで、不要なオブジェクト生成を防ぐ。

キャッシュの活用

[1:1]に記載したが、
扱う数値が-128から127の範囲内であればオートボクシングによるパフォーマンス低下はあまり発生しない。
もし、キャッシュ範囲外で値の範囲が定まっているオートボクシングが発生するのであれば
以下のJVMオプションをつける事でもパフォーマンス向上が期待できる。
-XX:AutoBoxCacheMax
ただし、上記値を大きくすればするほど常時メモリを喰う量が大きくなるので注意が必要。

なんでラッパークラスがあるのか

そもそもプリミティブ型のみであれば、オートボクシングが発生する余地がないのでは?となったのでどうしてラッパークラスがあるのか調べてみた。

コレクションに格納するため

Javaのコレクション(List、Set、Mapなど)やジェネリクスは、オブジェクトを要素として格納することが前提となっているのでプリミティブ型を直接格納することはできない。
そのため、例えばList<int>のようにプリミティブ型を直接使えない代わりに、
List<Integer>とすることでプリミティブ型の値もコレクションに格納できるようになる。
プリミティブ型のままで集合を扱う場合は、配列のみになる。

プリミティブ型にはないメソッドが利用できる

ラッパークラスには、プリミティブ型の値を操作するための便利なメソッドが提供されている。
例えば、IntegerクラスにはparseIntメソッドがあり、文字列を整数に変換することができたりする。

nullの利用

プリミティブ型はnullを許容しないが、ラッパークラスはオブジェクトであるためnullを保持できる。
これにより、「値が設定されていない」や「未初期化」という状態を表現することができる。
データベースやJSONなどの外部からデータを受け取る場面で、
「値が存在しない」場合をnullで表現することがたまにあったりする。
ただし、一長一短でプリミティブで扱っていれば気にしなくて済むnullが入ってくるので
booleanなどは選択肢がtruefalseだけでなくnullも考慮に入れる必要が出てくる。

まとめ

パフォーマンスが劇的に悪くなることはないが、
nullの選択肢が入ってくるラッパークラスが良い場面と悪い場面がある。

意識して使い分けることは必須ではないが、出来れば意識して使い分けていきたいところ。

参考

ボクシングのキャッシュについて凄い詳しく書かれているサイト
https://medium.com/programmingmitra-com/java-integer-cache-why-integer-valueof-127-integer-valueof-127-is-true-e5076824a3d5

脚注
  1. Javaでは、-128から127の範囲内の整数値については、Integerオブジェクトがキャッシュされる。(他のラッパークラスも同様のキャッシュ機構を持っている)
    この範囲内の数値であれば、新しいオブジェクトを生成せずに既存のオブジェクトを再利用される模様。
    しかし、それ以外の値では新たなIntegerオブジェクトが生成される。 ↩︎ ↩︎

Discussion