Skip to content

包装类基础

凌晨 2 点,你在线上日志里看到一行诡异的报错:

java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value is null

明明 Integer 不是已经赋值了吗?怎么会 NPE?

这不是代码写错了,是你踩到了 Java 包装类的第一个坑:基本类型和对象是两套不同的逻辑

为什么需要包装类?

Java 的基本类型(intboolean)是「裸奔」的值,存在于栈内存,操作快但没有「身份」。

而有些场景必须用对象:

  • 集合框架(List<Integer>Map<String, Object>)只能存对象
  • 泛型参数不能是基本类型
  • 需要调用方法(如 toString()compareTo()

所以 Java 提供了 8 个包装类,把基本类型「包装」成对象:

基本类型包装类你需要知道的事
byteByte1 字节,-128 ~ 127
shortShort2 字节,-32768 ~ 32767
intInteger4 字节,21 亿多
longLong8 字节
floatFloat4 字节,单精度
doubleDouble8 字节,双精度
charCharacter2 字节,Unicode
booleanBoolean逻辑值,只有两个实例

继承结构:Number 和它的孩子们

六个数值型包装类(Byte、Short、Integer、Long、Float、Double)都继承自 Number 抽象类,这意味着它们共享一套数值转换方法:

java
Integer i = 100;

// 统一的方法签名,Number 定义的
i.byteValue();      // 转 byte: 100
i.shortValue();     // 转 short: 100
i.intValue();       // 转 int: 100
i.longValue();      // 转 long: 100L
i.floatValue();     // 转 float: 100.0f
i.doubleValue();    // 转 double: 100.0

BooleanCharacter 是两个「不听话」的孩子,没有继承 Number,各自为政。

创建包装类的正确姿势

方式一:valueOf() —— 官方推荐的正经玩法

java
// 数字类型
Integer i1 = Integer.valueOf(10);        // int → Integer
Integer i2 = Integer.valueOf("10");     // String → Integer
Integer i3 = Integer.valueOf("1010", 2); // 进制转换:二进制 "1010" = 10

Long l = Long.valueOf("255", 16);      // 十六进制 "FF" = 255
Boolean b = Boolean.valueOf("true");    // true(不区分大小写)
Boolean b2 = Boolean.valueOf("yes");    // false(只有 "true" 才返回 true)

方式二:自动装箱(JDK 5+)—— 偷懒神器

java
Integer i = 10;    // 编译器自动转为 Integer.valueOf(10)
Boolean b = true;  // 编译器自动转为 Boolean.valueOf(true)

看起来写起来爽了,但隐藏的坑在后面

方式三:parseXxx() —— 我要的是基本类型

java
int n = Integer.parseInt("123");           // String → int
double d = Double.parseDouble("3.14");    // String → double
boolean b = Boolean.parseBoolean("true"); // String → boolean(同样不区分大小写)

关键区别

  • parseInt() 返回 int(基本类型)
  • valueOf() 返回 Integer(包装类)

常用常量:它们是静默的守卫

java
public class WrapperConstants {

    public static void main(String[] args) {
        // Integer 的边界
        System.out.println("int 最小值: " + Integer.MIN_VALUE);  // -2147483648
        System.out.println("int 最大值: " + Integer.MAX_VALUE);  // 2147483647
        System.out.println("int 位数: " + Integer.SIZE);         // 32
        System.out.println("int 字节: " + Integer.BYTES);        // 4

        // Long 的边界
        System.out.println("long 最小值: " + Long.MIN_VALUE);
        System.out.println("long 最大值: " + Long.MAX_VALUE);

        // Double 的特殊值
        System.out.println("最大正数: " + Double.MAX_VALUE);         // 1.7976931348623157E308
        System.out.println("正无穷: " + Double.POSITIVE_INFINITY);
        System.out.println("负无穷: " + Double.NEGATIVE_INFINITY);
        System.out.println("NaN: " + Double.NaN);                // Not a Number

        // Boolean 的两个单例
        System.out.println(Boolean.TRUE == Boolean.valueOf(true));  // true
        System.out.println(Boolean.FALSE == Boolean.valueOf(false)); // true
    }
}

这些常量在边界值判断、序列化、算法实现中经常用到。

装箱与拆箱的底层

你以为 Integer i = 10; 很简洁?编译器帮你做了这些:

java
// 你写的代码
Integer i = 10;

// 编译器帮你翻译成
Integer i = Integer.valueOf(10);

// 当你这样用时
int n = i;

// 编译器翻译成
int n = i.intValue();

装箱:调用 valueOf()拆箱:调用 xxxValue()

这不是魔法,是语法糖。但语法糖吃多了,迟早要还的。

缓存机制:Java 的小心思

看这段代码,你猜结果是什么?

java
public class CacheDemo {

    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        System.out.println("a == b: " + (a == b));  // ?

        Integer c = 128;
        Integer d = 128;
        System.out.println("c == d: " + (c == d));  // ?
    }
}

答案是:truefalse

原因:Integer.valueOf()-128 到 127 之间的值做了缓存,复用已有对象。

java
// 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);
}

缓存范围

包装类缓存范围
Byte全部 -128 ~ 127
Short-128 ~ 127
Integer-128 ~ 127
Long-128 ~ 127
Character0 ~ 127
Float无缓存
Double无缓存
Booleantrue, false 两个实例

所以:

  • Integer a = 127; Integer b = 127;a == btrue(同一个对象)
  • Integer c = 128; Integer d = 128;c == dfalse(两个对象)
  • Double d1 = 0.1; Double d2 = 0.1;d1 == d2false(没有缓存)

最重要的提醒

1. 废弃的构造函数,别用了

java
// 这些构造函数早已废弃,JDK 源码里都打上了 @Deprecated
Integer i = new Integer(10);     // ❌ 别用
Boolean b = new Boolean("true"); // ❌ 别用

Integer i = Integer.valueOf(10); // ✅ 推荐
Boolean b = Boolean.valueOf(true); // ✅ 推荐

2. 拆箱的 NPE 陷阱

java
Integer nullable = null;

// 自动拆箱 → NPE
int value = nullable; // java.lang.NullPointerException

// ✅ 先检查
int value = nullable != null ? nullable : 0;

这就是开头那个报错的原因:包装类可以为 null,拆箱操作不会帮你检查。

3. 自动装箱有性能代价

java
// 循环中频繁装箱,JVM 要分配大量短期对象
Long sum = 0L;  // 注意是 Long,不是 long
for (int i = 0; i < 1000000; i++) {
    sum += i;    // 每次 i 都要装箱成 Long,然后拆箱相加,再装箱
}
// 这段代码在堆上创建了百万个 Long 对象,GC 压力巨大

// ✅ 用基本类型
long sum = 0L;
for (int i = 0; i < 1000000; i++) {
    sum += i;
}

要点回顾

  • 包装类把基本类型封装成对象,用于集合、泛型等场景
  • 六数值类继承 Number,可用统一的 xxxValue() 转换
  • valueOf() 会复用缓存对象(-128~127),new Integer() 不会
  • 自动装箱/拆箱是语法糖,但有性能和 NPE 风险
  • 比较包装类,用 equals() 而不是 ==

记住:基本类型是值,包装类是对象。值比大小用 ==,对象比大小用 equals()。这是 Java 的基本修养。

基于 VitePress 构建