写在前面
|本系列主要是笔者本人阅读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 的小伙伴都知道,对象(如 Integer 类 new 出对象)使用 equals 方法来判断是否相等,== 操作符实际上是用来判断这两个引用是否指向同一个对象,即存在同一个内存地址中。
在这个前提下,重新看这段代码,可以发现在 test3 与 test4 的声明中,new 了两个不同的 Integer 对象,也就是说这两者指向两个不同的对象,因此并不相等。但 test1 和 test2 为什么又相等呢?这样的结果意味着它们指向同一个对象,在对它们赋值的时候发生了什么导致了这种结果呢?
要解释这个问题,必须先了解 Java 的自动装箱机制:当程序将一个基本类型(如 int )赋值给一个包装类(如 Integer),JVM 会自动将其转化为包装类,而这个转化过程使用的是 valueOf() 方法。
也就是说,Java 会自动将
Integer test1 = 1;
转换为
Integer test1 = Integer.valueOf(1);
也就是说,如果想要知道给 test1 和 test2 赋值时发生了什么,就需要先知道 valueOf() 方法中发生了什么:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
上列代码中使用到的 IntegerCache 是 Integer 中的一个静态内部类,用于缓存一定范围(即 IntegerCache.low -> IntegerCache.high)内的 Integer 对象,结合 valueOf 的具体实现,可以看到当 i 的范围在缓存范围内时,对其装箱(即调用 valueOf 方法)将会返回 IntegerCache 中缓存的对象,而默认的缓存范围为-128~127。讲到这里,答案已经呼之欲出了。
小结:
在对 test1 与 test2 初始化时,由于直接将 int 值赋给 Integer 触发了 Java 的自动装箱机制,调用 valueOf() 方法,又由于赋的值 1 在 IntegerCache 缓存范围内,所以最终两个引用都指向了同一个对象->即 IntegerCache 中缓存的保存了 1 的 Integer 对象,因此 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);
}
-
疑惑出现
这段代码读起来很容易,但读完之后估计不少小伙伴都会有疑惑,为什么开头要对 Integer.MIN_VALUE 做特殊处理呢?其它 int 值都可以直接通过 getChars() 得到答案,为什么它不行呢?想要知道问题的答案,就需要知道 getChars() 的实现方式。
-
探秘 getChars()
-
首先我们来看看如果将 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; } }
这段代码看起来就复杂多啦,很多小伙伴可能已经脑壳疼了,这里我用文字简单描述一下这段代码的主要逻辑:
-
当 i >= 65536 时,每次循环从数字尾部取出两个数字,并从后往前插入 buf 数组
-
当 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() 中。
-
-
探秘 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() 等方法。
写在最后
码字不易,如果觉得这篇文章不错或者对你有帮助,请不要吝惜手中的赞哦~