java集合框架总结(二)
Set
Set集合实现框架:
image.pngSet是一种无序不重复的集合,它最多只允许有一个null元素。Set接口继承Collection接口,因为Set是无序的所以我们可以看到它的接口没有跟游标有管的方法。Set的遍历完全依赖迭代器来Iterator来完成。
image.pngSet有两个有两个子接口,SortedSet和NavigableSet,SortedSet提供了set集合的排序功能,NavigableSet扩展了SortedSet接口,提供了支持基于最接近匹配原则检索元素的行为。
HashSet
HashSet是扩展了Set接口,HashSet使用了Hash表作为支持(实际是HashMap)。如果散列均匀到到不同的桶的haul,HashSet的add、remove、contains和size方法的时间复杂度为O(1)。HashSet的迭代时间等于列表长度+桶数,所以如果迭代很重要,在初始化HashSet的时候应该将其设置的初始容量设置的小一些,或者将负载因子设置的小一些(如果负载因子大的话,桶的个数就多)。
HashSet底层是使用HashMap实现的,所以存储数据变量为map。
private transient HashMap<E,Object> map;
HashSet提供了6中构造方法,主要参数就是初始容量大小和负载因子。
public HashSet() {
//初始化容量为HashMap默认容量16,负载因子为HashMap默认负载因子0.75
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
//以指定集合元素作为新建的HashSet初始值,最少为HashMap的默认值16,如果大于默认值则为集合c的1.75倍容量
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
//指定初始容量和负载因子
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
//指定初始用量
map = new HashMap<>(initialCapacity);
}
//使用LinkedHashMap作为HashSet底层存储
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
HashSet将元素作为HashMap的key进行存储,所有操作都是针对HashMap的key进行操作。
public Iterator<E> iterator() {
//返回map key的迭代器
return map.keySet().iterator();
}
public int size() {
//返回map个数
return map.size();
}
public boolean isEmpty() {
//判断map是否为空
return map.isEmpty();
}
public boolean contains(Object o) {
//判断map的是否包含指定key
return map.containsKey(o);
}
public boolean add(E e) {
//将添加值作为map的key存储
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
}
HashSet也不是线程安全的,可以在构造时添加同步操作。
Set s = Collections.synchronizedSet(new HashSet(...));
LinkedHashSet
LinkedHashSet继承HashSet,它主要使用了HastSet的最后一个构造方法,创建LinkedHashMap。所以LinkedHashSet底层采用的LinkedHashMap进行操作。
//使用LinkedHashMap作为HashSet底层存储
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
LinkedHashSet的操作方法都是继承HashSet的。
image.pngTreeSet
TreeSet是基于TreeMap实现的NavigableSet有序集合,我们上面说了NavigableSet扩展了SortedSet接口,它能够保证元素有序,并且提供了最近匹配检索元素。元素顺序可以按照自然顺序(升序),或者提供比较器。
需要注意TreeSet中添加是引用类型元素,我们可以通过实现它的comapreTo方法来保证存取顺序。如果compareTo返回1(正数即可),则会按照添加元素的顺序进行存储,如果返回-1(负数即可)则会按照添加元素的倒序进行存储,如果返回0则添加完第一个元素后,后面所有元素都不会添加进去了。我们也可以指定排序方法,这样就会对元素进行排序。
TreeSet为基本操作保证O(logn)时间开销。
注意这里提供了许多NavigableSet中定义的匹配检索最近元素的方法,比如taiSet、subSet、headSet等。
TreeSet底层实现方式和HashSet一样,直接操作的TreeMap。
其它Set
除了上面说的Set,Java还提供了一下基类Set。
CopyOnWriteArraySet,它底层采用CopyOnWriteArrayList实现。它是线程安全的Set,它适合用于小数据量集合,并且读操作远远超过修改操作,因为修改操作代价很昂贵,需要复制数组。
用于枚举操作的EnumSet,EnumSet保证值存储单个枚举类型的元素。EnumSet底层使用Enum变量存储数据final Enum<?>[] universe;
。EnumSet是抽象类,它主要被用于JumboEnumSet和RegularEnumSet。
基于ConcurrentSkipListMap的实现的NavigableSet的实现类ConcurrentSkipListSet,它是线程安全的,并且元素有序。
Map集合框架
Map和Collection是同一级别的集合框架接口,Map定义了一组键值对的映射。Map中键值是唯一的,并且每一个键对应一个映射值。Map中的元素顺序取决于迭代器的迭顺序,有的实现类保证了输入、输出时的顺序,比如TreeMap;有的实现类则是无序,比如HashMap。
image.pngMap提供了三种视图来帮助分析Map:keySet、values和Entry。
image.pngkeySet是Map中键的集合,它使用Set保存,所以不允许重复,所以存储的键值需要重写equals和hashCode方法。可通过Map.keySet()获取键值集合。
values是Map中值的集合,它以Collection形式保存,因此可以重复。可以通过Map.values()获取值集合。
Entry是Map中存储键值对的实体,它是Map中内部定义的数据结构。通过Map.entrySet()可以的得到一组Entry集合保存在Set中。
下面是Map接口定义的方法:
- int size():返回Map元素个数,最大值为Interger.MAX_VALUE。
- boolean isEmpty():如果map集合为空,则返回true。
- boolean containsKey():如果map保证指定key值,则返回true。
- boolean containsValue():如果map包含指定value值,则返回true。
- V get(Object key):返回map映射中key对应的值,如果不存在该键,则返回null。
- V put(K key,V value):将指定kv存储到map中,如果已经存在对应的key,则新value会覆盖旧value值。
- V remove(Object key):删除指定key对应的键值对,并返回该键对应的value,如果不存在对应的key,则返回null。
- void putAll(Map<? extends K,? extends V> m):将指定Map中的映射复制到调用map中,它实际调用的是put方法。
- clear():清除map中所有的映射关系。
- Set<K> keySet():返回key的集合。
- Collection<V> values():返回映射的值集合。
- Set<Map.Entry<K,V>> entrySet():返回map映射的实体集合。
- equals(Object o):比较两个Map映射是否相等。
-
hashCode():返回map的hash值,该hash值是每个Entry实体哈希值的总和。
除了上面这些基础方法,Java在JDK8又新增了一些方法,这些方法都是接口默认方法实现。
image.png
Map接口还定义了内部接口Entry,下面是Entry中定义的方法。
image.png
Set替代了Dictionary抽象类,作为映射的顶级接口。
可以使用Map类型作为映射的value,但是禁止使用Map类型作为key。因为太复杂equals和hashCode方法很难确定。还有就是应该尽量避免key是可变的,因为这样会造成map的混乱。
AbstractMap
AbstractMap是Map接口的核心实现抽象类,这样以后map就不需要重新实现这些方法了。当我们要实现一个不可变Map时,可以直接继承该抽象类。如果想要实现可变的Map映射,则还需要覆盖put方法。因为AbstractMap中定义的put方法是不允许操作的。
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
AbstractMap中定义了唯一抽象方法entrySet,所以子类都需要重写这个方法,这个方法是返回Map中所有映射实体的集合。AbstractMap中其它方法,都是基于该方法返回的Set<Entry<K,V>>来实现的。
public abstract Set<Entry<K,V>> entrySet();
AbstractMap中定义了两个变量,用来存储键集合和值集合。transient表示该变量不可序列化,volatile表示并发环境的线程可见性。
transient volatile Set<K> keySet;
transient volatile Collection<V> values;
下面是AbstractMap具体方法实现:
//Set集合中Entry个数就是map中映射的个数
public int size() {
return entrySet().size();
}
public boolean isEmpty() {
//长度为0就是为空
return size() == 0;
}
public boolean containsValue(Object value) {
//获取Entry的迭代器,然后通过Entry.getValue()来获取映射值
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
//分是否为null进行判断
if (value==null) {
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
if (e.getValue()==null)
return true;
}
} else {
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
if (value.equals(e.getValue()))
return true;
}
}
return false;
}
public boolean containsKey(Object key) {
//获取Entry的迭代器,然后通过Entry.getKey来判断是否存在指定key
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
//通过这里来看可以知道,Map中的key是允许为null的。
if (key==null) {
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
if (e.getKey()==null)
return true;
}
} else {
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
return true;
}
}
return false;
}
public V get(Object key) {
//获取Entry的迭代器
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
if (key==null) {
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
if (e.getKey()==null)
return e.getValue();
}
} else {
while (i.hasNext()) {
Map.Entry<K,V> e = i.next();
//通过key的equals来比较key是否相同
if (key.equals(e.getKey()))
return e.getValue();
}
}
//如果没有找到,则返回null
return null;
}
public V remove(Object key) {
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
Map.Entry<K,V> correctEntry = null;
//查找要删除的Entry
if (key==null) {
while (correctEntry==null && i.hasNext()) {
Map.Entry<K,V> e = i.next();
if (e.getKey()==null)
correctEntry = e;
}
} else {
while (correctEntry==null && i.hasNext()) {
Map.Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
correctEntry = e;
}
}
//存储要删除的value,如果没有找到要删除的映射,则返回null。找到的话则返回删除key对应的value。
V oldValue = null;
if (correctEntry !=null) {
oldValue = correctEntry.getValue();
i.remove();
}
return oldValue;
}
public void putAll(Map<? extends K, ? extends V> m) {
//遍历指定的map,然后调用put方法。
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
public void clear() {
//直接使用Set的clear()方法
entrySet().clear();
}
public Set<K> keySet() {
//如果key为空,则返回一个空的set集合
if (keySet == null) {
keySet = new AbstractSet<K>() {
//AbstractSet中的抽象方法iterator方法实现
public Iterator<K> iterator() {
return new Iterator<K>() {
//直接使用entrySet的迭代器
private Iterator<Map.Entry<K,V>> i = entrySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public K next() {
return i.next().getKey();
}
public void remove() {
i.remove();
}
};
}
public int size() {
return AbstractMap.this.size();
}
public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}
public void clear() {
AbstractMap.this.clear();
}
public boolean contains(Object k) {
return AbstractMap.this.containsKey(k);
}
};
}
return keySet;
}
public Collection<V> values() {
//如果值集合为空,则返回一个新建的Collection
if (values == null) {
values = new AbstractCollection<V>() {
public Iterator<V> iterator() {
return new Iterator<V>() {
private Iterator<Map.Entry<K,V>> i = entrySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public V next() {
return i.next().getValue();
}
public void remove() {
i.remove();
}
};
}
public int size() {
return AbstractMap.this.size();
}
public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}
public void clear() {
AbstractMap.this.clear();
}
public boolean contains(Object v) {
return AbstractMap.this.containsValue(v);
}
};
}
return values;
}
除了上面基本方法,还有就是提供equals和hashCode:
public boolean equals(Object o) {
if (o == this)
return true;
//先判断类型
if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
//在判断长度,能够有效提高比较效率
if (m.size() != size())
return false;
try {
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
//比较key和value相同才相等
Map.Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
public int hashCode() {
int h = 0;
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
//map的hash值为所有Entry哈希值的和
while (i.hasNext())
h += i.next().hashCode();
return h;
}
除了这些方法,AbstractMap还定义了两个内部静态类SimpleImmutableEntry和SimpleEntry,它们实现了Entry接口,分别表示不可边的实体Entry和可变的Entry,二者唯一的区别就是setValue方法的支持和不支持。
HashMap
HashMap是使用哈希表(hash table)实现的键值对集合,它继承AbstractMap抽象类和实现了Map接口。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap在功能上和HashTable一样,只不过HashMap不是同步操作,而且HashMap允许键值为null。
HashMap的特殊存储结构,使得获取指定元素前,需要经过哈希计算,得到目标元素在哈希表中的位置,然后在进行少量的比较即可得到元素。
当发生哈希冲突时,HashMap采用拉链法进行解决,所以HashMap的底层实现其实是数组+链表。
下面我们看一下HashMap的具体实现。
HashMap中定义了下面13个变量:
- 默认初始容量16,比如为2的整数倍
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
- 最大容量2^30,所以map大小为2 ~ 2^30 中间的偶数个。
static final int MAXIMUM_CAPACITY = 1 << 30;
- 哈希表的默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
- 树型阈值,当使用树而不是使用列表时使用,必须大于2。JDK1.8新增
static final int TREEIFY_THRESHOLD = 8;
- 非树形阈值(TODO),JDK1.8新增
static final int UNTREEIFY_THRESHOLD = 6;
- 树的最小容量,至少是TREEIFY_THRESHOLD的4倍,避免扩容时冲突。
static final int MIN_TREEIFY_CAPACITY = 64;
- 哈希表中的链表数组。
transient Node<K,V>[] table;
- 缓存的键值对集合,另外两个视图在AbstractMap中的values()和keySet()中。
transient Set<Map.Entry<K,V>> entrySet;
- map中的长度
transient int size;
- HashMap的修改次数,用来保证快速失败(fail-fast)
transient int modCount;
- 下次需要扩容的值,等于容量 * 负载因子
int threshold;
- Hash表的负载因子
final float loadFactor;
其中Node[K,V] table是HashMap底层存储的链表数组,在JDK1.8之前这也是唯一存储数据结构。Node实现了Map.Entry接口。Node的定义就是链表的定义加上实现了Entry中的方法。
static class Node<K,V> implements Map.Entry<K,V> {
//哈希值,就是位置
final int hash;
//存储的键
final K key;
//存储的值
V value;
//指向下一个节点的指针
HashMap.Node<K,V> next;
Node(int hash, K key, V value, HashMap.Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//哈希值为键和值哈希值的异或
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
//kv相等才相等
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
HashMap扩容开销是非常大的(重新创建数组、重新哈希、分配等等呢个),所以我们应该尽量减少扩容次数。与扩容有关的两个因素:
- 容量:数组的容量。
- 加载因子:决定了HashMap中元素占有多少比例时扩容。
HashMap的默认加载因子是0.75,这是在时间和空间两方面的考虑。加载因子太大的话,产生冲突的可能性就会很大,查找的效率就会降低。太小的话,需要频繁rehash,导致性能低。
加载因子表示哈希表中元素填满的程度,加载因子越大,填充的元素越多,导致冲突的概率也会越大,查找效率也会越低,但是空间利用率会提高。如果加载因子越少,则填充的元素少,导致冲突的概率也会越小,查找的效率会提高,但是空间利用率会降低。
HashMap提供了四个构造方法:
//指定初始容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//容量最多为2^30个
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子需要大于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//指定容量
public HashMap(int initialCapacity) {
//使用默认加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用默认容量和默认加载因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//创建一个内容为m映射
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
我们看到上面第三个构造方法调用了tableSizeFor方法,该方法是根据你传入的容量,计算出最接近你传入初始容量的2^n。比如传入是5,则返回8。
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;
}
接下来我们看看添加kv操作:
//将指定kv添加到Map中
public V put(K key, V value) {
//需要先调用hash方法计算hash值
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//哈希表的一个引用
HashMap.Node<K,V>[] tab;
//插入kv的节点
HashMap.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 {
//如果插入位置不为空则有两种情况:插入key已经存在或该key是产生hash冲途的链表内
HashMap.Node<K,V> e; K k;//e为当前插入节点
//拿出链表的第一个节点进行匹配,如果匹配成功则直接返回给e(对于引用类型需要使用equals比较,基础类型使用==比较)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果第一个节点不为目标节点,并且该节点类型是树形(JDK1.8后)则新增节点到树中
else if (p instanceof HashMap.TreeNode)
e = ((HashMap.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);
//当链表中节点个数大于8时,就需要树型化,提高效率
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果匹配到则直接停止,此时e已经指向了对应节点
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()方法计算哈希值。
- 然后调用putVal()方法根据哈希值进行相关操作。
- 如果当前链表为空,则新建一个链表。
- 如果插入的桶中没有元素,则新建一个节点放进去。
- 匹配桶中链表第一个元素,如果匹配成功,则直接替换新节点,结束查找。
- 如果第一个元素,并且第一个元素是JDK8以后的树型节点,则调用putTreeVal()将新数据插入到树中。
- 否则从传统链表中查找、替换。
- 当当前链表长度大于8时,调用treeIfyBin(),将链表树形话。
- 最后检查是否需要扩容
上面设计的方法:
- hash():计算哈希值,也就是数组中对应的位置
- resize():扩容
- putTreeVal():向树型节点添加元素
- treeIfByBin():树形化容器
我们先看一下上面计算hash值的方法。hash方法将当前key的哈希值与该哈希值右移16位的结果进行异或。之所以这么做是因为,元素的hashCode在很多时候低位是相同的,这将容易导致冲突。将key的hashCode与hashCode值右移16位进行异或,效果如下:
image.png
代码实现如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这样能够避免只依靠低位计算的结果导致冲突,将高低位结合能够避免哈希值的分布不均。
resize()是初始化或扩容时候使用的,思路是如果是初始化并且
final HashMap.Node<K,V>[] resize() {
HashMap.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;
}
//扩容到旧容量的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//旧容量为0,但是旧阈值大于0,说明创建的了哈希表,但是还没有添加元素
else if (oldThr > 0) // initial capacity was placed in threshold
//容量扩容到旧阈值
newCap = oldThr;
//旧容量和阈值都为0,说明还没有创建哈希表,则使用默认规则创建哈希表
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新阈值为0,则使用新容量重新计算一次
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"})
HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
HashMap.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 HashMap.TreeNode)
((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//否则保留旧哈希表中桶的元素顺序
HashMap.Node<K,V> loHead = null, loTail = null;
HashMap.Node<K,V> hiHead = null, hiTail = null;
HashMap.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;
}
扩容过程中的键点:
- 初始化哈希表时,容量为默认容量,阈值为 容量* 负载因子。
- 已有哈希表扩容时,容量、阈值均翻倍。
- 如果之前节点类型是树,需要把新哈希表表里当前桶也变成树形结构。
- 复制到新哈希表时需要重新索引(rehash),这里采用的计算方法为
e.hash() & (newCap - 1);等价于e.hash() % newCap
可以发现扩容开销确实大,需要迭代所有元素,rehash、复制,还得保留原来的数据结构。所以应该尽量少扩容,最好在初始化的时候指定好HashMap的长度,尽量避免resize()。
获取元素时最常用的就是get(Object key)了,我们看下它是如何实现的。
public V get(Object key) {
HashMap.Node<K,V> e;
//先计算key的hash值,然后通过getNode获取
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final HashMap.Node<K,V> getNode(int hash, Object key) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
//如果哈希表为空,则返回null。tab[n-1 & hash]是获取哈希表中的数组位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果拉链表中的一个元素和给定的key匹配,则返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//第一个元素不符合,则开始向后遍历
if ((e = first.next) != null) {
//如果节点类型为树类型,则通过getTreeNode获取节点值
if (first instanceof HashMap.TreeNode)
return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//节点类型为链表,则开始遍历链表
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
获取值的关键步骤:
- 先计算哈希值hash。
- 利用(n-1) & hash计算桶的位置。
- 在桶里遍历链表或树查找元素。
HashMap提供的其它方法实现和上面类似,都是先通过hash值找到哈希表位置,然后遍历链表进行操作,这里就不一一介绍了。下面主要看下JDK1.8新增的红黑树。
[图片上传失败...(image-f0783a-1548676054872)]
在JDK8之后如果桶中的链表长度大于8之后,就会将其转换为一棵红黑树,目的在于能够提高查大量哈希冲突后的元素遍历。
首先看一下HashMap中的红黑树定义:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
...
}
TreeNode中提供了红黑树的基本操作左右转:rotateLeft和rotateRight。同时还提供了红黑树节点的添加、删除、修剪等操作。
JDK8新增的红黑树有三个关键参数变量:
//桶的树化阈值,当桶中元素个数大于8时,将链表转换为RB-tree
static final int TREEIFY_THRESHOLD = 8;
//桶中将树还原为链表的阈值,当树的节点个数为6时,将其转换为列表
static final int UNTREEIFY_THRESHOLD = 6;
//哈希表最小树形化容量,让hash表中元素大于这个值时才会进行树型化
static final int MIN_TREEIFY_CAPACITY = 64;
关于HashMap的红黑树内部具体实现之后再看:TODO
HashMap不是同步的,如果想要同步使用,可以加锁或使用
Collections.synchronizedMap
包一层,变成线程安全的Map。
HashMap返回的三个视图,都是fail-fast,所以在遍历过程中,如果修改了Map内容,会抛出ConcurrentModificationException。
如果HashMap中大量元素放在一个桶中,这时候在遍历桶中元素时,时间复杂度就为O(n),就失去HashMap的优势了。针对这个问题HashMap在JDK1.8新增了红黑树优化这个问题。
之所以容量要为2的幂次方,主要是以下两个原因:
- 容量为2的整次幂的话,h&(length-1)就相当于取模运算,使用减法替代取模运算提升效率。
- 保证了length-1为奇数,这样length-1二进制最后一位为1,它与hash结果进行&运算可以随着hash值得到奇数或偶数。如果为偶数,则与后的值一定为0(因为0与任何值都是0),这样所有值都会散列到数组下标偶数位置。
TreeMap
TreeMap继承AbstractMap,并且实现了NavigableMap,再看TreeMap前,我们先看下NavigableMap实现。
NavigableMap
NavigableMap接口继承SortedMap接口。
Map中键值对是无序的,我们在遍历Map时,遍历键的顺序是随机的。SortedMap接口是Map接口进一步扩充,通过对键(key)的排序(自然升序或提供比较器),使得在遍历Map视图时能够根据指定顺序遍历(entrySet()、keySet()、values()方法返回的迭代器)。
public interface SortedMap<K,V> extends Map<K,V> {
//返回此Map中使用的比较器,如果采用自然顺序,则返回null。
Comparator<? super K> comparator();
//返回此映射的部分视图,范围从指定的fromKey(包含)到toKey(不包含)
java.util.SortedMap<K,V> subMap(K fromKey, K toKey);
//返回次映射的部分视图,范围为键值小于toKey的所有键值对
java.util.SortedMap<K,V> headMap(K toKey);
//返回此映射的部分视图,范围为键值大于等于fromKey的所有键值对
java.util.SortedMap<K,V> tailMap(K fromKey);
//返回此映射中的第一个键
K firstKey();
//返回次映射的最后一个键值
K lastKey();
//返回包含所有键的集合视图
Set<K> keySet();
//返回所有值的列表视图
Collection<V> values();
//返回所有键值对实体的集合视图
Set<Map.Entry<K, V>> entrySet();
}
所有实现可排序的Map的实现类,都应该实现四个标准构造方法。
- 无参构造函数,它将会根据键的自然顺序创建一个空的有序映射。
- 具有单个参数类型为Comparator的构造方法,它会根据提供的比较器对键值对进行排序。
- 具有单个参数类型为Map的构造方法,它会创建一个与参数具有相同键值对新映射,并且按照自然顺序对键进行排序。
- 具有单个参数类型为SortedMap的构造方法,它会创建一个与参数具有相同键值对映射,并且顺序和SortedMap的排序一致。
需要注意,所有键都需要实现Comparable接口,或能够接受comparator比较器。
NavigableMap扩展了Sorted接口,提供了要查找的元素最接近匹配相关方法。
[图片上传失败...(image-316f83-1548676054872)]
NavigalbeMap除了提供SortedMap定义的方法外,还提供了以下几类方法:
- lowerEntry、floorEntry、ceilingEntry、higherEntry分别返回小于指定key、小于或等于指定key、大于或等于指定key和大于指定key的键值对(只返回最接近的一个键值对)。
同理lowerKey、floorKey、ceilingKey、higherKey返回对应的键key。 - 同时还提供了返回最小key、最大key的firstEntry、pollFirstEntry(返回并删除)、lastEntry、pollLastEntry(返回并删除)。
- 最后NavigableMap还提供了倒序视图descendingMap、倒序键集合decendingKeySet和返回NavigableSet类型的键集合navigableKeySet()。
TreeMap实现
TreeMap是基于红黑树实现的有序键值对集合,该映射根据键的自然顺序或传入的比较器进行排序,具体取决于使用的构造函数。
TreeMap的基本操作containsKey、get、put和remove的时间复杂度为log(n)。
TreeMap继承了AbstractMap抽象类,并且实现了NavigableMap、Cloneable、Serializable接口。
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
首先TreeMap的构造方法就是包含了第一开始所说的可排序Map四类构造函数。
//无惨构造函数
public TreeMap() {
comparator = null;
}
//指定比较器的构造函数
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
//传入指定Map,以自然升序构造的新TreeMap
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
//传入排好序的SortedMap
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
//采用有序方式构造TreeMap
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
我们看到第三个构造函数最后调用了putAll方法,我们再看下putAll的具体实现。
public void putAll(Map<? extends K, ? extends V> map) {
int mapSize = map.size();
//如果Map类型为SortedMap类型,通过buildFromSorted方法构造
if (size==0 && mapSize!=0 && map instanceof SortedMap) {
Comparator<?> c = ((SortedMap<?,?>)map).comparator();
if (c == comparator || (c != null && c.equals(comparator))) {
++modCount;
try {
buildFromSorted(mapSize, map.entrySet().iterator(),
null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
return;
}
}
//如果是无序Map,则使用AbstractMap中定义的putAll方法
super.putAll(map);
}
从上面可以看到如果传入Map类型如果SortedMap则采用和第四个构造函数一样的方法构建TreeMap:buildFromSorted。如果为普通Map则使用AbstractMap的putAll,上面我们看到过AbstractMap的putAll就是挨个调用子类的put方法实现。
首先我们看下红黑树的数据结构:
static final class Entry<K,V> implements Map.Entry<K,V> {
K key; //存储Map的key
V value;//存储Map的value
Entry<K,V> left; //树左节点引用
Entry<K,V> right; //树右节点应用
Entry<K,V> parent;
boolean color = BLACK; //颜色
/**
* Make a new cell with given key, value, and parent, and with
* {@code null} child links, and BLACK color.
*/
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}
除了HashMap和TreeMap,我们看下Map中的其它实现:
- Attributes:Attributes实现了Map接口,它将Manifest属相映射到关联的字符串,底层使用Map实现key-value存储。
- HashTable:继承Dictionary类,并且实现了Map接口。线程安全的哈希表实现,实现方式和HashMap类似采用数组实现存储,采用链表解决hash冲突(没有提供红黑树的优化)。HashTable通过为每个方法增加synchronized同步锁的方式,保证线程安全。如果不需要线程安全建议使用HashMap,如果需要线程安全,并且对高并发有要求,建议使用ConcurrentHashMap。
- ConcurrentHashMap:功能和HashTable一样,但是不是通过锁来来保证方法访问的线程安全,所以能够应对高并发下的使用。ConcurrentHashMap采用的是锁分段技术,将数据分成一段段的存储,然后给每一段分配一把锁,当线程占用锁访问其中一段数据时,其它端还能够被访问。
- LinkedHashMap:LinkedHashMap继承了HashMap和Map接口,它与HashMap不同的是将所有元素通过一个双向链表连接。这样就提供了可以按照插入顺序遍历元素的可能,而不需要使用TreeMap那样引入额外成本(提供排序顺序)。LinkedHashMap提供了一个特殊构造函数,其迭代顺序为其元素最后一次访问的顺序,这个非常适合构建LRU缓存。
- EnumMap:专门用于枚举类型键的Map实现,枚举映射中的所有键必须来创建映射时指定的枚举类型。
- IdentityHashMap:不是通用Map的实现,因为它违反了Map的规定,它要求在比较对象时使用equals方法。此类仅在引用相等语义的情况下使用。
- Properties:继承HashTable,Properties类表示一组持久的属性,可以将Properties保存到流中,会从流中加载。Properties中每个键和值都是一个字符串。
- WeakHashMap:WeakHashMap继承AbstractMap,并且实现了Map接口。WeakHashMap和HashMap类似,但是WeakHashMap中键为弱键,意思是当键不在正常使用时,会被从WeakHashMap中自动移除。比如WeakHashMap中键没有其它对象引用使用时,就会被GC回收。
- ConcurrentSkipListMap
集合相关问题
-
Iterable和Iterator的关系是什么?
Iterator是迭代器接口,而Iterable是使用迭代器需要实现的接口。所以我们如果想要使用迭代器就需要实现Iterable接口,并且实现它的iterator()方法,来返回一个迭代器。
之所以这样主要是因为Iterator中的next()、hasNext()等方法是依赖当前位置进行的,如果所有需要使用迭代器的类都直接继承Iterator接口的话,都需要为其维护一套指针,并且当同时操作一个类的迭代器时,就会出现这个指针混乱。
所以一个明智的方法就是通过Iterable的iterator方法来为每个需要使用迭代器的方法返回一个迭代器。 -
什么是深拷贝和浅拷贝?
拷贝方法定义在Object中,Object对clone方法进行了默认实现,这个默认实现其实就是一种浅拷贝。它对于拷贝对象中的应用类型会将其地址拷贝出来,这样就会导致两个对象操作相同的引用变量。
所以如果想要使用深拷贝,需要重新实现clone方法,不仅利用浅拷贝将基础类型进行拷贝,而且还还需将引用类型进行深拷贝。
还有一种实现深拷贝的方法就是利用序列化与反序列化,将对象序列化成字节,然后反序列化成对象返回。