[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位,最高位为符号位,所以表示范围是,最小值为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