包装类、自动装箱与自动拆箱 | JavaSE

包装类、自动装箱与自动拆箱

一、学习目标

完成本章后,你应该能够:

  • 能够说明 Java 为什么为 8 种基本数据类型提供对应的包装类。
  • 能够写出 8 种基本类型与包装类型之间的对应关系。
  • 能够解释自动装箱(Autoboxing)和自动拆箱(Unboxing)。
  • 能够正确使用 Integer.valueOf()Integer.parseInt()Integer.toString() 等常用 API。
  • 能够解释为什么泛型中可以写 Integer,却不能直接写 int
  • 能够判断包装类参与运算时是否发生了自动拆箱。
  • 能够识别 null 自动拆箱导致的 NullPointerException
  • 能够理解包装类对象比较时 ==equals() 的区别。
  • 能够避免依赖 Integer 缓存进行数值比较。

二、核心知识

2.1 基本数据类型与对象世界

Java 中已经学习过 8 种基本数据类型:

byte
short
int
long
float
double
char
boolean

例如:

int age = 18;
double score = 95.5;
boolean passed = true;

这些都是:

基本数据类型(Primitive Type)

而 Java 又是一门高度面向对象的语言,大量 API、集合、泛型机制主要围绕:

引用数据类型(Reference Type)

工作。

例如后面会大量使用:

ArrayList<String>
ArrayList<Student>

但是下面这种代码是不允许的:

// ArrayList<int> list = new ArrayList<>();

原因在于:

Java 泛型的类型实参不能直接使用基本数据类型。

那么问题来了:

如果我就是想把整数放进泛型集合怎么办?

Java 的解决方案就是:

包装类(Wrapper Class)

2.2 什么是包装类

包装类可以把基本类型的数据表示为对应的对象类型。

8 种基本数据类型都有对应的包装类:

| 基本类型 | 包装类 | | --------- | ----------- | | byte | Byte | | short | Short | | int | Integer | | long | Long | | float | Float | | double | Double | | char | Character | | boolean | Boolean |

其中要特别注意两个命名:

int  → Integer
char → Character

并不是简单首字母大写。


2.3 包装类解决了什么问题

假设我们要保存多个整数:

ArrayList<Integer> numbers =
        new ArrayList<>();

这里泛型参数使用:

Integer

而不是:

int

因为:

Integer 是引用类型
int 是基本类型

于是包装类就在:

基本数据类型
        ↕
面向对象 / 泛型 / 集合体系

之间建立了一座桥梁。


2.4 手动装箱

过去可以显式创建 Integer 对象:

Integer number = Integer.valueOf(100);

这里:

int 100
    ↓
Integer 对象

就是一种:

装箱(Boxing)

过程。

在 JDK 21 中,推荐使用:

Integer.valueOf(100);

而不是:

// 不作为现代 Java 主方案
// Integer number = new Integer(100);

现代 Java 中通常甚至不需要手动调用 valueOf(),因为语言已经支持:

自动装箱

2.5 自动装箱

自动装箱(Autoboxing)表示:

Java 可以在需要包装类型的上下文中,自动把基本类型转换成对应的包装类型。

例如:

Integer number = 100;

左边需要:

Integer

右边:

100

本来是:

int

Java 自动完成:

int
 ↓
Integer

所以可以近似理解成:

Integer number = Integer.valueOf(100);

2.6 自动拆箱

自动拆箱(Unboxing)正好相反:

Java 可以把包装类对象自动转换成对应的基本类型值。

例如:

Integer number = 100;

int value = number;

这里:

Integer
   ↓
int

发生了自动拆箱。

概念上可以理解成:

int value = number.intValue();

2.7 装箱和拆箱对应关系

int
 │
 │ 自动装箱
 ▼
Integer
 │
 │ 自动拆箱
 ▼
int

其他包装类同理:

double  ↔ Double
boolean ↔ Boolean
char    ↔ Character
long    ↔ Long

2.8 包装类与集合

