掘金 后端 ( ) • 2024-04-20 16:48

知识点

  • 自动装箱和拆箱
  • IntegerCache机制
  • toString()实现算法优化

从一道面试题开始

Integer a = 100;
int b = 100;
if (a == b) {
    System.out.println("a == b");
} else {
    System.out.println("a != b");
}

聪明的你应该马上可以知道答案了,输出就是 a == b

自动装箱和自动拆箱

那你可以说说自动装箱和拆箱是怎么回事吗?

image.png

把代码编程成字节码,很容易就可以看出来。在这里推荐Compiler Explorer这个小工具,在浏览器直接搜索就可以了。

当代码中声明Integer a = 100; 实际上在编译成的时候会自动转换成 Integer a = Integer.valueOf(100); 这个过程就是自动装箱

当Integer类型和基础类型int在比较的时候,Integer类型会自动转换为int类型。a == b 实际上编译成 a.intValue() == b, 这个过程就是自动拆箱。 这个过程就是自动拆箱

在上面的截图中,可以看过这个过程。

Integer.valueof() 方法

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

在自动装箱过程中,会调用Integer.valueOf()方法,首先会检查需要变量i在不在IntegerCache范围内,如果有直接返回,没有则new一个出来。

Integer.intValue()

public int intValue() { return value; }

该方法非常简单,直接返回value。

IntegerCache

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

上述代码的实现逻辑比较简单:

1、比较IntegerCache的取值范围,low是-128,high是127,其中high可以通过修改系统变量去设置。

2、创建一个cache数组,依次遍历创建Integer对象放到数组之中。

接着继续看看下面的程序会输出什么结果

Integer a = 128;
int b = 128;
if (a == b) {
    System.out.println("a == b");
} else {
    System.out.println("a != b");
}

Integer c = 128;
if (a == c) {
    System.out.println("a == c");
}else {
    System.out.println("a != c");
}

输出结果

a == b
a != c

解释一下:

a和b在比较的时候,a是Integer类型,b是int类型,a在编译的时候会自动拆箱,实际是 a.intValue() == b, 两个int之间的比较,实际上比较是数值,所以是相等的。

a和c在时间的时候,a和c都是Integer类型,不会发生自动拆箱,而是两个对象之间的比较,所以是不相等的。

趁热打铁,看看下面这两组

Integer d = 100;
Integer e = 100;
if (d == e) {
    System.out.println("d == e");
}else {
    System.out.println("d != e");
}

Integer m = new Integer(100);
Integer n = new Integer(100);
if (m == n) {
    System.out.println("m == n");
}else {
    System.out.println("m != n");
}

输出结果

d == e
m != n

解释

为什么d和e两个对象之间比较,输出结果是相等呢,这个就要说到IntegerCache机制了,在声明d和e的时候,编译时会自动装箱,也就是实际上时 Integer e = Integer.valueOf(100). 在默认情况下,IntegerCache的范围在-128-127. 所以d和e时相同的。而m和n强制使用new Integer去初始化变量,m和n返回的是不同的对象,所以是不相等的。

小结

1、自动装箱是发生在声明Integer变量时,且初始化表达式是个常量,会调用Integer.valueOf()方法,创建对象有IntegerCache机制。

2、自动拆箱时发生在Integer类型和int类型比较的时候, 会自动调用Integer.intValueOf()方法。

3、IntegerCache缓存机制默认范围是-128-127。

Integer.toString()实现

public static String toString(int i, int radix) {
    if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
        radix = 10;

    /* Use the faster version */
    if (radix == 10) {
        return toString(i);
    }

    char buf[] = new char[33];
    boolean negative = (i < 0);
    int charPos = 32;

    if (!negative) {
        i = -i;
    }

    while (i <= -radix) {
        buf[charPos--] = digits[-(i % radix)];
        i = i / radix;
    }
    buf[charPos] = digits[-i];

    if (negative) {
        buf[--charPos] = '-';
    }

    return new String(buf, charPos, (33 - charPos));
}

