ArrayList、LinkedList、HashMap、HashTable集合源码分析
JDK1.8和JDK1.7及以下 两个版本中集合类区别还是很大的,所以就按这两个版本来说
一、ArrayList
1.JDK1.8和JDK1.7及以下版本中,ArrayList集合底层都是由数组实现,不同的是默认长度
2.JDK1.8中,在不指定集合长度时,默认长度为0,只有在添加第一个元素的时候,集合长度才会变为10
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
默认构造方法给的是一个空的数组,长度为0
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private static final int DEFAULT_CAPACITY = 10;
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
add元素时,先调用ensureCapacityInternal()
,在该方法中会先判断集合是否是一个空集合,是的话,则从默认长度10和minCapacity
中选择一个最大的值作为集合长度,minCapacity表示集合当前元素个数+1,
当我们先调用默认构造方法,再add一个元素时,底层数据长度的变化是:先是一个长度为0的空数组,添加第一个元素的时候长度变为10.
3.在指定集合长度时,源码如下:
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);
}
}
若给定的集合长度大于0,则数组长度为指定的长度
若给定的集合长度为0,则数组是一个空数组,长度为0,
若给定的集合长度小于0,则抛出参数非法的异常
4.JDK1.8和JDK1.7及以下版本中,ArrayList的扩容机制是相同的,都是扩容为原来的1.5倍。看下源码是怎么进行扩容的。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
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);
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
size
为当前集合中元素的个数,+1表示添加新元素后的元素个数,注意目前还没有添加元素
(1)先通过ensureCapacityInternal
方法判断集合中当前元素个数+1后是否大于集合容量,大的话则进行扩容,否则采用原数组长度,扩容时会将旧数组元素复制到新的数组中
(2)执行代码elementData[size++] = e;
数组容量确定后,开始添加新的元素,先把新元素e添加到size索引处,再将元素个数+1
多线程环境下,ArrayList是线程不安全的,主要问题就出现在add()
方法中,想要线程安全的话,可以使用Vector,
在Vector中,添加元素的方法加了锁synchronized
,可以保证多线程下的线程安全。
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
5.基于数组的ArrayList查找效率高,知道索引的前提下,一次就可以查到,时间复杂度我O(1),增加和删除的效率低,增删会涉及到元素的移动,最慢是O(n),正常情况是O(n-i) ,i是索引。
二、LinkedList
JDK1.8和JDK1.7及以下版本LinkedList没有区别,LinkedList用的也不多,说下和ArrayList的区别:
1.LinkedList底层是双向链表。
2.相比ArrayList,LinkedList查询效率低,因为要从第一个元素开始向下寻找,时间复杂度为O(n),但是增删的效率高,除尾节点外,ArrayList增加或删除一个元素都需要移动其他元素,而LinkedList只需要修改节点的前后指针。
3.LinkedList占用内存更多,因为每一个节点都有头尾指针。
三、HashMap
1.JDK1.8和JDK1.7及以下版本中HashMap区别较大,主要是在底层实现上。
JDK1.7及以下版本中,HashMap底层由数组+链表实现,
JDK1.8中,HashMap底层由数组+链表+红黑树实现
HashMap是以键值对key-value的形式存储元素的,在执行put
方法时,会先通过
int hash = hash(key);
int i = indexFor(hash,length)
计算key的hash值,得到int类型的数据,再把hash值和集合长度取模,最后得到的数就是键值对存储在数组中的索引位置,当集合中元素过多时,不同的key可能得到的索引是相同的,数组中一个索引位置就有多个键值对,这就是Hash冲突,链表结构的引入就是为了解决Hash冲突,把索引相同的键值对放在一条链表上,当再有冲突时,直接把键值对加到链表尾部,但是这样又引出一个问题,当链表长度过长时,查询的效率会变低,几乎是线性查找了,时间复杂度为O(n),所以针对这个问题,JDK1.8版本中引入了红黑树,当链表长度大于8时,链表转为红黑树,以此来提高查询的效率。
2.初始化一个HashMap,不指定集合长度,看下它的构造方法如下:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
构造方法中只是指定了负载因子为0.75,没有指定集合的默认大小,我们通过反射来看下HashMap默认的长度是多少。
HashMap<String,String> map = new HashMap<>();
Class mapClass = map.getClass();
Method method = mapClass.getDeclaredMethod("capacity");
method.setAccessible(true);
System.out.println(method.invoke(map));
执行结果如下:
从结果中可以看出,HashMap若不指定集合长度,则默认长度为16,负载因子为0.75,也就是说,当集合中的元素个数达到集合长度的0.75时,就会进行扩容,扩容到原来的两倍。可是为什么会是16呢,原来JDK的开发工程师在计算存储的索引位置时,把原来的对集合长度的取模运算改进为位运算,提高计算效率,那既然是位运算,就要保证数组的长度是2的次幂,过大不好,过小也不好,经过大量的反复认证,觉得16就合适。
那我们如果初始化集合的时候指定集合长度为不是2的次幂的一个数,比如7,那么集合的长度会是多少呢,再使用反射验证下,返回的结果如下:
结果竟然是8,再指定长度为11,其真实集合长度为16,也就是说,HashMap的长度一定是2的次幂,如果指定的长度不是2的次幂,那么底层计算出的长度就是比指定数值要大的2的次幂的第一个数。
capacity
方法会返回集合的长度,源码如下:
final int capacity() {
return (table != null) ? table.length :
(threshold > 0) ? threshold :
DEFAULT_INITIAL_CAPACITY;
}
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
默认长度就是2的4次幂,16
当然初始化HashMap的时候也可以指定长度和负载因子,源码如下:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
static final int MAXIMUM_CAPACITY = 1 << 30;
这个代码也比较好懂,其中有一点,当指定的集合长度大于2的30次幂时,集合长度也就是2的30次幂了,也就是说集合最大长度为2的30次幂。
3.HashMap是线程不安全的,多线程下可以使用ConcurrentHashMap
来保证线程安全,该类的put方法中加了锁,如下:
四、HashTable
1.初始化一个HashTable,不指定长度的话,其默认长度为11,负载因子为0.75,构造方法如下:
public Hashtable() {
this(11, 0.75f);
}
HashTable底层数据结构为数组+链表,所以put元素时和HashMap原理相差不大,具体方法如下:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
//通过key计算出存储键值对的索引
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//添加元素
addEntry(hash, key, value, index);
return null;
}
int hash = key.hashCode();
HashTable中key不能为null,否则会报空指针异常
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//判断是否需要扩容,需要的话执行rehash()方法进行扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
//扩容方法
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
int newCapacity = (oldCapacity << 1) + 1;
从这行代码中可以看出,HashTable扩容时会扩容到原来的2倍+1
2.初始化集合时指定长度
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
当集合长度小于0时,抛出非法参数的异常
当集合长度等于0时,集合长度为1
当集合长度大于0时,为指定长度大小。