JDK源码阅读笔记|Integer类中那些有趣的设计

436 阅读7分钟

写在前面

|本系列主要是笔者本人阅读JDK8源码的笔记,整理出其中比较有意思的部分分享出来。由于笔者能力有限,如有不妥之处,欢迎各位大佬评论或私信指出~

|本文共2486字,预计阅读时间7分钟。

1.探秘 IntegerCache

这里先贴上一段测试代码,不运行它,你能看出程序会输出什么吗?

Integer test1 = 1;
Integer test2 = 1;
Integer test3 = new Integer(1);
Integer test4 = new Integer(1);
if(test1 == test2) System.out.println("test1 == test2");
else System.out.println("test1 != test2");
if(test3 == test4) System.out.println("test3 == test4");
else System.out.println("test3 != test4");

程序运行的结果是:

test1 == test2
test3 != test4

有趣的事情出现了,从测试代码中我们可以看出四个 Integer 对象的值都为1,为什么 test1 等于 test2 ,test3 则不等于 test4 呢?

熟悉 Java 的小伙伴都知道,对象(如 Integernew 出对象)使用 equals 方法来判断是否相等,== 操作符实际上是用来判断这两个引用是否指向同一个对象,即存在同一个内存地址中。

在这个前提下,重新看这段代码,可以发现在 test3test4 的声明中,new 了两个不同的 Integer 对象,也就是说这两者指向两个不同的对象,因此并不相等。但 test1test2 为什么又相等呢?这样的结果意味着它们指向同一个对象,在对它们赋值的时候发生了什么导致了这种结果呢?

要解释这个问题,必须先了解 Java 的自动装箱机制:当程序将一个基本类型(如 int )赋值给一个包装类(如 Integer),JVM 会自动将其转化为包装类,而这个转化过程使用的是 valueOf() 方法。

也就是说,Java 会自动将

Integer test1 = 1;

转换为

Integer test1 = Integer.valueOf(1);

也就是说,如果想要知道给 test1test2 赋值时发生了什么,就需要先知道 valueOf() 方法中发生了什么:

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

上列代码中使用到的 IntegerCacheInteger 中的一个静态内部类,用于缓存一定范围(即 IntegerCache.low -> IntegerCache.high)内的 Integer 对象,结合 valueOf 的具体实现,可以看到当 i 的范围在缓存范围内时,对其装箱(即调用 valueOf 方法)将会返回 IntegerCache 中缓存的对象,而默认的缓存范围为-128~127。讲到这里,答案已经呼之欲出了。

小结:

在对 test1test2 初始化时,由于直接将 int 值赋给 Integer 触发了 Java 的自动装箱机制,调用 valueOf() 方法,又由于赋的值 1IntegerCache 缓存范围内,所以最终两个引用都指向了同一个对象->即 IntegerCache 中缓存的保存了 1Integer 对象,因此 test1 == test2

那么,聪明的你,可以说出这段代码的输出结果吗?

Integer test1 = 288;
Integer test2 = 288;
if(test1 == test2) System.out.println("test1 == test2");
else System.out.println("test1 != test2");

IntegerCache具体实现:

/**
 * Cache to support the object identity semantics of autoboxing for values between
 * -128 and 127 (inclusive) as required by JLS.
 *
 * The cache is initialized on first usage.  The size of the cache
 * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
 * During VM initialization, java.lang.Integer.IntegerCache.high property
 * may be set and saved in the private system properties in the
 * sun.misc.VM class.
 */

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() {}
}

2.探秘 toString()

此处直接贴上Integer类中***toString()***的实现(中文注释为笔者添加):

