2020年1月1号全面小康社会已经来了,2020年了第一批90后也已经30岁了。在此元旦,新的一年托尼祝大家代码永无bug,新的一年升职且加薪。
好了,言归正传,其实我们在编码的过程中,有的时候真是不是特意写bug的,我们也是想好好写代码的。
只不过有的时候不熟悉源码,不小心就踩入坑中,而且摔个底朝天,程序员真的好难🤯。
序幕
上代码if (a < b)
和if (a - b < 0)
?这不就是两个参数的比较吗?难道还有什么诡异之处?应该是一样的呀。
你觉得上面代码会打印什么?先别看下面的内容,第一感觉是什么样??
字节码
从字面理解就是比较a和b的大小,按道理执行速度、字面意思都一样。其实机器是死的,它不会按照人类的理解去比较的。
有的时候我们需要通过字节码去思考到底程序是怎么执行的?
// access flags 0x1
public test()V
@Lorg/junit/Test;()
L0
LINENUMBER 22 L0
LDC 2147483647
ISTORE 1
L1
LINENUMBER 23 L1
LDC -2147483648
ISTORE 2
L2
LINENUMBER 24 L2
ILOAD 1
ILOAD 2
IF_ICMPGE L3
L4
LINENUMBER 25 L4
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "a < b"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
LINENUMBER 27 L3
FRAME APPEND [I I]
ILOAD 1
ILOAD 2
ISUB
IFGE L5
L6
LINENUMBER 28 L6
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "a - b < 0"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L5
LINENUMBER 30 L5
FRAME SAME
RETURN
L7
托尼带着大家读字节码。其实字节码很简单的,看上面截图红色框。挑选你熟悉的看LINENUMBER、LDC、ISTORE、IF_ICMPGE、ISUB
- LINENUMBER 代表的行号
- LDC 代表JVM 采用 LDC 指令将常量压入栈中
- ISTORE 将一个数值从操作数栈存储到局部变量表,还有``istore、istore_<n>、lstore、lstore_<n>等等。
- IF_ICMPGE 比较栈顶两int型数值大小,当结果大于等于0时跳转
- ISUB 将栈顶两int型数值相减并将结果压入栈顶
以上大概讲了下JVM的指令,希望对你有点帮助。
再谈溢出感知代码
@Test
public void test() {
int a = Integer.MAX_VALUE;
int b = Integer.MIN_VALUE;
if (a < b) {
System.out.println("a < b");
}
if (a - b < 0) {
System.out.println("a - b < 0");
}
}
这段代码只能输出 a-b<0 ,然后a<b 是不会打印出来的。
在JDK源码中为什么ArrayList 中用if (a - b < 0)
而不用if (a < b)
?这个涉及到溢出感知代码。
我们来分析List 如何扩容的grow 方法?
/**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) {
modCount++;
// Overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// Overflow-conscious code
// /把当前数组的长度赋给oldCapacity
int oldCapacity = elementData.length;
// 新的数组容量=老的数组长度的1.5倍,oldCapacity >> 1 相当于除以2
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新的数组长度小于传入的参数,那么当前新的
// 数组的长度则为传入进来的长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 新的数组的长度和 数组中最大值也就是(ArrayList 的数组最大值 Integer.MAX_VALUE - 8)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 传入的旧的数组容量如果大于数组中最大默认值
// 则取最大的默认值
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
上面的注释也加了,下面来说重点。 oldCapacity 这个值如果是非常接近Integer.MAX,那么执行这一句话
int newCapacity = oldCapacity + (oldCapacity >> 1);
,那么此刻newCapacity 是一个负数 ,如果JDK的代码是这么判断大小
if (newCapacity<minCapacity)
这种写法,那么这个逻辑判断条件永远是TRUE,显然和期望是相反的。
溢出
以下代码是ArrayList 到数组扩容源码, 我们来好好分析何为溢出感知代码?英文叫做 overflow-conscious code。
在计算机中当整型变量的值为 Integer.MAX_VALUE(2147483647)时,继续累加一个正的整型值,就会变成一个负数,这种情况称之为上溢。
当整型变量的值为 Integer.MIN_VALUE(-2147483648) 时,继续累加一个负的整型值,就会变成一个非负数,这种情况称之为下溢。
好比水缸就能只能盛这么多水,你继续添加水,水就会自然而然到溢出来了。
结论
在读源码过程中的我们要考虑到代码的健壮性,逻辑到衍生性。希望这篇博文能让你学到一点东西,哪怕一点点,我就觉得挺开心的。
衍生
在AarryList 的源码中private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
,为什么要再减去 8 呢?
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
如上所诉,有些JVM保留头部信息怕溢出了,所以设置了减8 。这也是JVM向上兼容的一种方式。
谢谢你们看到了这里,感谢🙏。新的一年祝升职加薪。
关注公众号回复视频、书籍关键字更多免费资料等着你。