Skip to content

包装类常量池

你有没有注意到这个奇怪的现象?

java
Integer a = 127;
Integer b = 127;
System.out.println(a == b);  // true

Integer c = 128;
Integer d = 128;
System.out.println(c == d);  // false

同一个类,同一个值域,127 相同,128 不同。

这背后,是 Java 设计者精心设计的常量池机制。

常量池是什么?

常量池(Constant Pool)是 JVM 内存中的一块特殊区域,用于存储复用率高的对象,避免重复创建浪费内存。

Java 中有多种常量池:

常量池位置存储内容作用域
Class 常量池元空间类的元数据、字面量类加载时
字符串常量池String 字面量全局共享
包装类常量池Byte、Short、Integer、Long、Character、Boolean全局共享
┌─────────────────────────────────────────────┐
│                 JVM 内存                      │
├─────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────┐ │
│  │                 堆 (Heap)                │ │
│  │  ┌─────────────────────────────────────┐ │ │
│  │  │         字符串常量池 (String Pool)   │ │ │
│  │  │  ┌────┬────┬────┬────┐             │ │ │
│  │  │  │"ok"│"hi"│     │     │             │ │ │
│  │  │  └────┴────┴────┴────┘             │ │ │
│  │  └─────────────────────────────────────┘ │ │
│  │  ┌─────────────────────────────────────┐ │ │
│  │  │        包装类常量池 (Integer Cache)   │ │ │
│  │  │  ┌──────────────────────────────┐   │ │ │
│  │  │  │  [-128] [127] 范围内复用对象  │   │ │ │
│  │  │  └──────────────────────────────┘   │ │ │
│  │  └─────────────────────────────────────┘ │ │
│  └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

包装类常量池详解

Integer 常量池

java
public class IntegerPoolDemo {

    public static void main(String[] args) {
        // 直接赋值:自动装箱调用 valueOf()
        Integer a = 127;
        Integer b = 127;

        // 在缓存范围内,返回同一个对象
        System.out.println("127 == 127: " + (a == b));  // true

        // 超出缓存范围,每次创建新对象
        Integer c = 128;
        Integer d = 128;
        System.out.println("128 == 128: " + (c == d));  // false

        // new Integer() 永远创建新对象
        Integer e = new Integer(127);
        System.out.println("127 == new Integer(127): " + (a == e));  // false
    }
}

缓存范围-128127(可通过 JVM 参数 -Djava.lang.Integer.IntegerCache.high=XXX 调整上限)

其他包装类缓存

包装类缓存范围特殊情况
Byte-128 ~ 127全部缓存(只有 256 个值)
Short-128 ~ 127
Integer-128 ~ 127可配置上限
Long-128 ~ 127
Character0 ~ 127ASCII 字符范围
Float无缓存
Double无缓存
Booleantrue, false两个固定实例
java
public class OtherPools {

    public static void main(String[] args) {
        // Long 也有缓存
        Long l1 = 127L;
        Long l2 = 127L;
        System.out.println("Long 127: " + (l1 == l2));  // true

        // Character 缓存
        Character c1 = 'A';  // 65
        Character c2 = 'A';
        System.out.println("Character 'A': " + (c1 == c2));  // true

        // Double 没有缓存,每次 new
        Double d1 = 0.1;
        Double d2 = 0.1;
        System.out.println("Double 0.1: " + (d1 == d2));  // false

        // Boolean 缓存(但别依赖 == 比较)
        Boolean b1 = true;
        Boolean b2 = true;
        System.out.println("Boolean true: " + (b1 == b2));  // true
    }
}

valueOf() vs new Integer()

这是面试常考题:

java
public class ValueOfVsNew {

    public static void main(String[] args) {
        // valueOf() 使用常量池
        Integer a = Integer.valueOf(100);
        Integer b = Integer.valueOf(100);
        System.out.println("valueOf(100): " + (a == b));  // true

        // new Integer() 每次创建新对象
        Integer c = new Integer(100);
        Integer d = new Integer(100);
        System.out.println("new Integer(100): " + (c == d));  // false

        // 自动装箱底层调用 valueOf()
        Integer e = 100;  // 自动装箱
        System.out.println("auto-box 100 == valueOf 100: " + (a == e));  // true
    }
}

结论

  • Integer.valueOf() 或自动装箱:在缓存范围内复用,超出范围创建新对象
  • new Integer():每次创建新对象,永远不用常量池

字符串常量池:另一个世界

字符串常量池和包装类常量池是两个独立的池

java
public class TwoPools {

