Set接口
基本介绍
注:取出的顺序是固定的,不会变。遍历方法:迭代器,增强for,不能用for循环因为没有索引,也没有get方法。 用HashSet演示。
HashSet
底层是HashMap 使用 Hash + equals 方法
public HashSet() {
map = new HashMap<>(); //创建一个HashMap
}
HashSet的add方法大概思路:
1. 先获取元素的哈希值(hashCode方法) 2. 对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号。 3. 如果该位置上没有其他元素,则直接存放。4. 如果该位置上已经有其他元素,则需要进行equals判断,如果相等则不再添加。如果不相等,则以链表形式添加。
其中PRESENT是一个Object对象,只起到占位的作用。
key是输入的关键字,value就是PRESENT。hash方法计算出key的哈希值,注意并不是简单调用了hashCode方法,而是与 h>>>16进行了异或,计算的伪哈希值,最终在putVal方法中用 按位与 计算出索引。
重点是理解 putVal这个方法,源码自己去看,这里只写说明。resize方法用于修改Node数组大小。afterNodeInsertion(evict) 无实际用处,是留给子类实现的方法。
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;
//table是HashMap的属性,是放Node的数组。刚开始table为null,resize对tab初始化,扩容到16个空间
if ((p = tab[i = (n - 1) & hash]) == null) //计算出真正的索引,把对应位置赋给p
tab[i] = newNode(hash, key, value, null); //如果p为空,说明没有元素,创建一个结点,把内容放进去。
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//准备添加的key的hash值与当前索引位置对应链表的首元结点hash值相同。
//并且满足下面两个条件之一:1. p指向的Node结点的key和准备加入的key是同一个对象
2. p指向的Node结点的key用equals方法和准备加入的key比较后相同
//此时不能加入,e指向p,不做任何处理
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果p是红黑树,那么调用putTreeVal方法加入
else {
// 最后一种情况,说明虽然位置被占,但是与首元结点不相同,找首元结点对应的链表
for (int binCount = 0; ; ++binCount) { //开始遍历链表
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);// 依次和该链表的每一个元素比较后,都不相同(到了最后一个结点),则加入到该链表的最后
if (binCount >= TREEIFY_THRESHOLD - 1) // 注意在把元素添加到链表后,立即判断该链表是否已经达到8个结点(也就是>=7)
treeifyBin(tab, hash);// 如果已经达到,就调用 treeifyBin()。在这个方法里,要先进行判断 if(tab == null || (n=tab.length)<MIN_TREEIFY_CAPACITY) 也就是看table是否为空或者是否小于64,如果成立,先table扩容。如果不成立才转成红黑树。
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 和该链表的每一个元素比较过程中,如果有相同情况,就直接break(判断条件与上方一致)
p = e;
}
}
if (e != null) { // 说明有重复元素(主要针对HashMap,用于覆盖)
V oldValue = e.value; //记录value,HashSet都是PRESENT,但HashMap是自己定义的
if (!onlyIfAbsent || oldValue == null)
e.value = value; //HashMap要覆盖,HashSet就不用了,因为PRESENT是null
afterNodeAccess(e);
return oldValue; //add失败(不是null)
}
}
++modCount; //记录修改次数
if (++size > threshold) //threshold在resize方法中,是表的临界值(初始12)
resize(); //如果size大于临界值,扩容
afterNodeInsertion(evict);//留给子类实现的方法,对HashMap来说是个空方法
return null;//返回空 成功
}
要注意的一个地方:table扩容的两个时机—— 1. 大于临界值 2. 链表加入结点并且大于8个
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); // treeifyBin的部分代码
// 这告诉我们,如果链表上已经超过8个结点,但是table还没达到64,那么会先扩容
重写判断是否加入的方法
需要重写Employee类的equals方法和hashCode方法(直接输入equals)
注意这样生成的方法就是重写后的方法,不用更改。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == employee.age && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); //name,age等属性都被塞进一个object数组里
}
如果类里面有别的类属性(比如A里面有B类的对象),那么B里面也要重写equals和hashCode方法
LinkedHashSet
(结点应该放在绿色框里,这里画的不清楚)
1. LinkedHashSet 加入顺序和取出元素的顺序一致。
2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
3. LinkedHashSet 底层结构—— 数组table + 双向链表
4. 添加第一次时,直接将数组table扩容到16,数组table是 HashMap$Node[]类型,但存放的结点类型是 LinkedHashMap$Entry(多态),是Node的子类(可以从左下角的structure查看)
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}