overflow-conscious code

背景

在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倍扩容。这种行为并不统一吧?

 

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
ArrayList是Java中常用的动态数组实现,它可以根据需要动态地增加或缩小数组的大小。当ArrayList中的元素数量超过了初始容量时,ArrayList会自动扩容,以便能够存储更多的元素。下面我们来看一下ArrayList扩容的源码实现。 在ArrayList中,扩容是由ensureCapacity方法实现的。当元素数量超过了数组容量时,该方法会调用grow方法来扩容数组。 ``` private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } 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); } ``` 首先,ensureCapacityInternal方法会调用ensureExplicitCapacity方法,该方法会检查是否需要扩容,并在需要时调用grow方法。 grow方法会首先计算新的数组容量,它的计算方式是将原来的容量增加一半。然后,grow方法会检查新容量是否大于最大数组容量,如果是,则调用hugeCapacity方法来返回一个足够大的容量值。最后,grow方法会调用Arrays.copyOf方法来将原来的数组复制到新的数组中。 需要注意的是,在进行扩容操作时,ArrayList会创建一个新的数组,并将原来的元素复制到新的数组中。这个过程会占用一定的时间和空间,因此,在使用ArrayList时,应该尽量避免频繁地进行扩容操作,以提高性能。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值