- 0
- 252 words
上一篇文章我们聊了枚举,把那些固定取值的东西变安全了。但这还不够——你会发现 Java 里有些很基础的问题一直隐隐作痛:
int能放进ArrayList<Integer>吗?不能,泛型只能接对象。boolean能赋值为null吗?不能,基本类型不是引用。- 那你写
List<int>试试?编译器直接锤你。
为了解决基本类型和对象类型之间的鸿沟,Java 给每种基本类型配了一个「对象版」——包装类(Wrapper Class)。
一、包装类全家福
| 基本类型 | 包装类 | 父类 |
|---|---|---|
byte | Byte | Number |
short | Short | Number |
int | Integer | Number |
long | Long | Number |
float | Float | Number |
double | Double | Number |
char | Character | Object |
boolean | Boolean | Object |
除了 Character 和 Boolean 直接继承 Object,剩下的数值型包装类都继承自 java.lang.Number 抽象类。所有包装类都是 final 的,不能继承,且都实现了 Serializable 和 Comparable。
二、装箱与拆箱
把基本类型变成包装类叫装箱,反过来叫拆箱。
手动装箱拆箱(Java 5 之前)
// 手动装箱:基本类型 → 包装类
Integer i = Integer.valueOf(100);
// 或者用构造方法(已弃用,别用了)
// Integer i = new Integer(100); // ❌ 不推荐
// 手动拆箱:包装类 → 基本类型
int n = i.intValue();
自动装箱拆箱(Java 5+)
Java 5 引入了自动装箱拆箱,从此你可以这么写:
Integer i = 100; // 自动装箱:编译器调用了 Integer.valueOf(100)
int n = i; // 自动拆箱:编译器调用了 i.intValue()
// 混合运算
Integer a = 10;
Integer b = 20;
Integer c = a + b; // a 和 b 自动拆箱做加法,结果再自动装箱
System.out.println(c); // 30
这看起来就像 Integer 和 int 可以无缝混用了。但别忘了,编译器帮你在背后插入了 valueOf() 和 xxxValue() 调用。
自动装箱拆箱是个语法糖,不是运行时能力。你编译出来的字节码里,该调的方法一个不少。
三、Integer 缓存池——一个经典大坑
来看个面试题常客:
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false ???
是不是觉得 Java 不讲武德?127 和 128 就差 1 而已,怎么结果就反转了?
原因在 Integer.valueOf() 的实现里:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache 默认缓存了 -128 到 127 之间的所有 Integer 对象。在这个范围内,valueOf() 返回的是缓存里的同一个对象;超出这个范围,每次调用都会 new 一个新的对象。
所以:
127 == 127→ 两个引用指向缓存中的同一个对象 →true128 == 128→ 两个不同的新对象 →false
怎么避免踩坑?比较包装类永远用 .equals():
Integer a = 128;
Integer b = 128;
System.out.println(a.equals(b)); // true ✅
记住一句话:包装类比较用.equals(),基本类型比较用==。别混着来。
类似的缓存机制也存在其他包装类里:
Byte:全部缓存(-128~127,总共才 256 个)Short:-128~127Long:-128~127Character:0~127Integer:-128~127(可以通过 JVM 参数调整上限)
四、空指针陷阱——自动拆箱可能翻车
自动拆箱看着方便,但有个隐藏杀手——NPE(NullPointerException)。
Integer score = null;
int realScore = score; // 编译通过,运行抛 NullPointerException!
// 等价于 int realScore = score.intValue(); // null.intValue() 想想就知道要炸
或者更隐蔽的场景:
// 假设从数据库查到的 count 可能是 null
Integer count = getCountFromDb(); // null
if (count > 0) { // 自动拆箱 → NullPointerException
// 这行都到不了
}
经验法则:包装类参与自动拆箱操作之前,先判空。
if (count != null && count > 0) {
// 安全了
}
五、字符串与包装类的互转
开发中特别常见的需求:
// 字符串 → 基本类型
int n = Integer.parseInt("123");
double d = Double.parseDouble("3.14");
boolean b = Boolean.parseBoolean("true");
// 字符串 → 包装类
Integer i = Integer.valueOf("123");
Double dd = Double.valueOf("3.14");
// 基本类型/包装类 → 字符串
String s1 = Integer.toString(123);
String s2 = String.valueOf(123);
String s3 = 123 + ""; // 偷懒写法,编译器会优化
注意 parseXxx() 和 valueOf() 的区别——前者返回基本类型,后者返回包装类:
int n = Integer.parseInt("123"); // int
Integer i = Integer.valueOf("123"); // Integer
六、Number 抽象类与常用方法
六个数值型包装类都继承自 Number 抽象类,所以它们都有以下方法:
Integer i = 100;
byte b = i.byteValue();
short s = i.shortValue();
int n = i.intValue();
long l = i.longValue();
float f = i.floatValue();
double d = i.doubleValue();
这些方法把 Integer 转成别的数值类型。比如你用 Integer 存了一个 int,但某个方法需要 long,直接 .longValue() 就行,不用你手动强转。
七、包装类的实际用途总结
| 场景 | 说明 | 示例 |
|---|---|---|
| 泛型 | 泛型只能使用引用类型 | List<Integer> ✅ List<int> ❌ |
| 可为 null | 包装类可以表示「未设置」 | 数据库字段可能为 NULL |
| 集合框架 | 集合只能存对象 | Map<String, Integer> |
| 工具方法 | 包装类提供了丰富的工具方法 | Integer.parseInt() |
| 同步锁 | 包装类实例可作为锁对象 | 但要注意缓存池问题 |
八、几个容易翻车的小例子
1. 三元运算符的类型提升
Integer a = null;
int b = 1;
// 下面这行会抛 NPE
int result = (a != null) ? a : b;
// 因为三元运算符要求两个分支的类型一致,b 是 int,a 会被自动拆箱
// 而 a 是 null → NPE
2. 包装类作为锁对象
class Counter {
private Integer count = 0;
public void increment() {
synchronized (count) { // ❌ 危险!
count++; // count++ 实际上是 count = Integer.valueOf(count + 1)
// 每次赋值都创建新对象,锁的不是同一个对象!
}
}
}
count++ 是 count = Integer.valueOf(count + 1) 的语法糖,每次都会返回一个新的 Integer 对象。你锁的 count 变了,锁就废了。
3. 混用 int 和 Integer 时的性能
每次装箱都会创建一个新对象(除了缓存范围内的),大量循环里频繁装箱拆箱会有性能开销:
// 这个循环很慢——每次加法都要装箱拆箱
Integer sum = 0;
for (int i = 0; i < 10_000_000; i++) {
sum += i; // 装箱 1000 万次
}
// 改成这样就快了
int sum2 = 0;
for (int i = 0; i < 10_000_000; i++) {
sum2 += i;
}
九、结语
包装类不是什么高深的东西,但坑不少。记住三条准则:
- 比较用
.equals(),别图方便用== - 自动拆箱前记得判空
- 大量数值运算用基本类型,别让包装类拖慢你的程序
到这篇为止,JavaSE 面向对象(03)的全部子分类都写完了。static、封装、继承、抽象类、多态、接口、内部类、Object、枚举、包装类——整整 21 篇文章,把 Java 面向对象这块啃下来了。
下一篇我们将进入 04. 异常处理,聊聊 try-catch-finally、受检异常 vs 运行时异常、以及自定义异常的那些事。