Skip to content
第 6 / 250 章后端⏱ 10 分钟阅读

第 6 章:自动装箱与拆箱

学习目标

  • 理解基本类型与包装类的关系
  • 掌握自动装箱(Autoboxing)和拆箱(Unboxing)机制
  • 警惕 == vs equals 在包装类的陷阱
  • 了解 IntegerCache 缓存池设计

一、为什么需要包装类?

Java 是面向对象语言,但 8 种基本类型不是对象。这导致一个矛盾:

java
// ❌ 基本类型不能放进 Object[] / List
List<int> numbers = new ArrayList<int>();  // 编译报错
Object[] objects = {1, 2, 3};              // 编译报错

为了解决这个矛盾,Java 为每个基本类型设计了对应的包装类(Wrapper Class):

基本类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean
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 ~ 127Integer.IntegerCache.high
Long-128 ~ 127Long.LongCache
Character0 ~ 127Character.CharacterCache
BooleanTRUE / 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 是 null

NPE(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: 为什么要有包装类?

  1. 泛型需要:泛型参数必须是引用类型
  2. 集合需要List<int> 非法,必须 List<Integer>
  3. null 语义:包装类可以为 null,表示"无值"
  4. 面向对象统一:所有类型都继承自 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;
}

推荐阅读


下一章第 7 章:运算符详解

你已经完成了

  • ✅ 理解包装类的本质和必要性
  • ✅ 掌握自动装箱与拆箱机制
  • ✅ 警惕 == vs equals 的陷阱
  • ✅ 了解 IntegerCache 缓存池设计
  • ✅ 避免 NPE 和性能陷阱

本站基于 VitePress 构建 · 由 Codebook 团队维护