包装类与自动装箱:基本类型的对象壳
本教程共 100 篇 · 第 58 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
58. 包装类与自动装箱
本节目标:理解 8 种基本类型为什么要穿一层对象外套,自动装箱拆箱背后做了什么,以及 Integer 缓存带来的相等陷阱。
Java 有 8 种基本类型:byte、short、int、long、float、double、char、boolean。它们又快又省,但有三个硬伤:不是对象、不能为 null、进不了集合和泛型。
为了补上这三块,Java 给每个基本类型都配了一个包装类。
八兄弟对照表
| 基本类型 | 包装类 | 默认值 | 取值范围 |
|---|---|---|---|
byte | Byte | 0 | -128 ~ 127 |
short | Short | 0 | -32768 ~ 32767 |
int | Integer | 0 | 约 ±21 亿 |
long | Long | 0L | 约 ±9.2×10¹⁸ |
float | Float | 0.0f | 单精度浮点 |
double | Double | 0.0d | 双精度浮点 |
char | Character | ’\u0000’ | 0 ~ 65535 |
boolean | Boolean | false | true / false |
包装类的默认值全是 null,这点和基本类型完全不同——表里的默认值说的是基本类型。这个差异是空指针异常的常见来源。
前六个数值类都继承自抽象类 Number,所以互相转换有统一方法:intValue()、longValue()、doubleValue()。Character 和 Boolean 直接继承 Object,没有这套。
包装类还顺带带来一堆常量和工具方法,这些是基本类型给不了的:
System.out.println(Integer.MAX_VALUE); // 2147483647
System.out.println(Integer.MIN_VALUE); // -2147483648
System.out.println(Integer.SIZE); // 32,占多少位
System.out.println(Double.MAX_VALUE);
```bash
写边界判断时直接引用这些常量,比自己硬写数字可靠。
## 自动装箱:编译器帮你转
把基本类型塞进集合,现代 Java 不用手写转换:
```java
List<Integer> list = new ArrayList<>();
list.add(100); // 自动装箱:编译成 Integer.valueOf(100)
int n = list.get(0); // 自动拆箱:编译成 list.get(0).intValue()
自动装箱是编译器悄悄把数字包成对象,自动拆箱是反过来把对象剥回数字。你写起来像在直接操作数字,中间其实插了一层转换。这个特性 Java 5 才有,之前必须全部手写。
方便归方便,两个代价要知道:
- 有性能开销。循环里反复装箱会产生大量对象。计数器、循环变量这类高频场景,坚持用基本类型。
- 拆箱可能抛空指针。这是最要命的一条。
Warning包装类可以是 null,拆箱时会调用它的方法,于是直接炸:
Integer x = null; int y = x; // NullPointerException,因为等价于 x.intValue()更隐蔽的版本是三元运算符和 Map 取值:
map.get("k")取不到键时返回 null,赋给 int 立刻空指针。拆箱前先判空,或者用Optional。
别再用 new Integer
早期写法是 new Integer(100),从 Java 9 起被标记废弃,Java 16 起编译会直接警告。正确写法是静态工厂方法:
Integer a = Integer.valueOf(100); // 推荐
Integer b = 100; // 更推荐,自动装箱等价于上一行
```bash
原因和字符串常量池类似:`valueOf` 会优先复用缓存里的对象,`new` 每次硬造一个新对象,白白占内存又拖慢速度。
> [!NOTE]
> 历史兼容:老代码里满眼 `new Integer(...)`、`new Long(...)`、`new Boolean(...)`,那是 Java 5 之前的写法,不推荐。今天一律换成 `valueOf` 或直接依赖自动装箱。
## Integer 缓存:小数字的特殊待遇
### 127 和 128 的分水岭
`Integer.valueOf` 有个心机:**-128 到 127 之间的值直接从缓存数组里取**,同一个数字永远返回同一个对象。超出这个范围才新建。于是出现了教科书级的反直觉现象:
```java
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true,都是缓存里那一个对象
Integer x = 128;
Integer y = 128;
System.out.println(x == y); // false!超出缓存,是两个不同对象
同样的代码,只把 127 改成 128,结果就翻了个面。这个坑之所以致命,是因为测试时用小数字全都对,上线跑真实数据就错——而且错得毫无规律。
其他包装类的缓存范围
Byte、Short、Long:同样缓存 -128 ~ 127。Character:缓存 0 ~ 127。Boolean:只有两个值,TRUE和FALSE永远是同一对象。Float、Double:没有缓存,每次装箱都是新对象,==一律为 false。
Integer 的缓存上界还能通过 JVM 参数调大,但下界固定是 -128。既然它可配置,就更不能依赖 == 的结果了。
比相等要用 equals
规则很简单:包装类比数值一律用 equals。
Integer x = 128, y = 128;
System.out.println(x.equals(y)); // true,比的是数值
```java
也可以强制拆箱后比:`x.intValue() == y.intValue()`,或者干脆声明成 `int`。但 `equals` 最直白。
`equals` 有个细节容易忽略:它要求**类型也相同**。
```java
Integer i = 100;
Long l = 100L;
System.out.println(i.equals(l)); // false!类型不同,直接返回 false
数值明明一样却返回 false,因为 Integer.equals 第一步就检查参数是不是 Integer。跨类型比较请先统一类型,或者拆箱后用 ==。
要按大小排序或比较,用 compareTo,返回负数、0、正数:
System.out.println(Integer.valueOf(10).compareTo(20)); // -1,表示小于
```bash
## valueOf 与 parseXxx 怎么选
这两个名字很像,返回值不同:
| 方法 | 返回类型 | 用途 |
|------|----------|------|
| `Integer.parseInt("123")` | `int`(基本类型) | 只要数值参与计算 |
| `Integer.valueOf("123")` | `Integer`(对象) | 要放进集合、要可空 |
| `Integer.valueOf(123)` | `Integer`(对象) | 基本类型装箱,走缓存 |
| `Integer.toString(123)` | `String` | 数字转字符串 |
```java
int n = Integer.parseInt("123"); // 拿去算数
Integer boxed = Integer.valueOf("123"); // 塞进 List
String s = Integer.toString(123);
选择原则一句话:接下来当数字用就 parseInt,当对象用就 valueOf。valueOf(String) 内部也是先调 parseInt 再装箱,多绕一步。
两者遇到非法字符串都抛 NumberFormatException,处理用户输入必须防。
按进制转换也很常用:
System.out.println(Integer.toString(255, 16)); // ff
System.out.println(Integer.parseInt("ff", 16)); // 255
System.out.println(Integer.toBinaryString(10)); // 1010
```bash
## 无符号与浮点比较
处理网络字节、二进制协议时经常需要无符号视角。Java 没有无符号类型,但包装类提供了辅助方法:
```java
byte b = (byte) 0xFF;
System.out.println(b); // -1,按有符号解读
System.out.println(Byte.toUnsignedInt(b)); // 255,按无符号解读
浮点包装类的比较更要小心。Double.compare 能正确处理 NaN 和正负零这些特殊值,直接用 == 会翻车:
System.out.println(0.1 + 0.2 == 0.3); // false!
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(Double.NaN == Double.NaN); // false,NaN 不等于自己
System.out.println(Double.valueOf(Double.NaN).equals(Double.NaN)); // true
```bash
第 1 行是浮点二进制表示的固有误差,和 Java 无关,所有语言都这样。金额计算绝不能用 `Float` 或 `Double`,请用下一章讲的 `BigDecimal`。
## 什么时候必须用包装类
一个原则:**要放进集合、要用泛型、或者值可能为空,就用包装类**。
数据库里的空值映射到 Java 就是 null,`int` 装不下 null 只能被迫写 0,而「0 分」和「没考试」是两回事。这正是实体类字段推荐用 `Integer` 而不是 `int` 的原因。
反过来,方法内部的临时计算、循环变量、数组下标,一律用基本类型,省掉装箱开销。
> [!TIP]
> 别一股脑全用包装类。一个跑一亿次的循环,把 `int` 写成 `Integer` 会产生上亿个临时对象,性能差几十倍。该省的地方省,该可空的地方才包装。
## 常见疑问
**问:为什么两个 Integer 相加不用 equals?**
`x + y` 会先自动拆箱成 int 再相加,算术运算本来就作用在数值上。只有比较相等时 `==` 才有歧义。
**问:包装类是不可变的吗?**
是。所有包装类都是 final 类,内部的值字段也是 final。所以它们能安全共享,缓存机制才成立。
**问:`Integer i = 1; i++;` 是怎么执行的?**
先拆箱成 int,加一,再装箱回一个新的 Integer 对象赋给 i。原来那个对象没变。看着是一行简单代码,实际有两次转换。
## 小结
8 种基本类型各配一个包装类,靠自动装箱拆箱在数值和对象间来回切换。主线用 `valueOf` 或直接自动装箱,`new Integer` 属于废弃写法。`Integer.valueOf` 缓存 -128 ~ 127,超出范围 `==` 会返回 false,所以比数值一律用 `equals`,且注意 `equals` 要求类型一致。要数值用 `parseInt`,要对象用 `valueOf`。字段可空、泛型、集合里用包装类,纯计算用基本类型。