目录
Set 接口和常用方法
Set 接口基本介绍
Set 接口的常用方法
和 List 接口一样, Set 接口也是 Collection 的子接口,因此,常用方法和 Collection一样
Set 接口的遍历方式
@SuppressWarnings({"all"}) public class SetMethod { public static void main(String[] args) { //1. 以Set 接口的实现类 HashSet 来讲解Set 接口的方法 //2. set 接口的实现类的对象(Set接口对象), 不能存放重复的元素, 可以添加一个null //3. set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致) //4. 注意:取出的顺序的顺序虽然不是添加的顺序,但是他的固定. Set set = new HashSet(); set.add("john"); set.add("lucy"); set.add("john");//重复 set.add("jack"); set.add("hsp"); set.add("mary"); set.add(null);// set.add(null);//再次添加null for(int i = 0; i <10;i ++) { System.out.println("set=" + set); } //遍历 //方式1: 使用迭代器 System.out.println("=====使用迭代器===="); Iterator iterator = set.iterator(); while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println("obj=" + obj); } set.remove(null); //方式2: 增强for System.out.println("=====增强for===="); for (Object o : set) { System.out.println("o=" + o); } //set 接口对象,不能通过索引来获取 } }
1. 以Set 接口的实现类 HashSet 来讲解Set 接口的方法。
2. set 接口的实现类的对象(Set接口对象), 不能存放重复的元素, 可以添加一个null。
3. set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)。
4. 注意:取出的顺序虽然不是添加的顺序,但是他取出的顺序是固定的。它不能用普通的for循环去遍历。
Set 接口实现类-HashSet
HashSet 的全面说明
@SuppressWarnings({"all"}) public class HashSet_ { public static void main(String[] args) { //老韩解读 //1. 构造器走的源码 /* public HashSet() { map = new HashMap<>(); } 2. HashSet 可以存放null ,但是只能有一个null,即元素不能重复 */ Set hashSet = new HashSet(); hashSet.add(null); hashSet.add(null); System.out.println("hashSet=" + hashSet); } }
HashSet 案例说明
@SuppressWarnings({"all"}) 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个 System.out.println("====================="); set = new HashSet(); System.out.println("set=" + set);//0 //4 Hashset 不能添加相同的元素/数据? set.add("lucy");//添加成功,lucy指向常量池 set.add("lucy");//加入不了 set.add(new Dog("tom"));//OK set.add(new Dog("tom"));//Ok System.out.println("set=" + set); System.out.println("====================="); //在加深一下. 非常经典的面试题. //看源码,做分析, 先给小伙伴留一个坑,以后讲完源码,你就了然 //去看他的源码,即 add 到底发生了什么?=> 底层机制. set.add(new String("hsp"));//ok set.add(new String("hsp"));//加入不了. 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 + '\'' + '}'; } }
HashSet 底层机制说明
@SuppressWarnings({"all"}) 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; } }
为什么要用链表,就是为了存储的高效,随着将来存储数据的增多,一条链表存储达到了一定的量。它会把链表变成一条树,也就是红黑树,为什么要把它变成树,因为树的存储效率比链表还要高。
第五点: 这里的equals我们程序员可以控制,应该可以重写,程序员可以控制去比较内容还是什么。比如:比较字符串就比较内容。然后我们在讲比较,比如数组的索引位置没有数据,就直接加入。如果有就用equals比较,沿着链表一个一个执行比较,一直比较到链表的尾部,如果发现有相同的节点就放弃添加,如果都没有相同的节点,就添加在链表的尾部。
第六点:如果你的链表的元素达到8,并且你的table数组的长度达到了64,那么它就会树化,成为红黑树。那链表的元素达到8,table数组的长度并没有达到了64,那怎么办呢,它会把table数组的长度扩容,扩容两倍。
0521_韩顺平Java_HashSet源码解读1_哔哩哔哩_bilibili
![]()
它不等同与hashcode,它是对hashcode处理了 得到的值,原因防止和hashcode值碰撞(混乱)。
package com.hspedu.set_; import java.util.HashSet; @SuppressWarnings({"all"}) public class HashSetSource { public static void main(String[] args) { HashSet hashSet = new HashSet(); hashSet.add("java");//到此位置,第1次add分析完毕. hashSet.add("php");//到此位置,第2次add分析完毕 hashSet.add("java"); System.out.println("set=" + hashSet); /* 老韩对HashSet 的源码解读 1. 执行 HashSet() public HashSet() { map = new HashMap<>(); } 2. 执行 add() public boolean add(E e) {//e = "java" return map.put(e, PRESENT)==null;//(static) PRESENT = new Object(); } 3.执行 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); } 4.执行 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[] //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; } */ } }
0522_韩顺平Java_HashSet源码解读2_哔哩哔哩_bilibili
0523_韩顺平Java_HashSet源码解读3_哔哩哔哩_bilibili
调试的代码:
package com.hspedu.set_; import java.util.HashSet; import java.util.Objects; @SuppressWarnings({"all"}) public class HashSetIncrement { public static void main(String[] args) { /* HashSet底层是HashMap, 第一次添加时,table 数组扩容到 16, 临界值(threshold)是 16*加载因子(loadFactor)是0.75 = 12 如果table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32, 新的临界值就是 32*0.75 = 24, 依次类推 */ HashSet hashSet = new HashSet(); // for(int i = 1; i <= 100; i++) { // hashSet.add(i);//1,2,3,4,5...100 // } /* 在Java8中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ), 并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树), 否则仍然采用数组扩容机制 */ // for(int i = 1; i <= 12; i++) { // hashSet.add(new A(i));// // } } } class A { private int n; public A(int n) { this.n = n; } @Override public int hashCode() { return 100; } }
重写hashCode,返回一样的hashCode,是让我们的的值加到同一个链表上面
0524_韩顺平Java_HashSet源码解读4_哔哩哔哩_bilibili
package com.hspedu.set_; import java.util.HashSet; import java.util.Objects; @SuppressWarnings({"all"}) public class HashSetIncrement { public static void main(String[] args) { /* 当我们向hashset增加一个元素,-> Node -> 加入table , 就算是增加了一个size++ */ for(int i = 1; i <= 7; i++) {//在table的某一条链表上添加了 7个A对象 hashSet.add(new A(i));// } for(int i = 1; i <= 7; i++) {//在table的另外一条链表上添加了 7个B对象 hashSet.add(new B(i));// } } } class B { private int n; public B(int n) { this.n = n; } @Override public int hashCode() { return 200; } } class A { private int n; public A(int n) { this.n = n; } @Override public int hashCode() { return 100; } }
这里有个误区,table数组达到12才会扩容,从上面视频里面,我们可以看到,两个链表的长度之和达到12,也会扩容。
从源码看:
所以这里的12,不是从数组的0号索引,加够12个索引才算,而是所有的链表节点的个数达到12,才会触发扩容机制。
HashSet 课堂练习1
alt加insert键(del旁边)
![]()
![]()
完整代码:
@SuppressWarnings({"all"}) public class HashSetExercise { public static void main(String[] args) { /** 定义一个Employee类,该类包含:private成员属性name,age 要求: 创建3个Employee 对象放入 HashSet中 当 name和age的值相同时,认为是相同员工, 不能添加到HashSet集合中 */ HashSet hashSet = new HashSet(); hashSet.add(new Employee("milan", 18));//ok hashSet.add(new Employee("smith", 28));//ok hashSet.add(new Employee("milan", 18));//加入不成功. //回答,加入了几个? 如果不重写hashcode,equlas就是加入3个 System.out.println("hashSet=" + hashSet); } } //创建Employee class Employee { private String name; private int age; public Employee(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } @Override public String toString() { return "Employee{" + "name='" + name + '\'' + ", age=" + age + '}'; } public void setAge(int age) { this.age = age; } //如果name 和 age 值相同,则返回相同的hash值 @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); } }
Set 接口实现类-LinkedHashSet
LinkedHashSet 的全面说明
![]()
0528_韩顺平Java_LHashSet源码解读_哔哩哔哩_bilibili
package com.hspedu.set_; import java.util.LinkedHashSet; import java.util.Set; /** * @author 韩顺平 * @version 1.0 */ @SuppressWarnings({"all"}) public class LinkedHashSetSource { public static void main(String[] args) { //分析一下LinkedHashSet的底层机制 Set set = new LinkedHashSet(); set.add(new String("AA")); set.add(456); set.add(456); set.add(new Customer("刘", 1001)); set.add(123); set.add("HSP"); System.out.println("set=" + set); //1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致 //2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类) //3. LinkedHashSet 底层结构 (数组table+双向链表) //4. 添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry //5. 数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap$Entry类型 /* //继承关系是在内部类完成. 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); } } */ } } class Customer { private String name; private int no; public Customer(String name, int no) { this.name = name; this.no = no; } @Override public String toString() { return "Customer{" + "name='" + name + '\'' + ", no=" + no + '}'; } }
1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致
2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
3. LinkedHashSet 底层结构 (数组table+双向链表)
4. 添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry
5. 数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap$Entry类型LinkedHashMap$Entry继承了 HashMap$Node[],要不然加不进去。
LinkedHashSet 课后练习题
package com.hspedu.set_; import java.util.LinkedHashSet; import java.util.Objects; @SuppressWarnings({"all"}) public class LinkedHashSetExercise { public static void main(String[] args) { LinkedHashSet linkedHashSet = new LinkedHashSet(); linkedHashSet.add(new Car("奥拓", 1000));//OK linkedHashSet.add(new Car("奥迪", 300000));//OK linkedHashSet.add(new Car("法拉利", 10000000));//OK linkedHashSet.add(new Car("奥迪", 300000));//加入不了 linkedHashSet.add(new Car("保时捷", 70000000));//OK linkedHashSet.add(new Car("奥迪", 300000));//加入不了 System.out.println("linkedHashSet=" + linkedHashSet); } } /** * Car 类(属性:name,price), 如果 name 和 price 一样, * 则认为是相同元素,就不能添加。 5min */ class Car { private String name; private double price; public Car(String name, double price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } @Override public String toString() { return "\nCar{" + "name='" + name + '\'' + ", price=" + price + '}'; } //重写equals 方法 和 hashCode //当 name 和 price 相同时, 就返回相同的 hashCode 值, equals返回t @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Car car = (Car) o; return Double.compare(car.price, price) == 0 && Objects.equals(name, car.name); } @Override public int hashCode() { return Objects.hash(name, price); } }
重写hashcode和equals方法,是让他们都不相同,这样重复的才添加不上去,如果只重写hashcode或者equals方法也是可以添加上去的