嘿,Friends!
我们每天写代码,int
和 Integer
估计是用得最多的类型之一了,对吧?Integer i = 100;
,简单得就像呼吸一样。但你有没有想过,这句简单的代码背后,JDK 的大神们都做了些什么?
一:IntegerCache
—— 你以为的 new,其实是“二手”的
我们先来看一个经典的面试题:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false
为啥会这样?答案就在 Integer
的一个静态内部类:IntegerCache
。
简单来说,IntegerCache
就是个缓存。JDK 觉得,大家用的小整数(-128 到 127)实在是太多了,每次都 new
一个新对象太浪费内存和性能了。干脆,我启动的时候就一次性把这 256 个整数对象全创建好,放一个数组里。以后你要用,直接从数组里拿,别 new 了!
这就是典型的 “空间换时间” 思想。用一点点内存(一个 256 大小的数组)来换取极高的访问速度和更少的垃圾回收(GC)压力。
1.1.源码探秘:
// Integer.java
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static final Integer[] archivedCache; // 用于 GraalVM 的一个优化
static {
// high 的值可以由外部属性配置,但默认是 127
int h = 127;
String s = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (s != null) {
// ... 解析属性,设置 h ...
}
high = h;
// ... 省略 GraalVM 相关逻辑 ...
// 创建缓存数组
cache = new Integer[high - low + 1];
int j = low;
// 循环创建对象,填满缓存
for(int k = 0; k < cache.length; k++) {
cache[k] = new Integer(j++);
}
}
}
当我们写 Integer i = 100;
时,实际上编译器会把它转换成 Integer.valueOf(100);
。我们来看看 valueOf
方法:
// Integer.java
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)]; // 直接从缓存拿
return new Integer(i); // 超出范围,才 new 一个新的
}
真相大白!当值在 [-128, 127]
范围内,valueOf
直接从 IntegerCache.cache
数组里返回一个已经创建好的对象。所以 a
和 b
指向的是同一个对象,a == b
为 true
。而 200 超出了这个范围,每次都会 new
一个新对象,所以 c
和 d
指向不同对象,c == d
为 false
。
1.2.图解:
这个小小的缓存,完美地体现了设计的智慧:优化最常见的场景。
二:toString()
背后的功臣 —— stringSize
和 getChars
把一个 int
转成 String
,比如 Integer.toString(12345)
,这背后也有大学问。
一个直观但低效的做法是:
-
创建一个可变
StringBuilder
。 -
用循环和
% 10
、/ 10
操作,一位一位地把数字算出来,append
到StringBuilder
。 -
最后
reverse()
一下,再toString()
。
这个过程涉及到多次方法调用、可能的扩容和反转,效率不高。JDK 的大神们当然不屑于这么做。他们用了两个“秘密武器”:stringSize
和 getChars
。
2.1. stringSize(int x)
: 内联优化, 循环展开
你怎么算一个 int
的十进制长度?log10
?太慢了!循环除以10?还是慢!看看大神们的写法:
// 开启内联优化, jdk 16 开启有的
@IntrinsicCandidate
public static String toString(int i) {
int size = stringSize(i);
if (COMPACT_STRINGS) {
byte[] buf = new byte[size];
getChars(i, size, buf);
return new String(buf, LATIN1);
} else {
byte[] buf = new byte[size * 2];
StringUTF16.getChars(i, size, buf);
return new String(buf, UTF16);
}
}
static int stringSize(int x) {
// d 表示符号位长度:负数时 d = 1(需要负号),非负数时 d = 0
int d = 1;
if (x >= 0) {
d = 0;
// 将整数变为负数统一处理, 防止溢出.
// 若 x >= 0,将 x 转为负数(统一用负数处理,避免边界问题)
x = -x; // 非负数转为负数(如 42 → -42)
}
int p = -10;
for (int i = 1; i < 10; i++) {
if (x > p)
return i + d;
p = 10 * p;
}
return 10 + d;
}
核心功能
-
输入:整数
x
(任意 int 值) -
输出:
x
的字符串表示的长度(如-42
的长度为 3,123
的长度为 3)
关键逻辑
-
处理符号位:
d
表示符号位长度:负数时d = 1
(需要负号),非负数时d = 0
。- 若
x >= 0
,将x
转为负数(统一用负数处理,避免边界问题):
if (x >= 0) {
d = 0;
x = -x; // 非负数转为负数(如 42 → -42)
}
2.2.循环检测数字位数:
-
初始化
p = -10
(代表-10^1
,即数字位数为 1 的阈值)。 -
循环
i
从 1 到 9(i
表示当前检测的数字位数): -
for (int i = 1; i < 10; i++) { if (x > p) // 判断 x 的绝对值是否 < 10^i return i + d; // 满足则返回总长度 = 数字位数(i) + 符号位(d) p = 10 * p; // 更新 p 为 -10^(i+1)(如 -10 → -100) }
终止条件:当
x > p
时,说明x
的绝对值小于10^i
,数字位数为i
。 -
例:
x = -5
(绝对值 5),首次循环p = -10
,-5 > -10
→ 返回1 + d
2.3.处理最大位数(10 位):
-
若循环结束未返回,说明
x
是 10 位数(如Integer.MIN_VALUE
或Integer.MAX_VALUE
):
return 10 + d; // 10 位数字 + 符号位
边界示例:
x 值 | 字符串 | 长度 | 计算过程 |
---|---|---|---|
0 | "0" | 1 | x>=0 → d=0 , x=0 → 循环: 0 > -10 → 1+0=1 |
42 | "42" | 2 | x>=0 → d=0 , x=-42 → 循环: -42 > -100 → 2+0=2 |
-10 | "-10" | 3 | d=1 , 循环: -10 > -10 不成立 → 下一轮 -10 > -100 → 2+1=3 |
2147483647 (MAX_VALUE) | "2147483647" | 10 | x>=0 → d=0 , x=-2147483647 → 循环 9 次后未返回 → 10+0=10 |
-2147483648 (MIN_VALUE) | "-2147483648" | 11 | d=1 , 循环 9 次未返回 → 10+1=11 |
2.4.设计特点
-
负数统一处理:将非负数转为负数,避免正数边界问题(如
Integer.MAX_VALUE
)。 -
线性搜索优化:基于数据偏向小数值的特点,优先检查较小位数,平均效率高。
-
循环展开友好:固定循环 9 次,编译器易优化(如循环展开内联)。
-
固定循环次数(9 次)的意义
在代码中,循环边界是硬编码的:
for (int i = 1; i < 10; i++)
由于 Java
int
的范围是-2^31
到2^31-1
(即-2147483648
到2147483647
),其字符串表示的最大长度是:负数:11 位(如
-2147483648
)正数:10 位(如
2147483647)
9 次循环的合理性:
数字 1~9 位:通过循环内返回(如
123
在第 3 次循环返回)数字 10 位:循环结束后返回(如
2147483647
)负数符号位:通过变量
d
单独处理编译器优化:循环展开(Loop Unrolling):
编译器会将固定次数的循环转换为顺序执行的代码块,消除循环控制开销
```java
for (int i = 1; i < 10; i++) {
if (x > p) return i + d;
p = 10 * p;
}```
循环展开后的等效代码(编译器自动生成)
// i=1
if (x > -10) return 1 + d; // 检查 1 位数
p = -100;// i=2
if (x > -100) return 2 + d; // 检查 2 位数
p = -1000;// i=3
if (x > -1000) return 3 + d;
p = -10000;// ... 重复直到 i=9
// i=9
if (x > -1000000000) return 9 + d;// 默认返回 10 位数
return 10 + d;为何循环展开更高效
优化类型 原始循环 循环展开后 优势 指令跳转 每次迭代需跳转回循环起始 无跳转(顺序执行) 消除分支预测错误惩罚 循环变量维护 需检查 i<10
和i++
无需维护循环计数器 减少 CPU 指令 CPU 流水线 循环控制中断流水线 连续顺序指令 提高指令级并行 缓存局部性 代码分散 连续代码块 更好利用 CPU 缓存 内联(Inlining)优化
当此方法被频繁调用时(如
Integer.toString()
中):内联机制:编译器将方法体直接复制到调用处
与循环展开协同
// 调用点示例 void print(int num) { int size = stringSize(num); // 方法调用 // ... 使用 size } // 内联+展开后: void print(int num) { // 直接展开的 stringSize 逻辑 int d = 1; if (num >= 0) { ... } if (num > -10) { ... } // 展开代码块 1 if (num > -100) { ... } // 展开代码块 2 // ... }
-
MIN_VALUE
处理:直接使用原值(不取负),避免溢出。 -
符号位独立:负数长度 = 数字位数 + 1,非负数长度 = 数字位数。
2.5.固定9次循环:
固定 9 次循环的设计本质是用空间换时间:
1.空间:通过循环展开轻微增加代码体积(约 9 个条件块)
2.时间:获得显著性能提升:
-
消除循环控制开销
-
优化分支预测
-
实现高效内联
-
利用 CPU 流水线和缓存局部性
结语:于细微处见真章
我们今天只是探索了 Integer
源码的冰山一角,但已经能感受到其中蕴含的巨大智慧:
-
IntegerCache
教会我们:缓存高频数据,用空间换时间,这是提升性能最直接有效的手段之一。 -
stringSize
告诉我们:条条大路通罗马,但有些路就是快得多。要敢于用看似“笨拙”但极度高效的if-else
替代复杂的数学运算。
代码是冰冷的,但优秀的代码背后,是工程师对性能的极致追求和对计算机体系的深刻理解。
下次当你再写下 Integer i = 123;
时,希望你能会心一笑,想起它背后那些默默无闻但功勋卓著的“骚操作”。
有空多逛逛 JDK 源码,你会发现一个全新的、充满惊喜的世界!