Set接口基本介绍
- 无序(添加和取出的顺序不一致),没有索引
- 不允许重复元素,所以最多包含一个null
- JDK API中Set接口的实现类有:
Set接口的常用方法
和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样
下面以Set
接口的实现类HashSet
来讲解Set
接口的方法
HashSet set = new HashSet();
set.add("john");
set.add("lucy");
set.add("john"); //重复
set.add("jack");
set.add(null);
set.add(null); //再次添加null
System.out.println("set=" + set); //set=[null, john, lucy, jack]
可以看出:
- Set 接口的实现类的对象(Set接口对象),不能存放重复的元素,可以添加一个null
- Set接口对象存放数据是无序的(即添加的顺序和取出的顺序不一致)
- 取出的顺序虽然不是添加的顺序,但是固定的
Set接口的遍历方法
同Collection的遍历方法一致,因为Set接口是Collection接口的子接口
-
可以使用迭代器
Iterator iterator = set.iterator(); while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println("obj=" + obj); }
-
增强for
System.out.println("=======增强for========"); for (Object o : set) { System.out.println("o=" + o); }
- 不能使用索引的方式来获取
Set接口类——HashSet
-
HashSet实现了Set接口
-
HashSet实际上是HashMap,看下源码:
public HashSet() { map = new HashMap<>(); }
-
可以存放null值,但是只能有一个null
-
HashSet不保证元素是有序的,取决于hash值,再确定索引的结果 (即,不保证存放元素的顺序和取出顺序一致)
-
不能有重复元素/对象,在前面Set 接口使用已经讲过
注意:
-
在执行add方法后,会返回一个boolean值,如果添加成功,返回 true,否则返回false
-
HashSet不能添加相同的元素/数据?
set.add("lucy"); //添加成功 set.add("lucy"); //加入不了 set.add(new Dog("tom")); //OK set.add(new Dog("tom")); //OK
HashSet底层源码机制说明
- 分析HashSet的底层是HashMap,HashMap底层是(数组+链表+红黑树)
- 分析HashSet的添加元素底层是如何实现的
HashSet hashSet = new HashSet();
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println("set=" + hashSet);
源码解读:
-
执行
HashSet()
public HashSet() { map = new HashMap<>(); }
-
执行
add()
public boolean add(E e) { //e = "java" return map.put(e, PRESENT)==null; //(static) PRESENT = new Object(); }
-
执行
put()
,该方法会执行hash(key)
,得到key对应的hash值,算法是h = key.hashCode() ^ (h >>> 16)
public V put(K key, V value) { //Key = "java" value = PRESENT 共享 return putVal(hash(key), key, value, false, true); }
-
执行
putVal
方法final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i; //定义了辅助变量
table
就是HashMap
的一个属性,类型是Node[]
下面的if语句,表示如果当前table是null 或者大小为0,就是第一次扩容,到16个空间
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
- 根据
key
,得到的hash
,去计算该key应该存放到table表的哪个索引位置,并把这个位置的对象,赋给p - 判断p是否为null
- 如果p为null,表示还没有存放元素,就创建一个
Node (key="java", value=PRESENT)
- 就放在该位置
tab[i] = newNode(hash, key, value, null)
- 如果p为null,表示还没有存放元素,就创建一个
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //一个开发技巧提示:在需要局部变量(辅助变量)时候,再创建 HashMap.Node<K,V> e; K k;
如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样,并且满足下面的两个条件之一:
- 准备加入的key和p指向的Node节点的key是同一个对象
- p指向的Node节点的key的
equals()
和准备加入后的key比较后相同就不能加入
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
再判断p是不是一颗红黑树,如果是一颗红黑树就调用
putTreeVal
,来进行添加如果table对应索引位置,已经是一个链表,就是用for循环比较
-
依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后
注意把元素添加到链表后,立即判断,该链表是否 已经达到8个结点,到达后就调用
treeifyBin)
,对当前这个链表进行树化(转成红黑树)在转为红黑树时,还要再进行判断,判断条件
if (tab == null || (n = tab.length) < MIN_TREEIFY_ACPacity(64))
resize();
如果上面的条件不成立,先
table
扩容只有上面的条件都成立时,才能转成红黑树
-
依次和该链表的每一个对象的比较过程中,如果有相同的情况,就直接break
else if (p instanceof HashMap.TreeNode) e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { 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); 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; //size 就是我们每加入一个结点 Node(k,v,h,next), size++ if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
- 根据
总结:
-
HashSet的底层是 HashMap
-
添加一个元素时,先得到hash值-会转成–> 索引
-
转到储存数据表table,看到这个索引位置是否已经存放有元素
-
如果没有,直接加入
-
如果有,调用equals 比较(能重写),如果相同就放弃添加,如果不相同,则添加到最后
-
在Java8中,如果一条链表的元素个数到达
TREEIFY_THRESHOLD
(默认是8),并且table的大小 >=MIN_TREEIFY_CAPACITY
(默认64),就会进行树化(红黑树)
- 分析HashSet的扩容和转成红黑树机制
-
HashSet底层是HashMap,第一次添加时, table 数组扩容到 16, 临界值(
threshold
)是 16*加载因子(loadFactor
)是0.75 = 12(当我们向hashSet增加了一个元素,-> Node -> 加入table,就算是增加了一个size++)
-
如果table 数组使用了临界值 12, 就会扩容到 16*2 = 32,新的临界值就是32*0.75 = 24,依次类推
-
在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制
Set接口实现类——LinkedHashSet
LinkedHashSet的全面说明
-
LinkedHashSet 是 HashSet 的子类
-
LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个数组 + 双向链表
-
LinkedHashSet 根据元素的 hashSet 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的
-
LinkedHashSet 不允许插入重复元素
说明:
-
在LinkedHashSet 中维护了一个hash表和双向链表(LinkedHashSet 有 head 和 tail)
-
每一个结点有 pre 和 next属性,这样就可以形成双向链表
-
在添加一个元素时,先求hash值,再求索引,确定该元素在hashtable的位置,然后将添加的元素加入到 双向链表(如果已经存在,不添加) [原理和hashset一样]
tail.next = newElement //简单指定 newElement.pre = tail; tail = newElement;
-
这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致