代码改变世界

【程序员】你知道 if(a<b) 和 if (a - b < 0) 的区别吗?

2020-01-02 10:11  兔子托尼啊  阅读(285)  评论(0编辑  收藏

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向上兼容的一种方式。

谢谢你们看到了这里,感谢🙏。新的一年祝升职加薪。

关注公众号回复视频、书籍关键字更多免费资料等着你。