public static String toString(int i) {
	// 奇怪的代码出现了,为什么要对MIN_VALUE做特殊处理??
    if (i == Integer.MIN_VALUE)
        return "-2147483648";
    // stringSize()用于获取 i 的位数,size用于确定返回值的长度,若i为负数则多加一位用于存放负号'-'
    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
    // 创建字符数组buf用于存放转化结果
    char[] buf = new char[size];
    // 将int类型转化为char[]的逻辑处理部分
    getChars(i, size, buf);
    // 返回结果
    return new String(buf, true);
}
  1. 疑惑出现

    这段代码读起来很容易,但读完之后估计不少小伙伴都会有疑惑,为什么开头要对 Integer.MIN_VALUE 做特殊处理呢?其它 int 值都可以直接通过 getChars() 得到答案,为什么它不行呢?想要知道问题的答案,就需要知道 getChars() 的实现方式。

  2. 探秘 getChars()

    1. 首先我们来看看如果将 Integer.MIN_VALUE 作为参数 i 传入 getchars() 会发生什么。

      程序报了数组越界异常

      Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2147442688 at Main.getChars(Main.java:36) at Main.main(Main.java:7)

      这又是为什么呢?光看报错还不够,我们再来看看 getChars() 的实现。

      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); 相当于对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;
          }
      }
      

    这段代码看起来就复杂多啦,很多小伙伴可能已经脑壳疼了,这里我用文字简单描述一下这段代码的主要逻辑:

    1. i >= 65536 时,每次循环从数字尾部取出两个数字,并从后往前插入 buf 数组

    2. i < 65536 时,每次循环取一个数字并插入 buf 数组

    其中使用了一些位运算优化性能,所以读起来比较困难,但主要逻辑就是上述两条,可以看到代码里可能发生数组越界的地方只有 buf 数组。所以我们重点关注 buf 数组下标的变化。

    数组的下标由 charPos 控制,charPos又是由传入的参数 index 初始化,index 是什么呢?重看 toString() 的代码

    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);

    getChars(i, size, buf);

    哦~原来 index 就是通过 stringSize() 获取的字符串长度,看来问题可能是出现在 stringSize() 中。

  3. 探秘 stringSize()

    话不多说,直接贴上源码

    final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,
                                      99999999, 999999999, Integer.MAX_VALUE };
    
    // Requires positive x
    static int stringSize(int x) {
         for (int i=0; ; i++)
            if (x <= sizeTable[i])
                return i+1;
    }
    

    看完这段代码,答案就呼之欲出了。

    stringSize() 的实现方式是通过传入的值与 sizeTable 中储存的值对比来获取 int 值的位数,而 sizeTable() 中使用了 Integer.MAX_VALUE 作为最大值,对于Java 有一定了解的同学应该都知道,Java 中使用四个 字节来存储整型数据,按理最大正数和最小负数的绝对值应该要相同,但由于还需要额外代表零,所以Integer.MAX_VALUE 实际上要比 Integer.MIN_VALUE 要小一。

    也就是:Integer.MIN_VALUE = -2147483648

    Integer.MAX_VALUE = 2147483647

    换句话说,由于 stringSize() 方法的特殊实现,需要传入正数,但无法找到与 MIN_VALUE 对应的正数,所以计算出的 size 是错误的,整体错误的分析请看下方小结。

小结

在 Integer 类的 toString() 方法中为什么要对 MIN_VALUE 做特殊处理?

答:由于 stringSize() 方法特殊的实现(直接与9、99、999、9999...进行比较获取位数),要求传正数作为参数,又无法找到与 Integer.MIN_VALUE 对应的正数(太大)。经测试,强行将其转化为正数是无法生效的,依旧是原值,在 stringSize() 的实现中,循环第一次就直接结束,也就是说计算出的 size 为1,最终创建出只能存储一个字符的数组保存结果,而在 getchars() 方法中循环时其实际长度远大于1,会导致数组越界异常,故单独对其处理。

3.探秘 getInteger()

getInteger() 是一个很容易被初学者误解的方法,传入的参数是 String ,返回值是 Integer ,不仔细看会混淆它和 parseInt() 的作用,以为是用来将 String 值转化为 Integer 的,笔者当年就被坑惨了,如果你也有这样的经历或者感到好奇,不妨一起来看看它的作用吧~

照例先贴上源码

public static Integer getInteger(String nm) {
        return getInteger(nm, null);
}
public static Integer getInteger(String nm, Integer val) {
    String v = null;
    try {
    	//根据 nm 获取系统属性
        v = System.getProperty(nm);
    } catch (IllegalArgumentException | NullPointerException e) {
    }
    if (v != null) {
        try {
        	// decode() 方法用于将 String 转化为 Integer,类似于 parseInt() ,区别是可以自动解析进制
            return Integer.decode(v);
        } catch (NumberFormatException e) {
        }
    }
    return val;
}

可以看到 getInteger() 方法也是将 String 值转化为 Integer ,不过并不是转化传入的 String 参数,而是通过该参数获取对应的系统属性,再将其转化为 Integer 返回。

依笔者的看法,getInteger() 的命名很糟糕,非常容易产生歧义,通常我们传入的字符串都没有对应的系统属性,因此返回的是 null ,发生错误还能比较容易发现。而与之类似却更糟糕的是 Boolean 类中的 getBoolean() 方法,作用与 getInteger() 类似,区别是返回的是 Boolean 值,没有对应的系统属性时会返回 false ,如果误用了此方法,debug 起来会非常困难。

小结

getInteger()getBoolean() 并不是用于转化字符串的方法,如果有这样的需求请调用 valueOf()parseInt()parseBoolean() 等方法。

写在最后

码字不易,如果觉得这篇文章不错或者对你有帮助,请不要吝惜手中的赞哦~