JDK21 getChars 源码深度剖析:整数转字节的极致艺术

嘿,老铁们!又见面了。

今天我们不聊别的,就死磕一个方法——java.lang.Integer.getChars(int i, int index, byte[] buf)

你可能会说:“一个 intbyte 数组,能有多复杂?” 别急,看完这篇文章,你可能会想给写这段代码的大神献上膝盖。这短短十几行代码,简直就是一本微型《性能优化圣经》,里面塞满了算法、巧思和对计算机底层原理的深刻理解。

咱们要分析的源码就是这个版本:

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',
        } ;

一:设计哲学:为什么它要这么“怪”?

在分析代码前,我们得先搞懂,它为啥要设计成这样?

  1. 性能,性能,还TM是性能! 这个方法不是给你平时 System.out.println() 用的。它的目标场景是那些对性能要求到极致的地方,比如网络编程(像 Netty 这样的框架)、日志系统(像 Log4j2 里的无GC日志)、序列化等。在这些场景里,每秒钟可能有成千上万次调用,任何一点性能损耗都会被无限放大。所以,它的首要目标就是:快!快到没朋友!

  2. 拒绝创建新对象,跟GC说拜拜! 为什么是传入一个 byte[] buf,而不是自己返回一个?因为 new 对象是Java里相对昂贵的操作,更要命的是,它会给垃圾回收(GC)带来压力。在高性能场景下,GC 的一次小小停顿(STW, Stop-The-World)都可能是致命的。所以,这个方法的设计就是:“你把“盘子”(buf)给我,我负责把“菜”(数字字节)给你装好就行了,我绝不自己造盘子。” 这就是所谓的“零GC”设计理念。

  3. 从后往前写:神之一手的“倒行逆施” 你注意到 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,6482,147,483,647。如果你试图对 Integer.MIN_VALUE 取正(-i),会发生什么?它会溢出!因为正数的最大值 Integer.MAX_VALUEMIN_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?因为这样可以保证循环体内至少能抠出两位数。一次处理两位,循环次数直接减半,这是一种简单有效的“循环展开”,能减少循环本身的判断开销。

  • 神奇的 rr = (q * 100) - i; 这行代码是干啥的? 假设 i = -123q = -123 / 100,结果是 -1r = (-1 * 100) - (-123),结果是 23。 它巧妙地计算出了 i 的后两位数的正值!这比取模运算 % 在某些JVM和CPU上可能更快。

  • 真正的“黑科技”:DigitTensDigitOnes 这是典型的 “空间换时间”。JDK 预先创建了两个数组: DigitTens 存了 0099 这些数的“十位”字符。 DigitOnes 存了 0099 这些数的“个位”字符。

    比如,当 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?

回顾这十几行代码,就像看了一场精彩的魔术表演,背后全是你看不到的准备和巧思。

  1. 全局思维:为了解决 Integer.MIN_VALUE 这个小小的边角问题,采用了全局负数化的优雅方案。

  2. 算法先行:用“一次处理两位”和“倒序填充”的算法,从根本上减少了操作步骤。

  3. 空间换时间:毫不吝啬地使用 DigitTensDigitOnes 两个查找表,将计算变成了简单的内存访问。

  4. 死磕细节:用位移代替乘法(在其他版本中),用减法代替取模,不放过任何一个可以压榨性能的细节。

  5. GC-Free:通过外部传入 buffer 的方式,实现了零对象创建,对高性能应用极度友好。

so,下次当你在抱怨“CRUD”写得没意思的时候,不妨点开JDK的源码看一看。你会发现,即便是最基础的 Integer 类,也充满了这样值得我们反复品味和学习的“神仙代码”。

向下扎根,才能向上生长。把这些基础打扎实了,你的技术大厦才能建得更高、更稳。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nextera-void

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值