容量设置相关的优化,对于数组类型的集合如何预先设置的容量不够了,就需要扩容。因为数组必须要一整块连续的内存,合理性推测=>它后面的内存如果被占用则不能直接就在后面加东西,就需要重新申请一块内存,如果没被占用,那就运气好,可以直接其后面增加(C语言里面有的realloc函数)。如果是不够重新申请一整块更大的内存,就需要把现在的数据拷贝到新的那块内存里面去,并且还要回收原来的数组内存,这样消耗是很大的,尤其大数组。所以对于数组集合,一般能够预先估计大小的,并且后续添加时间间隔不就的,都预先设置好大小。
看看Java里面各种集合的内存分配策略吧
StringBuilder
StringBuilder也算到集合一起吧。关于StringBuilder容量的,从源码中可以看到,
StringBuilder设置的默认容量是16.
StringBuilder的append会调用到AbstractStringBuilder的append方法,
然后经过ensureCapacity调用到expandCapacity方法,在里面确定容量,其中参数是append之后需要的长度
/**
* This implements the expansion semantics of ensureCapacity with no
* size check or synchronization.
*/
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
从中可以看到,容量增加的策略是在原来容量的基础上 * 2 +2 ,如果还是比需要的容量少,就设置为需要的容量。
对于ArrayList,在Java 7上它的初始默认大小是10,扩容策略是增加1.5倍,具体的代码就不贴了。
HashMap
HashMap的具体实现很复杂,值得探究,这儿简述其原理,在Java 8 上,HashMap的结构为数组 + (链表 + 红黑树)的结构,如下图所示:
元素放入的过程是,放入元素时,通过hash值取模,得到地址,将元素放到改地址,如果该地址已经有数据(发生冲突),就放到链表后面,如果链表超长(大于8),就改为红黑树存储,如果超过最大容量,就扩容。其中碰撞概率主要取决于容量和hash算法。
其中默认数组长度为16,默认负载因子是0.75。最大容量 = 内部数组长度 * 负载因子,即threshold = length * Load factor。扩容过程是原来的长度 * 2,超过int的最值不能在扩容时就取int的最值。
关于HashMap中的几个点:
1、其中的数组,即用来取余的长度是取的2的幂,不是一般的hash理论中的素数,因为这样利于后面计算上的各种优化。
2、采用红黑树应该是有利于那种自定义hash函数冲突大的情况,一般Java自带的hash函数其实是不会那么大冲突的。
3、Java 8还优化了resize过程,因为发现了一个位运算的规律,对于取余运算又一个规律,若 m1 = x % n 与 m2 = x % 2n, m2 = m1或者n + m1. 而又通过位运算发现当扩容2倍后,重新用hash去对map的size取余,显然满足这个规律。并且还有一个东西,m2 = m1还是m2与n的最高位是0还是1有关,并且hash对应于n的最高位的值是随机的,所以不用担心重复。根据这个规律优化了resize过程中,Hash数组上一个链表的重新放置的过程,因为重新放置的位置只有两种可能,所以不用再把链表中的节点一个一个取出来再放,只需要遍历整个链表,根据上面规律分成两个链表,分别插到对应位置即可,并且这样不会像以前的头插法那样倒序一遍。
4、Java中的HashMap是要追求最优性能的,因为jdk可是要用到很广要求很高的场景中的。里面采用了各种极限性能优化,各种位运算等。比如hash取余确定位置就是用的 hash & (n -1)