这是包装类最经典的应用场景之一。

import java.util.ArrayList;

public class WrapperDemo {
    public static void main(String[] args) {
        ArrayList<Integer> numbers =
                new ArrayList<>();

        numbers.add(100);
        numbers.add(200);
        numbers.add(300);

        int number = numbers.get(0);

        System.out.println(number);
    }
}

看起来:

numbers.add(100);

传入的是 int

实际上集合要求:

Integer

于是发生:

100
↓ 自动装箱
Integer

而:

int number = numbers.get(0);

get() 得到:

Integer

赋值给:

int

于是发生:

Integer
↓ 自动拆箱
int

完整过程:

int
 ↓ boxing
Integer
 ↓ 存入集合
Integer
 ↓ 从集合取出
int
 ↓ unboxing
基本类型继续计算

2.9 包装类也可以参与运算

例如:

Integer a = 10;
Integer b = 20;

int result = a + b;

System.out.println(result);

+ 是数值运算。

因此 ab 会参与拆箱:

Integer a
 ↓
int 10

Integer b
 ↓
int 20

10 + 20
 ↓
30

甚至可以写:

Integer number = 10;

number++;

这背后会涉及:

拆箱
↓
数值运算
↓
再次装箱

因此包装类虽然写起来很像基本类型,但:

包装类依然是引用类型,不能忘记它本质上是对象。


三、使用方法

3.1 基本类型转字符串

例如:

int number = 23;

想得到:

"23"

可以:

String text =
        Integer.toString(number);

结果:

"23"

3.2 String.valueOf()

实际开发中还经常使用:

String text =
        String.valueOf(number);

例如:

int age = 18;

String result =
        String.valueOf(age);

System.out.println(result);

3.3 字符串转 int

这是非常重要的 API。

假设用户输入:

String text = "98";

虽然看起来像数字,

但类型仍然是:

String

不能直接:

// int result = text;

可以使用:

int result =
        Integer.parseInt(text);

结果:

98

3.4 parseInt() 与 valueOf()

下面两个 API 很容易混淆:

Integer.parseInt("98");

返回:

int

而:

Integer.valueOf("98");

返回:

Integer

对比:

| API | 返回类型 | | ------------------------ | --------- | | Integer.parseInt("98") | int | | Integer.valueOf("98") | Integer |

例如:

int a = Integer.parseInt("98");

Integer b = Integer.valueOf("98");

如果目标就是:

得到基本类型 int

parseInt() 语义更加直接。


3.5 Double 同样支持转换

例如:

String text = "98.8";

double value =
        Double.parseDouble(text);

System.out.println(value + 2);

结果:

100.8

也可以:

Double value =
        Double.valueOf(text);

返回包装对象。


3.6 非法数字字符串

下面代码:

int number =
        Integer.parseInt("Java");

无法把:

Java

解释成整数。

于是运行时会抛出:

NumberFormatException

这正好和前面异常章节连接起来。

因此:

String → 数值

并不是无条件成功。

实际程序需要考虑:

输入是否合法?

四、原理与进阶

4.1 null 自动拆箱的危险

这是包装类非常重要的坑。

Integer number = null;

int value = number;

表面上只是:

Integer → int

但拆箱本质类似:

number.intValue();

而:

number == null

因此相当于:

在 null 上调用实例方法

会产生:

NullPointerException

所以:

Integer count = null;

if (count > 0) {
}

同样可能出问题。

为什么?

因为比较:

count > 0

需要先把 Integer 拆成 int


4.2 基本类型和包装类型默认值不同

成员变量中:

private int age;

默认值:

0

而:

private Integer age;

默认值:

null

所以在业务模型中:

int

和:

Integer

有时表达不同语义。

例如:

int age = 0

只能表达:

年龄值为 0

而:

Integer age = null

还可以额外表达:

年龄尚未提供

这也是以后数据库实体类、DTO、JSON 映射中经常使用包装类型的重要原因之一。


