SJ blog
beginner
S

信頼度ランク

S 公式ソース確認済み
A 成功実績多数・失敗例少数
B 賛否両論
C 動作未確認・セキュリティリスク高
Z 個人所感

intとIntegerはなぜ両方あるのか — プリミティブ型とラッパークラスの存在理由

なぜJavaにはintとIntegerが共存するのか。プリミティブ型の存在理由、ラッパークラスが必要な場面、オートボクシングの仕組みと落とし穴を根本から解説。

一言結論

プリミティブ型は速さのため、ラッパークラスはオブジェクトとして扱う必要があるときのため。両方あるのはJavaの歴史的な設計判断で、オートボクシングはその橋渡し。ただしInteger==Integerの罠に注意。

なぜ同じものが2種類あるのか

Javaには整数型が2種類ある:

int x = 42;          // プリミティブ型
Integer y = 42;      // ラッパークラス(オブジェクト)

どちらも整数を扱うのに、なぜ両方必要なのか。「Integerはintをオブジェクトにしたもの」という説明はよく見るが、じゃあなぜオブジェクト版が必要になったのか、なぜプリミティブ版を消さなかったのか、が説明されないことが多い。

プリミティブ型はなぜ存在するのか — 速さのため

Javaは「純粋なオブジェクト指向言語」を目指したが、整数や小数点をすべてオブジェクトにするとパフォーマンスが悲惨なことになる。

なぜか?オブジェクトは「参照」で扱われる(前の記事で説明した通り)。

int x = 42 の場合:
変数x: [ 42 ]  ← 値がそのまま入っている

Integer y = 42 の場合:
変数y: [ 0x100 ]  ← アドレスが入っている

     [ 42 ](メモリのどこか)  ← 本体はここ

intは変数の中に値が直接入る。Integerはアドレスを経由して本体を参照する。

1億回のループを考えよう:

// プリミティブ版
int sum = 0;
for (int i = 0; i < 100_000_000; i++) {
    sum += i;  // 直接加算
}

// オブジェクト版(仮の話)
Integer sum = 0;
for (Integer i = 0; i < 100_000_000; i++) {
    sum += i;  // オブジェクトを作って→アドレス経由でアクセスして→計算
}

オブジェクト版は毎回のループでメモリ確保が発生し、10〜100倍遅くなることもある。科学計算やゲームのようにループが多い処理では致命的だ。

だから数値の計算にはプリミティブ型を使う。これはパフォーマンスのための設計判断だ。

ラッパークラスはなぜ必要なのか — オブジェクトが必要な場面があるから

では「すべてプリミティブ型でいい」かというと、そうでもない。

コレクション(ListやMapなど)にプリミティブ型は入れられない:

List<int> list = new ArrayList<int>();  // ❌ コンパイルエラー
List<Integer> list = new ArrayList<Integer>();  // ✅

Javaのコレクションは「オブジェクト」しか扱えない。intは値そのものであってオブジェクトではないので、入れられない。

なぜコレクションがオブジェクト限定なのか?コレクションは「どんな型でも入れられる汎用的なもの」として設計されており、Javaの型システムでその「何でも」を表現するのがObject型だ。プリミティブ型はObjectを継承しないので弾かれる。

nullを表せない:

int x = null;     // ❌ コンパイルエラー(プリミティブはnullになれない)
Integer y = null; // ✅ オブジェクトはnullになれる

データベースから取得した値が「未設定」かどうかを表したいとき、intでは0か-1などのマジックナンバーで表すしかない。Integerならnullで「未設定」を明示できる。

メソッドを持てる:

String s = Integer.toBinaryString(42);  // "101010"(2進数文字列)
int max = Integer.MAX_VALUE;            // 2147483647(intの最大値)
int parsed = Integer.parseInt("123");   // 文字列→int の変換

プリミティブのintにはメソッドがない。Integerクラスには便利なユーティリティが詰まっている。

オートボクシング:橋渡しの仕組み

プリミティブとラッパーを自分で変換するのは面倒だ:

// 昔(Java 5以前)はこう書く必要があった
Integer y = Integer.valueOf(42);  // int → Integer
int x = y.intValue();             // Integer → int

