包装类:基本类型的对象化
本章定位:Java 为每种基本类型提供了对应的包装类,实现"万物皆对象"。自动装箱/拆箱让代码更简洁,但也隐藏了缓存机制和空指针的陷阱——这正是面试中的重灾区。
小崔的诡异 Bug
小崔在飞翔科技的薪资系统中写了这样一段代码:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true ✓ 符合预期
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false ??? 什么鬼!
"同样是 Integer,为什么 100 相等,200 就不等了?"小崔怀疑自己产生了幻觉。
白歌微微一笑:"这是 Integer 的缓存机制。来,我们深入源码看看。"
八种包装类速览
| 基本类型 | 包装类 | 大小 | 缓存范围 |
|---|---|---|---|
byte | Byte | 8 bit | -128 ~ 127(全部) |
short | Short | 16 bit | -128 ~ 127 |
int | Integer | 32 bit | -128 ~ 127(可配置上限) |
long | Long | 64 bit | -128 ~ 127 |
float | Float | 32 bit | 无缓存 |
double | Double | 64 bit | 无缓存 |
char | Character | 16 bit | 0 ~ 127(ASCII) |
boolean | Boolean | — | TRUE / FALSE(两个常量) |
六种数值型包装类(Integer、Long、Short、Byte、Float、Double)都继承自抽象类
Number。
自动装箱(Autoboxing)与拆箱(Unboxing)
定义
| 概念 | 方向 | 编译后等价调用 | 触发时机 |
|---|---|---|---|
| 装箱 | 基本类型 → 包装对象 | Integer.valueOf(int) | 赋值给包装类型、方法参数、集合操作 |
| 拆箱 | 包装对象 → 基本类型 | Integer.intValue() | 赋值给基本类型、算术运算、比较运算 |
字节码验证
// 源码
Integer i = 100; // 装箱
int j = i; // 拆箱
int k = i + 1; // 先拆箱,再运算,再装箱(如果赋值给 Integer)
// 编译后等价代码
Integer i = Integer.valueOf(100);
int j = i.intValue();
int k = i.intValue() + 1;
Integer 缓存机制(深度原理)
源码分析
// Integer.IntegerCache 内部类(JDK 8 源码简化版)
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
int h = 127; // 默认上限
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
h = Math.min(Integer.parseInt(integerCacheHighPropValue), Integer.MAX_VALUE - (-low) -1);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for (int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++); // 预先创建 -128 ~ 127 的所有 Integer 对象
}
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]; // 从缓存取
return new Integer(i); // 超出范围才 new
}
缓存行为图解
可自定义上限:启动 JVM 时添加
-Djava.lang.Integer.IntegerCache.high=512,可将缓存上限扩展到 512。但下限 -128 不可改变。
完整示例一:装箱拆箱与缓存陷阱
场景
小崔写了一个薪资等级判断函数,用来比较员工的级别系数。
代码
public class WrapperTrapDemo {
public static void main(String[] args) {
// ========== 1. 缓存范围内的 == 比较 ==========
Integer a = 100; // Integer.valueOf(100) → 缓存
Integer b = 100; // Integer.valueOf(100) → 缓存(同一对象)
System.out.println("100 == 100: " + (a == b)); // true
// ========== 2. 超出缓存范围的 == 比较 ==========
Integer c = 200; // Integer.valueOf(200) → new Integer(200)
Integer d = 200; // Integer.valueOf(200) → 又 new Integer(200)
System.out.println("200 == 200: " + (c == d)); // false!
// ========== 3. equals() 始终比较数值 ==========
System.out.println("200 equals 200: " + c.equals(d)); // true
// ========== 4. 与基本类型比较 → 自动拆箱 ==========
int primitive200 = 200;
System.out.println("c == 200: " + (c == primitive200)); // true(拆箱后比数值)
// ========== 5. new Integer 始终不缓存 ==========
Integer e = new Integer(100);
Integer f = new Integer(100);
System.out.println("new 100 == new 100: " + (e == f)); // false
// ========== 6. 包装类的 null 陷阱 ==========
Integer salaryLevel = null;
// int level = salaryLevel; // 运行时 NPE!拆箱时 null.intValue()
int level = salaryLevel != null ? salaryLevel : 0; // 安全做法
System.out.println("安全处理 null: level = " + level);
}
}
运行输出
100 == 100: true
200 == 200: false
200 equals 200: true
c == 200: true
new 100 == new 100: false
安全处理 null: level = 0
parseXxx() 与 toString()
常用转换方法
| 方法 | 功能 | 示例 |
|---|---|---|
Integer.parseInt(String) | 字符串 → int | Integer.parseInt("42") → 42 |
Double.parseDouble(String) | 字符串 → double | Double.parseDouble("3.14") → 3.14 |
Boolean.parseBoolean(String) | 字符串 → boolean | Boolean.parseBoolean("true") → true |
Integer.toString(int) | int → 字符串 | Integer.toString(42) → "42" |
Integer.toBinaryString(int) | int → 二进制字符串 | Integer.toBinaryString(10) → "1010" |
Integer.toHexString(int) | int → 十六进制字符串 | Integer.toHexString(255) → "ff" |
Integer.valueOf(String) | 字符串 → Integer | Integer.valueOf("42") → Integer(42) |
parseInt返回基本类型int;valueOf返回包装对象Integer(会走缓存)。
完整示例二:飞翔科技薪资数据处理
场景
小崔从配置文件中读取薪资数据(字符串形式),需要解析为数值、比较范围、格式化输出。
代码
public class SalaryDataProcessor {
public static void main(String[] args) {
// ========== 1. 字符串解析为基本类型 ==========
String baseSalaryStr = "15000";
String bonusStr = "3.5"; // 3.5 个月年终奖
String stockOptionsStr = "2000";
int baseSalary = Integer.parseInt(baseSalaryStr);
double bonusMonths = Double.parseDouble(bonusStr);
long stockOptions = Long.parseLong(stockOptionsStr);
System.out.println("基本工资: " + baseSalary);
System.out.println("年终奖月数: " + bonusMonths);
System.out.println("期权数量: " + stockOptions);
// ========== 2. 数值比较(注意拆箱) ==========
Integer threshold1 = 10000;
Integer threshold2 = 20000;
// 直接用 > < 比较 → 自动拆箱为 int → 放心用
if (baseSalary > threshold1 && baseSalary < threshold2) {
System.out.println("薪资在合理范围内");
}
// ========== 3. equals 安全的比较 ==========
Integer salary1 = 15000;
Integer salary2 = 15000;
// ✅ 安全:用 equals 或 Objects.equals
System.out.println("薪资相等: " + salary1.equals(salary2));
System.out.println("(null安全): " + java.util.Objects.equals(salary1, salary2));
// ========== 4. 数字格式化输出 ==========
double totalAnnual = baseSalary * (12 + bonusMonths) + stockOptions;
System.out.println("年度总收入: " + String.format("%,.2f", totalAnnual));
// ========== 5. 进制转换(工号生成用) ==========
int employeeId = 255;
System.out.println("十进制: " + employeeId);
System.out.println("二进制: " + Integer.toBinaryString(employeeId));
System.out.println("十六进制: " + Integer.toHexString(employeeId).toUpperCase());
System.out.println("八进制: " + Integer.toOctalString(employeeId));
// ========== 6. 字符串 → Integer(valueOf 走缓存) ==========
Integer id1 = Integer.valueOf("100"); // 从缓存取
Integer id2 = Integer.valueOf("100"); // 同一个对象
System.out.println("valueOf 100 缓存: " + (id1 == id2)); // true
}
}
运行输出
基本工资: 15000
年终奖月数: 3.5
期权数量: 2000
薪资在合理范围内
薪资相等: true
(null安全): true
年度总收入: 234,500.00
十进制: 255
二进制: 11111111
十六进制: FF
八进制: 377
valueOf 100 缓存: true
包装类的不可变性
所有包装类都是 final 类,内部值由 private final 字段持有,一旦创建不可更改。
Integer x = 5;
x = x + 1; // 看似修改了 x,实际上创建了新的 Integer(6) 对象
任何算术操作都会产生新对象。这是和 int 最大的语义区别。
易错场景
❌ 反例一:用 == 比较两个包装类
// ❌ 错误:== 比较引用,超出缓存范围就出错
public boolean isSameLevel(Integer level1, Integer level2) {
return level1 == level2; // 缓存内碰巧对,缓存外错!
}
纠正:
// ✅ 正确:使用 equals 或拆箱后比较
public boolean isSameLevel(Integer level1, Integer level2) {
return Objects.equals(level1, level2);
// 或 return level1 != null && level1.equals(level2);
}
❌ 反例二:包装类 null 参与运算
// ❌ 错误:自动拆箱时 NPE
Integer salary = null;
int bonus = salary + 5000; // NullPointerException! salary 拆箱时调用 null.intValue()
纠正:
// ✅ 正确:使用前做 null 检查,或设默认值
Integer salary = null;
int safeSalary = (salary != null) ? salary : 0;
int bonus = safeSalary + 5000;
❌ 反例三:在集合中混合基本类型和包装类
// ❌ 可能造成意外的装箱/拆箱开销
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add(i); // 每次装箱 Integer.valueOf(i),10万次
}
int sum = 0;
for (Integer val : list) {
sum += val; // 每次拆箱 val.intValue(),10万次
}
这种情况开销可以接受(因为 Integer 缓存了 -128~127),但超出缓存范围的数值会创建大量对象。
面试考点
Q1:int 和 Integer 有什么区别?
①
int是基本类型,存数值,默认值 0;Integer是包装类,存对象引用,默认值 null。②int不参与面向对象特性;Integer可作为泛型参数、调用方法。③Integer内部有 -128~127 的缓存,valueOf()在该范围内复用对象。④ 自动装箱/拆箱让两者在多数场景下可互换,但性能、null 安全、== 比较仍有差异。
Q2:Integer 的缓存机制是什么?为什么设计这个缓存?
Integer 在类加载时预创建 -128~127(共 256 个)Integer 对象,存入
IntegerCache数组。调用valueOf()时先查缓存,命中则返回已有对象,否则新建。设计目的是减少小数值频繁装箱时的对象创建开销(这些小整数在循环、数组索引等场景使用频率极高)。可通过 JVM 参数-Djava.lang.Integer.IntegerCache.high=xxx调整上限。
Q3:自动装箱和拆箱的底层原理是什么?
装箱:编译器将
Integer i = 100替换为Integer i = Integer.valueOf(100)。拆箱:编译器将int j = i替换为int j = i.intValue()。可通过javap -c反编译字节码验证。本质上是一种语法糖,编译后还原为显式的方法调用。
Q4:下面代码创建了几个 Integer 对象?
Integer a = 100; // 和
Integer b = 200;
Integer a = 100调用valueOf(100),在 -128~127 范围内,从缓存获取,创建 0 个新对象(类加载时已创建)。Integer b = 200超出范围,valueOf(200)内部new Integer(200),创建 1 个新对象。总共创建 1 个对象。
Q5:三目运算符中的装箱陷阱你知道吗?
Integer i = true ? 1 : null; // 输出 i = ?
三目运算符的两个分支类型必须一致。
1是int,null会被隐式装箱推断为Integer,所以1也会被装箱。但更危险的写法是:int result = flag ? integerObj : integerObj2;如果其中一个为 null,拆箱时 NPE!因为三目运算符要求两个分支类型一致,会触发拆箱。最佳实践:三目运算符两端类型显式保持一致,避免自动装箱/拆箱的隐式行为。
白歌在技术分享会上总结:"包装类是 JDK 5 引入的最重要的语法糖之一。用了十几年都没问题,但一旦在 == 比较或 null 值上栽跟头,debug 起来就会怀疑人生。记住两条铁律:比内容用 equals,比大小先拆箱再 > <。"