首页 / Java 入门教程 / 包装类与自动装箱:基本类型的对象壳

Java 入门教程

包装类与自动装箱:基本类型的对象壳

本教程共 100 篇 · 第 58 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

JavaJava 入门教程包装类自动装箱Integer

58. 包装类与自动装箱

本节目标:理解 8 种基本类型为什么要穿一层对象外套,自动装箱拆箱背后做了什么,以及 Integer 缓存带来的相等陷阱。

Java 有 8 种基本类型:byteshortintlongfloatdoublecharboolean。它们又快又省,但有三个硬伤:不是对象、不能为 null、进不了集合和泛型。

为了补上这三块,Java 给每个基本类型都配了一个包装类。

八兄弟对照表

基本类型包装类默认值取值范围
byteByte0-128 ~ 127
shortShort0-32768 ~ 32767
intInteger0约 ±21 亿
longLong0L约 ±9.2×10¹⁸
floatFloat0.0f单精度浮点
doubleDouble0.0d双精度浮点
charCharacter’\u0000’0 ~ 65535
booleanBooleanfalsetrue / false

包装类的默认值全是 null,这点和基本类型完全不同——表里的默认值说的是基本类型。这个差异是空指针异常的常见来源。

前六个数值类都继承自抽象类 Number,所以互相转换有统一方法:intValue()longValue()doubleValue()CharacterBoolean 直接继承 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 缓存:小数字的特殊待遇

### 127128 的分水岭

`Integer.valueOf` 有个心机:**-128127 之间的值直接从缓存数组里取**,同一个数字永远返回同一个对象。超出这个范围才新建。于是出现了教科书级的反直觉现象:

```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,结果就翻了个面。这个坑之所以致命,是因为测试时用小数字全都对,上线跑真实数据就错——而且错得毫无规律。

其他包装类的缓存范围

  • ByteShortLong:同样缓存 -128 ~ 127。
  • Character:缓存 0 ~ 127。
  • Boolean:只有两个值,TRUEFALSE 永远是同一对象。
  • FloatDouble没有缓存,每次装箱都是新对象,== 一律为 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,当对象用就 valueOfvalueOf(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`。字段可空、泛型、集合里用包装类,纯计算用基本类型。