Set
- Set 基本介绍
- Set 常用方法
- Set 遍历方式
- HashSet 的全面说明
- 练习
- HashSet 的底层机制说明
- HashSet 的扩容机制&转成红黑树机制
- 练习1
- 练习2
- LinkedHashSet
- LinkedHashSet底层源码
- 练习
Set 基本介绍
- 无序(添加和取出的顺序不一致),没有索引 [后面演示]
- 不允许重复元素,所以最多包含一个null
- JDK API中Set接口的实现类有很多。最常用的是HashSet、TreeSet
Set 常用方法
和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样。
Set 遍历方式
同Collection的遍历方式一样,因为Set接口是Collection接口的子接口。
- 可以使用迭代器
- 增强for
- 不能使用索引的方式来获取
以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法 。
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,但最后set中只有一个null//遍历十次set 发现输出顺序是固定的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: 增强forSystem.out.println("=====增强for====");for (Object o : set) {System.out.println("o=" + o);}//set 接口对象,不能通过索引来获取
}
HashSet 的全面说明
见 HashSet.java
- HashSet实现了Set接口
- HashSet实际上是HashMap,看下源码
//源码public HashSet() {map = new HashMap<>();}
- HashSet 可以存放null值,但是只能有一个null,元素不能重复
- HashSet 不保证元素是有序的,取决于hash后,再确定索引的结果。即不保证存放元素的顺序与取出顺序一致(有可能一样也有可能不同)
- 不能有重复元素/对象。在前面Set 接口使用已经讲过
练习
HashSet01.java
HashSet set = new HashSet();//set引用指向一个新的对象System.out.println("set=" + set);//0//4 Hashset 不能添加相同的元素/数据?set.add("lucy");//添加成功set.add("lucy");//加入不了set.add(new Dog("tom"));set.add(new Dog("tom"));System.out.println("set=" + set);
分析:
两个lucy是常量池的,同一个
两个 new Dog(“tom”) 不是同一个元素!!
set.add(new String("hsp"));//ok
set.add(new String("hsp"));//加入不了.
System.out.println("set=" + set);
分析:
这后面要通过看源码才能知道。
去看他的源码,即 add 到底发生了什么?=> 底层机制
HashSet 的底层机制说明
HashSet底层是HashMap, HashMap底层是(数组+链表+红黑树)。
HashSetStructure.java 模拟简单的数组+链表结构。
HashSetSource.java 模拟简单的数组+链表结构。
- HashSet底层是HashMap
- 添加一个元素时,先得到hash值-会转成->索引值
- 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有,直接加入
- 如果有,调用equals(请注意equals是按照内容还是地址比较,是程序员可以控制的。比如String类就重写了方法equals,比较的是字符串的内容。)比较,如果相同,就放弃添加,如果不相同,则添加到最后
- 在Java8中,如果一条链表的元素个数到达了TREEIFY_THRESHOLD(默认是8),并且table的大小 >=
MINTREEIFY_CAPACITY(默认64)。就会进行树化(红黑树)
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"这里的E是泛型后面会讲,debug过程中看到e是字符串常量return map.put(e, PRESENT)==null;//(static) PRESENT = new Object();}3.执行 put() ,public V put(K key, V value) {//key = "java" value = PRESENT(static) 共享return putVal(hash(key), key, value, false, true);}该方法会执行 hash(key) 得到key对应的hash值 算法h = key.hashCode()) ^ (h >>> 16)4.执行 putValfinal 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; //resize就是开辟空间的//(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) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接breakfor (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1sttreeifyBin(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 keyV 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,就会扩容到162=32,新的临界值就是320.75 = 24,依次类推。
- 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8)并且table的大小 >=
MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
HashSetIncrement.java
通过重写hashcode满足底层代码条件
练习1
定义一个Employee类,该类包含:private成员属性name,age 要求:
创建3个Employee 对象放入 HashSet中
当 name和age的值相同时,认为是相同员工, 不能添加HashSet集合中
ADD添加
① 先获取元素的哈希值(hashCode方法)
② 对哈希值进行运算,得出一个索引值。即为要存放在哈希表中的位置号
③ 如果该位置上没有其他元素,则查接存放。
如果该位置上已经有其他元素,则需要进行equals判断。如果相等,则不再添加,如果不相等,则以链表的方式添加。
不同的对象具有不同的哈希值(hashCode),相同的对象不能反复添加到hashset中。
题目要求name和age的值相同时,认为是相同员工,不能添加(hashset应该把这种情况视作相同的对象),也就是new的时候,如果是相同的name和age,其hashcode应该相同,所以重写hashCode。(用快捷键 alt+insert)
如果不重写hashCode
如果不重写equals
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));//okhashSet.add(new Employee("smith", 28));//okhashSet.add(new Employee("milan", 18));//加入不成功.//回答,加入了几个? 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;}@Overridepublic String toString() {return "Employee{" +"name='" + name + '\'' +", age=" + age +'}';}public void setAge(int age) {this.age = age;}//如果name 和 age 值相同,则返回相同的hash值@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(name, age);}
}
练习2
LinkedHashSet
set接口的另外一个实现子类。
- LinkedHashSet是 HashSet的子类
- LinkedHashSet底层是一个 LinkedHashMag,底层维护了一个数组+双向链表
- LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
- LinkedHashSet不允许添重复元素
说明:
- 在LinkedHastSet 中维护了一hash表和双向链表(LinkedHashSet有head和 tail)
- 每一个节点有pre和next属性,这样可以形成双向链表
- 在添加一个元素时,先求hash值,在求索引。确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样])
tail.next = newElement //简单指定
newElement.pre = tail
tail = newEelment;
- 这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍
历顺序一致
LinkedHashSet底层源码
LinkedHashSetSource.java
练习
Car类(属性:name.price),如果name和price一样。则认为是相同元素,就不能添加。
@SuppressWarnings({"all"})
public class LinkedHashSetExercise {public static void main(String[] args) {LinkedHashSet linkedHashSet = new LinkedHashSet();linkedHashSet.add(new Car("奥拓", 1000));//OKlinkedHashSet.add(new Car("奥迪", 300000));//OKlinkedHashSet.add(new Car("法拉利", 10000000));//OKlinkedHashSet.add(new Car("奥迪", 300000));//加入不了linkedHashSet.add(new Car("保时捷", 70000000));//OKlinkedHashSet.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;}@Overridepublic String toString() {return "\nCar{" +"name='" + name + '\'' +", price=" + price +'}';}//重写equals 方法 和 hashCode//当 name 和 price 相同时, 就返回相同的 hashCode 值, equals返回t@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(name, price);}
}
new 对象,会有对应的 hashcode。但是现在不想
重写hashcode,new出来一个对象对应一个hashcode,我们题目要求 name和price相同就不能添加。LinkedHashSet规定相同的hashcode不能添加。但是如果按照下面的方式写,因为new出来一个对象对应一个hashcode,所以会运行正确。但是不符合题干。所以我们要【设置:当name和price一样的时候,返回相同的hashcode】
linkedHashSet.add(new Car("奥迪", 300000));
linkedHashSet.add(new Car("奥迪", 300000));
当执行第二句话的时候,得到了和第一句话相同的hashcode,就去tables索引位置,此时 equals 比较,如果是相同的name和price就不准添加。
重写hashcode:保证两句存放的tables索引位置一样(因为不同的hashcode对应的位置有可能不一样)。重写equals保证不准添加。
如果只保留了hashcode没有equals ,就会存放在同一个链表上。没有重写equals 就会调用Object的equals方法,比较的是地址。
本笔记是对韩顺平老师的Java课程做出的梳理。方便本人和观看者进行复习。
课程请见: https://www.bilibili.com/video/BV1fh411y7R8/?spm_id_from=333.999.0.0&vd_source=ceab44fb5c1365a19cb488ab650bab03