备战JAVA面试系列
序号 | 文章 | 地址 |
---|---|---|
1 | MySQL篇 | 博客地址 |
2 | 集合框架篇 | 博客地址 |
~~~~~~~ 前言:一直以来,对于JAVA的集合框架,只是会用,知道其应用场景,并不知道其内部的实现原理,现在来总结一下关于集合框架的一些知识以及面试经常会被问到的题目。
目录
Collection
1、ArrayList
ArrayList通过数组实现,在其内部维护了一个数组,对外表现可以动态扩容。
初始化
通过查看源代码中的构造方法,可以看到如果调用其带参数的构造方法,传入初始容量,则在该对象创建时就将其内部的数组也同时进行初始化;如果调用其无参构造方法,则采用lazy-load方式,直到第一次添加元素时才会初始化整个数组。
/* 默认的初始容量 */
private static final int DEFAULT_CAPACITY = 10;
/* 用于默认大小的共享空数组实例 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
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);
}
}
添加元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
扩容
/* 要分配的数组的最大大小 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
2、Vector
Vector作为一个比较古老的实现了多线程中对数组进行操作,其内部实现与ArrayList基本类似,只是在所有的public方法上都加了synchronized关键字来保证
public synchronized boolean add(E e){}
public synchronized E get(int index){}
public synchronized E remove(int index){}
public synchronized boolean equals(Object o){}
...
3、LinkedList
LinkedList通过双向链表实现,增删效率高。
/* 维护了头节点和尾节点 */
transient Node<E> first;
transient Node<E> last;
/* 内部双向链表节点 */
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
4、HashSet
HashSet是由哈希散列表实现的Set接口,它不保证集合的迭代顺序,但是保证了其内部元素的唯一性。
通过查看源码可以发现,实际上HashSet中维护了一个HashMap对象,把元素存入HashMap的Key中以实现唯一性
private transient HashMap<E,Object> map;
/* 每次插入元素时,将Key设置为要插入的元素,Value则设置为该虚拟空对象 */
private static final Object PRESENT = new Object();
5、TreeSet
TreeSet是由红黑树实现的一个具有排序功能的Set,它能够让元素按照一定规则进行排序。
通过查看源码可以发现,与HashSet类似,它实际上也是内部维护了一个TreeMap对象,把元素存入Key中
构造方法
这里有两种构造方法,使用无参构造方法时,要求所有元素必须实现Comparable接口,即自然排序
还可传入Comparator比较器来实现排序功能
Map
Map中存储的数据为具有映射关系的键值对,即Key, Value,常见的Map主要有三种:HashMap、HashTable、ConcurrentHashMap。
1、HashMap
HashMap是面试中最常考的一种集合类,它主要存储具有映射关系的键值对,在JDK1.8之前底层数据结构为数组+链表,在JDK1.8后改为数组+链表+红黑树。
总的来说,HashMap存储数据的空间被称为Bucket(桶),当在添加键值对时,会根据key的hash值选择合适的桶存入,若发生哈希冲突,则在该桶中初始化一个单向链表,将后插入的元素插入链表的尾部,如果链表的值达到一定阈值,会把链表转为红黑树进行存储。
构造方法
通过查看源码可以看到,HashMap也属于lazy-load方式,即在初始化时并不初始化Bucket,而是等到第一次put值时再来给Bucket做初始化。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/* 在默认的构造方法中,只初始化了负载因子为默认的负载因子 */
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
哈希函数
在HashMap中,并没有对要添加的元素直接使用其hashcode()方法返回的hash值,而实在其基础上又做了一次hash计算。
即将hash值的高16位与低16位进行异或操作后再将其返回
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
选择合适的Bucket
在经过哈希函数后,要对其进行取模操作来选择合适的Bucket进行存储,但是HashMap并没有直接进行取模运算,而是进行了一次非常巧妙的运算,即用Bucket的数量(size - 1) ^ hash,由于HashMap中Bucket的数量必须是2的n次幂,将其-1后得到的数必然每一个二进制位都为1,将其与hash做异或操作,得到的必然是一个小于size的数,即完成取模操作。
Bucket的数量(HashMap的容量)
HashMap中的Bucket数量必须是2的n次幂,如果我们在构造方法内传入一个不是2的n次方幂的参数会怎么处理呢?
可以看到,HashMap并不会直接将传入的初始容量作为真正的初始容量使用,而是会调用一个名为tableSizeFor的方法来重新计算容量,该方法会将传入的容量转为2的n次幂。
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 tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
添加元素
- 如果HashMap未被初始化过,则初始化
- 对Key求Hash值,然后再计算下标
- 如果没有碰撞,直接放入桶中
- 如果碰撞了,以链表的方式连接到后面
- 如果链表长度超过阈值,就把链表转成红黑树
- 如果节点已经存在就替换旧值
- 如果桶满了(容量16 * 负载因子0.75),就需要resize(扩容2倍后重排)
public V put(K key, V value) {
// 对key求hash并调用putVal方法
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果是第一次调用put方法,先初始化Bucket
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果Bucket为空,创建一个新的节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//哈希冲突的处理逻辑
Node<K,V> e; K k;
//如果传入的key与原来的key相等,则直接覆盖原值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果Bucket中存储的是红黑树,则遍历红黑树节点并在其内部添加元素
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果Bucket中存储的是链表,则遍历链表并将元素添加到链表的尾部
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果添加节点后,链表的节点的个数大于阈值,则将链表转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2、Hashtable
HashTable也是JDK早期实现线程安全的集合类,与Vector类似的,HashTable也只是在HashMap的所有public方法加了synchronized关键字。这里有一个小tip,Hashtable这个类名并不遵循驼峰命名原则,也是属于历史遗留问题了。
3、ConcurrentHashMap
ConcurrentHashMap是面试官最喜欢问的一种线程安全集合类,其重要性母庸置疑
早期的ConcurrentHashMap通过分段锁Segment来实现
JDK8的ConcurrentHashMap则通过CAS+synchronized实现,使得锁更加细化,并且其底层数据结构与HashMap相同,都是数组+链表+红黑树。synchronized只锁定当前链表或者红黑树的头节点,所以只要哈希不冲突,就不会产生并发,所以其效率得到了进一步提高。
sizeCtl
在ConcurrentHashMap中,有一个比较核心的变量sizeCtl,即size-control,就是用来做大小控制的。是在初始化和调整大小时的控制变量
- 当为负数时,代表正在初始化或调整大小
- -1用于初始化
- 否则该值为-(1+正在调整大小的线程数)
- 正数或0代表哈希表还未被初始化,这个数值表示初始化或下一次要进行扩容的大小
因为使用了volatile作为修饰符,所以该变量是多线程间可见的,对它的改动别的线程可以立即看到
private transient volatile int sizeCtl;
添加元素
- 判断Node[]数组是否初始化,没有则进行初始化操作
- 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环
- 检查到内部正在扩容,就协助其一起进行扩容
- 如果头节点不为空,则使用synchronized锁住头节点
- 如果是链表结构则执行链表的添加操作
- 如果是红黑树结构则执行红黑树的添加操作
- 判断链表长度是否达到阈值8,如果超过就需要把链表转为红黑树
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key和value均不能为空
if (key == null || value == null) throw new NullPointerException();
//求Hash值,与HashMap中的hash()方法类似,但是屏蔽了符号位
int hash = spread(key.hashCode());
int binCount = 0;
//这里使用到了CAS的自旋锁,所以使用这个循环来一直重试
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//当哈希表还没有初始化的时候,初始化哈希表
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//当Bucket为空时(没有发生哈希碰撞),使用CAS尝试新建节点,失败则会重新循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//判断别的线程是否正在进行扩容,如果有别的线程正在进行扩容,则协助其进行扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//锁住当前链表或红黑树的头节点f
synchronized (f) {
//再次确认当前头节点是否为f
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//当key相等时,使用新值覆盖原值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
//否则将当前元素插入到尾部
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果当前Bucket内存储的是红黑树,则按照红黑树的方式进行插入
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//如果当前Bucket的节点数大于等于阈值,则将链表转为红黑树进行存储
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//检查是否需要扩容
addCount(1L, binCount);
return null;
}