第 6 章:自动装箱与拆箱
学习目标
- 理解基本类型与包装类的关系
- 掌握自动装箱(Autoboxing)和拆箱(Unboxing)机制
- 警惕
==vsequals在包装类的陷阱 - 了解
IntegerCache缓存池设计
一、为什么需要包装类?
Java 是面向对象语言,但 8 种基本类型不是对象。这导致一个矛盾:
java
// ❌ 基本类型不能放进 Object[] / List
List<int> numbers = new ArrayList<int>(); // 编译报错
Object[] objects = {1, 2, 3}; // 编译报错为了解决这个矛盾,Java 为每个基本类型设计了对应的包装类(Wrapper Class):
| 基本类型 | 包装类 |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
java
// ✅ 包装类可以放进集合
List<Integer> numbers = new ArrayList<>();
Object[] objects = {1, 2, 3}; // 自动装箱为 Integer二、包装类的本质
包装类就是把基本类型包装成对象,提供方法、字段、对象特性。
java
public class Integer extends Number implements Comparable<Integer> {
private final int value; // ① 内部仍然存一个 int 值
// ② 构造器(已废弃,JDK 9+)
@Deprecated(since = "9")
public Integer(int value) {
this.value = value;
}
// ③ 静态工厂方法(推荐)
public static Integer valueOf(int i) { ... }
// ④ 实例方法
public int intValue() { return value; } // 拆箱
public static int parseInt(String s) { ... } // 字符串解析
// ⑤ 常用常量
public static final int MIN_VALUE = 0x80000000;
public static final int MAX_VALUE = 0x7fffffff;
}包装类的继承体系
6 种数值类型继承自
Number,提供xxxValue()方法统一拆箱。
三、手动装箱与拆箱(早期写法)
在 JDK 5 之前,必须手动转换:
java
// ❌ 手动装箱:基本类型 → 包装类
int i = 100;
Integer boxed = Integer.valueOf(i); // ① 装箱
// ❌ 手动拆箱:包装类 → 基本类型
int unboxed = boxed.intValue(); // ② 拆箱四、自动装箱与拆箱(JDK 5+)
JDK 5 引入了自动装箱(Autoboxing)和自动拆箱(Unboxing),编译器自动帮你转换。
自动装箱
java
Integer boxed = 100; // ① 编译器自动调用 Integer.valueOf(100)
Long longBoxed = 100L; // ② 自动调用 Long.valueOf(100L)
Double doubleBoxed = 3.14; // ③ 自动调用 Double.valueOf(3.14)反编译后等价于:
java
Integer boxed = Integer.valueOf(100);自动拆箱
java
Integer boxed = 100;
int unboxed = boxed; // ① 编译器自动调用 boxed.intValue()
// 包装类参与运算
Integer a = 10;
Integer b = 20;
int sum = a + b; // ② 自动拆箱:a.intValue() + b.intValue()集合中的自动装箱
java
List<Integer> list = new ArrayList<>();
list.add(1); // ① 自动装箱:list.add(Integer.valueOf(1))
list.add(2);
list.add(3);
int first = list.get(0); // ② 自动拆箱:list.get(0).intValue()五、自动装箱的陷阱
5.1 经典陷阱:== vs equals
java
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(命中缓存池)
System.out.println(a.equals(b)); // true(值相等)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false(超过缓存池范围)
System.out.println(c.equals(d)); // true(值相等)这是 Java 面试最高频的坑之一
永远用
equals比较包装类,不要用==!
5.2 原因:IntegerCache 缓存池
JDK 为了优化性能,在 Integer 内部缓存了 -128 ~ 127 的整数对象:
java
// Integer 源码(简化版)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) {
return IntegerCache.cache[i + (-IntegerCache.low)]; // ① 命中缓存
}
return new Integer(i); // ② 新建对象
}缓存池范围:
| 类型 | 缓存范围 | 来源 |
|---|---|---|
Integer | -128 ~ 127 | Integer.IntegerCache.high |
Long | -128 ~ 127 | Long.LongCache |
Character | 0 ~ 127 | Character.CharacterCache |
Boolean | TRUE / FALSE 两个静态常量 | Boolean.TRUE/FALSE |
Float / Double | ❌ 不缓存 | 浮点数太多 |
5.3 另一个陷阱:自动拆箱 NPE
java
Integer boxed = null;
int unboxed = boxed; // ❌ NullPointerException
// 包装类参与运算时自动拆箱
Integer a = null;
Integer b = 10;
int sum = a + b; // ❌ NPE:a.intValue() 时 a 是 nullNPE(NullPointerException):访问 null 引用的方法/字段。 包装类参与运算时会拆箱,null 拆箱就是 NPE。
5.4 性能陷阱:频繁装箱
java
// ❌ 性能差:每次循环都装箱一次
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i; // ① 每次循环都拆箱 sum + 装箱回 Long
}
// 耗时:约 6 秒
// ✅ 性能好:用基本类型 long
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i; // ② 纯基本类型运算,无装箱拆箱
}
// 耗时:约 0.5 秒性能差距 10 倍以上。在性能敏感的循环中,优先使用基本类型。
六、包装类的常用方法
6.1 类型转换
java
// 字符串 → 基本类型
int i = Integer.parseInt("123"); // 123
double d = Double.parseDouble("3.14"); // 3.14
boolean b = Boolean.parseBoolean("true"); // true
// 基本类型 → 字符串
String s1 = Integer.toString(123); // "123"
String s2 = String.valueOf(123); // "123"(更通用,可处理 null)
String s3 = "" + 123; // "123"(不推荐,仅作演示)6.2 进制转换
java
// 十进制 → 其他进制
System.out.println(Integer.toBinaryString(10)); // "1010"(二进制)
System.out.println(Integer.toHexString(255)); // "ff"(十六进制)
System.out.println(Integer.toOctalString(64)); // "100"(八进制)
// 其他进制 → 十进制
System.out.println(Integer.parseInt("1010", 2)); // 10(二进制)
System.out.println(Integer.parseInt("ff", 16)); // 255(十六进制)6.3 常量字段
java
System.out.println(Integer.MAX_VALUE); // 2147483647
System.out.println(Integer.MIN_VALUE); // -2147483648
System.out.println(Integer.SIZE); // 32(位数)
System.out.println(Integer.BYTES); // 4(字节数,JDK 8+)6.4 自动装箱 vs 手动 valueOf
java
// ✅ 推荐:用 valueOf(编译后和自动装箱等价)
Integer a = Integer.valueOf(100);
// ⚠️ 不推荐:用 new(绕过缓存池)
Integer b = new Integer(100); // 总是新建对象,不走缓存池
// ⚠️ 编译警告(JDK 9+):构造器已废弃
// The constructor Integer(int) has been deprecated七、装箱 / 拆箱的应用场景
场景 1:集合存储
java
// ✅ 集合只能用包装类
List<Integer> ids = Arrays.asList(1, 2, 3, 4, 5);
Map<String, Integer> userAges = new HashMap<>();
userAges.put("张三", 18); // 自动装箱场景 2:泛型参数
java
// ✅ 泛型 T 必须是引用类型
public <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
max(10, 20); // 自动装箱为 Integer
max("abc", "def"); // String 本身就是引用类型场景 3:数据库 / JSON 字段
java
// 数据库字段可能是 NULL
public class User {
private Long id; // ✅ 用 Long,因为 id 可能为 null(未分配)
private int age; // ❌ 用 int 不行,因为 age 不可能为 null
}场景 4:方法参数允许 null
java
// ✅ 用包装类,调用方可以传 null 表示"不传"
public void setAge(Integer age) {
if (age != null) {
this.age = age;
}
}
setAge(null); // 不修改年龄
setAge(18); // 修改为 18八、完整代码示例
java
/**
* 自动装箱与拆箱综合示例
*
* 配套文档:第 6 章 自动装箱与拆箱
* 运行方式:javac BoxingDemo.java && java BoxingDemo
*/
public class BoxingDemo {
public static void main(String[] args) {
// ============ 1. 自动装箱 ============
Integer boxed = 100; // ① 自动装箱:Integer.valueOf(100)
Long longBoxed = 100L; // ② Long.valueOf(100L)
Double doubleBoxed = 3.14; // ③ Double.valueOf(3.14)
// ============ 2. 自动拆箱 ============
int unboxed = boxed; // ④ 自动拆箱:boxed.intValue()
long longUnboxed = longBoxed; // ⑤ longBoxed.longValue()
double doubleUnboxed = doubleBoxed; // ⑥ doubleBoxed.doubleValue()
// ============ 3. 缓存池测试(核心面试题)============
Integer a = 100;
Integer b = 100;
System.out.println("a == b (100): " + (a == b)); // true(缓存池内)
Integer c = 200;
Integer d = 200;
System.out.println("c == d (200): " + (c == d)); // false(缓存池外)
System.out.println("c.equals(d) (200): " + c.equals(d)); // true(值相等)
// ============ 4. 包装类参与运算 ============
Integer x = 10;
Integer y = 20;
int sum = x + y; // ⑦ 自动拆箱 + 装箱(x.intValue() + y.intValue())
System.out.println("x + y = " + sum);
// ============ 5. 警惕 NPE ============
Integer nullable = null;
try {
int crash = nullable; // ⑧ 拆箱时 NPE
} catch (NullPointerException e) {
System.out.println("NPE: 自动拆箱 null 会抛出异常");
}
// ============ 6. 类型转换方法 ============
int parsedInt = Integer.parseInt("123"); // ⑨ String → int
double parsedDouble = Double.parseDouble("3.14");
String intStr = Integer.toString(456); // ⑩ int → String
System.out.println("parseInt('123') = " + parsedInt);
System.out.println("toString(456) = " + intStr);
// ============ 7. 进制转换 ============
System.out.println("\n=== 进制转换 ===");
System.out.println("10 → binary: " + Integer.toBinaryString(10)); // 1010
System.out.println("255 → hex: " + Integer.toHexString(255)); // ff
System.out.println("64 → octal: " + Integer.toOctalString(64)); // 100
// ============ 8. 集合中的自动装箱 ============
java.util.List<Integer> numbers = new java.util.ArrayList<>();
numbers.add(1); // ⑪ 自动装箱
numbers.add(2);
numbers.add(3);
int first = numbers.get(0); // ⑫ 自动拆箱
System.out.println("\n=== 集合 ===");
System.out.println("numbers = " + numbers);
System.out.println("first = " + first);
// ============ 9. 性能对比 ============
System.out.println("\n=== 性能对比(1 亿次循环)===");
long start, end;
// ❌ 慢:用 Long
Long sumLong = 0L;
start = System.currentTimeMillis();
for (long i = 0; i < 100_000_000L; i++) {
sumLong += i; // 每次循环都装箱拆箱
}
end = System.currentTimeMillis();
System.out.println("Long 累加耗时: " + (end - start) + " ms");
// ✅ 快:用 long
long sumlong = 0L;
start = System.currentTimeMillis();
for (long i = 0; i < 100_000_000L; i++) {
sumlong += i; // 纯基本类型运算
}
end = System.currentTimeMillis();
System.out.println("long 累加耗时: " + (end - start) + " ms");
}
}运行结果:
a == b (100): true
c == d (200): false
c.equals(d) (200): true
x + y = 30
NPE: 自动拆箱 null 会抛出异常
parseInt('123') = 123
toString(456) = 456
=== 进制转换 ===
10 → binary: 1010
255 → hex: ff
64 → octal: 100
=== 集合 ===
numbers = [1, 2, 3]
first = 1
=== 性能对比(1 亿次循环)===
Long 累加耗时: 4500 ms
long 累加耗时: 350 ms性能差距约 13 倍,在性能敏感场景必须用基本类型。
九、常见面试题
Q1: Integer 缓存池的范围能改吗?
可以,通过 JVM 参数:
bash
# 把 Integer 缓存池范围改为 -128 ~ 500
java -XX:AutoBoxCacheMax=500 BoxingDemo但生产环境不要改,这是 JVM 内部优化,调整会影响性能。
Q2: 为什么要有包装类?
- 泛型需要:泛型参数必须是引用类型
- 集合需要:
List<int>非法,必须List<Integer> - null 语义:包装类可以为 null,表示"无值"
- 面向对象统一:所有类型都继承自 Object
Q3: 包装类是值类型还是引用类型?
引用类型。Integer a = 100 创建的是一个对象。
Q4: 包装类用 == 比较有什么风险?
- 缓存池范围内(-128~127):可能相等
- 缓存池范围外:永远不等
- 一边包装类一边基本类型:自动拆箱比较值
结论:包装类永远用 equals!
十、本章小结
| 要点 | 关键 |
|---|---|
| 包装类的作用 | 把基本类型包装成对象,用于泛型、集合、可空场景 |
| 自动装箱 | 基本类型 → 包装类(编译器自动调用 valueOf) |
| 自动拆箱 | 包装类 → 基本类型(编译器自动调用 xxxValue) |
| 缓存池 | Integer/Long/Character 缓存 -128~127,Boolean 只有两个常量 |
== 陷阱 | 缓存池外用 == 比较永远是 false |
| NPE 陷阱 | 包装类为 null 时拆箱会 NPE |
| 性能陷阱 | 大量循环用基本类型,不要用包装类 |
动手练习
练习 1:基础题
java
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // ?
System.out.println(a.equals(b)); // ?
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // ?
System.out.println(c.equals(d)); // ?
// 你的预期答案是什么?实际跑一下看对不对练习 2:NPE 修复
java
public class UserService {
public int getAgeOrDefault(Integer age) {
return age == null ? 0 : age;
}
public int getAgeOrDefaultBroken(Integer age) {
return age; // ❌ 如果 age 是 null 会怎样?
}
}修改 getAgeOrDefaultBroken 让它安全处理 null。
练习 3:性能优化
把下面的代码从用 Integer 改成用 int,预测性能提升多少:
java
public long sumTo(int n) {
Integer sum = 0;
for (Integer i = 1; i <= n; i++) {
sum += i;
}
return sum;
}推荐阅读
- 📖 《Effective Java》第 6 章 — Item 61 优先使用基本类型
- 📖 《Java 核心技术》卷 1 第 5.2 节 — 自动装箱详解
- 🌐 IntegerCache 源码 — JDK 源码
下一章:第 7 章:运算符详解 →
你已经完成了:
- ✅ 理解包装类的本质和必要性
- ✅ 掌握自动装箱与拆箱机制
- ✅ 警惕
==vsequals的陷阱 - ✅ 了解 IntegerCache 缓存池设计
- ✅ 避免 NPE 和性能陷阱