一、Set接口和常用方法
1.Set接口基本介绍
(1)Set集合中元素无序(即添加顺序和取出顺序不一致),不允许重复,最多包含一个null。
注意:取出顺序虽然与添加顺序不一致,但无论取出多少次,取出顺序是固定不变的。
(2)Set集合中元素没有索引。
(3)JDK API中Set接口的实现类有很多,常用的有:HashSet,LinkedHashSet,TreeSet。
2.Set接口常用方法
Set接口是Collection的子接口,因此同Collection接口的常用方法一样。
- add:添加单个元素。
- addAll:添加多个元素(多个元素放在一个集合里)。
- remove:删除指定元素。
- removeAll:删除多个元素(多个元素放在一个集合里)。
- clear:清空。
- contains:查找元素是否存在。
- containsAll:查找多个元素是否都存在(多个元素放在一个集合里)。
- size:获取元素个数。
- isEmpty:判断是否为空。
3.Set接口的遍历方式
Set接口是Collection的子接口,因此同Collection接口的遍历方式一样。
- 使用迭代器
- 使用增强for(增强for循环,底层仍然是迭代器)
- 因为Set集合中元素没有索引,所以不能使用普通for循环。
二、HashSet
1.HashSet全面说明
(1)HashSet实现了Set接口。
(2)HashSet实际上是HashMap。
/*HashSet的无参构造器里调用的是HashMap
public HashSet() {
map = new HashMap<>();
}
*/
Set hashSet = new HashSet();
(3)HashSet中不能有重复元素/对象。
(4)HashSet可以存放null,但只能存放一个。
(5)HashSet中元素的存放顺序与取出顺序可能不一致,取决于hash后再确定索引的结果。
注意:取出顺序虽然与添加顺序不一致,但无论取出多少次,取出顺序是固定不变的。
@SuppressWarnings({"all"})
public class HashSet01 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
//add:添加单个元素,返回一个boolean值。添加成功,返回true;添加失败,返回false。
System.out.println(hashSet.add("john"));//t
System.out.println(hashSet.add("lucy"));//t
System.out.println(hashSet.add("john"));//f
System.out.println(hashSet.add("jack"));//t
System.out.println(hashSet.add("Rose"));//t
//remove:删除指定元素。
hashSet.remove("john");
System.out.println(hashSet);//[Rose, lucy, jack]
//HashMap不能添加重复的元素/对象
hashSet = new HashSet();
hashSet.add("lucy");//t
hashSet.add("lucy");//f
hashSet.add(new Dog("tom"));//t
hashSet.add(new Dog("tom"));//t,两只名字都叫tom的狗
System.out.println(hashSet);//[Dog{name='tom'}, lucy, Dog{name='tom'}]
//TODO add的底层机制是什么?
hashSet.add(new String("hsp"));//t
hashSet.add(new String("hsp"));//f,添加失败!!
System.out.println(hashSet);//[hsp, Dog{name='tom'}, lucy, Dog{name='tom'}]
}
}
class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
- add:添加单个元素,返回一个boolean值。添加成功,返回true;添加失败,返回false。
- remove:删除指定元素。
2.HashSet底层机制
(1)模拟简单的数组+链表结构
public class HashSetStructrue {
public static void main(String[] args) {
//创建一个数组,数组类型是Node[],有些人称Node[]为表
Node[] table = new Node[16];
Node john = new Node("john", null);
table[2] = john;//将john结点添加到table中索引为2的位置
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的位置
}
}
class Node {//结点,存储数据,可以指向下一个结点,从而形成链表
Object item;//存放数据
Node next;//指向下一个结点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
(2)HashSet的添加元素底层的实现
- HashSet的底层是HashMap,HashMap的底层是数组+链表+红黑树。
- 添加一个元素时,先得到 key 的 hash 值,然后 hash 值转化为在 table 的索引。
- 找到存储数据表table,看这个索引位置是否已经存放元素。
- 如果没有,直接加入;如果有,调用equals比较,相同就放弃添加,不同则添加到最后(next)。
- 在Java8中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树)。
import java.util.HashSet;
@SuppressWarnings({"all"})
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println(hashSet);
/*源码:
1.类HashSet的无参构造器-HashSet()
public HashSet() {
map = new HashMap<>();
}
2.类HashSet—add方法
public boolean add(E e) {//e:"java"
return map.put(e, PRESENT)==null;//PRESENT(静态):private static final Object PRESENT = new Object();
}
3.类HashMap-put方法:该方法会执行hash(key)得到key对应的hash值
涉及算法(h = key.hashCode()) ^ (h >>> 16) ^按位异或 >>>无符号右移16位
为了让不同的key尽量得到不同的hash值
public V put(K key, V value) {//key:"java" value:PRESENT
return putVal(hash(key), key, value, false, true);
}
4.类HashMap-hash方法
static final int hash(Object key) {//key:"java"
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
5.类HashMap-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;//定义辅助变量,p:指向将要存储Node的位置的现有Node
//table:HashMap的一个数组,类型是 Node[]
//if语句:如果当前table是null,或者大小为0,就第一次扩容到16个位置
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//if语句:根据key得到的hash值,去计算该key应该存放到table表的哪个索引位置i
// 将这个位置的对象赋给p,判断p是否为null
//如果p为null,表示还没有存放元素,就创建一个Node(key="java",value=PRESENT);并将该Node放置在该位置tab[i]
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//开发tip:在需要局部变量(辅助变量)时再定义
//如果p不为空,表示该位置处已存放元素
//第1种情况:p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
//如果当前索引位置对应的链表的第一个元素和准备添加的元素(key)的hash值一样
//并且满足下面两个条件之一:
//(1)p指向的Node结点的key和准备添加的key是同一个对象
//(2)准备添加的key的equals()方法和p指向的Node结点的key比较后相同(非String类对象,equals的具体实现程序员自己决定)
//就不能加入
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//第2种情况:如果p是一颗红黑树,就调用putTreeVal来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//第3种情况:如果要添加元素的索引位置已经是一个链表,就使用for循环依次和该链表的每一个元素进行比较
//(1)依次比较后都不相同,则加入到该链表的最后
// 把元素添加到链表后,立即判断该链表是否已经达到8个结点
// 如果达到8个就调用treeifyBin()对当前这个链表进行树化(转成红黑树)
// 注意:在转成红黑树时,要进行判断,判断条件(table数组的大小<MIN_TREEIFY_CAPACITY(64))
// if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// resize();
// 如果条件成立,先对table进行扩容
// 如果条件不成立,将table转成红黑树
//(2)依次比较的过程中,如果有相同的情况,直接break
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;//对应(1)
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//对应(2)
p = e;
}
}
//HashMap.add(key,value):原本的table中已存在要添加的key,就更新value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果当前table的大小 > 12(临界值),就再次扩容
//size:每加入一个结点Node(k,v,h,next),不管是加在链表后还是加在table的索引位置,size++
if (++size > threshold)
resize();//扩容
//空方法,为了让HashMap的子类实现该方法
afterNodeInsertion(evict);
return null;
}
*/
}
}
(3)HashSet的扩容和转成红黑树机制
- HashSet的底层是HashMap。
- 第一次添加时,table数组扩容到16,临界值(threshold)是16*0.75=12(0.75是加载因子(loadFactor))。
- 如果table数组使用到了临界值12,就会继续扩容到16*2=32,新的临界值是32*0.75=24。依次类推... 16(12)→32(24)→64(48)→128(96)...
- 不管是在链表后缀接结点,还是在table新的索引位置上增加结点,都会增加table使用,size++。
- 在Java8中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
import java.util.HashSet;
@SuppressWarnings({"all"})
public class HashSetIncrement {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
// for (int i = 0; i < 100; i++) {
// hashSet.add(i);
// }
//HashSet底层是HashMap
//第一次添加时,table数组扩容到16,临界值是12
//table数组使用达到12,table数组再次扩容到32,临界值是24
//table数组使用达到24,table数组再次扩容到64,临界值是48
//table数组使用达到48,table数组再次扩容到128,临界值是96
//table数组使用达到96,table数组再次扩容到256,临界值是192
//...
//当向hashSet中增加一个Node,不管是添加到链表后,还是加在table的索引位置,都算是增加了一个table数组使用
for (int i = 0; i < 7; i++) {//在table的某一个索引位置处添加了一个7结点(A对象)链表
hashSet.add(new A(i));
}
for (int i = 0; i < 7; i++) {//在table的另一个索引位置处添加了一个7结点(B对象)链表
hashSet.add(new B(i));
}
//第 13 次添加,table数组使用已达到12,因此table表扩容至32
// for (int i = 0; i < 12; i++) {
// hashSet.add(new A(i));
// }
//在Java8中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制
//第 1 次添加,在table的索引为 4 的位置上添加一个结点
//第 2-8 次添加,在table的索引为 4 的位置的最后缀接一个结点,第 8 次添加后,table的索引为 4 的位置上:一个8结点的链表
//第 9 次添加,table表扩容至 32,table的索引为 4 的位置上:一个9结点的链表
//第 10 次添加,table表扩容至 64,table的索引为 36 的位置上:一个10结点的链表(位置由4变至36)
//第 11 次添加,树化,table表容量不变,table的索引为 36 的位置上:链表->红黑树
}
}
class A {
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 100;//不管 n 为多少,hash 值都为100,保证添加在table的同一个索引位置
}
}
class B {
private int n;
public B(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 200;//不管 n 为多少,hash 值都为100,保证添加在table的同一个索引位置
}
}
3.HashSet课后练习
(1)定义一个Employee类,该类包含:private成员属性name和age。
要求:a:创建3个Employee对象放入HashSet中。
b:当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中。
import java.util.HashSet;
import java.util.Objects;
@SuppressWarnings({"all"})
public class HashSetExercise {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("张三", 25));//第1个对象
hashSet.add(new Employee("李四", 30));//第2个对象
hashSet.add(new Employee("张三", 25));//第3个对象
// System.out.println(hashSet);//[Employee{name='李四', age=30}, Employee{name='张三', age=25}, Employee{name='张三', age=25}]
//没有重写equals和hashCode:hashSet中添加了3个元素。因为第1和3个对象是不同的对象实例,所以hash值也不同,所在的索引位置也不同。
System.out.println(hashSet);//[Employee{name='李四', age=30}, Employee{name='张三', age=25}]
//重写equals和hashCode:hashSet中添加了2个元素。因为第1和3个对象虽然是不同的对象实例,但名字与年龄相同,hash值也就相同,equals判断两个对象相等。
}
}
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 obj) {
//如果比较的两个对象是同一个对象,则直接返回true
if(this == obj) {
return true;
}
//判断obj的运行类型是否为Employee,如果是才比较属性是否相同
if(obj instanceof Employee) {
Employee employee = (Employee) obj;//向下转型,以访问到Employee类所特有的属性
return this.name.equals(employee.name) && this.age == employee.age;
}
//如果obj的运行类型不是Employee,直接返回false
return false;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
(2)定义一个Employee类,该类包含:private成员属性name、sal、birthday(MyDate类),其中birthday为MyDate类型(属性包括:year,month,day)。
要求:a:创建3个Employee对象放入HashSet中。
b:当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中。
@SuppressWarnings({"all"})
public class HashSetExercise02 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("张三", 2000, new MyDate(2000, 11, 9)));
hashSet.add(new Employee("李四", 2000, new MyDate(2000, 12, 4)));
hashSet.add(new Employee("张三", 2000, new MyDate(2000, 11, 9)));
System.out.println(hashSet);
/*
[Employee{name='李四', sal=2000, birthday=MyDate{year=2000, month=12, day=4}},
Employee{name='张三', sal=2000, birthday=MyDate{year=2000, month=11, day=9}}]
*/
}
}
class Employee {
private String name;
private int sal;
private MyDate birthday;
public Employee(String name, int sal, MyDate birthday) {
this.name = name;
this.sal = sal;
this.birthday = birthday;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", sal=" + sal +
", birthday=" + birthday +
'}';
}
@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);
}
}
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;
}
@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
@Override
public boolean equals(Object o) {
//如果比较的两个对象是同一个对象,则直接返回true
if (this == o) return true;
//如果比较的对象是null 或 比较的两个对象类型不同,则直接返回false
if (o == null || getClass() != o.getClass()) return false;
//比较的两个对象类型相同
MyDate myDate = (MyDate) o;//向下转型,以比较MyDate类特有的属性
return year == myDate.year &&
month == myDate.month &&
day == myDate.day;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
}
三、LinkedHashSet
1.LinkedHashSet全面说明
(1)LinkedHashSet是HashSet的子类。
(2)LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表。
(3)LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
(4)LinkedHashSet不允许添加重复元素。
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Set;
@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);//set = [AA, 456, com.hspedu.set_.Customer@1540e19d, 123, HSP]
/*
1.LinkedHashSet 元素的加入顺序和取出顺序一致
知识点1:Set集合中元素不允许重复,最多包含一个null。
知识点2:toString方法默认返回 全类名(包名+类名)+@+哈希值的十六进制,子类往往重写toString方法,用于返回对象的属性信息
2.LinkedHashSet 底层维护的是一个 LinkedHashMap(是HashMap的子类),底层结构是 数组(table)+双向链表
3.第一次添加元素,将table扩容至16
4.数组(table)的类型是 HashMap$Node,存放的元素类型是 LinkedHashMap$Entry(多态数组)
LinkedHashMap$Entry继承HashMap$Node:
LinkedHashMap的静态内部类Entry 继承 HashMap的静态内部类Node(Node只给HashMap这条线使用,因此定义在内部)
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);
}
}
5.在LinkedHashSet中添加元素:HashSet.add -> HashMap.put -> HashMap.putVal
*/
}
}
class Customer {
private String name;
private int no;
public Customer(String name, int no) {
this.name = name;
this.no = no;
}
}
2.LinkedHashSet课后练习
Car类(属性:name,price),如果name和price一样,则认为是相同元素,就不能添加。
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));
linkedHashSet.add(new Car("奥迪", 300000));
linkedHashSet.add(new Car("法拉利", 10000000));
linkedHashSet.add(new Car("奥迪", 300000));
linkedHashSet.add(new Car("保时捷", 70000000));
linkedHashSet.add(new Car("奥迪", 300000));
System.out.println("linkedHashSet = " + linkedHashSet);
/*未重写equals和hashCode方法:三辆奥迪是三个不同的对象,hash值不同,因此都可以添加进LinkedHashSet
linkedHashSet = [
Car{name='奥拓', price=1000.0},
Car{name='奥迪', price=300000.0},
Car{name='法拉利', price=1.0E7},
Car{name='奥迪', price=300000.0},
Car{name='保时捷', price=7.0E7},
Car{name='奥迪', price=300000.0}]
重写equals和hashCode方法:三辆奥迪name和price相同,hash值相同且equals返回true,因此只能添加一个进LinkedHashSet
(只重写一个不起作用)
linkedHashSet = [
Car{name='奥拓', price=1000.0},
Car{name='奥迪', price=300000.0},
Car{name='法拉利', price=1.0E7},
Car{name='保时捷', price=7.0E7}]
*/
}
}
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() {
//\n换行符
return "\nCar{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
//重写equals和hashCode方法:当name和price相同时,返回相同的hash值,equals返回true
@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);
}
}