1. Set接口基本介绍 517
1)无序(添加和取出的顺序不一致),没有索引[后面演示]
2)不允许重复元素,所以最多包含一个null
3) JDK API中Set接口的实现类有:
2. Set 接口的常用方法
和 List 接口一样, Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样.
2.1 Set 接口的遍历方式 517
同Collection的遍历方式样,因为Set接口是Collection接口的子接口。
1.可以使用迭代器
2.增强for
3.不能使用索引的方式来获取.
代码在com.stulzl.set_method.包中
Set_Method
package com.stulzl.set_method;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
//Set接口常用方法 517
@SuppressWarnings({"all"})
public class Set_Method {
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
System.out.println("set="+set);
//取10次
for(int i = 0; i <10;i ++) {
System.out.println("set=" + set);
}
//遍历可以使用迭代器
System.out.println("====使用迭代器====");
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接口对象不能通过索引来获取,因此也就不能使用普通for循环遍历
//删除
set.remove("jack");
System.out.println("set="+set);
}
}
3. Set 接口实现类-HashSet 518
3.1 HashSet 的全面说明
1) HashSet实现了 Set接口
2) HashSet实际上是HashMap,看下源码. (图)
3)可以存放null值,但是只能有一个null
4) HashSet不保证元素是有序的,取决于hash后,再确定索引的结果(即,不
保证存放元素的顺序和取出顺序一 致)
5)不能有重复元素/对象.在前面Set接口使用已经讲过
代码在com.stulzl.set_hashset.包中
Set_HashSet
package com.stulzl.set_hashset;
import java.util.HashSet;
import java.util.Set;
//HashSet 的全面说明 518
@SuppressWarnings({"all"})
public class Set_HashSet {
public static void main(String[] args) {
//解读
//1. 构造器走源码
/*
public HashSet() {
map = new HashMap<>();
}
*/
//2. HashSet 可以存放 null ,但是只能有一个 null,即元素不能重复
//不能有重复元素/对象.在前面Set接口使用已经讲过
Set hashSet = new HashSet();
hashSet.add(null);
hashSet.add(null);
System.out.println("hashSet="+hashSet);
}
}
4. HashSet案例说明 深度解析其特性518
代码在com.stulzl.hashset_exercise01.包中
HashSet_Exercise01
package com.stulzl.hashset_exercise01;
import java.util.HashSet;
//HashSet案例说明 518
@SuppressWarnings({"all"})
public class HashSet_Exercise01 {
public static void main(String[] args) {
HashSet set = new HashSet();
//说明
//1. 在执行 add 方法后,会返回一个 boolean 值
//2. 如果添加成功,返回 true, 否则返回 false
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
//3. 可以通过 remove 指定删除哪个对象
set.remove("john");
System.out.println("set=" + set);//3个 set=[Rose, lucy, jack]
//逐步深入研究HashSet特性
set = new HashSet();
System.out.println("set="+set);//0set=[]
//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);//set=[Dog{name='tom'}, lucy, Dog{name='tom'}]
//再次深入,非常经典的面试题
set.add(new String("lzl"));//ok
set.add(new String("lzl"));//不行,看源码分析可知
System.out.println("set="+set);
}
}
class Dog{
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
5. HashSet底层机制说明
5.1 模拟简单的数组+链表结构 519
➢分析HashSet底层是HashMap, HashMap底层是(数组+链表+红黑树)为了让大家真正理解,模拟简单的数组+链表结构
代码在com.stulzl.hashset_structure.包中
HashSet_Structure
package com.stulzl.hashset_structure;
//模拟简单的数组+链表结构 519
@SuppressWarnings({"all"})
public class HashSet_Structure {
public static void main(String[] args) {
//模拟HashSet的底层(HashMap的底层结构)
//1. 创建一个数组,数组的类型是 Node[]
//2. 有些人,直接把 Node[] 数组称为 表
Node[] table = new Node[16];
System.out.println("table="+table);
//创建一个节点将jonh放在索引为2的位置
Node jonh = new Node("jonh",null);
table[2] = jonh;//把jonh放在table表的索引为2的位置
Node jack = new Node("jack", null);//在创建一个结点jack
jonh.next = jack;//将jack结点挂载到jonh后面
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;
}
}
5.2 HashSet底层分析 520-521-522
➢分析HashSet的添加元素底层是如何实现(hash() + equals())先说结论,再Debug源码+图解[前方高能,非战斗人员,做好撤退准备]
1. HashSet底层是HashMap
2.添加一个元素时,先得到hash值会转成-->索引值
3.找到存储数据表table ,看这个索引位置是否已经存放的有元素
4.如果没有,直接加入
5.如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则挂载到最后面
6.在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小 > =MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
代码在com.stulzl.hashset_source.包中
HashSet_Source
package com.stulzl.hashset_source;
import java.util.HashSet;
//HashSet底层原码分析 521-522 太复杂了,看视频解析吧
@SuppressWarnings({"all"})
public class HashSet_Source {
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);//set=[java, php]
/*
对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[] tab; Node 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 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)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;
}
*/
}
}
6. 分析HashSet的扩容和转成红黑树机制 523-524
先说结论,代码演示补充说明threshold
1. HashSet底层是HashMap, 第次添加时,table 数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75 = 12
2.如果table数组使用到了临界值12,就会扩容到16*2 = 32,新的临界值就是32*0.75 = 24,依次类推
3.在Java8中,如果-条链表的元素个数到达TREEIFY_THRESHOLD(默认是8 ),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制
代码在com.stulzl.hashset_increment.包中
HashSetIncrement
package com.stulzl.hashset_increment;
import javax.crypto.spec.PSource;
import java.util.HashSet;
import java.util.Objects;
//分析HashSet的扩容和转成红黑树机制 523-524
@SuppressWarnings({"all"})
public class HashSetIncrement {
public static void main(String[] args) {
/* 523
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 = 0; i <= 100; i++) {
// hashSet.add(i);//1,2,3,4,5...100
// }
/* 523
在Java8中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ),
并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),
否则仍然采用数组扩容机制
*/
// for(int i =1;i<=12;i++){
// hashSet.add(new A(i));
// }
// System.out.println("hashSet="+hashSet);
/* 524
当我们向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 B;
public B(int b) {
B = b;
}
@Override
public int hashCode() {
return 200;
}
}
class A{
private int n;
public A(int n) {
this.n = n;
}
//重写hashcode方法,保证返回的hashcode值一样从而使数据再一条链表上
@Override
public int hashCode() {
return 100;
}
}
7. HashSet练习 525
重写hash值方法小提示Alt+Insert
定义一个Employee类, 该类包含: private成员属性name,age要求:
1.创建3个Employee对象放入HashSet中
2.当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
代码在com.stulzl.hashset_exercise01.包中
HashSet_Exercise01
package com.stulzl.hashset_exercise01;
import java.util.HashSet;
import java.util.Objects;
//HashSet练习 525
//定义一个Employee类, 该类包含: private成员属性name,age要求:
//1.创建3个Employee对象放入HashSet中
//2.当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
@SuppressWarnings({"all"})
public class HashSet_Exercise01 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("milan",18));//ok
hashSet.add(new Employee("smith",18));//ok
hashSet.add(new Employee("milan",18));//no
//重写hash值方法之后
System.out.println("hashSet="+hashSet);
}
}
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;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", 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);
}
}
8. HashSet练习2 526
//定义一个Employee类,该类包含: private成员属性name,sal,birthday(MyDate类型), 526
// 其中birthday为MyDate类型(属性包括: year, month, day),要求:
//1.创建3个Employee放入HashSet中
//2.当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
代码在com.stulzl.hashset_exercise02.包中
HashSet_Exercise02
package com.stulzl.hashset_exercise02;
import java.util.HashSet;
import java.util.Objects;
//定义一个Employee类,该类包含: private成员属性name,sal,birthday(MyDate类型), 526
// 其中birthday为MyDate类型(属性包括: year, month, day),要求:
//1.创建3个Employee放入HashSet中
//2.当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
@SuppressWarnings({"all"})
public class HashSet_Exercise02 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("milan",1800,new MyDate(2020,5,3)));
hashSet.add(new Employee("smith",1850,new MyDate(2021,5,3)));
hashSet.add(new Employee("milan",1820,new MyDate(2020,5,3)));
System.out.println("hashSet="+hashSet);
}
}
class MyDate{
private int year;
private int month;
private int day;
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
@Override
public String toString() {
return year + "-" + month + "-" + day;
}
//重写birthday的equals和hash值
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyDate myDate = (MyDate) o;
return year == myDate.year &&
month == myDate.month &&
day == myDate.day;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
}
class Employee{
private String name;
private double salary;
private MyDate birthday;
public Employee(String name, double salary, MyDate birthday) {
this.name = name;
this.salary = salary;
this.birthday = birthday;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public MyDate getBirthday() {
return birthday;
}
public void setBirthday(MyDate birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", salary=" + salary +
", birthday=" + birthday +
'}';
}
//重写name和birthday的equals和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 name.equals(employee.name) &&
birthday.equals(employee.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, birthday);
}
}
9. Set 接口实现类-LinkedHashSet 527
9.1 LinkedHashSet 的全面说明 527
1) LinkedHashSet是HashSet的子类
2) LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双
向链表
3) LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
4) LinkedHashSet不允许添重复元素
3.2 LinkedHashSet底层 528
说明
1)在LinkedHastSet 中维护了一个hash表和双向链表( LinkedHashSet有head和tail )
2)每一个节点有before和after属性,这样可以形成双向链表
3)在添加一个元素时,先求hash值,在求索引确定该元素在table的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样])
tail.next = newElement //示意代码
newElement.pre = tail
tail = newEelment;
4)这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致
代码在com.stulzl.linkedhashset_source.包中
linkedhashset_source
package com.stulzl.linkedhashset_source;
import java.util.LinkedHashSet;
import java.util.Set;
//LinkedHashSet_Source底层源码 528 看视频
@SuppressWarnings({"all"})
public class LinkedHashSet_Source {
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 extends HashMap.Node {
Entry before, after;
Entry(int hash, K key, V value, Node 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 +
'}';
}
}
10. LinkedHashSet 课后练习题 529
Car类(属性:name,price),如果 name和price一一样,
则认为是相同元素,就不能添加。
代码在com.stulzl.linkedhashset_exercise01.包中
LinkedHashSet_Exercise01
package com.stulzl.linkedhashset_exercise01;
import java.util.LinkedHashSet;
import java.util.Objects;
//Car类(属性:name,price),如果 name和price一样, 529
//则认为是相同元素,就不能添加。
@SuppressWarnings({"all"})
public class LinkedHashSet_Exercise01 {
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);
}
}
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 +
'}';
}
//重写hash值和equals方法
//当 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);
}
}