[jdk源码阅读系列]overflow-conscious code

本文部分转载,原文链接

overflow-conscious code_lijianqingfeng的专栏-CSDN博客  https://blog.csdn.net/lijianqingfeng/article/details/107912190

背景

在jdk源码中,会有很多考虑了溢出而编写的代码,这些代码前会有注释:"overflow-conscious code",说明下面这段代码是考虑了溢出的情况的。最经典的代码就是ArrayList里的grow方法(因为网上能搜到好多对于这个方法进行讨论的文章和问题,可能大家都在研究ArrayList源码),我是看ByteArrayOutputStream源码的时候考虑这个问题的,但也都是grow方法,内容几乎一样。

代码如下(ArrayList.grow):

    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
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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 static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

分析:

这里说考虑了溢出的情况,是如何考虑了溢出呢?考虑了哪个变量的溢出呢?在溢出的情况下是怎么应对的呢?

小朋友瞬间出现了上面这些问号,于是开始探究。

代码功能:

我觉得首先要知道这段代码的目的是什么,才能知道它需要对什么变量做溢出管理。

简单来说,这段代码的功能是对ArrayList的存储进行扩容,扩大为原来的1.5倍。那么在计算扩展后的容量时就有可能会溢出。

另一个,传入的minCapacity其实是有上下文信息的,肯定是在一个限定范围内,不然需要考虑的兼容情况会更复杂。(当然也是这个给我的分析过程产生了最大的困扰)

神奇的补码

在分析代码之前,先需要知道一些补码的知识,溢出与它是息息相关的。本文不细说补码的知识了,网上很多文章介绍原码、反码、补码以及为什么计算机要选择补码。

补码在表示有符号数的时候,最高位用来当做符号位,0代表正数,1代表负数。

java中的int用了32位,最高位为符号位,所以表示范围是[-2^{31}, 2^{31}-1],最小值为0x80000000,最大值为0x7fffffff。最大值加1就会变成最小值。其实,int的这些数字看起来很像是一个圆环,如下图所示:

 

从0开始,逆时针增大,到最大值的时候,再加1就变为最小值,然后再逆时针增大到0。

考虑溢出的代码含义

有了上面的知识,我们看一下代码中到底真正代表什么含义。

