1是什么
Java 的泛型、集合、反射等机制只能操作对象,而 int、long、char 这类基本类型不是对象。为了让两者互通,JDK 为每个基本类型提供了一个包装类 (wrapper class):
| 基本类型 | 包装类 | 值位宽 |
|---|---|---|
boolean | Boolean | — |
byte | Byte | 1 字节 |
short | Short | 2 字节 |
char | Character | 2 字节 |
int | Integer | 4 字节 |
long | Long | 8 字节 |
float | Float | 4 字节 |
double | Double | 8 字节 |
- 装箱 (boxing):把基本类型值转成对应的包装类对象。
- 拆箱 (unboxing):把包装类对象里的值取出来,变成基本类型。
从 Java 5 开始,这两种转换在赋值、方法调用、算术运算、比较等场景下由编译器自动插入,称为自动装箱 / 自动拆箱 (autoboxing / auto-unboxing)。
Integer a = 100; // 自动装箱:Integer a = Integer.valueOf(100);
int b = a; // 自动拆箱:int b = a.intValue();
List<Integer> list = new ArrayList<>();
list.add(1); // 自动装箱,1 -> Integer.valueOf(1)
int first = list.get(0); // 自动拆箱
2编译器到底做了什么
自动装箱/拆箱完全是编译期行为,没有任何运行时“魔法”。用 javap -c 反编译可以看到插入的调用:
static Integer box(int i) { return i; }
static int unbox(Integer i) { return i; }
static java.lang.Integer box(int);
Code:
0: iload_0
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: areturn
static int unbox(java.lang.Integer);
Code:
0: aload_0
1: invokevirtual #3 // Method java/lang/Integer.intValue:()I
4: ireturn
由此可得两条结论,后面几乎所有坑都源于此:
- 装箱调用的是
Integer.valueOf(int),不是new Integer(int)。 - 拆箱调用的是
intValue(),这是一个实例方法——对象为null时必然抛NullPointerException。
new Integer(1)、new Long(1L)等包装类构造器自 Java 9 起已标记为@Deprecated,应改用valueOf或直接赋值。
3缓存:valueOf 不是每次都 new
Integer.valueOf(int) 内部维护了一个静态缓存数组,命中缓存时返回同一个对象:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
各包装类的缓存范围 (在 JDK 8 上实测):
| 包装类 | 缓存范围 | 是否可调整 |
|---|---|---|
Boolean | true / false 两个常量 | 否 |
Byte | 全部 256 个值 (-128 ~ 127) | 否 |
Short | -128 ~ 127 | 否 |
Character | 0 ~ 127 ('\u0000' ~ '\u007f') | 否 |
Integer | -128 ~ 127 (high 默认 127) | 是,-XX:AutoBoxCacheMax=N |
Long | -128 ~ 127 | 否 |
Float / Double | 无缓存 | — |
实测输出:
System.out.println(Integer.valueOf(-128) == Integer.valueOf(-128)); // true
System.out.println(Integer.valueOf(128) == Integer.valueOf(128)); // false
System.out.println(Short.valueOf((short) 200) == Short.valueOf((short) 200)); // false
System.out.println(Character.valueOf((char) 65) == Character.valueOf((char) 65)); // true
System.out.println(Double.valueOf(1.0) == Double.valueOf(1.0)); // false
Integer 的上限可以通过 JVM 参数放大 (下限固定为 -128):
java -XX:AutoBoxCacheMax=1000 Main
Integer x = 128, y = 128;
System.out.println(x == y); // 未加参数:false;加了上面参数:true
注意:缓存范围是实现细节,不要在业务代码里依赖
127以内==恰好为true。它只会让你的程序在某个值上“碰巧正确”。
4常见坑
4.1用 == 比较包装类
== 对两个引用比较的是地址,对 Integer/int 混合比较才会拆箱比数值。
Integer a = 128, b = 128;
System.out.println(a == b); // false —— 超出缓存,两个不同对象
System.out.println(a.equals(b)); // true
Integer c = 127, d = 127;
System.out.println(c == d); // true —— 命中缓存,同一对象(纯属巧合)
正确的做法:
a.equals(b) // 包装类之间
Objects.equals(a, b) // 允许 null
Integer.compare(a, b) == 0 // 或 a.intValue() == b.intValue()
4.2不同类型包装类的 equals 恒为 false
Integer.equals 的实现是 obj instanceof Integer && value == ((Integer) obj).intValue(),类型不匹配直接返回 false:
Integer i = 1;
Long l = 1L;
System.out.println(i.equals(l)); // false,且不抛异常,极难发现
对比之下,Integer 与 long 用 == 比较时会各自拆箱后做数值提升,结果是 true:
Integer i = 1;
System.out.println(i == 1L); // true
而两个不同类型的包装类直接用 == 是编译错误:
Integer i = null; Long l = null;
System.out.println(i == l); // 编译错误:incomparable types: Integer and Long
4.3null 拆箱 → NullPointerException
这是生产环境最常见的空指针来源之一:错误栈指向的往往是一行看不出任何解引用的代码。
Integer count = map.get("missing"); // null
int c = count; // NPE
int sum = count + 1; // NPE
if (count > 0) { } // NPE(> 触发拆箱)—— 应为 count != null && count > 0
Integer x = null;
switch (x) { ... } // NPE,switch 的表达式也会拆箱
例外是 == / != 与 null 比较时不拆箱:
Integer x = null;
System.out.println(x == null); // true,安全
System.out.println(x == 1); // NPE
4.4三元运算符的隐式拆箱
当条件表达式的两个分支一个是包装类、一个是基本类型时,按二元数值提升规则,结果类型是基本类型,包装类分支会被拆箱——即使你的目标变量是 Object:
Integer maybeNull = null;
Integer r1 = true ? maybeNull : 0; // NPE
Object r2 = true ? maybeNull : 0; // 同样是 NPE
只要两个分支都是包装类,就不会拆箱:
Integer r3 = true ? maybeNull : Integer.valueOf(0); // null,安全
另一个相关的类型提升陷阱:
Object o = true ? 1 : 2.0;
System.out.println(o.getClass().getName()); // java.lang.Double,结果是 2.0 而不是 1
4.5重载解析:宽化优先于装箱
Java 的重载选择分阶段进行:先找不需要装箱/拆箱的严格匹配 (允许 int -> long 这类宽化),找不到才进入允许装箱/拆箱的阶段,最后才是可变参数。所以下面这段会选中 f(long) 而不是 f(Integer):
static void f(long x) { System.out.println("f(long)"); }
static void f(Integer x) { System.out.println("f(Integer)"); }
f(1); // 输出 f(long):int -> long 的宽化属于严格匹配,优先于装箱
null 不能匹配基本类型形参,因此只能落到包装类版本上,方法体内若直接拆箱就会 NPE。
static void g(int x) { System.out.println("g(int)"); }
static void g(Integer x) { System.out.println("g(Integer) " + x); }
g(1); // g(int) —— 直接匹配基本类型
g(null); // g(Integer) null —— 编译通过,进入方法后一旦拆箱就 NPE
4.6List.remove 的重载歧义
List 有 remove(int index) 和 remove(Object o) 两个重载,传 int 时选索引版本:
List<Integer> list = new ArrayList<>(Arrays.asList(10, 20, 30));
list.remove(1); // 删除下标 1 的元素 -> [10, 30]
list.remove(Integer.valueOf(10)); // 删除值 10 的元素 -> [20, 30]
一行 list.remove(i) (i 是 int) 如果本意是“删掉这个值”,就会静默删错元素。
4.7赋值时的类型限制
只有编译期常量且落在目标类型范围内时,才允许窄化后装箱:
Byte b = 1; // 可以,常量 1 在 byte 范围内
Short s = 1; // 可以
Character c = 'a'; // 可以
Long l = 1L; // 可以(1L 是 long 字面量)
Long bad = 1; // 编译错误:int cannot be converted to Long
Float f2 = 1; // 编译错误:int cannot be converted to Float
Byte b2 = 200; // 编译错误:超出 byte 范围
4.8包装类“不可变”带来的循环累加开销
Integer/Long 是 final 且字段不可变,sum += i 等价于每次新建对象:
Long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // 每次都拆箱 + 相加 + 装箱,产生约 100 万个临时对象
}
正确写法是全程使用基本类型 long sum = 0L;。
5性能影响
装箱的成本有三块:对象分配 (堆/Young GC 压力)、内存占用 (64 位 HotSpot 开启指针压缩时,Integer = 12 字节对象头 + 4 字节 int 值 = 16 字节,而 int 只占 4 字节),以及拆箱时的额外方法调用 (JIT 内联后很小,但未充分预热或分支复杂时仍可见)。
本机 JDK 8 上的一组小规模对照 (非严谨 JMH 基准,仅供感受量级):
| 场景 | 耗时 |
|---|---|
Integer[] 增量累加 500 万次 | 约 17 ms |
int[] 同等操作 | 约 4 ms |
HashMap<Integer, Integer> 计数 | 约 70 ms |
结论:单纯数组/循环内的装箱拆箱开销约 3~4 倍;而把计数放进 HashMap<Integer, Integer> 后,除装箱本身还要叠加哈希计算与对象头占用,实测约慢一个数量级 (上表中约 17 倍)。在热点路径 (批量计算、序列化、流式处理) 上尤其明显。
6实践建议
- 泛型 (集合、Stream、
Optional<T>) 的类型参数只能是包装类;局部变量和循环累加器一律用基本类型。 - 包装类比较数值用
equals/Objects.equals/Integer.compare,永不用==。 - 可能为
null的包装类,参与运算或比较前显式判空;三元表达式两侧保持类型一致。 - 方法返回“可能没有值”时,用
OptionalInt/OptionalLong或返回null并明确文档化,不要靠调用方猜。 - 涉及金额、精度用
BigDecimal,不要用Double(BigDecimal.valueOf(double)而非new BigDecimal(double))。 - 对性能敏感的代码若发现大量
Integer临时对象,优先考虑int[]、long、或专门的集合库 (如fastutil的基本类型集合)。 - 不要依赖
Integer缓存范围写业务逻辑;需要判断身份时用==之外的手段。
7小结
自动装箱/拆箱让 int 与 Integer 之间的代码更简洁,代价是把“引用是否相等”和“数值是否相等”混在了一起,并引入了编译器隐式插入的拆箱点。记住三条即可规避绝大多数问题:
- 装箱 =
valueOf,可能返回缓存对象,因此==的结果在小范围内是“真”,在范围外是“假”。 - 拆箱 =
xxxValue(),对象为null就抛 NPE,且可能发生在你看不见的地方 (算术、比较、三元、switch)。 - 不同包装类型之间
equals恒为false,跨类型比较先统一到同一个类型。