原码拆解与流程图为原创,转载请注明。
本文为集合类解析(二),从根本上了解集合类,请先移步:
Collection 集合类、Iterator 迭代器、List解析移步:集合类解析(一):表结构与集合类Collection,Iterator,List基础讲解
集合类之Set与Map
阅读Set与Map的实现类源码即可发现,为何set与Map要一起描述,主要的Set实现类如HashSet和TreeSet,均是由其对应的Map结构实现而成。
Set
Java 源码中Set架构图:
- Set是一个接口,继承于Collection,包含方法与Collection一致,是一个不允许有重复元素的集合。
- AbstractSet是一个抽象类,继承AbstractCollection,并实现了Set接口。AbstractCollection实现了Set中的绝大部分函数,为Set的实现类提供了便利。
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E>
- HastSet 和 TreeSet 是Set的两个实现类,均由其对应的map实现而成。
HashSet
HashSet继承至AbstractSet,并实现Set,Cloneable,Serializable接口。
HashSet依赖于HashMap,其方法是通过HashMap实现的,同时HashSet中的元素是无序的。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
注意,此实现不是同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet
方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:
Set s = Collections.synchronizedSet(new HashSet(...));
- 基本属性:
private transient HashMap<E,Object> map; //map集合,HashSet存放元素的容器
private static final Object PRESENT = new Object(); //map中键对应的value值,常量,相当于占位符,逻辑上并不使用。
- 构造方法:
//无参构造方法,创建要使用的map
public HashSet() {
map = new HashMap<>();
}
//构造包含指定集合中的元素的新集合。
//使用默认的负载因子(0.75)和足以包含指定集合中的元素的初始容量创建。如果传入为空抛空指针异常
public HashSet(Collection<? extends E> c) {
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);
}
- 遍历:HashSet的遍历操作,解释了HashSet对HashMap的使用方式。
//HashSet调用了HashMap存放,因为HashSet并不是键值对存储,只是把Map中的键key设置为了Set的值,
//在遍历HashSet的集合元素时,实际上是遍历的Map中Key的集合。
public Iterator<E> iterator() {
return map.keySet().iterator();
}
- 主要方法源码:
public boolean isEmpty() {
return map.isEmpty();
}
//contains使用map的containsKey方法来判断
public boolean contains(Object o) {
return map.containsKey(o);
}
//add时,放入传入的值,而map的value设置为常量,所以说在set中,PRESENT这个值用于占位,本身无意义
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//与add()同理
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
}
//输出流写方法
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
...
}
//输入流读取
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
...
}
}
- LinkedHashSet
LinkedHashSet继承于HashSet,是具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。
此实现与 HashSet 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代。此实现可以让客户免遭未指定的、由 HashSet
提供的通常杂乱无章的排序工作,而又不致引起与 TreeSet
关联的成本增加。使用它可以生成一个与原来顺序相同的 set 副本,并且与原 set 的实现无关。
//LinkedHashSet使用其父类HashSet的构造方法
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
LinkedHashSet使用父类HashSet的构造方法,使用LinkedHashMap来处理LinkedHashSet操作。
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
Map
观察类图就会发现,Map的继承关系实际和Set的非常相似。也是由AbstractMap类实现大多数Map的基本操作。
而HashMap是HashSet的方法实现,LinkedHashMap是LinkedHashSet的实现方式。
HashMap
- 基于哈希表的 Map 接口的实现。根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,具有很快的访问速度。
- 此实现提供所有可选的映射操作,并允许使用 一个null键 key和多个null值value。
- 此类不保证映射的顺序,同时不保证顺序不变。
- 非线程安全。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)
- 如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
数据结构
HashMap的基本数据结构是一个由链表组成的数组。(在JDK1.8之后,当链表长度大于8时,转换为红黑树)
两张图示意:
举例来说:
HashMap重要的内部类 Entry
可以看出 Entry 实际上就是一个单向链表。这也是为什么说HashMap是通过拉链法解决哈希冲突的。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
//Entry包含key,value,hash和next指针
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//Entry实现的方法
public final K getKey() {return key; }
public final V getValue() {return value;}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); }
public final String toString() { return getKey() + "=" + getValue();}
void recordAccess(HashMap<K,V> m) {}
void recordRemoval(HashMap<K,V> m) {}
}
}
HashMap的核心代码在于put()操作,如果键值存在则更新数据,用新的value取代旧的value;如果键值不存在,调用addEntry(),将“key-value”添加到table中,此处便涉及hash数组和链表的插入操作。
//put方法
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值(然后将其添加到该哈希值对应的链表中)
int hash = hash(key.hashCode());
//用indexFor来计算entry对象在table数组中的索引值,indexFor方法在下面单独列出
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//遍历数组[i]位的链表,若“该key”对应的键值对已经存在,则用新的value取代旧的value。
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到table中
modCount++;
addEntry(hash, key, value, i);
return null;
}
//增加键值,修改链表的关键操作
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
// 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
resize(2 * table.length);
//如果key为null,添加至0位
hash = (null != key) ? hash(key) : 0;
//计算添加位置(数组下标)
bucketIndex = indexFor(hash, table.length);
}
//1.7后,addEntry整合了creatEntry,执行添加节点操作
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//在Entry数组table中获取bucketIndex位置的数据
Entry<K,V> e = table[bucketIndex];
//向table数组bucketIndex位置添加Entry对象,且放在链表第一位,把原值e设置成新数据的next
//此操作的原因是通常情况下,后置入的数据有更多被访问的可能性,可提升性能
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
//返回位置和值
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
}
上面的代码可以按此流程图来对照理解:
至此,HashMap的主要构建方法就可以理解了。
HashTable
- Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类。
- 线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
- 底层数组+链表实现,无论key还是value都不能为null,除了非同步和不允许使用 null 之外,Hashtable与HashMap 类 大致相同。
- 并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。
ConcurrentHashMap
- 底层采用分段的数组+链表实现,线程安全。
- ConcurrentHashMap是使用了锁分段技术来保证线程安全的。ConcurrentHashMap中包括了“Segment(分段)数组”,每个Segment就是一个哈希表,也是可重入的互斥锁。通过把整个Map分为N个Segment,可以提供相同的线程安全,而且效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
而Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行。锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁, 当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
- 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
- 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容。