这段代码定义了一个方法,用于将长整型(long)数字转换为指定基数(radix)的字符串表示

1、基数检查,只有是2-36进制

2、10进制特殊处理

3、声明一个char数据,为什么是33呢,int类型是32位,加上'-',所以一共是33

4、处理负数

5、while循环,计算余数和商,直到最后一个商小于基数的时候退出循环

6、处理最后一个商

7、处理负数

8、将char数组转换位String对象

public static String toString(int i) {
    if (i == Integer.MIN_VALUE)
        return "-2147483648";
    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
    char[] buf = new char[size];
    getChars(i, size, buf);
    return new String(buf, true);
}
static void getChars(int i, int index, char[] buf) {
    int q, r;
    int charPos = index;
    char sign = 0;

    if (i < 0) {
        sign = '-';
        i = -i;
    }

    // Generate two digits per iteration
    while (i >= 65536) {
        q = i / 100;
    // really: r = i - (q * 100);
        r = i - ((q << 6) + (q << 5) + (q << 2));
        i = q;
        buf [--charPos] = DigitOnes[r];
        buf [--charPos] = DigitTens[r];
    }

    // Fall thru to fast mode for smaller numbers
    // assert(i <= 65536, i);
    for (;;) {
        q = (i * 52429) >>> (16+3);
        r = i - ((q << 3) + (q << 1));  // r = i-(q*10) ...
        buf [--charPos] = digits [r];
        i = q;
        if (i == 0) break;
    }
    if (sign != 0) {
        buf [--charPos] = sign;
    }
}

在上述代码中,比较引人注意的是有两个小片段

// Generate two digits per iteration
while (i >= 65536) {
    q = i / 100;
// really: r = i - (q * 100);
    r = i - ((q << 6) + (q << 5) + (q << 2));
    i = q;
    buf [--charPos] = DigitOnes[r];
    buf [--charPos] = DigitTens[r];
}

这里使用了两个技巧。

第一个不再是用除以10去计算商,而是使用除以100,一次循环中计算两次

第二个是计算余数,不是使用%运算,也不是 r = i - (q * 100); 而是使用 r = i - ((q << 6) + (q << 5) + (q << 2));

这里没有使用乘法,而是使用左移和加法代替。

让我们逐步解释这个表达式:

q << 6:这是将 q 左移 6 位,相当于 q 乘以 (2^6 = 64)。

q << 5:这是将 q 左移 5 位,相当于 q 乘以 (2^5 = 32)。

q << 2:这是将 q 左移 2 位,相当于 q 乘以 (2^2 = 4)。

这三个位移操作分别模拟了 q 乘以 64、32 和 4。加在一起,它们就模拟了 q 乘以 100(因为 (64 + 32 + 4 = 100))。

再来看另外一个片段

for (;;) {
        q = (i * 52429) >>> (16+3);
        r = i - ((q << 3) + (q << 1));  // r = i-(q*10) ...
        buf [--charPos] = digits [r];
        i = q;
        if (i == 0) break;
    }

首先,在计算商的时候并不是一个常规的除法运算。看到 q = (i * 52429) >>> (16+3); 这里并没有使用除法,代替的是 乘法 和 右移运算。

这行代码计算了i除以10的商。这里使用了52429这个特殊的数。这个数是通过100000 / 10(即10000除以10,再左移5位)得到的。这样,i * 52429相当于i乘以10000再左移5位。然后,通过无符号右移操作>>>来除以(2^19) (即右移19位),相当于再除以(2^14)(即65536)。这样,q就得到了i除以10的商的近似值。

由上面两个小片段中可以知道,在库函数级别的代码中,对代码和性能的要求非常高。

总结

1、自动装箱和拆箱 - 是一种语法糖,在编译器就实现。

2、IntegerCache机制 - 一种缓存机制,节约内存,避免对象重复创建。

3、toString()实现算法优化 - 使用位移算法替换乘法运算,使用乘法运算替换除法运算。