包装类常量池
你有没有注意到这个奇怪的现象?
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
}
}缓存范围:-128 到 127(可通过 JVM 参数 -Djava.lang.Integer.IntegerCache.high=XXX 调整上限)
其他包装类缓存
| 包装类 | 缓存范围 | 特殊情况 |
|---|---|---|
| Byte | -128 ~ 127 | 全部缓存(只有 256 个值) |
| Short | -128 ~ 127 | |
| Integer | -128 ~ 127 | 可配置上限 |
| Long | -128 ~ 127 | |
| Character | 0 ~ 127 | ASCII 字符范围 |
| Float | 无缓存 | |
| Double | 无缓存 | |
| Boolean | true, 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 IntegerCachejava
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 十有八九出在这里。