4.3 Integer 对象不要用 == 比数值

考虑:

Integer a = 100;
Integer b = 100;

System.out.println(a == b);

可能得到:

true

再:

Integer c = 130;
Integer d = 130;

System.out.println(c == d);

常见 JDK 实现中会得到:

false

为什么?

因为:

==

如果两边都是引用类型,比较的是:

引用是否指向同一个对象

不是单纯比较数值。


4.4 Integer 缓存

Java 对部分常用装箱值存在对象复用规则。

对于一定范围内的常量装箱值,Java 规范保证重复装箱得到相同引用。

所以:

Integer a = 100;
Integer b = 100;

可能共享包装对象。

而对于:

Integer a = 130;
Integer b = 130;

不能依赖:

a == b

判断数值是否相等。

正确思路:

a.equals(b)

或者如果已经拆箱:

a.intValue() == b.intValue()

核心规则是:

包装类比较数值时,不要把对象身份比较 == 当成数值比较。


4.5 为什么 valueOf() 比构造器更合适

例如:

Integer.valueOf(100);

允许 Java:

复用已有包装对象

而直接创建一个全新包装对象会失去这种优化空间。

因此现代 Java API 已经明确引导开发者使用:

valueOf()
自动装箱

而不是手动调用 Integer 构造器。


4.6 自动装箱并没有把 int 变成“同一种类型”

下面:

int a = 10;
Integer b = 10;

虽然 Java 可以自动转换,

但是:

int

和:

Integer

仍然是两个不同类型:

int      → primitive type
Integer  → reference type

自动装箱只是:

语言提供了自动转换能力

并不是:

基本类型与包装类从此完全没有区别

五、实践应用

5.1 集合中存储数字

ArrayList<Integer> scores =
        new ArrayList<>();

scores.add(90);
scores.add(95);
scores.add(88);

这是以后非常常见的写法。


5.2 接收文本参数

Web、控制台、文件中的数据经常最初表现为:

String

例如:

"18"
"99.5"

业务计算前需要:

String
 ↓ parse
int / double

例如:

int age =
        Integer.parseInt("18");

double score =
        Double.parseDouble("99.5");

5.3 可空业务字段

例如用户注册时:

age 没有填写

如果使用:

Integer age;

可以用:

null

表达:

未填写

这与:

0

是不同业务状态。


六、常见问题

6.1 为什么泛型不能写 int?

因为泛型类型参数使用的是引用类型体系。

所以:

ArrayList<int>

不合法。

应该:

ArrayList<Integer>

6.2 自动装箱是不是必须自己调用 valueOf()?

不是。

例如:

Integer value = 100;

Java 会自动执行装箱转换。


6.3 自动拆箱是不是必须调用 intValue()?

通常不需要。

例如:

Integer value = 100;

int number = value;

会自动拆箱。


6.4 Integer 可以为 null 吗?

可以。

因为:

Integer 是引用类型

所以:

Integer number = null;

合法。


6.5 int 可以为 null 吗?

不可以。

// int number = null;

不能通过编译。


6.6 为什么 null 拆箱会出现 NullPointerException?

因为拆箱需要从包装对象中取得基本类型值。

例如:

Integer → int

本质需要访问这个对象。

如果引用为:

null

自然无法取得对应值。


6.7 Integer.parseInt() 和 Integer.valueOf() 有什么区别?

核心区别:

parseInt()
→ int

valueOf()
→ Integer

6.8 为什么 Integer == 比较很危险?

因为 Integer 是对象。

==

可能变成:

对象引用比较

而且包装对象还存在缓存行为。

因此数值相等通常使用:

a.equals(b)

6.9 可以继续使用 new Integer(...) 吗?

在现代 Java 中不应该把它作为主方案。

优先:

Integer.valueOf(...)

或者直接:

Integer number = 100;

七、练习与验收

