Set接口及其 实现类
基本
- 无序(添加和取出的顺序不一样),没有索引
- 不允许有重复的元素
- 常用方法和Collection的子接口相同
- 遍历的方式和Collection 相同
- 可以使用iterator和增强for 但是不能使用索引的方式来获取
- 补充第一点:取出的顺序不是添加的顺序,但是取出后 顺序固定
-
Set接口实现类-HashSet
(1)HashSet基础说明:
-
HashSet实现了Set接口
-
HashSet实际上是基于HashMap实现的
public HashSet(){ map = new HashMap<>(); }
-
可以存放null值,但是只能有一个null
-
HashSet不保证元素是有序的,取决于hash后,在确定索引的结果
-
不能有重复的元素 / 对象
(2)HashSet底层机制说明:
-
HashSet底层是HashMap ;HashMap的底层是(数组+链表+红黑树)
-
-------> 用一段代码来模拟一下 (数组+链表)
-
/** * 模拟一个HashSet底层(一个HashMap结构) */ public class HashSetMode { public static void main(String[] args) { // 创建一个数组 数组类型是Node[] Node01[] table = new Node01[16]; // 创建一个 join的节点 Node01 join = new Node01("join", null); // 将join作为第一个节点放入 第二个数组内 table[1] = join; // 将 节点 mark 悬挂在join后 形成链表 Node01 mark = new Node01("mark", null); join.next = mark; for (Object o :table) { System.out.println(o); } } } class Node01{ Object item; // 存放内容 Node01 next; // 指向下一个节点 public Node01(Object item, Node01 next) { this.item = item; this.next = next; } @Override public String toString() { return "Node01{" + "item=" + item + ", next=" + next + '}'; } }
null Node01{item=join, next=Node01{item=mark, next=null}} null null . . .
(3)分析HashSet的添加元素底层是如何实现的(hash() + equals())
- 结论:
HashSet底层是HashMap
添加一个元素时,先得到hash值 -会转成 -> 索引值
找到存储数据表table,看这个索引位置是否已经存放元素
如果没有,直接加入
如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则悬挂到后面
java8中,如果链表元素个数超过TREE_THRESHOLD(默认为8),并且table的大小 >= MIN_TREELFY-CAPACITY(默认为64),就进行树化(红黑树)
-
debug 测试代码
public static void main(String[] args) { HashSet hashSet = new HashSet(); hashSet.add("java"); hashSet.add("php"); hashSet.add("java"); System.out.println(hashSet); }
-
执行 HashSet
public HashSet() { map = new HashMap<>(); }
-
执行add() 方法
public boolean add(E e) { // e: "java" return map.put(e, PRESENT)==null; // e: "java" map: "{}" } // 这里的 PRESENT 来自HashSet的 Static final 类型的一个对象,作用是占位,为了HashSet能够使用HashMap
-
追进 执行put()方法
public V put(K key, V value) { // key:"java" value = PRESENT 不变 return putVal(hash(key), key, value, false, true); }
-
追进 看hash(key) ,是按照这个算法得到key对应的hash值
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
返回 追进 putVal()
- 这里的 table 是HashMap的一个属性,是一个Node数组
- 第一次执行add 的情况
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 1.table = null 执行进入 ;if语句表示 如果当前的table为 null 或者大小为 0,就第一次扩容大小为16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // resize 缓冲扩容机制 n: 16 // 2.根据key得到的hash值,去计算该key应该存放到table表的哪个索引位置,并把这个位置的对象赋给p // 3. 判断p是否为null = 判断对应table索引位置有没有存放元素 // 3. 如果p为null,表示还未放入元素,就创建一个Node(key,value) if ((p = tab[i = (n - 1) & hash]) == null) // i: 3 tab[i] = newNode(hash, key, value, null); // key:"java" value:PRESENT else { Node<K,V> e; K k; if (p.hash == hash && // p指向的是当前索引位置对应的最后一个元素;判断与预添加的key的hash是否相同 ((k = p.key) == key || (key != null && key.equals(k)))) // 同一个对象或内容相同 e = p; // 判断p 是不是红黑树 // 如果是红黑树 就调用 putTreeVal();进行添加 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 如果对应索引的table 已经形成了一个链表;就用这个for循环去判断 // 依次和链表的每一个元素比较,如果都不相同,直接悬挂到最后面,否则 break for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 当前链表如果到达8 就进行树化;treeifyBin()内要求table>= 64 才会树化,不然只会先扩容 break; } 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) // * 这里的size 增加的条件: 只要成功加入了一个节点 不管是加入table内还是Node内都满足条件,从而触发扩容 resize(); // 扩容 afterNodeInsertion(evict); return null; }
// resize()方法 第一次进入的处理 标示上面的 第二步 // 扩容机制: 0.75 x 16 = 12 // 当使用12个空间的时候,就开始缓存=此时开始扩容,不等到16个空间全部使用完再扩容 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // DEFAULT_LOAD_FACTOR(加载因子) =0.75 DEFAULT_INITIAL_CAPACITY = 1 >> 4 =16 }
-
LinkedHashSet
- 为HashSet的一个子类
- 底层是 LinkedHashMap,底层维护的是一个(数组table+双向链表)
- 根据元素的Hashcode值,来决定元素的存储位置。同时使用链表维护元素的次序,使得元素的输出与插入次序一致。(加入次序和取出次序一致)
- 不允许添加重复的元素
-
补充说明:
- 在LinkedHashSet 中维护了一个hash表和一个双向链表(LinkedHashSet有 head 和 tail)
- 每个节点有pre 和 next 属性,这样形成一个双向链表
- 在添加一个元素时,先求hash值,再求索引。确定该元素再hashtable的位置,然后将添加的元素添加到双向链表(如果已经存在不添加,原则和hashSet一样)
- 这样就实现了遍历LinkedHashSet时,插入顺序和输出顺序一致
-
-
扩容机制:
添加第一次时,直接将table数组 扩容到 16 ,存放的节点类型是 LinkedHash E n t r y , 数 组 是 H a s h M a p Entry ,数组是 HashMap Entry,数组是HashMapNode[]