Java 5でオートボクシングが導入され、自動で変換されるようになった:

Integer y = 42;    // オートボクシング(int → Integerに自動変換)
int x = y;         // アンボクシング(Integer → intに自動変換)

コレクションに入れるときも自動変換される:

List<Integer> list = new ArrayList<>();
list.add(1);  // オートボクシング(int 1 → Integer 1 に自動変換)
list.add(2);
int first = list.get(0);  // アンボクシング(Integer → int)

見た目はすっきりするが、内部ではオブジェクトの生成が起きていることを忘れてはならない。

オートボクシングの落とし穴

1. パフォーマンスへの影響

Long sum = 0L;  // Longで宣言してしまった場合
for (long i = 0; i < 1_000_000; i++) {
    sum += i;  // 毎回: Long→long→計算→Long という変換が起きる
}

このコードは100万回のオブジェクト生成が発生する。long sum = 0L;(プリミティブ)にするだけで大幅に速くなる。

2. Integer同士の==比較の罠

これが最大の罠だ:

Integer a = 127;
Integer b = 127;
System.out.println(a == b);   // true

Integer c = 128;
Integer d = 128;
System.out.println(c == d);   // false ← !?

127ではtrueで、128ではfalse。なぜか?

Javaの仕様で、-128から127までのIntegerはキャッシュされる。この範囲の整数は毎回新しいオブジェクトを作らず、プールされたオブジェクトを使い回す。

Integer a = 127;  → キャッシュから 0x100 を取得
Integer b = 127;  → キャッシュから 0x100 を取得(同じ!)
a == b            → 0x100 == 0x100 → true

Integer c = 128;  → 新しいオブジェクト 0x200 を生成
Integer d = 128;  → 新しいオブジェクト 0x300 を生成
c == d            → 0x200 == 0x300 → false

これはStringプール問題の仲間で、「たまたま同じオブジェクトを指しているから==がtrueになる」という副作用だ。

Integer同士の比較は必ずequals()compareTo()を使う:

Integer c = 128;
Integer d = 128;
System.out.println(c.equals(d));  // true(正しい比較)

3. nullのアンボクシングでNullPointerException

Integer x = null;
int y = x;  // NullPointerException!(nullをアンボクシングしようとした)

Integerはnullになれるが、それをプリミティブのintに変換しようとするとクラッシュする。

型の対応表

プリミティブ型ラッパークラスサイズ
booleanBoolean1bit
byteByte8bit
shortShort16bit
intInteger32bit
longLong64bit
floatFloat32bit
doubleDouble64bit
charCharacter16bit

int → Integer のように、ほとんどは頭文字を大文字にするだけだが、int → Integerchar → Characterだけ名前が変わる。

使い分けの基準

プリミティブ型を使う場面:
  ✅ 計算・ループに使う変数(パフォーマンス優先)
  ✅ nullになりえない値
  ✅ ローカル変数や引数

ラッパークラスを使う場面:
  ✅ コレクション(List, Map, Set)に入れる
  ✅ nullを表す必要がある(DBのNULL値など)
  ✅ ジェネリクス(<T>)で型パラメータとして使う
  ✅ Integer.parseInt()などのユーティリティを使う

まとめ

プリミティブ型  = 値をそのまま変数に格納。速い。nullなし。メソッドなし
ラッパークラス  = オブジェクト。コレクションに入れられる。nullあり。メソッドあり
なぜ両方あるか  = 速さ(プリミティブ)と汎用性(オブジェクト)のトレードオフ
オートボクシング = プリミティブ↔ラッパーの自動変換(Java 5以降)

落とし穴:
  Integer == Integer → -128〜127はtrue、それ以外はfalseになることがある → equals()を使う
  Integer null → int に変換 → NullPointerException
  ラッパー型でのループ → パフォーマンス劣化(プリミティブに変える)

「intとIntegerは同じようなもの」ではなく、速さと汎用性のトレードオフで共存している。この背景を知っておくと、「なぜListがダメなのか」「なぜIntegerをnullチェックしないといけないのか」が自然に分かるようになる。