原码拆解与流程图为原创,转载请注明。
本文为集合类解析(二),从根本上了解集合类,请先移步:
Collection 集合类、Iterator 迭代器、List解析移步:集合类解析(一):表结构与集合类Collection,Iterator,List基础讲解
阅读Set与Map的实现类源码即可发现,为何set与Map要一起描述,主要的Set实现类如HashSet和TreeSet,均是由其对应的Map结构实现而成。
Java 源码中Set架构图:
public abstract class AbstractSet extends AbstractCollection implements Set
HashSet继承至AbstractSet,并实现Set,Cloneable,Serializable接口。
HashSet依赖于HashMap,其方法是通过HashMap实现的,同时HashSet中的元素是无序的。
public class HashSet
extends AbstractSet
implements Set, Cloneable, java.io.Serializable
注意,此实现不是同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet
方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:
Set s = Collections.synchronizedSet(new HashSet(...));
private transient HashMap 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调用了HashMap存放,因为HashSet并不是键值对存储,只是把Map中的键key设置为了Set的值,
//在遍历HashSet的集合元素时,实际上是遍历的Map中Key的集合。
public Iterator 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 newSet = (HashSet) super.clone();
newSet.map = (HashMap) 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继承于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的继承关系实际和Set的非常相似。也是由AbstractMap类实现大多数Map的基本操作。
而HashMap是HashSet的方法实现,LinkedHashMap是LinkedHashSet的实现方式。
数据结构
HashMap的基本数据结构是一个由链表组成的数组。(在JDK1.8之后,当链表长度大于8时,转换为红黑树)
两张图示意:
举例来说:
HashMap重要的内部类 Entry
可以看出 Entry 实际上就是一个单向链表。这也是为什么说HashMap是通过拉链法解决哈希冲突的。
static class Entry implements Map.Entry {
final K key;
V value;
Entry next;
int hash;
//Entry包含key,value,hash和next指针
Entry(int h, K k, V v, Entry 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 m) {}
void recordRemoval(HashMap 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 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 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的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行。