Set接口和常用方法
set借口基本介绍
- 以Set 接口的实现类 HashSet 来讲解Set 接口的方法
- set 接口的实现类的对象(Set接口对象), 不能存放重复的元素, 可以添加一个null
- set接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
- 注意:从set中取元素没有类似
set.get()
的方法,只能用set.iterator()
或for(Object o : set){...}
,第二种方式本质还是set.iterator()
- 用
set.iterator()
从set中取元素不是从set中随机取,具体按什么方法从set中取元素取决于set.iterator()
方法返回的Iterator(迭代器) - set无序性:元素放入set的顺序和取出的顺序不一定一样(例如:将1 2 3依次放入set中,
set.iterator()
取出的第一个元素不一定是1)
set接口常用方法
添加元素
HashSet 类提供了很多有用的方法,添加元素可以使用 add() 方法:
// 引入 HashSet 类*
**import** java.util.HashSet;
**public** **class** RunoobTest {
**public** **static** **void** main(String[] args) {
HashSet<String> sites = **new** HashSet<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Zhihu");
sites.add("Runoob"); *// 重复的元素不会被添加*
System.out.println(sites);
}
}
执行以上代码,输出结果如下:
[Google, Runoob, Zhihu, Taobao]
在上面的实例中,Runoob 被添加了两次,它在集合中也只会出现一次,因为集合中的每个元素都必须是唯一的。
判断元素是否存在
我们可以使用 contains() 方法来判断元素是否存在于集合当中:
// 引入 HashSet 类*
**import** java.util.HashSet;
**public** **class** RunoobTest {
**public** **static** **void** main(String[] args) {
HashSet<String> sites = **new** HashSet<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Zhihu");
sites.add("Runoob"); *// 重复的元素不会被添加*
System.out.println(sites.contains("Taobao"));
}
}
执行以上代码,输出结果如下:
true
删除元素
我们可以使用 remove() 方法来删除集合中的元素:
// 引入 HashSet 类*
**import** java.util.HashSet;
**public** **class** RunoobTest {
**public** **static** **void** main(String[] args) {
HashSet<String> sites = **new** HashSet<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Zhihu");
sites.add("Runoob"); *// 重复的元素不会被添加*
sites.remove("Taobao"); *// 删除元素,删除成功返回 true,否则为 false*
System.out.println(sites);
}
}
执行以上代码,输出结果如下:
[Google, Runoob, Zhihu]
删除集合中所有元素可以使用 clear 方法:
// 引入 HashSet 类*
**import** java.util.HashSet;
**public** **class** RunoobTest {
**public** **static** **void** main(String[] args) {
HashSet<String> sites = **new** HashSet<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Zhihu");
sites.add("Runoob"); *// 重复的元素不会被添加*
sites.clear();
System.out.println(sites);
}
}
执行以上代码,输出结果如下:
[]
计算大小
如果要计算 HashSet 中的元素数量可以使用 size() 方法:
// 引入 HashSet 类*
**import** java.util.HashSet;
**public** **class** RunoobTest {
**public** **static** **void** main(String[] args) {
HashSet<String> sites = **new** HashSet<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Zhihu");
sites.add("Runoob"); *// 重复的元素不会被添加*
System.out.println(sites.size());
}
}
执行以上代码,输出结果如下:
4
迭代 HashSet
- Set接口不能使用普通for循环进行迭代
增强for
可以使用 for-each 来迭代 HashSet 中的元素。
*// 引入 HashSet 类*
**import** java.util.HashSet;
**public** **class** RunoobTest {
**public** **static** **void** main(String[] args) {
HashSet<String> sites = **new** HashSet<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Zhihu");
sites.add("Runoob"); *// 重复的元素不会被添加*
**for** (String i : sites) {
System.out.println(i);
}
}
}
执行以上代码,输出结果如下:
Google
Runoob
Zhihu
Taobao
迭代器
*// 引入 HashSet 类*
**import** java.util.HashSet;
**public** **class** RunoobTest {
**public** **static** **void** main(String[] args) {
HashSet<String> sites = **new** HashSet<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Zhihu");
sites.add("Runoob"); *// 重复的元素不会被添加*
Iterator iterator = sites.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
}
}
执行以上代码,输出结果如下:
obj=Google
obj=Runoob
obj=Zhihu
obj=Taobao
HashSet
概述
- HashSet 实现了 Set 接口
- HashSet 底层是 HashMap 来实现的,是一个不允许有重复元素的集合。
public HashSet() {
map = new HashMap<>();
}
-
HashSet 允许有 null 值,但只能有一个null值。
-
HashSet 是无序的,即不会记录插入的顺序。但是在取出元素后,他的元素顺序是被固定的。
-
HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。
-
案例:
public class HashSet01 {
public static void main(String[] args) {
HashSet set = new HashSet();
//说明
//1. 在执行add方法后,会返回一个boolean值
//2. 如果添加成功,返回 true, 否则返回false
//3. 可以通过 remove 指定删除哪个对象
System.out.println(set.add("john"));//T
System.out.println(set.add("lucy"));//T
System.out.println(set.add("john"));//F
System.out.println(set.add("jack"));//T
System.out.println(set.add("Rose"));//T
set.remove("john");
System.out.println("set=" + set);//3个
//
set = new HashSet();
System.out.println("set=" + set);//0
//4 Hashset 不能添加相同的元素/数据?
set.add("lucy");//添加成功
set.add("lucy");//加入不了
set.add(new Dog("tom"));//OK
set.add(new Dog("tom"));//Ok
System.out.println("set=" + set);
//在加深一下. 非常经典的面试题.
//看源码,做分析, 先给小伙伴留一个坑,以后讲完源码,你就了然
//去看他的源码,即 add 到底发生了什么?=> 底层机制.
set.add(new String("h"));//ok
set.add(new String("h"));//加入不了.
System.out.println("set=" + set);
}
}
class Dog { //定义了Dog类
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
为什么?
set.add(new String("h"));//ok
set.add(new String("h"));//加入不了.
System.out.println("set=" + set);
HashSet底层机制说明
- HashSet底层实际是HashMap,HashMap底层是(数组+链表+红黑树)
简单的数组+链表结构
public class HashSetStructure {
public static void main(String[] args) {
//模拟一个HashSet的底层 (HashMap 的底层结构)
//1. 创建一个数组,数组的类型是 Node[]
//2. 有些人,直接把 Node[] 数组称为 表
Node[] table = new Node[16];
//3. 创建结点
Node john = new Node("john", null);
table[2] = john;
Node jack = new Node("jack", null);
john.next = jack;// 将jack 结点挂载到john
Node rose = new Node("Rose", null);
jack.next = rose;// 将rose 结点挂载到jack
Node lucy = new Node("lucy", null);
table[3] = lucy; // 把lucy 放到 table表的索引为3的位置.
System.out.println("table=" + table);
}
}
class Node { //结点, 存储数据, 可以指向下一个结点,从而形成链表
Object item; //存放数据
Node next; // 指向下一个结点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
HashSet 源码解读
步骤:
-
获取元素的哈希值(hashCode方法)
-
对哈希值进行运算,得到一个索引值即为要存放在哈希表中的位置号
-
如果该位置上没有其他元素,则直接存放,如果该位置上有其他元素需要进行equals判断,如相等则不添加,如不相等,则已链表的形式添加。
具体实现
1. 执行 *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)
注意:
hash值不完全等价于hashcode,算法的想法是使不同的key尽量得到不同的hash值
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) { Node<K,V>[] tab; Node<K,V> p; int n, i; //定义了辅助变量 //table 就是 HashMap 的一个数组,这个数组就是存放Node节点的,类型是 Node<K,V>[] //if 语句表示如果当前table 是null, 或者 大小=0 //就是第一次扩容,到16个空间. if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //(1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置 //并把这个位置的对象,赋给 p //(2)判断p 是否为null //(2.1) 如果p 为null, 表示还没有存放元素, 就创建一个Node (key="java",value=PRESENT) //(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //一个开发技巧提示: 在需要局部变量(辅助变量)时候,在创建 Node<K,V> e; K k; // //如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样 //并且满足 下面两个条件之一: //(1) 准备加入的key 和 p 指向的Node 结点的 key 是同一个对象 //(2) p 指向的Node 结点的 key 的equals() 和准备加入的key比较后相同 //就不能加入 if (p.hash == 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循环比较 //(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后 // 注意在把元素添加到链表后,立即判断 该链表是否已经达到8个结点 // , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树) // 注意,在转成红黑树时,要进行判断, 判断条件 // if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64)) // resize(); // 如果上面条件成立,先table扩容. // 只有上面条件不成立时,才进行转成红黑树 //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD(8) - 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扩容机制
-
HashSet底层是HashMap, 第一次添加时,table 数组扩容到 16,临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12。
-
如果table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,新的临界值就是 32*0.75 = 24, 依次类推。
-
如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ),并且table>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
-
当我们向hashset增加一个元素,-> Node -> 加入table , 就算是增加了一个size++。。
-
所以并不是table表中存放数组元素到12才触发扩容机制,而是size到达12是触发扩容机制。
LinkedHashSet (底层数组+双向链表)、有序存放数组
概述
底层机制
-
LinkedHashSet 加入顺序和取出元素/数据的顺序一致
-
LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
-
LinkedHashSet 底层结构 (数组table+双向链表)
-
添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry
-
数组是 HashMap N o d e [ ] 存放的元素 / 数据是 L i n k e d H a s h M a p Node[] 存放的元素/数据是 LinkedHashMap Node[]存放的元素/数据是LinkedHashMapEntry类型
//继承关系是在内部类完成. 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); } }