×

上一篇文章我们聊了枚举,把那些固定取值的东西变安全了。但这还不够——你会发现 Java 里有些很基础的问题一直隐隐作痛:

  • int 能放进 ArrayList<Integer> 吗?不能,泛型只能接对象。
  • boolean 能赋值为 null 吗?不能,基本类型不是引用。
  • 那你写List<int> 试试?编译器直接锤你。

为了解决基本类型和对象类型之间的鸿沟,Java 给每种基本类型配了一个「对象版」——包装类(Wrapper Class)

一、包装类全家福

基本类型包装类父类
byteByteNumber
shortShortNumber
intIntegerNumber
longLongNumber
floatFloatNumber
doubleDoubleNumber
charCharacterObject
booleanBooleanObject

除了 CharacterBoolean 直接继承 Object,剩下的数值型包装类都继承自 java.lang.Number 抽象类。所有包装类都是 final 的,不能继承,且都实现了 SerializableComparable

二、装箱与拆箱

把基本类型变成包装类叫装箱,反过来叫拆箱

手动装箱拆箱(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

这看起来就像 Integerint 可以无缝混用了。但别忘了,编译器帮你在背后插入了 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 → 两个引用指向缓存中的同一个对象 → true
  • 128 == 128 → 两个不同的新对象 → false

怎么避免踩坑?比较包装类永远用 .equals()

Integer a = 128;
Integer b = 128;
System.out.println(a.equals(b));  // true ✅

记住一句话:包装类比较用.equals(),基本类型比较用==。别混着来。

类似的缓存机制也存在其他包装类里:

  • Byte:全部缓存(-128~127,总共才 256 个)
  • Short:-128~127
  • Long:-128~127
  • Character:0~127
  • Integer:-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 运行时异常、以及自定义异常的那些事。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

作者

2248768396@qq.com

相关文章

20. 接口基础:语法、特性与抽象类对比

抽象类讲完了。你知道父类可以只声明方法签名,...

读出全部

19. 多态的应用:参数多态化、工厂模式与支付系统实战

上一篇我们拆解了多态的底层机制——动态绑定、...

读出全部

18. 多态的本质:动态绑定、向上转型与向下转型

抽象类学完了,你现在应该知道 「父类只声明、...

读出全部