集合框架常用的数据结构
ArrayList动态扩容机制
ArrayList三种初始化方式:
//默认的构造器,将会以默认的大小来初始化内部的数组
public ArrayList();
//用一个Collection对象来构造,并将该集合的元素添加到ArrayList
public ArrayList(Collection<? extends E> c);
// 用指定的大小来初始化内部的数组
public ArrayList(int initialCapacity);
扩容条件:根据传入的最小需要容量minCapacity来和数组的容量长度对比,若minCapactity大于或等于数组容量,则需要进行扩容。jdk7中采用>>位运算,右移动一位, 容量相当于扩大了1.5倍。(1+右移一位0.5)
在JDK1.7中,如果通过无参构造的话,初始数组容量为0,当真正对数组进行添加时,才真正分配容量(默认10)。每次按照1.5倍(位运算)的比率通过copeOf的方式扩容。(newCapacity = oldCapacity + (oldCapacity >> 1))
ArrayList源码中的构造方法:
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
另外需要区分容量和大小的关系,通常情况下容量要大于等于大小(Capacity >= size)
import java.lang.reflect.Field;
import java.util.ArrayList;
public class ArrayListTest {
public static void main(String[] args) {
//创建初始容量长度为20的数组
ArrayList<String> list = new ArrayList<>(20);
/** 利用反射机制,获取ArrayList的容量*/
int capacity = 0;
Class c = list.getClass();
Field f;
try {
f = c.getDeclaredField("elementData");
f.setAccessible(true);
Object[] o = (Object[]) f.get(list);
capacity = o.length;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
System.out.println("Capacity: " + capacity);
/** ArrayList 的大小*/
int size = list.size();
System.out.println("Size: " + size);
}
}
//Output:
//Capacity: 20
//Size: 0
HashMap及其扩容机制
HashMap
存储数据采用的是散列表结构(数组+链表的结构),在JDK8中HashMap
的底层数据结构已经变为数组+链表+红黑树的结构,这主要原因是为了减少hash冲突带来的影响。
HashMap
的基本原理是散列表+拉链法,就是在往HashMap
中put元素时,会先根据key的hash值得到这个元素在数组中的位置(即下标),然后把这个元素放到对应的位置中。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。
实际情况下,我们希望HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素是我们想要元素,而不用再去遍历链表。最容易的做法就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是“模”运算的消耗是比较大的,在Java中选择了另外一种更快速,消耗更小的方式:首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)
static int indexFor(int h, int length) {
return h & (length-1);
}
在HashMap
中get()方法的执行过程是:首先计算key的hashcode
,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。所以hashcode
与equals
方法是找到对应元素的两个关键方法,通过改写key对象的equals
和hashcode
方法,我们就可以将任意的业务对象作为map的key。在判断两个对象是否真的相等时,必须保证它们的hashcode
相同,且保证调用 equals()
方法返回true。
HashMap扩容
在JDK7中,HashMap
数据结构是数组+链表的方式,HashMap
内存储数据的Entry数组默认是16,如果没有对Entry扩容机制的话,当存储的数据一多,Entry内部的链表会很长,这就失去了HashMap
的存储意义了,所以HasnMap
内部有扩容机制来进行处理。
在HashMap
内部有:变量size,记录HashMap
的底层数组中已用槽的数量;变量threshold,它是HashMap
的阈值,用于判断是否需要调整HashMap
的容量(threshold = 容量*加载因子);变量DEFAULT_LOAD_FACTOR = 0.75f,默认加载因子为0.75。
HashMap
扩容的条件是:当size大于threshold时,对HashMap
进行扩容。
//举个例子假如现在有三个元素(3,5,7)要放入map里面,table的的容量是2,如下
[0]=null
[1]=3->5->7
//现在将table的大小扩容成4,分布如下:
[0]=null
[1]=5->7
[2]=null
[3]=3
在JDK8里面,HashMap
的底层数据结构已经变为数组+链表+红黑树的结构了,因为在hash冲突严重的情况下,链表的查询效率是O(n),所以JDK8做了优化对于单个链表的个数大于8的链表,会直接转为红黑树结构算是以空间换时间,这样以来查询的效率就变为O(logN)。
简单总结就是,JDK7里面是先判断table的存储元素的数量是否超过当前的threshold=table.length*loadFactor(默认0.75)
,如果超过就先扩容,在JDK8里面是先插入数据,插入之后在判断下一次++size
的大小是否会超过当前的阈值,如果超过就扩容。