Integer
Integer 源码分析
int 的包装类,让基本类型能进集合、泛型和一切只认对象的场合。核心设计只有两件事:不可变(value 是 final),以及自动装箱走 valueOf 时的 [-128, 127] 缓存池,所有 == 陷阱都从这个池子来。
代码块收起展开
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.Integer
@jdk.internal.ValueBased // 值类型候选:别拿 Integer 当锁,缓存对象是全局共享的
public final class Integer extends Number
implements Comparable<Integer>, Constable, ConstantDesc {
@Native public static final int MIN_VALUE = 0x80000000; // @Native: 该常量会生成进 JNI 头文件供 C 代码使用
@Native public static final int MAX_VALUE = 0x7fffffff;
// ...
private static final class IntegerCache {
static final int low = -128; // 下界写死:JLS 5.1.7 只强制 [-128, 127],即 byte 的全域
static final int high; // 上界只能调大,不能调小
@Stable // 提示 JIT:数组元素一旦非 null 就不再变,读取可被常量折叠
static final Integer[] cache;
static Integer[] archivedCache; // CDS 共享归档里的缓存,多个 JVM 进程共用同一份只读内存
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); // -XX:AutoBoxCacheMax 最终也落到这个属性
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127); // 想调小?127 兜底,调不动
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class); // 归档命中时 256 个对象一个都不用 new
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
// If archive has Integer cache, we must use all instances from it.
// Otherwise, the identity checks between archived Integers and
// runtime-cached Integers would fail.
int archivedSize = (archivedCache == null) ? 0 : archivedCache.length;
for (int i = 0; i < archivedSize; i++) {
c[i] = archivedCache[i]; // 归档实例必须原样复用,否则 == 语义会因 CDS 开关而变
assert j == archivedCache[i];
j++;
}
// Fill the rest of the cache.
for (int i = archivedSize; i < size; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
@IntrinsicCandidate // JIT 可用内建实现替换;配合逃逸分析,装箱可能被整个消除
public static Integer valueOf(int i) { // 自动装箱 Integer a = 100 编译后调的就是它
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]; // 命中池子:永远同一个对象
return new Integer(i); // 出了池子:每次新对象,== 必然 false
}
private final int value; // final 是 Integer 不可变的全部来源:可安全做 HashMap key、跨线程共享
@Deprecated(since="9")
public Integer(int value) { // 直接 new 绕过缓存池,官方已废弃,永远用 valueOf
this.value = value;
}
@IntrinsicCandidate
public int intValue() { // 自动拆箱调的就是它,null 拆箱的 NPE 就抛在对它的调用上
return value;
}
public boolean equals(Object obj) {
if (obj instanceof Integer i) { // 严格判类型:传 Long/null 直接 false,不做跨类型比较
return value == i.intValue();
}
return false;
}
@Override
public int hashCode() {
return Integer.hashCode(value);
}
public static int hashCode(int value) {
return value; // hash 就是值本身:完美散列,零冲突
}
}parseInt 是字符串进 int 的唯一入口,最反直觉的一点是全程在负数域累加:
代码块收起展开
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.Integer
public final class Integer extends Number
implements Comparable<Integer>, Constable, ConstantDesc {
// ...
public static int parseInt(String s, int radix)
throws NumberFormatException {
/*
* WARNING: This method may be invoked early during VM initialization
* before IntegerCache is initialized. Care must be taken to not use
* the valueOf method.
*/
if (s == null) {
throw new NumberFormatException("Cannot parse null string");
}
// ... radix 越界检查省略:不在 [Character.MIN_RADIX, MAX_RADIX] 即 [2, 36] 内直接抛 NumberFormatException
int len = s.length();
if (len == 0) {
throw NumberFormatException.forInputString("", radix);
}
int digit = ~0xFF; // 哨兵值:区分"首字符是符号"和"首字符是数字"两条路
int i = 0;
char firstChar = s.charAt(i++);
if (firstChar != '-' && firstChar != '+') {
digit = digit(firstChar, radix); // 静态导入的 Character.digit,非法字符返回 -1
}
if (digit >= 0 || digit == ~0xFF && len > 1) { // 只有符号没有数字位("-"/"+")不放行
int limit = firstChar != '-' ? MIN_VALUE + 1 : MIN_VALUE; // 负域比正域宽 1,正数的界是 -(MAX_VALUE)
int multmin = limit / radix;
int result = -(digit & 0xFF); // 符号开头: ~0xFF & 0xFF == 0;数字开头: 就是首位数字取负
boolean inRange = true;
/* Accumulating negatively avoids surprises near MAX_VALUE */
while (i < len && (digit = digit(s.charAt(i++), radix)) >= 0
&& (inRange = result > multmin
|| result == multmin && digit <= radix * multmin - limit)) { // 乘之前先判溢出
result = radix * result - digit; // 负数域累加,MIN_VALUE 也装得下
}
if (inRange && i == len && digit >= 0) {
return firstChar != '-' ? -result : result; // 正数最后才翻转符号
}
}
throw NumberFormatException.forInputString(s, radix);
}
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s, 10);
}
public static Integer valueOf(String s) throws NumberFormatException {
return Integer.valueOf(parseInt(s, 10)); // 先解析成 int 再装箱,照样享受缓存池
}
}装箱缓存导致的经典陷阱:
代码块收起展开
Integer a = 127, b = 127;
System.out.println(a == b); // true:都命中缓存池,同一个对象
Integer c = 128, d = 128;
System.out.println(c == d); // false:各 new 一个对象
System.out.println(c.equals(d)); // true:equals 比的是值
int e = 128;
System.out.println(c == e); // true:一边是 int 时 c 先拆箱,变成纯值比较原理串讲
代码块收起展开
一次 `Integer a = 127; Integer b = 127; a == b` 的完整链路:javac 把 `Integer a = 127` 编译成 `Integer.valueOf(127)`(字节码里是一条 invokestatic),首次调用触发 IntegerCache 类加载。
static 块先读 `java.lang.Integer.IntegerCache.high` 决定上界(`-XX:AutoBoxCacheMax=1000` 会把它抬到 1000,下界永远 -128),再通过 `CDS.initializeFromArchive` 尝试从共享归档直接映射整个 cache 数组,命中时连这 256 个对象都不用 new,且必须原样复用归档实例,否则同一个 127 在开关 CDS 的两次运行里 `==` 结果会不一样。
之后 `valueOf(127)` 落在 `[low, high]` 内,返回 `cache[127 + 128]`,a 和 b 拿到同一个引用,`==` 为 true;换成 128 则两次 `new Integer(128)`,引用必不同。反方向 int x = a 编译成 a.intValue(),a 为 null 时 NPE 就抛在这里,三目运算符里混用 int 和 Integer 触发自动拆箱是这个 NPE 的高发现场。
为什么缓存只强制 [-128, 127]?JLS 5.1.7 的承诺是”这个范围内装箱结果必须可复用”,因为 byte 全域小到可以无条件预热,而真实程序里的小整数(循环变量、状态码、下标)又占绝对多数,收益最大。
上界可调、下界不可调也是同一逻辑:扩大池子不破坏规范承诺,缩小就破坏了,所以源码里 Math.max(parseInt(...), 127) 直接把调小的企图顶回去。
为什么 parseInt 在负数域累加?int 的负域比正域多一个数:MIN_VALUE 是 -2^31 而 MAX_VALUE 是 2^31-1。
若按正数累加,解析 “-2147483648” 时中间值 2147483648 已经溢出;在负数域累加则正负输入都装得下,最后按 firstChar 决定翻不翻转符号。
溢出检查也顺势前置:每轮乘 radix 之前先和 multmin 比较,把”乘完才发现溢出”变成”乘之前就能判定”,循环体内不需要任何 long 或额外分支。
为什么 valueOf(String) 不自己 new?它拆成 parseInt + valueOf(int) 两步,字符串解析和对象获取正交,解析出来的小整数照样走缓存。
所有通往 Integer 对象的路最终都汇到 valueOf(int) 这一个口子,这是缓存语义能守住的前提,也是构造器被 @Deprecated 的根本原因:new 出来的对象永远不进池,等于单方面撕毁 == 的可复用承诺。
设计取舍
- 值比较永远用 equals 或先拆成 int。
==只在两边都命中缓存池时碰巧为 true,128 就翻车。 - equals 严格判类型:
Integer.valueOf(1).equals(1L)是 false。跨类型比较前先统一成基本类型。 - 不可变意味着
i++实为拆箱、加一、再装箱出新对象。热路径累加用 int,别用 Integer。 - 别对 Integer 加 synchronized:缓存池对象全局共享,两段无关代码锁同一个
Integer 42会互相阻塞,@ValueBased 类未来会直接禁止当锁。 new Integer(...)自 JDK 9 起废弃,它绕过缓存池且骗过一切依赖对象复用的假设。