7.1 知识问答

  1. 为什么 Java 为基本类型设计包装类?
  2. int 对应什么包装类?
  3. char 对应什么包装类?
  4. 8 种基本类型分别对应哪些包装类?
  5. 什么是自动装箱?
  6. 什么是自动拆箱?
  7. Integer number = 100; 发生了什么?
  8. int number = integer; 发生了什么?
  9. 为什么 ArrayList<int> 不合法?
  10. Integer.parseInt() 返回什么类型?
  11. Integer.valueOf() 返回什么类型?
  12. Integer.toString() 解决什么问题?
  13. 非法字符串执行 parseInt() 可能产生什么异常?
  14. 为什么包装类可以保存 null
  15. 为什么 null 自动拆箱可能产生 NPE?
  16. 为什么不能依赖 Integer== 比较数值?

7.2 代码阅读

阅读:

public class WrapperRead01 {
    public static void main(String[] args) {
        Integer a = 20;
        int b = a;

        Integer c = b + 10;

        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
    }
}

回答:

  1. Integer a = 20 发生什么转换?
  2. int b = a 发生什么转换?
  3. b + 10 的结果是什么类型?
  4. 赋给 c 时又发生什么?

阅读:

public class WrapperRead02 {
    public static void main(String[] args) {
        Integer count = null;

        if (count > 0) {
            System.out.println("positive");
        }
    }
}

不运行程序回答:

  1. 代码能否通过编译?
  2. 比较前需要发生什么?
  3. 运行时可能产生什么异常?
  4. 根本原因是什么?

7.3 手写代码

任务一:基本类型与字符串转换

给定:

int number = 98;

要求:

  • 使用 Integer.toString() 转成字符串。
  • 使用 String.valueOf() 再实现一次。
  • 再将 "123" 转换回 int

任务二:包装类集合

创建:

ArrayList<Integer>

保存:

90
85
100
95

遍历并计算总分。

观察:

add 时发生什么
get 后参与加法时发生什么

任务三:parseInt 与 valueOf

分别使用:

Integer.parseInt("100")
Integer.valueOf("100")

保存返回值。

要求写出两个变量的准确类型。


7.4 Debug

import java.util.ArrayList;

public class WrapperDebug {
    public static void main(String[] args) {
        // ArrayList<int> list =
        //         new ArrayList<>();

        Integer count = null;

        int value = count;

        System.out.println(value);
    }
}

要求:

  1. 为什么 ArrayList<int> 不合法?
  2. Integer count = null 是否合法?
  3. int value = count 会发生什么?
  4. 问题发生在编译期还是运行期?
  5. 应该如何根据业务语义修复?

再分析:

Integer a = 130;
Integer b = 130;

if (a == b) {
    System.out.println("相等");
}

回答:

  1. a == b 比较的到底是什么?
  2. 为什么这不是可靠的数值比较方法?
  3. 应该如何改写?

7.5 综合训练

实现一个成绩录入程序。

原始数据:

String[] scores = {
        "90",
        "88",
        "100",
        "95"
};

要求:

  1. 将每个字符串转换成整数。
  2. 保存到 ArrayList<Integer>
  3. 计算总分。
  4. 计算平均分。
  5. 输出最高分。
  6. 分析其中发生了哪些:
    • 字符串转换
    • 自动装箱
    • 自动拆箱
  7. 修改其中一个字符串为 "Java",观察异常并说明原因。

7.6 本章验收

  • [ ] 能闭卷写出 8 种基本类型与包装类对应关系。
  • [ ] 能解释自动装箱与自动拆箱。
  • [ ] 能独立使用 Integer.parseInt()
  • [ ] 能独立使用 Integer.valueOf()
  • [ ] 能解释二者返回类型的区别。
  • [ ] 能解释为什么泛型使用 Integer 而不是 int
  • [ ] 能发现 null 拆箱导致的 NPE。
  • [ ] 能解释 Integer 的 == 为什么不是可靠的数值比较。
  • [ ] 能独立完成 String → int → Integer 集合 → int 运算的完整流程。