前几天公司的测试指给了大白一个bug,这个bug是在查询一个列表的时候,页面一直在打转,最后请求超时。在这里提一下,大白公司使用的数据库持久框架是spring data,底层封装的是hibernate,所以性能上会有问题,大白通过自己写sql和集合优化处理了这个问题,今天我们讨论的是集合,就不多说框架的事情了。
大白先描述一下这个问题,从数据库中查询出数据之后,需要将这些数据循环再做处理,然后设置到一个新的集合中返回,大家一定疑问,这有什么值得优化的地方,不要急,大白慢慢给解释一下。
我们平时经常使用集合,一般是通过new声明一个集合,然后使用集合的add、remove等方法进行操作,因为集合是自动管理长度的,所以我们并不关心超长的问题,这是一个非常好的优点,但是有一些我们必须要注意的事项!
下面就以经常使用的ArrayList为例了解一下java是如何实现长度的动态管理的!
如下图,我们先以add方法为例
我们都知道ArrayList其实就是一个可变数组,图中的size是数组中元素总数量,进行add方法的时候,首先先将这个元素总数加一,然后将需要添加的元素放在总数加一后的位置,elementData[size++] = e;这段代码大家都能看懂,就是一个简单的赋值操作,我们主要看一下第一个方法,如下图所示:
如果新增后的数组实际长度大于原数组的长度,就会调用grow方法,我们再看grow方法是什么鬼?
这段代码中,我们可以看到,oldCapacity是数组的初始化长度,newCapacity是需要扩容的数组的长度,大白稍微解释一下oldCapacity + (oldCapacity >> 1)这段代码的意思,这段代码相当于oldCapacity + (oldCapacity / 2),旧数组的长度扩大1.5倍,如果旧数组增大1.5倍后还小于小于这个新增后的长度,那就让newCapacity等于新增后的长度,再往下的代码是数组重新计算的长度如果超过Integer的最大值时做的一个处理方案,这不是大白今天要说的重点,所以不做讨论,然后再往下是数组的扩容。
通过这段代码我们知道,重新计算数组长度的时候,并不是增加一个元素,elementData的长度就加一,而是到达elementData长度的临界点时,才将elementData扩容1.5倍,这样做的好处就是避免多次调用copyOf方法进行扩容消耗性能。为什么是1.5倍呢?如果一次扩容太大,占用的内存也会跟着增大,造成内存浪费,如果一次扩容太小的话,又导致性能低下,经过测试验证,扩容1.5倍既满足了性能要求,又减少内存的消耗。
讲了这么多,貌似和大白开头讲的那个性能问题没什么关系,其实不然,elementData的默认长度是多少呢?我们看下图:
当然了,除了这个构造方法之外,还有带参构造方法,下图所示:
所以到这里我们可能有结论了,如果不设置初始值,那么如果数据量过大的话,执行add方法势必会造成频繁扩容,这样会非常消耗资源,我们写个小例子验证一下:
从面的小例子就可以看出,如果指定初始化大小的话,性能提升了起码3倍以上,因为我们平时开发中,涉及的逻辑业务肯定比这个小例子要复杂些,所以为集合指定初始化大小是一个好习惯。