嘿,老铁们!又见面了。
今天我们不聊别的,就死磕一个方法——java.lang.Integer.getChars(int i, int index, byte[] buf)。
你可能会说:“一个 int 转 byte 数组,能有多复杂?” 别急,看完这篇文章,你可能会想给写这段代码的大神献上膝盖。这短短十几行代码,简直就是一本微型《性能优化圣经》,里面塞满了算法、巧思和对计算机底层原理的深刻理解。
咱们要分析的源码就是这个版本:
static int getChars(int i, int index, byte[] buf) {
// q: 商(quotient), r: 余数
int q, r;
int charPos = index;
boolean negative = i < 0;
if (!negative) {
// 转为负数处理。
i = -i;
}
// Generate two digits per iteration
while (i <= -100) {
q = i / 100;
r = (q * 100) - i;
i = q;
buf[--charPos] = DigitOnes[r];
buf[--charPos] = DigitTens[r];
}
// We know there are at most two digits left at this point.
buf[--charPos] = DigitOnes[-i];
if (i < -9) {
buf[--charPos] = DigitTens[-i];
}
if (negative) {
buf[--charPos] = (byte)'-';
}
return charPos;
}
static final byte[] DigitOnes = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
} ;
static final byte[] DigitTens = {
'0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
'1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
'2', '2', '2', '2', '2', '2', '2', '2', '2', '2',
'3', '3', '3', '3', '3', '3', '3', '3', '3', '3',
'4', '4', '4', '4', '4', '4', '4', '4', '4', '4',
'5', '5', '5', '5', '5', '5', '5', '5', '5', '5',
'6', '6', '6', '6', '6', '6', '6', '6', '6', '6',
'7', '7', '7', '7', '7', '7', '7', '7', '7', '7',
'8', '8', '8', '8', '8', '8', '8', '8', '8', '8',
'9', '9', '9', '9', '9', '9', '9', '9', '9', '9',
} ;
一:设计哲学:为什么它要这么“怪”?
在分析代码前,我们得先搞懂,它为啥要设计成这样?
-
性能,性能,还TM是性能! 这个方法不是给你平时
System.out.println()用的。它的目标场景是那些对性能要求到极致的地方,比如网络编程(像 Netty 这样的框架)、日志系统(像 Log4j2 里的无GC日志)、序列化等。在这些场景里,每秒钟可能有成千上万次调用,任何一点性能损耗都会被无限放大。所以,它的首要目标就是:快!快到没朋友! -
拒绝创建新对象,跟GC说拜拜! 为什么是传入一个
byte[] buf,而不是自己返回一个?因为new对象是Java里相对昂贵的操作,更要命的是,它会给垃圾回收(GC)带来压力。在高性能场景下,GC 的一次小小停顿(STW, Stop-The-World)都可能是致命的。所以,这个方法的设计就是:“你把“盘子”(buf)给我,我负责把“菜”(数字字节)给你装好就行了,我绝不自己造盘子。” 这就是所谓的“零GC”设计理念。 -
从后往前写:神之一手的“倒行逆施” 你注意到
buf[--charPos]了吗?它是从index(通常是数组末尾)往前填充的。这是为什么? 因为一个整数,比如12345,你不把它算完,是不知道它到底有几位数的。常规操作是先算出来,再从前往后填。但大神们不这么干,他们直接从后往前填,最后返回填充结束的位置。这样就省去了预计算长度和最后反转数组的步骤,一气呵成,干净利落!
图解“倒行逆施”填充法:
假设我们要把 123 填入一个长度为5的 byte 数组,index 是 5。

二、源码逐行“解剖”,全是细节!
好了,热身结束,上正菜!
1:妙到毫巅的“负数大法”
boolean negative = i < 0;
if (!negative) {
i = -i;
}
这是什么操作?为啥要把正数也变成负数来处理?
答案是为了优雅地处理 Integer.MIN_VALUE。
Integer 的范围是 -2,147,483,648 到 2,147,483,647。如果你试图对 Integer.MIN_VALUE 取正(-i),会发生什么?它会溢出!因为正数的最大值 Integer.MAX_VALUE 比 MIN_VALUE 的绝对值小 1。Math.abs(Integer.MIN_VALUE) 仍然是它自己(一个负数)。
so,为了统一处理逻辑,避免溢出这个天坑,大神们干脆把所有数都变成负数。因为负数范围比正数大1,所以任何正数转成负数都不会溢出。这一招,四两拨千斤,实在是高!
2:“双龙出海”的循环体
// Generate two digits per iteration
while (i <= -100) {
q = i / 100;
r = (q * 100) - i;
i = q;
buf[--charPos] = DigitOnes[r];
buf[--charPos] = DigitTens[r];
}
这是整个算法的心脏,也是最秀的地方。
-
一次处理两位:为啥是
i <= -100?因为这样可以保证循环体内至少能抠出两位数。一次处理两位,循环次数直接减半,这是一种简单有效的“循环展开”,能减少循环本身的判断开销。 -
神奇的
r:r = (q * 100) - i;这行代码是干啥的? 假设i = -123。q = -123 / 100,结果是-1。r = (-1 * 100) - (-123),结果是23。 它巧妙地计算出了i的后两位数的正值!这比取模运算%在某些JVM和CPU上可能更快。 -
真正的“黑科技”:
DigitTens和DigitOnes这是典型的 “空间换时间”。JDK 预先创建了两个数组:DigitTens存了00到99这些数的“十位”字符。DigitOnes存了00到99这些数的“个位”字符。比如,当
r = 23时:DigitOnes[23]直接就取出'3'的字节。DigitTens[23]直接就取出'2'的字节。这避免了任何计算!不用
r / 10,也不用r % 10。就是查表!在CPU眼里,从内存(尤其是一级缓存L1 Cache里)取个数,比做一次整数除法和取模运算快到不知道哪里去了。图解查表法:
-

3:利落的“收尾工作”
// We know there are at most two digits left at this point.
buf[--charPos] = DigitOnes[-i];
if (i < -9) {
buf[--charPos] = DigitTens[-i];
}
循环结束时,i 的值肯定在 [-99, -1] 之间。
-
buf[--charPos] = DigitOnes[-i];:先把个位填上。注意这里用了-i把负数转正。 -
if (i < -9):如果i小于 -9(也就是在[-99, -10]之间),说明还有一位十位数,再用DigitTens把它填上。
这里没有循环,只有一次判断,代码执行路径清晰,分支预测友好,效率极高。
最后,如果一开始是负数,就把负号加上。完美收官。
总结:从 getChars 我们got what?
回顾这十几行代码,就像看了一场精彩的魔术表演,背后全是你看不到的准备和巧思。
-
全局思维:为了解决
Integer.MIN_VALUE这个小小的边角问题,采用了全局负数化的优雅方案。 -
算法先行:用“一次处理两位”和“倒序填充”的算法,从根本上减少了操作步骤。
-
空间换时间:毫不吝啬地使用
DigitTens和DigitOnes两个查找表,将计算变成了简单的内存访问。 -
死磕细节:用位移代替乘法(在其他版本中),用减法代替取模,不放过任何一个可以压榨性能的细节。
-
GC-Free:通过外部传入
buffer的方式,实现了零对象创建,对高性能应用极度友好。
so,下次当你在抱怨“CRUD”写得没意思的时候,不妨点开JDK的源码看一看。你会发现,即便是最基础的 Integer 类,也充满了这样值得我们反复品味和学习的“神仙代码”。
向下扎根,才能向上生长。把这些基础打扎实了,你的技术大厦才能建得更高、更稳。
1836

被折叠的 条评论
为什么被折叠?



