以前在研究ArrayList源码的时候看到过一篇文章Java 8 容器源码-ArrayList里面说当ArrayList在进行插入的时候,若是容量不够那么就会进行自动扩容,扩容大小是现有容量的1.5倍,具体代码能够参考下面。html
此处的默认容量是指当构建空的ArrayList构造函数时给分配的默认数组容量大小,为10。java
/**
* 扩容,保证ArrayList至少能存储minCapacity个元素
* 第一次扩容,逻辑为newCapacity = oldCapacity + (oldCapacity >> 1);即在原有的容量基础上增长一半。第一次扩容后,若是容量仍是小于minCapacity,就将容量扩充为minCapacity。
*
* @param minCapacity 想要的最小容量
*/
private void grow(int minCapacity) {
// 获取当前数组的容量
int oldCapacity = elementData.length;
// 扩容。新的容量=当前容量+当前容量/2.即将当前容量增长一半。
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);
}
/**
复制代码
因此在网上就会有许多的人说当知道了要用ArrayList存储多少数据的前提下,咱们要指定ArrayList的容量大小,这样会减小ArrayList自动扩容的次数而增长效率。程序员
此时相信你们内心就会和我有同样的疑问,在不一样数据量的面前使用默认容量和指定容量大小效率到底会有多少的差异。相信有些人(好比我)一开始上来就会写以下的代码进行验证。编程
public class TestListAdd {
public static void main(String[] args){
System.out.println("Test 10000000 List add start");
System.out.println("Default Capacity: "+ listAdd(10000000));//默认容量
System.out.println("10000000 Capacity: "+ listAdd(10000000,10000000));//指定容量
}
public static Long listAdd(int num){
Long starTime = System.currentTimeMillis();
List<Object> list = new ArrayList<>();
for (Integer i = 0; i < num; i++) {
list.add(i);
}
Long endTime = System.currentTimeMillis();
return endTime - starTime;
}
public static Long listAdd(int capatity,int num){
Long starTime = System.currentTimeMillis();
List<Object> list = new ArrayList<>(capatity);
for (Integer i = 0; i < 10000000; i++) {
list.add(i);
}
Long endTime = System.currentTimeMillis();
return endTime - starTime;
}
}
复制代码
结果固然也是符合预期的,下面的输出比上面的要少量多数组
Test 10000000 List add start
Default Capacity: 2416
10000000 Capacity: 397
复制代码
若是试验到此为止我也就不会写这篇博客了,由于没什么可写的了,可是当我将上面默认容量大小和指定容量大小两行代码换个位置,那么结果就会与咱们所想的不相同。bash
Test 10000000 List add start
10000000 Capacity: 2130
Default Capacity: 933
复制代码
通过查询各类资料,中间走了许多的坑,例如一开始我想着是第一次的调用会将int从0-10000000的数第一次建立到常量池里面,而第二次就不用建立直接去常量池里面去取第一次建立好的int类型就行了,因此每次的第一次调用会比第二次调用速度要慢许多。固然随后通过学习查资料,否认了我第一次的蠢想法。两个不一样的方法内的变量根本不会有交互。随后看到了一篇文章和个人问题差很少java循环长度的相同、循环体代码相同的两次for循环的执行时间相差了100倍?里面的回答提到了一句话。并发
在HotSpot VM上跑microbenchmark切记不要在main()里跑循环计时就完事。这是典型错误。重要的事情重复三遍:请用JMH,请用JMH,请用JMH。除非很是了解HotSpot的实现细节,在main里这样跑循环计时获得的结果其实对通常程序员来讲根本没有任何意义,由于没法解释。框架
固然里面的其余术语我是看不懂的,只是简单的明白了两点函数
JMH 是 Java Microbenchmark Harness(微基准测试)框架的缩写(2013年首次发布)。与其余众多测试框架相比,其特点优点在于它是由 Oracle 实现 JIT 的相同人员开发的。性能
我会在后面贴出一些学习JMH的经典文章。下面就开始咱们的测试吧。
使用JMH进行测试十分的简单,只须要加入一些注解便可。他就能屏蔽一些JVM对于测试的影响。让咱们只关注于测试结果。
@Benchmark
public static void listAdd(Blackhole blackhole){
List<Object> list = new ArrayList<>(10000);
for (Integer i = 0; i < 10000; i++) {
list.add(i);
}
blackhole.consume(list);
}
复制代码
个人JMH的配置以下
具体的测试代码我就不展现了,其中无非就是循环次数和初始化的容量。我就直接将数据展现出来吧。
循环次数 | 初始化容量 | 平均时间(微秒) |
---|---|---|
10 | 10 | 55.38 |
100 | 79.06 | |
1000 | 361.85 | |
10000 | 2355.82 |
循环次数 | 初始化容量 | 平均时间(微秒) |
---|---|---|
100 | 10 | 524.50 |
100 | 506.01 | |
1000 | 851.23 | |
10000 | 2804.22 |
循环次数 | 初始化容量 | 平均时间(微秒) |
---|---|---|
1000 | 10 | 7233.56 |
100 | 6923.32 | |
1000 | 5284.91 | |
10000 | 7723.32 |
循环次数 | 初始化容量 | 平均时间(微秒) |
---|---|---|
10000 | 10 | 81188.92 |
100 | 64393.20 | |
1000 | 59316.76 | |
10000 | 51382.20 |
下面是根据上面数据画的折线图,可以更加直观的感觉到变化。
大概由上面的试验,咱们可以得出如下结论
尤为这种效率的提高在数据量大的时候更为明显,由于数据量大而致使初始化容量不够,扩容次数不断的增长。致使效率下降。