一、Set接口
1、无序,没有索引
2、不允许重复元素,最多一个null
3、Set遍历可以使用迭代器和增强for,不能使用下标遍历
package com.level7.Set_;
import sun.font.EAttribute;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Set01 {
public static void main(String[] args) {
Set set = new HashSet();
set.add(2);
set.add(2);
set.add(3);
set.add("jack");
set.add(5);
set.add("xrj");
set.add("xrj");
set.add("xyy");
System.out.println(set);
set.remove("xrj");
System.out.println(set);
// 迭代器遍历
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
// 增强for遍历
for (Object o : set) {
System.out.println(o);
}
// set不能使用下标遍历
}
}
二、HashSet
1、HashSet
实际上是HashMap
2、HashSet
不保证元素是有序的,hash后再确定索引的值
3、不能有重复元素和对象
package com.level7.Set_;
import java.util.HashSet;
public class HashSetSource {
public static void main(String[] args) {
HashSet set = new HashSet();
// 添加成功返回true,添加失败,返回false
System.out.println(set.add("xrj"));
System.out.println(set.add("xrj"));
System.out.println(set.add("xyy"));
System.out.println(set.add("xyy"));
System.out.println(set.add("abc"));
set.remove("xrj");
System.out.println(set);
System.out.println("=========================");
set = new HashSet();
set.add("xrj"); // 添加成功
set.add("xrj"); // 添加失败
set.add(new Dog("xyy")); // 添加成功
set.add(new Dog("xyy")); // 添加成功
System.out.println(set);
set.add(new String("abc")); // 添加成功
set.add(new String("abc")); // 添加失败
System.out.println(set);
}
}
class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
HashSet底层机制
HashSet底层机制是HashMap,HashMap底层是数组+链表+红黑树
- 如果一条链表元素个数达到8,但是table大小没有达到64,会先对table进行扩容
package com.level7.Set_;
import java.util.HashSet;
public class HashSource02 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("Java");
hashSet.add("C++");
hashSet.add("Java");
System.out.println(hashSet);
}
}
1、第一次add
1、HashSet hashSet = new HashSet();
执行该语句,其实是创建了一个HashMap
2、hashSet.add("Java");
第一次执行add方法。实际调用的是map.put
方法,里边的PRESENT
是一个final类型的静态对象,默认为空。
3、put
方法的key
就是准备add的变量Java,value
就是默认的PRESENT
。调用putVal
方法,传入的是key的hash值(传这个是为了在下次传入相同元素时做判断),key、value
3.1、首先计算key的hash值
,如果key
为空,他的hash值就是0,否则,他的hash值就是key的hashCode
和它本身无符号右移16位做异或运算
4、进入putVal
方法。Node[] tab; Node p; int n, i;
这是定义辅助变量
table
就是HashMap
的一个数组,初始为null
4.1 将table
赋值给tab
,如果tab
为空或者他的长度为0,那么就进resize()
方法
4.3 resize()
后,现在tab
就是大小为16的数组,n的大小为16,然后if ((p = tab[i = (n - 1) & hash]) == null)
表示,让 i 指向当前元素应该存在在 数组的那个下标位置,p是当前下标位置的对象,如果为null
,表示该位置没存放东西,就存放在这个位置。
4.4 afterNodeInsertion(evict)
是一个空实现,留给HashMap
的子类实现,最后返回null表示添加成功,然后第三点put
方法返回null,到第二点的add
方 法,如果返回null,表示add成功
4.2、进入resize()
方法,让oldTab
指向初始的table(初始为空),如果oldTab
为空oldCap = 0
否则 oldCap = oldTab.length
newCap
表示新的table长度,newThr
相当于新的阈值(当table容量达到阈值大小就扩容),第一次时,oldCap == 0
,进入else
语句,
newCap = DEFAULT_INITIAL_CAPACITY
,初始为16,表示第一次扩容大小为16,
newThr = (int)(DEFAULT_LOAD_FACTOR) * DEFAULT_INITIAL_CAPACITY
,DEFAULT_LOAD_FACTOR
默认为0.75,因此现在阈值为12,即超过12的大 小(即插入第13个元素时),数组就要继续扩容。然后创建大小为newCap
的数组newTab
,并赋给table
,最后返回newTab
2、第二次add不同的值
1、hashSet.add("C++");
执行这条语句,首先执行add方法
2、计算hash然后执行put方法
3、执行putVal
方法,此时table不为空,找到赋值的位置,加入进去,然后返回null
3、add相同的值
1、hashSet.add("Java");
执行这条语句。首先执行add方法
2、计算完hash值后,执行put方法
3、执行putVal
方法,此时table不为空,且当前元素为重复值,因此应该赋值的位置不为空,所以进入else
。p
是p = tab[i = (n - 1) & hash]
通过这条语句找到的当前值应该插入的数组对应下标的对象,如果当前值的hash
和p.hash
相等,并且(k = p.key) == key
表示是同一个对象或者(key != null && key.equals(k)))
表示通过equals
方法(该方法通过程序员自己指定)比较两个对象内容相同,那么就是和当前位置对象是相同的,就不能插入。如果不满足,进入else if
,判断当前链表是否是红黑树,如果是红黑树,那么用红黑树方法查找。如果不是红黑树,进入else
,此时表示就在当前这条链表上查找,e = p.next
从第二个元素开始查找,如果下一个元素为空,那么将该元素插入他的后边,然后判断元素个数是否达到8,如果达到8(即插入了第9个元素后),并且table大小达到64,就转为红黑树。如果下一个元素不为空,就判断当前元素和链表下一个元素是否是同一个对象或者equals
后是否相等,如果相等,插入失败,否则继续往后查询
HashSet扩容机制和转换红黑树机制
1、HashSet
底层是HashMap
,第一次添加时,table数组扩容到16,临界值(threshold
)是16 × 加载因子(loadFactor = 0.75
) = 12
2、如果table数组使用到了临界值12,就会扩容到 16 × 2 = 32,新的临界值就是32 × 0.75 = 24,以此类推。不管新增加的元素是增加到数组上,还是链表上,都要size++,size大于临界值就扩容。
3、在Java8中,如果一条链表的元素个数达到TREEIFY_THRESHOLD
(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY
(默认64),就会进行树化(红黑树),否则仍采用数组扩容机制。
package com.level7.Set_;
import java.util.HashSet;
import java.util.Objects;
public class HashSource03 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
// for(int i = 1; i <= 100; i++) {
// hashSet.add(i);
// }
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 10;
}
}
HashSet练习
定义一个Employee对象放入HashSet
当name和age的值相同时,认为是相同员工,不能添加到HashSet中
必须重写Employee的equals和hashCode方法,如果不重写hashCode方法,它他们的hash值是不同的就可以加入重复元素了
package com.level7.Set_;
import java.util.HashSet;
import java.util.Objects;
public class HashSetExercise01 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee("xrj", 20));
hashSet.add(new Employee("xrj", 20));
hashSet.add(new Employee("xyy", 10));
hashSet.add(new Employee("xyy", 10));
hashSet.add(new Employee("x1", 30));
hashSet.add(new Employee("x1", 30));
hashSet.add(new Employee("x2", 40));
hashSet.add(new Employee("x2", 40));
System.out.println(hashSet);
}
}
class Employee {
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
@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);
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
三、LinkedHashSet
1、LinkedHashSet
是HashSet
的子类
2、LinkedHashSet
底层是一个LinkedHashMap
,底层维护了一个数组 + 链表 + 红黑树 + 双向链表
3、LinkedHashSet
根据元素的hashCode
值来决定元素的存储位置,同时使用双向链表来维护元素的次序
4、LinkedHashSet
不允许添加重复元素
底层源码分析
package com.level7.Set_;
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetSource {
public static void main(String[] args) {
Set set = new LinkedHashSet();
set.add(new String("xrj"));
set.add(123);
set.add(123);
set.add(new People("xyy", 20));
set.add(666);
set.add("AAA");
// 输出顺序和插入顺序是相同的
System.out.println(set);
}
}
class People {
public String name;
public int age;
public People(String name, int age) {
this.name = name;
this.age = age;
}
}
1、Set set = new LinkedHashSet();
执行这条语句,实际创建的是LinkedHashMap
,它是HashMap
的子类
2、set.add(new String("xrj"));
执行add方法,和HashSet
一样,先执行put
方法,然后计算完key的hash值,执行putVal
方法,最后进入putVal
方法执行
和HashSet
不同点在于,这里newNode
调用的是LinkedHashMap
的newNode
方法
LinkedHashMap
的newNode
方法创建的是一个Entry
,它是Node
的子类,它当中有before
和after
节点,返回值类型为Node
相当于向上转型。然后执行linkNodeLast(p);
该方法会使用before
和after
节点将这次加入的和上次加入的值连接在一起
3、HashSet
加入的是Node节点
,而LinkedHashSet
加入的是Entry
节点
Entry
是HashMap.Node
的子类,它里面多了before
和after
属性,分别代表他的前一个节点,和后一个节点,以此形成双向链表
在table表中可以看到,第一次添加的是xrj
他在1号位置,他的before为空,因为他是第一个节点。
第二次添加的是123
,可以看到xrj
的after节点
存的是123
的位置,而123
的before节点
存的是xrj
的位置。
因为他们两个的单链表都只有一个元素,因此next
为空
LinkedHashMap
还有一个head
和tail
节点,存储第一个元素地址和最后一个元素地址,所以遍历的时候会按插入元素的顺序输出