已经进入秋招了,开始接连不断地进行各种面试了,在这总结一下集合的基础知识。
首先看一下Java集合的类图 :
可以看出集合一共分为两大类:Collection 和Map。Collection下又被分为两类:List和Set(集合数据不可重复)。
因此对集合的学习主要三个大类List、Set、Map。
(一)List
List集合是最常用的集合,用来存放数据,可以维持数据有序性。其下有多个子类,如图
最常用的是ArrayList、LinedList、Stack,其中Stack是JAVA实现的栈,内部维护一个ArrayList集合来实现栈的操作,此处暂时不说。
下面说一下ArrayList。ArrayList是基于数组实现的集合,内部维护一个Object类型的数组,其内部成员变量如图
DEFAULT_CAPACITY :初始数组长度。
EMPTY_ELEMENTDATA :一个空数组,如果数组为空,则替换成该数组。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA : 用来判断当前数组是不是为空。
elementData :存放元素的数组。
size :当前元素数量。
接下来看一下构造函数
无参构造函数即将空数组赋值给elementData。
入参为int的构造函数则按照输入参数的值给数组开辟指定空间大小。
最后一共构造函数会调用数组复制的方法拷贝数据到elementData数组中
接下来看一下比较常用的几个方法
内部维护一个size值保存当前数据大小,此处没有什么可说的。
contains方法实际上是调用equals方法来判断两个元素是否相等。
最常用的add和get方法以及set方法。再get方法第一行rangeCheck(index);实际上是判断index角标是否越界,越界则抛异常。
而set方法则是替换第index位置的元素并返回原来的值,依然需要检查角标是否越界。
add方法则需要判断是否当前数组已满,即第一行方法ensureCapacityInternal(size + 1); 代码如下图
其中第三个方法中grow(minCapacity)为重新扩容并赋值数组。而扩容的条件为当前最小容量大于了数组的长度。
接下来remove方法就和这些方法类似,每次remove完成要将后面的数组前移,因此很费时间。这里就不贴图讲解了。
ArrayList就讲完了,接下来看一下LinkedList。LinkedList是基于双向链表做的一个集合 其内部成员变量如下
其中size为链表长度、first为头结点、last为尾结点。
接下来看一下构造函数
对于第二种构造函数,调用的addAll(c)方法返回 addAll(size, c)
这里看一下addAll(int index, Collection<? extends E> c) 代码
第一行判断index值是否合法,接下来将集合c转成数组a然后获取第index位置的节点从该节点向后开始添加并且用succ存放原链表中index+1位置的节点,用来在最后将链表连接起来。
接下来看一下常用的方法
这两个方法直接返回头尾节点。
这两个方法则是将头尾节点移除。
add方法将节点添加到链表尾部,调用下图的方法
接下来remove方法删除指定节点,如果没有找到节点则返回false
接下来的方法都大同小异,都是基础的链表操作。这里就不细致讲解了。
(二)Map集合
这里先讲Map再讲Set是因为Set中用到了Map的一个类,所以先讲Map集合。
Map集合是一个键值对集合,其中键不可重复。其实现类如下图
最常用的几类分别是ConcurrentHashMap(线程安全),HashMap(线程不安全),Hashtable(线程安全),LinkedHashMap(有序)。
首先讲一下HashMap,HashMap是一个基于数组的集合,数组内部存放节点类,如下图
其中key存放键,value存放值,hash存放计算出来的hash值,next是下一个节点,用拉链法来解决冲突问题(一会儿会讲到)。
其中equals方法分别判断键是否相等和值是否相等来判断两个node节点是否相等。
接下来看一下构造函数
首先第一个构造函数入参initialCapacity表示这个数组大小,loadFactor表示这个数组的负载为多少,即这个数组最多能放loadFactor * initialCapacity 这么多个元素(可以这么理解,此处实际上是根据传入的参数通过算法计算出数组的大小应该为多少,并不是直接拿来initialCapacity的值直接使用),在向数组添加元素就需要对数组扩容。目的是为了减少查询的时间。
内部维护一个size来表示元素个数
接下来是最常用的put方法
public V put(K key, V value) {
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;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
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;
}
输入参数hash为计算出来的hash值,key存放的键,value存放的值,onlyIfAbsent是否不修改现有值,如果为true,则若当前table有该键所对应的值,不会修改。evict 是否处于创见模式。
其中代码的整体思路为:首先判断数组是否需要重构,需要重构则先重构,接下来根据hash值找到对应在数组中的角标,即下图中的代码,n为数组长度,此处也可以写成hash%(n - 1),由于计算出来的角标可能会多个不同的值计算结果相同,因此这里就
存在冲突问题,HashMap解决冲突使用的方法是拉链法,即,在每一个角标处存放一个链表,通过链表即可在一个位置存放多个元素。接下来找到角标之后开始遍历当前链表,找到对应的键,将值修改,若没找到,则添加到链表最后。
接下来就是get方法
和put方法类似,这里就不细讲。
接下来看一下HashMap如何计算hash值的
这里面的hashCode方法是key所对应的类中实现的方法。返回hashCode值。
因为上面说到过HashMap里面的数组会存在重构,接下来讲一下数组的重构,
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
大体思路:首先计算出我需要的数组应该多大,一般情况是将数组扩大一倍。接下来遍历之前的数组,将每一个节点拿出来重新计算角标值并放入新的数组中。
HashMap大体思路就是这些,接下来是HashTable,HashTable和HashMap实现原理没有区别,只是在方法上加上了 同步关键字 synchronized,所以HashTalbe是线程安全的。
接下来讲一下TreeMap,TreeMap是基于红黑树实现的集合,因此,键需要可以进行排序。可以保证增删查改的方法的时间复杂度为log(n)。
首先看一下成员变量
其中comparator为比较器,root为根节点,size为树大小,modCount为对树操作的次数
接下来看一下put方法
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
方法第一步判断根节点root是否为空,如果为空插入到根节点,,不为空的话判断是否传入有比较器,如果有传入比较器,则使用比较器比较,否则使用自然顺序进行比较大小,如果小于root节点,则root = root.left 大于的话root = root.right。直到找到相等的值或者找到空节点,如果是空节点则创建新节点放到这个空节点上,并调用 fixAfterInsertion(e);来调整树的结构。相等的话直接setValue(value)并返回即可。
下面看一下fixAfterInsertion(e)方法:
因为新添加的节点会造成平衡搜索树结构混乱,所以插入之后需要修改树的结构,而修改树的结构是通过节点的颜色进行左旋和右旋以及变黑来完成的,这里不细讲,不是特别清楚的同学可以先去看一看红黑树的实现就清楚了。
接下来再看一看remove方法
调用了deleteEntry方法,因为remove方法同样会造成树的结构混乱,因此在这个方法内部同样会调整树的结构,来看一下代码
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// If strictly internal, copy successor's element to p and then make p
// point to successor.
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) {
// Link replacement to parent
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else { // No children. Use self as phantom replacement and unlink.
if (p.color == BLACK)
fixAfterDeletion(p);
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
/** From CLR */
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
Entry<K,V> sib = leftOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}
setColor(x, BLACK);
}
这里的删除操作因为时间原因暂不讲解,后期会补上。
这样TreeMap中最难的几个方法实现就已经了解完毕了,剩下的方法很好理解就不一一贴图描述了。
接下来说一下ConcurrentHashMap,他也是一个线程安全的集合,他和HashTable的区别在于HashTable底层是一个数组+链表+同步的形式实现的。ConcurrentHashMap的底层是多个数组+链表+每个数组同步实现的,可以把ConcurrentHashMap粗略的理解成为多个HashTable,因此ConcurrentHashMap可以实现多个插入操作并发执行(在不同的数组段中)。
(三)Set集合
Set集合是一个不包含重复元素的集合。其实现类如下图
最常用的两个为HashSet和TreeSet
其中HashSet底层维护了一个HashMap,通过HashMap.put(key,object);方法将数据放入map集合中,其中object是全局的一个Object类型的对象,key为要存入set集合的对象。由于HashMap的键不可重复,因此通过该方法就实现了Set集合的不可重复的特性。
TreeSet是基于红黑树的一个集合,底层维护了一个TreeMap集合,和HashSet类似,也是通过TreeMap的键不可重复的原理实现Set集合数据不可重复的特性的。