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