    public static void main(String[] args) {
        // 字符串字面量在字符串常量池
        String s1 = "hello";
        String s2 = "hello";
        System.out.println("String == : " + (s1 == s2));  // true

        // new String() 在堆中,不在常量池
        String s3 = new String("hello");
        System.out.println("String new == : " + (s1 == s3));  // false

        // intern() 可以把堆中的字符串加入常量池
        String s4 = s3.intern();
        System.out.println("String intern: " + (s1 == s4));  // true
    }
}

字符串拼接的特殊规则

java
public class StringConcat {

    public static void main(String[] args) {
        // 编译时常量拼接:在编译时优化
        String a = "a" + "b";      // 编译时优化为 "ab"
        String b = "ab";
        System.out.println("常量拼接: " + (a == b));  // true

        // 变量拼接:在运行时创建
        String c = "ab";
        String d = new String("ab");
        String e = c + d;  // 运行时通过 StringBuilder
        System.out.println("变量拼接: " + (e == "ababcd"));  // false
    }
}

实际应用场景

1. 数据库 ID 缓存

java
public class IdCacheDemo {

    // 使用常量池优化内存
    private static final Integer CACHE_STATUS_NEW = 1;
    private static final Integer CACHE_STATUS_ACTIVE = 2;
    private static final Integer CACHE_STATUS_INACTIVE = 3;

    public static void main(String[] args) {
        Integer id = getStatusFromCache();

        // 如果状态是常用值,可能复用常量池对象
        if (id == CACHE_STATUS_ACTIVE) {  // == 比较(不建议,但理解缓存有用)
            System.out.println("激活状态");
        }
    }

    static Integer getStatusFromCache() {
        // 返回的可能是缓存对象
        return 2;  // 自动装箱
    }
}

2. 枚举替代常量池

java
// 更安全的做法:使用枚举替代 Integer 常量
public enum Status {
    NEW(1),
    ACTIVE(2),
    INACTIVE(3);

    private final int value;

    Status(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

// 使用时
Status status = Status.ACTIVE;
if (status == Status.ACTIVE) {  // 安全的 == 比较
    // ...
}

常量池的代价

内存占用

java
public class PoolSizeDemo {

    public static void main(String[] args) {
        // Integer 缓存数组大小:127 - (-128) + 1 = 256 个对象
        // 每个 Integer 对象占用多少内存?
        // 对象头 12 字节 + int 4 字节 + 对齐 = 16 字节
        // 256 * 16 = 4KB

        // 但实际使用中,这 256 个对象是预加载的,在类初始化时就创建了
        // 查看内存:
        // Integer[] cache = new Integer[256];  // 4KB 固定开销

        // 而超出范围的自动装箱:
        for (int i = 0; i < 100000; i++) {
            Integer j = i + 128;  // 超出缓存,每次 new
        }
        // 100000 个 Integer 对象 ≈ 1.6MB
    }
}

比较的坑

java
public class ComparisonPitfall {

    public static void main(String[] args) {
        // 缓存范围内,== 可能碰巧为 true
        Integer a = 100;
        Integer b = 100;
        if (a == b) {  // 碰巧 true
            System.out.println("碰巧相等");
        }

        // 超出范围,== 必为 false
        Integer c = 200;
        Integer d = 200;
        if (c == d) {  // 永远 false
            System.out.println("不可能到这里");
        }

        // 最佳实践:始终用 equals()
        System.out.println("equals: " + c.equals(d));  // true
    }
}

JVM 参数调优

bash
# 调整 Integer 缓存上限为 1000
java -Djava.lang.Integer.IntegerCache.high=1000 MyApp

# 查看实际缓存范围
java -XX:+PrintFlagsFinal -version | grep IntegerCache
java
public class CacheRangeDemo {

    public static void main(String[] args) {
        // 默认:-128 ~ 127
        // 调整后:-128 ~ 1000

        Integer a = 1000;
        Integer b = 1000;

        // 如果 JVM 参数设为 1000,这里会是 true
        System.out.println("1000 == 1000: " + (a == b));
    }
}

要点回顾

  • 包装类常量池是 JVM 在堆中预创建的缓存区
  • valueOf() 和自动装箱使用常量池,new 不使用
  • Integer/Long:-128 ~ 127;Character:0 ~ 127;Byte:全部
  • Float/Double 没有常量池
  • 比较包装类必须用 equals(),不能用 ==

血的教训:别用 == 比较包装类,除非你清楚知道在缓存范围内并且故意复用对象。线上 bug 十有八九出在这里。

基于 VitePress 构建