- JAVA
Java Integerとintの違い|ラッパークラスとオートボクシング
intは数値そのものを持つ基本型(プリミティブ型)、Integerはintの値を1つ包んだオブジェクト(ラッパークラス)です。 intはnullにできず計算が速い一方、Integerはnullを入れられ、List<Integer>のようにコレクションの要素にできます。両者はオートボクシング(int → Integer)とアンボクシング(Integer → int)で自動的に変換されます。
この記事では、掲載したコードをすべてOpenJDK 21.0.10で実行し、出力をそのまま載せています。 比較表と8つのラッパークラス一覧に加えて、127と128で==の結果が変わる理由(IntegerCache)、nullのアンボクシングで起きるNullPointerException、new Integerの非推奨、ループでのオートボクシングの計測、LongとIntegerのequals、List<Integer>.removeの取り違えまで、自動変換のしくみを実行結果で確認できます。
Integerとintの違い早見表
| 比較項目 | int |
Integer |
|---|---|---|
| 種類 | 基本型(プリミティブ型) | 参照型(ラッパークラス、java.lang.Integer) |
| 持っているもの | 値そのもの | 値を包んだオブジェクトへの参照 |
| nullを入れられるか | 入れられない(コンパイルエラー) | 入れられる |
| フィールドの既定値 | 0 |
null |
ジェネリクス(List<...>など) |
使えない | 使える |
| メソッド | 持たない | equals・compareTo・toStringなど |
| 同じ値かの比較 | == |
equals(==は同じオブジェクトかの比較) |
| メモリ(実測、500万件) | int[]で約19MB |
List<Integer>で約96MB |
| 速度(実測、1億回の合計) | 31ms(longで合計) |
約205ms(Longで合計) |
| 範囲 | -2,147,483,648〜2,147,483,647 | 同じ |
| 向いている用途 | 計算、ループのカウンタ、配列 | コレクションの要素、「値なし」を表したいとき |
迷ったら「まずintを使い、コレクションに入れるとき・nullで値なしを表すときだけIntegerを使う」と考えると選びやすくなります。メモリと速度の数値は、この記事の後半で計測したものです。
使い分けの判断表
| 場面 | 選ぶ型 | 理由 |
|---|---|---|
| ローカル変数、ループのカウンタ、計算 | int |
速く、nullの心配がない |
List・Map・Setの要素 |
Integer |
ジェネリクスには参照型が必要 |
| データベースのNULLを受け取る項目 | Integer |
0と「値なし」を区別できる |
| 画面やJSONで「未入力」があり得る項目 | Integer |
nullで未入力を表せる |
| 必ず値がある項目(個数、合計など) | int |
既定値0で始められ、nullチェックが不要 |
| 大量の数値をまとめて持つ | int[]、IntStream |
メモリが少なく、速い |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java Integerとintの違い|ラッパークラスとオートボクシング」
動作確認した環境
| 項目 | 内容 |
|---|---|
| JDK | OpenJDK 21.0.10(LTS、64ビット) |
| 実行方法 | java ファイル名.java(単一ファイル実行) |
| 文字コード | ソースはUTF-8。出力の日本語が「?」になる環境ではjava -Dstdout.encoding=UTF-8 ファイル名.java |
以降のコードは、クラス名と同じ名前のファイルに保存すれば、そのまま実行できます。計測値は環境によって変わるため、大小の関係を見る目安としてご覧ください。
intとIntegerの基本を動かして確かめる
import java.util.ArrayList;
import java.util.List;
public class Basic {
static int count; // int のフィールドの既定値
static Integer limit; // Integer のフィールドの既定値
public static void main(String[] args) {
System.out.println("int の既定値 : " + count);
System.out.println("Integer の既定値: " + limit);
int a = 10; // 値そのもの
Integer b = 10; // オートボクシング(Integer.valueOf(10) と同じ)
int c = b; // アンボクシング(b.intValue() と同じ)
System.out.println(a + b + c);
// Integer はメソッドを持つ
System.out.println(b.compareTo(20));
System.out.println(b.toString().length());
System.out.println(Integer.MAX_VALUE + " / " + Integer.MIN_VALUE);
// コレクションに入れられるのは Integer
List<Integer> stocks = new ArrayList<>();
stocks.add(3); // int → Integer に自動変換
int first = stocks.get(0); // Integer → int に自動変換
System.out.println(stocks + " / " + first);
// 「未入力」を null で表せる
Integer age = null;
System.out.println(age == null ? "未入力" : "入力済み");
}
}
実行結果:
int の既定値 : 0
Integer の既定値: null
30
-1
2
2147483647 / -2147483648
[3] / 3
未入力
フィールドの既定値がintは0、Integerはnullである点が、最初に押さえたい違いです。 クラスのフィールドをIntegerで宣言して値を入れないまま計算に使うと、後述のNullPointerExceptionにつながります。
intはnullにできず、ジェネリクスにも使えない
import java.util.ArrayList;
import java.util.List;
public class GenericErr {
public static void main(String[] args) {
List<int> list = new ArrayList<>();
int n = null;
}
}
実行結果(コンパイルエラー):
GenericErr.java:6: error: unexpected type
List<int> list = new ArrayList<>();
^
required: reference
found: int
GenericErr.java:7: error: incompatible types: <null> cannot be converted to int
int n = null;
^
2 errors
error: compilation failed
required: referenceは「参照型が必要」という意味です。List<int>とは書けないのでList<Integer>と書き、値の出し入れはオートボクシングに任せます。
ラッパークラスとは:8つの一覧
ラッパークラスとは、基本型の値をオブジェクトとして扱うために用意されたクラスです。 8つの基本型それぞれに、対応するクラスがjava.langパッケージにあります(importは不要です)。
| 基本型 | ラッパークラス | ビット数 | 範囲 | 既定値(基本型) | ==でキャッシュされる範囲 |
|---|---|---|---|---|---|
byte |
Byte |
8 | -128〜127 | 0 | すべての値 |
short |
Short |
16 | -32,768〜32,767 | 0 | -128〜127 |
int |
Integer |
32 | -2,147,483,648〜2,147,483,647 | 0 | -128〜127 |
long |
Long |
64 | -9,223,372,036,854,775,808〜9,223,372,036,854,775,807 | 0L | -128〜127 |
float |
Float |
32 | 約±3.4×10の38乗 | 0.0f | なし |
double |
Double |
64 | 約±1.8×10の308乗 | 0.0 | なし |
char |
Character |
16 | 0〜65,535(文字コード) | 文字コード0(ヌル文字) | 0〜127 |
boolean |
Boolean |
— | true / false | false | TRUEとFALSEの2つ |
名前は基本型の先頭を大文字にしたものがほとんどですが、int → Integer、char → Characterの2つだけ綴りが変わります。 ビット数と範囲は、各クラスの定数(SIZE・MIN_VALUE・MAX_VALUE)で確認できます。
public class Wrappers {
public static void main(String[] args) {
System.out.printf("%-10s %-4s %s 〜 %s%n", "Byte", Byte.SIZE, Byte.MIN_VALUE, Byte.MAX_VALUE);
System.out.printf("%-10s %-4s %s 〜 %s%n", "Short", Short.SIZE, Short.MIN_VALUE, Short.MAX_VALUE);
System.out.printf("%-10s %-4s %s 〜 %s%n", "Integer", Integer.SIZE, Integer.MIN_VALUE, Integer.MAX_VALUE);
System.out.printf("%-10s %-4s %s 〜 %s%n", "Long", Long.SIZE, Long.MIN_VALUE, Long.MAX_VALUE);
System.out.printf("%-10s %-4s %s 〜 %s%n", "Float", Float.SIZE, Float.MIN_VALUE, Float.MAX_VALUE);
System.out.printf("%-10s %-4s %s 〜 %s%n", "Double", Double.SIZE, Double.MIN_VALUE, Double.MAX_VALUE);
System.out.printf("%-10s %-4s %d 〜 %d%n", "Character", Character.SIZE, (int) Character.MIN_VALUE, (int) Character.MAX_VALUE);
System.out.printf("%-10s %s / %s%n", "Boolean", Boolean.TRUE, Boolean.FALSE);
}
}
実行結果:
Byte 8 -128 〜 127
Short 16 -32768 〜 32767
Integer 32 -2147483648 〜 2147483647
Long 64 -9223372036854775808 〜 9223372036854775807
Float 32 1.4E-45 〜 3.4028235E38
Double 64 4.9E-324 〜 1.7976931348623157E308
Character 16 0 〜 65535
Boolean true / false
Float.MIN_VALUEとDouble.MIN_VALUEは「いちばん小さい正の値」です(負の最小値ではありません)。負の方向の最小値は-Float.MAX_VALUE・-Double.MAX_VALUEです。
ラッパークラスの変換メソッド一覧
ラッパークラスは、文字列との変換や比較のためのstaticメソッドを多く持っています。 よく使うものを逆引きでまとめました。
| やりたいこと | メソッド | 戻り値 | 例 |
|---|---|---|---|
文字列 → int |
Integer.parseInt(s) |
int |
parseInt("123") → 123 |
文字列 → Integer |
Integer.valueOf(s) |
Integer |
valueOf("123") → 123 |
int → Integer |
Integer.valueOf(i) |
Integer |
オートボクシングと同じ |
Integer → int |
obj.intValue() |
int |
アンボクシングと同じ |
Integer → long・double |
obj.longValue()・obj.doubleValue() |
long・double |
300 → 300.0 |
| 数値 → 文字列 | Integer.toString(i)、String.valueOf(i) |
String |
255 → "255" |
| 2進数・16進数の文字列 | Integer.toBinaryString(i)・toHexString(i) |
String |
255 → "11111111"・"ff" |
16進数の文字列 → int |
Integer.parseInt(s, 16) |
int |
"ff" → 255 |
| 大小比較 | Integer.compare(a, b) |
int |
負・0・正 |
| 大きい方・合計 | Integer.max(a, b)・Integer.sum(a, b) |
int |
メソッド参照で使いやすい |
文字列 → double |
Double.parseDouble(s) |
double |
"3.14" → 3.14 |
文字列 → long |
Long.parseLong(s) |
long |
"9000000000" |
文字列 → boolean |
Boolean.parseBoolean(s) |
boolean |
"TRUE" → true(大文字小文字を区別しない) |
| 文字が数字か | Character.isDigit(c) |
boolean |
'7' → true |
| 数字の文字 → 数値 | Character.getNumericValue(c) |
int |
'7' → 7 |
public class Methods {
public static void main(String[] args) {
// 文字列 → 数値
int p = Integer.parseInt("123"); // int を返す
Integer v = Integer.valueOf("123"); // Integer を返す
System.out.println(p + " " + v);
// 数値 → 文字列
System.out.println(Integer.toString(255) + " " + String.valueOf(255));
System.out.println(Integer.toBinaryString(255) + " " + Integer.toHexString(255));
System.out.println(Integer.parseInt("ff", 16));
// Integer → ほかの型
Integer n = 300;
System.out.println(n.intValue() + " " + n.longValue() + " " + n.doubleValue() + " " + n.byteValue());
// 便利な static メソッド
System.out.println(Integer.compare(3, 5) + " " + Integer.max(3, 5) + " " + Integer.sum(3, 5));
System.out.println(Integer.signum(-8) + " " + Integer.bitCount(255));
// ほかのラッパークラス
System.out.println(Double.parseDouble("3.14") + " " + Long.parseLong("9000000000"));
System.out.println(Boolean.parseBoolean("TRUE") + " " + Boolean.parseBoolean("yes"));
System.out.println(Character.isDigit('7') + " " + Character.getNumericValue('7') + " " + Character.toUpperCase('a'));
}
}
実行結果:
123 123
255 255
11111111 ff
255
300 300 300.0 44
-1 5 8
-1 8
3.14 9000000000
true false
true 7 A
n.byteValue()は300が44になっています。byteの範囲(-128〜127)に収まらない値は、下位8ビットだけが残ります。Boolean.parseBoolean("yes")はfalseです。 大文字小文字を無視して"true"と一致するときだけtrueになり、それ以外は例外にならずfalseを返します。
文字列と数値の変換(parseIntとvalueOfの違い、全角数字や空白で起きるNumberFormatException、キャスト)は、別の記事「Java int⇔String変換」で詳しく扱っているため、ここでは一覧に留めます。
オートボクシングとアンボクシングのしくみ
オートボクシングはintをIntegerに、アンボクシングはIntegerをintに、コンパイラが自動で変換するしくみです(Java 5〜)。 コンパイラは次のメソッド呼び出しに置き換えています。
| 書いたコード | コンパイラが置き換える形 | 名前 |
|---|---|---|
Integer b = 10; |
Integer b = Integer.valueOf(10); |
オートボクシング |
int c = b; |
int c = b.intValue(); |
アンボクシング |
list.add(3); |
list.add(Integer.valueOf(3)); |
オートボクシング |
int x = list.get(0); |
int x = list.get(0).intValue(); |
アンボクシング |
b + 1(bはInteger) |
b.intValue() + 1 |
アンボクシング |
b++(bはInteger) |
b = Integer.valueOf(b.intValue() + 1); |
両方 |
「Integer.valueOf()とintValue()が見えないところで呼ばれている」と知っておくと、この後の==の結果やNullPointerExceptionの理由がつながります。
オーバーロードでは「拡大変換」がボクシングより先に選ばれる
同じ名前のメソッドが複数あるとき、intの引数はInteger版よりもlong版(拡大変換)が先に選ばれます。
public class Overload {
static void show(long v) { System.out.println("long 版: " + v); }
static void show(Integer v) { System.out.println("Integer 版: " + v); }
static void show(Object v) { System.out.println("Object 版: " + v); }
public static void main(String[] args) {
int n = 5;
show(n); // 拡大変換(int → long)がボクシングより先
show(Integer.valueOf(n));
show('x'); // char → long に拡大される
show(1.5); // double → Double → Object
}
}
実行結果:
long 版: 5
Integer 版: 5
long 版: 120
Object 版: 1.5
Java 5でオートボクシングが加わる前のコードの動きを変えないように、「ボクシングなしで呼べるメソッド」が先に探される決まりになっています。この順番は、後で見るList<Integer>.removeの動きにも関係します。
127と128で==の結果が変わる理由(IntegerCache)
Integerどうしを==で比べると、127ではtrue、128ではfalseになります。 ==は参照型では「同じオブジェクトか」を比べるためです。
public class Cache {
public static void main(String[] args) {
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println("127 == 127 : " + (a == b));
System.out.println("128 == 128 : " + (c == d));
System.out.println("128 equals 128 : " + c.equals(d));
Integer e = -128, f = -128;
Integer g = -129, h = -129;
System.out.println("-128 == -128 : " + (e == f));
System.out.println("-129 == -129 : " + (g == h));
// 片方が int なら、値の比較になる
int i = 128;
System.out.println("Integer == int : " + (c == i));
// 大小比較(< >)はアンボクシングされるので値で比べる
System.out.println("c <= d && c >= d: " + (c <= d && c >= d));
// 同じオブジェクトかどうかを確認
System.out.println(System.identityHashCode(a) == System.identityHashCode(b));
System.out.println(System.identityHashCode(c) == System.identityHashCode(d));
// Long も -128〜127 をキャッシュする
Long l1 = 127L, l2 = 127L, l3 = 128L, l4 = 128L;
System.out.println("Long 127: " + (l1 == l2) + ", Long 128: " + (l3 == l4));
}
}
実行結果:
127 == 127 : true
128 == 128 : false
128 equals 128 : true
-128 == -128 : true
-129 == -129 : false
Integer == int : true
c <= d && c >= d: true
true
false
Long 127: true, Long 128: false
しくみ:valueOfは-128〜127のオブジェクトを使い回す
Integer.valueOf(int)は、-128〜127の値についてあらかじめ用意した同じオブジェクトを返します。 Javadocにも「-128から127の範囲の値を常にキャッシュし、範囲外の値もキャッシュする場合がある」と書かれています。JDKの内部ではIntegerCacheというクラスがこの配列を持っています。
flowchart LR
A["Integer a = 127"] --> C["キャッシュ済みの 127"]
B["Integer b = 127"] --> C
D["Integer c = 128"] --> E["新しい Integer(128)"]
F["Integer d = 128"] --> G["別の新しい Integer(128)"]
| 比べ方 | 127どうし | 128どうし | 比べているもの |
|---|---|---|---|
a == b(IntegerとInteger) |
true | false | 同じオブジェクトか |
a.equals(b) |
true | true | 値が等しいか |
a == 128(Integerとint) |
— | true | 値(アンボクシングされる) |
a.intValue() == b.intValue() |
true | true | 値 |
Objects.equals(a, b) |
true | true | 値(nullにも対応) |
a <= b && a >= b |
true | true | 値(アンボクシングされる) |
出力の9行目と10行目(identityHashCodeの比較)から、127は同じオブジェクト、128は別のオブジェクトであることが確認できます。
テストでは小さい数で試すことが多いため==でもtrueになり、大きなIDや件数になってからfalseになる、という形で気づくことがあります。Integerどうしの比較はequals、nullがあり得るならObjects.equals(a, b)と決めておくと、値の大きさに関係なく同じ結果になります。
なお、JVMオプション-XX:AutoBoxCacheMax=1000を付けて同じコードを実行したところ、128 == 128もtrueになりました。キャッシュの上限は実行環境の設定で変わり得るため、==の結果に頼らない書き方が確実です。
nullのアンボクシングでNullPointerException
Integerがnullのときにintへ変換しようとすると、NullPointerExceptionが発生します。 アンボクシングはintValue()の呼び出しなので、nullに対してメソッドを呼ぶことになるためです。
import java.util.HashMap;
import java.util.Map;
public class Npe {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("ペン", 12);
int pen = stock.get("ペン");
System.out.println("ペン: " + pen);
int note = stock.get("ノート"); // 登録がないので null → int に入れられない
System.out.println("ノート: " + note);
}
}
実行結果:
ペン: 12
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
at Npe.main(Npe.java:12)
エラー文のCannot invoke "java.lang.Integer.intValue()"から、コード上には書いていないintValue()が呼ばれていることが読み取れます。Map.getはキーがないとnullを返すため、戻り値をそのままintで受けるとここで例外になります。
直し方:既定値を決める・Integerで受ける
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class NpeFix {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("ペン", 12);
// 方法1: getOrDefault で既定値を決める
int note1 = stock.getOrDefault("ノート", 0);
System.out.println("getOrDefault : " + note1);
// 方法2: Integer で受けて null を判定する
Integer note2 = stock.get("ノート");
System.out.println("null 判定 : " + (note2 == null ? "登録なし" : note2));
// 方法3: Objects.requireNonNullElse
int note3 = Objects.requireNonNullElse(stock.get("ノート"), 0);
System.out.println("NonNullElse : " + note3);
// null との比較・計算でも起きる
Integer limit = null;
try {
if (limit > 10) { System.out.println("over"); }
} catch (NullPointerException e) {
System.out.println("比較: " + e.getMessage());
}
Boolean flag = null;
try {
if (flag) { System.out.println("on"); }
} catch (NullPointerException e) {
System.out.println("if : " + e.getMessage());
}
// null を安全に比べるなら Boolean.TRUE.equals / Objects.equals
System.out.println(Boolean.TRUE.equals(flag) + " " + Objects.equals(limit, 10));
}
}
実行結果:
getOrDefault : 0
null 判定 : 登録なし
NonNullElse : 0
比較: Cannot invoke "java.lang.Integer.intValue()" because "<local5>" is null
if : Cannot invoke "java.lang.Boolean.booleanValue()" because "<local6>" is null
false false
(<local5>は変数limitのことです。単一ファイル実行ではデバッグ情報が付かないため番号で表示されます。javac -gでコンパイルすると変数名で表示されます。)
| アンボクシングが起きる場所 | 例 | nullのとき |
|---|---|---|
int変数への代入 |
int n = map.get(key); |
例外 |
| 算術・大小比較 | limit > 10、count + 1 |
例外 |
Integerとintの== |
limit == 10 |
例外 |
if文の条件(Boolean) |
if (flag) |
例外 |
三項演算子でIntegerとintが混在 |
cond ? obj : 0 |
例外 |
引数がintのメソッドに渡す |
Math.abs(obj) |
例外 |
| switch | switch (obj) |
例外 |
| 直し方 | 書き方 | 選ぶ基準 |
|---|---|---|
| 既定値を決める | map.getOrDefault(key, 0) |
「ない」を0として扱ってよい |
Integerで受けて判定 |
Integer v = map.get(key); if (v == null) ... |
「ない」と0を区別したい |
| nullを含めて比べる | Objects.equals(a, 10)、Boolean.TRUE.equals(flag) |
比較だけしたい |
| そもそもnullにしない | フィールドをintにする、初期値を入れる |
値が必ずある項目 |
NullPointerException全般の原因と対処は、別の記事(NullPointerExceptionの原因と対処法)で扱っています。
new Integerは非推奨(Java 9〜):valueOfを使う
new Integer(100)というコンストラクタは、Java 9で非推奨になりました。 代わりにInteger.valueOf(100)、またはオートボクシング(Integer a = 100;)を使います。
public class NewInteger {
public static void main(String[] args) {
Integer a = new Integer(100);
Integer b = new Integer(100);
System.out.println(a == b);
System.out.println(Integer.valueOf(100) == Integer.valueOf(100));
}
}
実行結果(Java 21。警告の後に実行されます):
NewInteger.java:3: warning: [removal] Integer(int) in Integer has been deprecated and marked for removal
Integer a = new Integer(100);
^
NewInteger.java:4: warning: [removal] Integer(int) in Integer has been deprecated and marked for removal
Integer b = new Integer(100);
^
2 warnings
false
true
new Integer(100)は毎回新しいオブジェクトを作るので、キャッシュ範囲内の100でも==はfalseです。valueOfはキャッシュ済みのオブジェクトを返すのでtrueになります。- Java 21の警告は
marked for removal(将来のバージョンで削除する予定)と表示されています。 Java 9で非推奨になり、その後Java 16で「削除予定」に引き上げられました。 - Javadocでは「このコンストラクタを使うのが適切な場面はまれで、
valueOf(int)のほうがメモリと速度の面でよい結果になりやすい」と説明されています。
| これまでの書き方 | 置き換え先 |
|---|---|
new Integer(100) |
Integer.valueOf(100)、またはInteger a = 100; |
new Integer("100") |
Integer.valueOf("100")、intがほしいならInteger.parseInt("100") |
new Long(1L)・new Double(1.5)・new Boolean(true) |
Long.valueOf(1L)・Double.valueOf(1.5)・Boolean.valueOf(true) |
オートボクシングのループは時間がかかる(計測)
ループの中でラッパークラスの変数に足し込むと、1回ごとにアンボクシング → 加算 → ボクシングが行われ、基本型より時間がかかります。
public class Bench {
static long sumPrimitive(int n) {
long sum = 0L;
for (int i = 0; i < n; i++) {
sum += i;
}
return sum;
}
static long sumBoxed(int n) {
Long sum = 0L; // ラッパークラスで合計
for (int i = 0; i < n; i++) {
sum += i; // 毎回 アンボクシング → 加算 → ボクシング
}
return sum;
}
public static void main(String[] args) {
int n = 100_000_000;
// ウォームアップ
for (int w = 0; w < 3; w++) { sumPrimitive(n); sumBoxed(n); }
for (int round = 1; round <= 3; round++) {
long t0 = System.nanoTime();
long r1 = sumPrimitive(n);
long t1 = System.nanoTime();
long r2 = sumBoxed(n);
long t2 = System.nanoTime();
System.out.printf("%d回目 long: %4d ms / Long: %4d ms(結果 %d / %d)%n",
round, (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000, r1, r2);
}
}
}
実行結果(OpenJDK 21.0.10、検証用のLinuxコンテナ):
1回目 long: 31 ms / Long: 205 ms(結果 4999999950000000 / 4999999950000000)
2回目 long: 31 ms / Long: 210 ms(結果 4999999950000000 / 4999999950000000)
3回目 long: 31 ms / Long: 208 ms(結果 4999999950000000 / 4999999950000000)
変数の型をLongからlongに変えるだけで、この環境では約6〜7倍の差になりました。 sum += iの1行が、Longではsum = Long.valueOf(sum.longValue() + i)に置き換えられ、キャッシュ範囲を超えた値は毎回新しいオブジェクトが作られるためです。数値は環境やJITコンパイラの最適化によって変わるので、「合計やカウンタの変数は基本型にする」という方針の目安としてご覧ください。
メモリ使用量:int配列とIntegerのリストを比べる
500万件の数値をint[]とList<Integer>に入れ、Runtimeで使用メモリの増え方を測りました。
import java.util.ArrayList;
import java.util.List;
public class Mem {
static long used() {
System.gc();
try { Thread.sleep(200); } catch (InterruptedException e) { }
Runtime rt = Runtime.getRuntime();
return rt.totalMemory() - rt.freeMemory();
}
public static void main(String[] args) {
int n = 5_000_000;
long base = used();
int[] arr = new int[n];
for (int i = 0; i < n; i++) arr[i] = i + 1000;
long afterArr = used();
List<Integer> list = new ArrayList<>(n);
for (int i = 0; i < n; i++) list.add(i + 1000);
long afterList = used();
System.out.printf("int[] : 約 %d MB%n", (afterArr - base) / 1024 / 1024);
System.out.printf("List<Integer> : 約 %d MB%n", (afterList - afterArr) / 1024 / 1024);
System.out.println(arr[n - 1] + " " + list.get(n - 1));
}
}
実行結果:
int[] : 約 19 MB
List<Integer> : 約 96 MB
5000999 5000999
int[]は1件あたり4バイト(500万件で約19MB)、List<Integer>は1件あたり約20バイト(約96MB)でした。 Integerは値の4バイトに加えてオブジェクトのヘッダーを持ち、リスト側にもそのオブジェクトへの参照が必要になるためです(一般的な64ビットJVMの既定設定の場合)。
| 場面 | おすすめ |
|---|---|
| 合計・カウンタ・ループ変数 | int・long(基本型) |
| 数万件までのリスト | List<Integer>で十分。読みやすさを優先 |
| 数百万件以上の数値 | int[]、またはIntStream |
| Streamで合計・平均 | mapToInt(...).sum()(IntStreamはボクシングしない) |
LongとIntegerのequalsはfalseになる
equalsは「同じ型で、同じ値」のときだけtrueを返します。値が同じ100でも、IntegerとLongではfalseです。
import java.util.HashMap;
import java.util.Map;
public class EqualsTrap {
public static void main(String[] args) {
Integer i = 100;
Long l = 100L;
System.out.println("Integer.equals(Long) : " + i.equals(l));
System.out.println("Long.equals(100) : " + l.equals(100)); // 100 は Integer になる
System.out.println("Long.equals(100L) : " + l.equals(100L));
System.out.println("longValue で比較 : " + (i.longValue() == l));
System.out.println("Short.equals(1) : " + Short.valueOf((short) 1).equals(1));
// Map<Long, ...> を int のキーで引くと見つからない
Map<Long, String> members = new HashMap<>();
members.put(1L, "佐藤");
int id = 1;
System.out.println("get(int) : " + members.get(id));
System.out.println("get((long)): " + members.get((long) id));
}
}
実行結果:
Integer.equals(Long) : false
Long.equals(100) : false
Long.equals(100L) : true
longValue で比較 : true
Short.equals(1) : false
get(int) : null
get((long)): 佐藤
l.equals(100)はfalseです。equalsの引数はObjectなので、100はIntegerにボクシングされ、Longとは型が違うと判定されます。100Lと書けばLongになりtrueです。Map<Long, String>をintのキーで引くとnullが返ります。Map.getの引数もObjectのため、intはIntegerになり、Longのキーとは一致しません。コンパイルエラーにならないので、キーの型をlongにそろえる((long) id、または変数をlongで宣言する)ことで防げます。
なお、IntegerとLongを==で直接比べると、コンパイルの時点で気づけます。
EqualsErr.java:5: error: incomparable types: Integer and Long
System.out.println(i == l);
^
1 error
error: compilation failed
比べ方(Integer i = 100; Long l = 100L;) |
結果 |
|---|---|
i.equals(l) |
false(型が違う) |
i == l |
コンパイルエラー |
i.longValue() == l |
true(longどうしの値の比較になる) |
i.intValue() == l.intValue() |
true(longの値がintの範囲内のとき) |
Long.valueOf(i).equals(l) |
true |
Integerのリストのremoveは添字と値で動きが変わる
Listにはremove(int index)(添字で削除)とremove(Object o)(値で削除)の2つがあります。List<Integer>にintを渡すと、添字での削除が選ばれます。
import java.util.ArrayList;
import java.util.List;
public class RemoveTrap {
public static void main(String[] args) {
List<Integer> a = new ArrayList<>(List.of(10, 20, 30, 1, 2));
a.remove(1); // remove(int index): 添字 1 を削除
System.out.println("remove(1) : " + a);
List<Integer> b = new ArrayList<>(List.of(10, 20, 30, 1, 2));
b.remove(Integer.valueOf(1)); // remove(Object o): 値 1 を削除
System.out.println("remove(Integer.valueOf(1)) : " + b);
List<Integer> c = new ArrayList<>(List.of(10, 20, 30, 1, 2));
Integer target = 30;
c.remove(target); // 変数の型が Integer なら値で削除
System.out.println("remove(Integer 変数) : " + c);
List<Integer> d = new ArrayList<>(List.of(10, 20, 30));
try {
d.remove(30); // 値のつもりが、添字 30 として扱われる
} catch (IndexOutOfBoundsException e) {
System.out.println("remove(30) : " + e.getMessage());
}
}
}
実行結果:
remove(1) : [10, 30, 1, 2]
remove(Integer.valueOf(1)) : [10, 20, 30, 2]
remove(Integer 変数) : [10, 20, 1, 2]
remove(30) : Index 30 out of bounds for length 3
| 書き方 | 選ばれるメソッド | 動き |
|---|---|---|
list.remove(1) |
remove(int index) |
添字1の要素を削除 |
list.remove(Integer.valueOf(1)) |
remove(Object o) |
値が1の最初の要素を削除 |
list.remove((Integer) 1) |
remove(Object o) |
同上 |
list.removeIf(x -> x == 1) |
— | 値が1の要素をすべて削除 |
「オーバーロードではボクシングより先に、そのままの型で呼べるメソッドが選ばれる」という決まりが、ここで効いています。intを渡せばremove(int)がぴったり合うため、Integerへのボクシングは行われません。値で消したいときはInteger.valueOf(値)で包むか、removeIfを使うと意図が伝わります。Set<Integer>には添字版がないので、set.remove(1)は値での削除になります。
Java 22〜25で関係する変更(公式で確認)
手元の検証環境はJava 21のため、この節はJDK 25の公式リリース情報(JEP一覧)とJava 25のAPIドキュメントで確認した内容で、Java 25での実行はしていません。 intとIntegerの違い、オートボクシング、-128〜127のキャッシュは、Java 21からJava 25まで変わっていません。
| 項目 | Java 25での状況(公式で確認) |
|---|---|
Integer.valueOf(int)のキャッシュ |
Java 25のJavadocでも「-128〜127を常にキャッシュする」と記載 |
new Integer(int)・new Integer(String) |
Java 25でも非推奨のまま(Since 9)。valueOf・parseIntの利用が案内されている |
| 値ベースのクラス | Integerは「値ベースのクラス」と明記され、インスタンスをsynchronizedの対象にしないよう案内されている |
| パターン・instanceof・switchでの基本型(JEP 507) | Java 25時点で3回目のプレビュー。obj instanceof int iのような書き方を試せる(--enable-previewが必要) |
Integerとintのチェックリスト
| 確認すること | 見るポイント |
|---|---|
Integerどうしを==で比べていないか |
equals、またはObjects.equals |
Map.getの結果をintで受けていないか |
getOrDefault、またはIntegerで受けてnull判定 |
フィールドのIntegerに初期値が入っているか |
値が必ずあるならintにする |
| ループの合計・カウンタがラッパークラスになっていないか | int・longにする |
LongとIntegerをequalsで比べていないか |
型をそろえる(100L、longValue()) |
Map<Long, ...>をintのキーで引いていないか |
(long) idにそろえる |
List<Integer>.remove(数値)は添字か値か |
値ならInteger.valueOf(値) |
new Integer(...)が残っていないか |
Integer.valueOf(...)に置き換える |
よくある質問
Q. 引数や戻り値はintとIntegerのどちらにすべきですか?
A. 値が必ずあるならintにします。nullチェックが不要になり、呼び出す側も迷いません。「値がない」ことを伝える必要があるときはInteger、またはOptionalIntを使うと、値がない場合があることを型で示せます。
Q. Integerどうしの比較に==を使ってよい場面はありますか?
A. 値を比べたいならequalsを使います。==は-128〜127ではたまたまtrueになりますが、範囲を超えるとfalseになります。片方がintであれば値の比較になりますが、もう片方がnullだとNullPointerExceptionになるため、nullがあり得るならObjects.equalsが確実です。
Q. Integerは変更できますか(setValueのようなメソッドはありますか)?
A. ありません。Integerは不変(イミュータブル)で、count++と書くと新しい値のIntegerが変数に代入し直されます。そのため、メソッドにIntegerを渡して中で++しても、呼び出し元の変数は変わりません。
Q. intとIntegerを混ぜて計算するとどうなりますか?
A. Integerがアンボクシングされ、intどうしの計算になります。結果はintです。Integerがnullでなければ、通常の計算と同じ結果になります。
まとめ
intは値そのもの、Integerは値を包んだオブジェクトです。intはnull不可・既定値0、Integerはnull可・既定値nullです。- まず
intを使い、コレクションの要素やnullで値なしを表すときにIntegerを使います。 - オートボクシングは
Integer.valueOf()、アンボクシングはintValue()の呼び出しに置き換えられます。 Integerどうしの==は127までtrue、128からfalse。 値の比較はequalsまたはObjects.equalsで行います。- nullの
Integerをintに変換するとNullPointerException。Map.getの結果はgetOrDefaultか、Integerで受けてnullを判定します。 new Integer()はJava 9から非推奨。Integer.valueOf()かオートボクシングに置き換えます。LongとIntegerのequalsはfalse、List<Integer>.remove(1)は添字での削除です。 型をそろえる・Integer.valueOfで包むことで意図どおりに動きます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java Integerとintの違い|ラッパークラスとオートボクシング」
株式会社RJCでは、Javaのラッパークラスやオートボクシングのような基礎を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。