这里有一个数学问题:a<b  和   a-b<0代表相同的含义吗?答案是:在计算机中不同,因为数字用的是有限位的补码,也正是因此才会有考虑溢出的代码。(Stack Overflow上有一个相关问题:https://stackoverflow.com/questions/33147339/difference-between-if-a-b-0-and-if-a-b

        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

这时候我们看上面的代码,这个已经不代表newCapacity大于MAX_ARRAY_SIZE了。那么有没有统一的说法能代表它的含义呢?不知道,但基于上面的圆环,我给它赋予了一个含义。

 

基于圆环,在逆时针上假设有两个点A、B,如果A领先B不超过半个圆,那么A-B>0,否则A-B<0

那么,newCapacity - MAX_ARRAY_SIZE > 0  也就是newCapacity 在图中的左侧半圆上。对于这部分数字(大部分是负数),程序会给其赋值为合理的数字(hugeCapacity(minCapacity)计算得出)。

同理,下边的代码代表当newCapacity在minCapacity右侧的半圆上(如果minCapacity,也就是newCapacity小于minCapacity),为newCapacity赋值为minCapacity。

额外信息

基于上面两个if条件,我们不知道到底是在做什么。那么就需要结合上下文去进行考虑了,我想作者也并没有想着把grow方法写成一个完全common的方法,也是在ArrayList这个类的上下文中根据场景去设计的,而且尽量考虑了性能(不然不会写的这么复杂难懂)。

额外信息1、newCapacity是oldCapacity扩大1.5倍,而oldCapacity原本是在合理范围内,也就是0到MAX_ARRAY_SIZE范围内。那么newCapacity要么是正常范围内,要么最大就是在MAX_ARRAY_SIZE的基础上乘以1.5倍后的越界值,那么就是最多超过MAX_ARRAY_SIZE 四分之一圆(严格来说不到四分之一,是MAX_ARRAY_SIZE/2)。这种情况下-1、-2……这种较大的负数是不会出现的。

额外信息2、minCapacity是根据需要加入的元素计算出来的最小需要容量,这个值有可能本身溢出而成为负值。

片面结论:

正常情况下,就是1.5倍扩容,或者扩容为需要的大小。

1.5倍扩容溢出时,就会扩容为需要的大小或者最大可扩容值。

如果需要扩容的大小溢出,要么扩容为1.5倍,要么报错。

遗留困惑

minCapacity如果溢出,但是能满足newCapacity - minCapacity < 0,也就是newCapacity在minCapacity的右侧半圆,即便newCapacity是正常的,也不会扩容,而是报错;但是minCapacity溢出很严重,到了-1这种很大的值,newCapacity即便是正常的,也会不满足newCapacity - minCapacity < 0,这时候就会做1.5倍扩容。这种行为并不统一吧?

原文链接:

overflow-conscious code_lijianqingfeng的专栏-CSDN博客  https://blog.csdn.net/lijianqingfeng/article/details/107912190

 

补充ArrayList扩容步骤

我们知道,如果访问数组元素时指定的索引值小于0,或者大于等于数组的长度,编译时编译器不会出现任何错误提示,但会出现运行时异常:数组索引越界异常 java.lang.ArrayIndexOutOfBoundsException:N,异常消息后的N就是程序员试图访问的数组索引。

所以,ArrayList里的elementData数组需要确保动态扩容,然后利用 type[] Arrays.copyOf(type[] original, int newLength)方法,这个方法将会把original数组复制成一个新数组,其中newLength是新数组的长度。如果newLength小于original数组的长度,则新数组就是原数组的前面newLength个元素;如果newLength大于original原数组的长度,则新数组的前面的元素就是原数组的所有元素,后面补充0(数值类型)、false(布尔类型)或者NULL(引用类型)。

MAX_VALUE的值是2147483647

ArrayList中可以分配的数组的最大值,即MAX_ARRAY_SIZE是2147483639

    
    public static final int   MAX_VALUE = 0x7fffffff;

    /**
     * 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;

当添加一个元素时,首先确保数组容量。当前元素个数size+1为必须的最小容量值minCapacity

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

如果数组elementData.length值比minCapacity小,意味着数组必须需要扩容。不然,就会发生数组索引越界异常。

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

grow(int minCapacity)方法对数组进行扩容。

对照源码分析,首先基于elementData数组当前的长度扩容1.5倍,变成newCapacity。

然后判断newCapacity是否满足所必须的最小值minCapacity,如果还不满足,则直接newCapacity = minCapacity;这里更多情况下应该是满足的,通常情况下,一定是1.5倍扩容后的值大于最低必须要分配的值minCapacity!

如果newCapacity的值大于MAX_ARRAY_SIZE,则意味着newCapacity已经超过了默认最大容量,还差8就发生int值溢出。所以,只有根据hugeCapacity(minCapacity)方法,根据最小值来扩容,而不是之前1.5倍的扩容大小。所以判断必须的最小值minCapacity是否溢出,如果溢出就抛出溢出。然后根据minCapacity的大小继续在最大边界的地方尽可能的扩容。

源码如下:

    /**
     * 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
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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 static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

附:关于int最大值最小值的溢出测试

    public void test2() {
        int max = Integer.MAX_VALUE;
        System.out.println(max);//2147483647
        max++;
        System.out.println(max);//-2147483648
        int min = Integer.MIN_VALUE;
        System.out.println(min);//-2147483648

        int a = 0x80000001;//根据 Integer.MIN_VALUE = 0x80000000
        System.out.println(a);//-2147483647

        int c = a - 1;
        System.out.println(c);//-2147483648
        c = a - 2;
        System.out.println(c);//2147483647

        if (-3 - 5 > 0)
            System.out.println("aaaa");

        if (-3 - (-8) > 0)
            System.out.println("BBBB");//BBBB

    }

 

参考链接:

数组索引越界异常 ArrayIndexOutOfBoundsException_鲜衣怒马楼兰月-CSDN博客  https://blog.csdn.net/loulanyue_/article/details/93669970

java中Arrays类的讲解_zhouning的博客-CSDN博客  https://blog.csdn.net/qq_41474648/article/details/105182817

 

posted @ 2020-12-20 15:49  西伯利亚虎  阅读(398)  评论(0编辑  收藏  举报

Permanence, perseverance and persistence in spite of all obstacles, discouragements and impossibilities: It is this, that in all things distinguishes the strong soul from the weak.