Set
前面写过一篇有关Set的笔记,发现写的过于基础,没有说明重点,在此重新更新一篇。
Set是一个不允许有重复元素出现的线性数据结构,它的实现类有:AbstractSet , ConcurrentHashMap.KeySetView , ConcurrentSkipListSet , CopyOnWriteArraySet , EnumSet , HashSet , JobStateReasons , LinkedHashSet , TreeSet。在这里我们主要讲解我们常用的HashSet,TreeSet.
HashSet
HashSet是基于HashMap来实现的,它的大部分操作都是使用HashMap来处理的,
public HashSet() {
map = new HashMap<>();
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
等等,HashSet的所有构造函数,都是通过HashMap,或LinkedHashMap来实现,相关的操作,eg:isEmpty(),size(),iterator()方法都是通过调用全局的map的相关方法来实现的,而这个map实例化则是通过构造函数来完成。
HashMap
HashMap继承AbstractMap,实现Map接口,Map是一种键值对<Key,Value>的方式来存储数据。
常量有:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
装载因子:0.75,一种用来判断已有数据量占总数据量的程度,即等于 已有数据/总的数据。
最大容量:则是指明HashMap能存放的最大数据量。
默认初始大小:默认的初始大小为16。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
......
......
......
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
transient Node<K,V>[] table;
省略号代表中间有代码,我只不过截取想要解释的代码。
我们可以看出HashMap是一个数组,数组中的元素是Node实例,Node则是一个实现Map.Entry<K,V>的实现类,Node是一个链表,每个元素都有指向下一个元素的地址。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
空参构造函数,使用的装载因子为0.75,容量为默认的 DEFAULT_INITIAL_CAPACITY = 1 << 4,即16。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
带有初始容量的构造器:则会调用另一个构造器,实例的实际容量大小还需要进行比较,才可以得出:
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);
}
如果容量为0或装载因子小于0或无限大,抛出异常;初始容量大于MAXIMUM_CAPACITY,则intialCapacity等于MAXIMUM_CAPACITY;threadshold为数组的实际大小容量,通过tableSizeFor函数计算,
注意:tableSizeFor这个函数,是为了求的大于输入参数且最近的2的整数次幂的数,并返回。
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;
}
这个算法效率特别高,简单解释就是:它将数据依次无符号右移,然后进行位或运算,最高位的1后面的位全变为1。n+1,得到了2的整数次幂的值了。网上有有关这个算法的解释,建议看看,(这就是阅读源码的好处)
threshold的值为HashMap的容量的大小;所以传入的initialCapacity并不是HashMap的大小,实际大小为大于initialCapacity的2的整数次幂,即例如7,则返回8。
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) {
......
return null;
}
put方法传入Key,Value值,然后调用内部的putVal方法进行实际的添加元素操作。源码放在这,有点长,所以建议亲自读取,这里进行解释:
putVal(int hash, K key, V value, boolean onlyIfAbsent boolean evict)
形参onlyIfAbsent表示是否改变存在的值,如果为true,则不改变存在的值,如果为false,则改变;
形参evict表示表所处的模式,如果为false,则说明table处于创建模式.
⑴先判断table数组是否为null或table的长度为0,如果为空或为0,则调用resize()方法创建table,
⑵接下来根据hash值和table大小-1做与运算,求出Entry的下标位置,如果为null,则证明该位置没有元素,则在此位置创建新的节点,如果不为null,则证明该位置已经有Entry对象,判断key是否等于第一个元素的key,hash值是否相等,如果相等,则将该节点赋值给这个元素,如果不相同,则证明该节点的key不在链表头,因此对链表进行遍历,找到key和hash相同的节点,则赋值,如果一直没有找到,则将其插入到链表的尾部。如果节点属于TreeNode类,则使用putTreeVal()方法进行存储。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((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;
}
get方法,先根据Key的值,计算hash,找到数组的位置,如果位置为null,返回null,否则计算出元素在table数组中的位置,然后遍历在该位置的链表,从头到尾遍历,找到key和hash相同的node节点,返回val值,如果没有,返回null。
remove方法过程与get类似。
HashSet线程不安全。
TreeSet
TreeSet基于TreeMap实现,支持排序。
public TreeSet() {
this(new TreeMap<>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
TreeSet的实例的实现都是通过TreeMap实现。
TreeSet的其他方法,eg:size,isEmpty,contains等都是通过NavigableMap<E,Object> m实例调用具体的方法进行,NavigableMap是一个接口,而TreeMap则是NavigableMap接口的实现类,这里通过多态的方式来进行访问,在TreeSet中使用接口定义实例,而在构造方法中,将具体的实现类TreeMap赋值给NavigableMap变量。
TreeMap
实现NavigableMap接口,
public TreeMap() {
comparator = null;
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
......
TreeMap空参构造器,将comparator置为null;
TreeMap(Comparator comparator)参数,接受一个Comparator实现类,可以自定义TreeMap中元素的存储顺序。
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
......
TreeMap是基于红黑树的实现,它的每个节点都有其左,右,父节点的指向。红黑树是一个平衡的二叉搜索树。如果红黑树失去平衡,则必须通过旋转令其恢复平衡。
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
TreeMap的get(key)通过调用内部的getEntry(key)方法,来匹配树中是否有此key的节点,有则输出其val值,没有则返回null。对于getEntry(key)方法,首先判断key是否null,然后检查key是否实现了Comparable接口,因为树的比较就是基于Comparable接口的comparaTo()方法来进行比较,对树从根节点root进行遍历,如果key大于比较节点,则将从右边部分开始遍历,如果key小于被比较的节点,则从左边部分遍历,类似与二分法。因为红黑树是一个二叉搜索树,每个节点大于它的左部分,小于它的右部分。
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;
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 {
......
}
......
return null;
}
先判断树是否为null,如果为空,则直接将元素作为根节点,然后判断是否有comparator传入进来,这个comparator变量是TreeMap在开头定义的,只有定义,没有实例化,而具体的实例化是通过构造函数中携带的Comparator实例,如果有,即传入comparator实例,则以次来进行比较,使用二分法,来找到具有相同key,并直接替换其value,如果没有找到相等的key,则创建一个新的Entry对象,将其插入到特定的位置,如果comaprator为null,则判断key是否为Null,如果为null,抛出空指针异常,否则则将key转为Comparable,因为很多类都实现了Comparable接口,进行同样的遍历。
TreeMap不是线程安全的。
总结的代码来自jdk1.8源代码,如有问题,敬请指出,与君共勉。