无序(添加和取出的顺序不一致),没有索引。不允许重复元素,所以最多包含一个 null。
HashSet、TreeSet、CopyOnWriteArraySet
和 List 接口一样,Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口是一样的。
同 Collection 的遍历方式一样,因为 Set 接口是 Collection 接口的子接口。可以使用迭代器、增强 for 循环、但是不能使用索引的方式来获取。
public class SetTest {
public static void main(String[] args) {
// Set 接口的实现类对象不能存放重复元素,可以添加一个 null
// Set 接口对象存放的数据是无序的(添加的顺序和取出的顺序不一致)
// 注意:取出元素的顺序虽然不是添加的顺序,但是他每次打印顺序都是固定的
Set set = new HashSet();
set.add("zhangSan");
set.add("liSi");
set.add("zhangSan");
set.add(null);
set.add(null);
System.out.println("set="+set);
System.out.println("----------------");
// 遍历的方式一:使用迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object o = iterator.next();
System.out.println(o);
}
System.out.println("----------------");
// 遍历的方式二:使用增强for循环
for (Object o :set) {
System.out.println(o);
}
}
}
HashSet 实现了 Set 接口,可以存放 null,但只能有一个 null。HashSet 不保证元素是有序的,即不保证存放元素的顺序和取出的顺序一致。HashSet 其实是 HashMap,源码如下:
public HashSet() {
map = new HashMap<>();
}
HashSet 底层是 HashMap,添加一个元素的时候,会先使用 hashCode() 方法计算该元素的 hash 值。然后对 hash 值进行运算得到一个索引值,这个索引值即为要存放在哈希表的位置号。
如果该位置上没有其他元素,则直接存放;如果该位置上已经有其他元素了,则需要进行 equals 判断(equals 方法一般都需要根据业务进行重写),如果相等,则不再添加,如果不相等,则以链表的形式添加到后面。
HashSet 底层是 HashMap,第一次添加元素的时候,table 数组扩容到 16,临界值 = 16 * 0.75 = 12;即临界值 = 容量 * 加载因子。
如果 table 数组使用到了临界值 12 ,就会扩容到 16 * 2 = 32,新的临界值就是 32 * 0.75 = 24,依次类推。
在 java8 中,如果一条链表的元素个数到达 8 个,此时 table 数组的长度是 16,那么此时就会发生数组扩容,此时的状态为:链表长度为 8,table 数组的长度为 32;此时再向链表里面添加元素,又会发生扩容,此时的状态为:链表长度为 9 ,table 数组的长度为 64;此时再像链表里面添加元素,就会触发树化,即链表演化为了红黑树。
在 java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认是 8 ) 并且 table 数组的大小 >= MIN_TREEIF_CAPACITY(默认是 64),就会进行树化,否则仍然采用数组的扩容机制。
定义一个 Employee 类,该类包含 private 成员属性 name 和 age,要求:创建 3 个 Employee 对象放入 HashSet 中,当 name 和 age 的值相同时,认为是相同员工,不能添加到 HashSet 中。代码如下:
public class Employee{
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public Employee() {
}
// setter、getter、toString()
// 重写 hashCode 方法,因为这三个元素的 hashCode 都不一样
@Override
public int hashCode() {
String name = this.getName();
int age = this.getAge();
int nameCode = name.hashCode();
int result = age + nameCode;
return result;
}
// 重写 equals 方法,若 name 和 age 相同就认为是同一个元素
@Override
public boolean equals(Object obj) {
Employee employee =(Employee)obj;
if(this.getName() == employee.getName() && this.getAge() == employee.getAge()){
return true;
}else{
return false;
}
}
}
测试代码类如下:
public class Test {
public static void main(String[] args) {
Set set = new HashSet();
Employee e1 = new Employee("张三",18);
Employee e2 = new Employee("李四",18);
Employee e3 = new Employee("张三",18);
set.add(e1);
set.add(e2);
set.add(e3);
System.out.println("set = "+set);
}
}
定义一个 Employee 类,该类包含 private 成员属性 name、sal 和 birthday,其中 birthady 为 MyDate 类型(属性包括:year、month、day)。要求
1、创建 3 个 Employee 对象放入 HashSet 中
2、当 name 和 birthday 的值相同时,认为是相同员工,不能添加到 HashSet 集合中。
public 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;
}
// setter、getter、toString()
@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 String sal;
private MyDate birthday;
// getter、setter、toString()
public Employee(String name, String sal, MyDate birthday) {
this.name = name;
this.sal = sal;
this.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 Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, birthday);
}
}
public class Test {
public static void main(String[] args) {
Set set = new HashSet();
set.add(new Employee("张三","30",new MyDate(1992,8,19)));
set.add(new Employee("李四","30",new MyDate(1993,8,19)));
set.add(new Employee("张三","50",new MyDate(1992,8,19)));
System.out.println("set =" + set);
}
}
LinkedHashSet 是 HashSet 的子类,LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个数组+双向链表。
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。也是不允许插入重复元素。
LinkedHashSet 中维护了一个 hash 表和双向链表(LinkedHashSet 有 head 和 tail),每一个节点有 before 和 after 属性,这样就形成了双向链表。
在添加一个元素时,先求 hash 值再求索引,根据索引确定元素在 table 表中的位置,然后将添加的元素加入到双向链表(如果已经存在则不添加)。
这样的话,我们遍历 LinkedHashSet 也能确保插入顺序和遍历顺序一致。
LinkedHashSet 的底层结构为数组 + 双向链表。第一次添加元素的时候,直接将数组 table 扩容到 16。临界值 = 16 * 0.75 = 12;即 临界值 = 容量 * 加载因子。
如果 table 数组使用到了临界值 12 ,就会扩容到 16 * 2 = 32,新的临界值就是 32 * 0.75 = 24,依次类推。
在 java8 中,如果一条链表的元素个数到达 8 个,此时 table 数组的长度是 16,那么此时就会发生数组扩容,此时的状态为:链表长度为 8,table 数组的长度为 32;此时再向链表里面添加元素,又会发生扩容,此时的状态为:链表长度为 9 ,table 数组的长度为 64;此时再像链表里面添加元素,就会触发树化,即链表演化为了红黑树。
在 java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认是 8 ) 并且 table 数组的大小 >= MIN_TREEIF_CAPACITY(默认是 64),就会进行树化,否则仍然采用数组的扩容机制。
LinkedHashSet 和 HashSet 相比,唯一的变化就是由单向链表变成了双向链表,这样做的好处是保证了元素的顺序性。
有一个 Car 类(属性为 name 和 price),如果 name 和 price 一样,则认为是相同元素,就不能添加。
class Car {
private String name;
private String price;
public Car(String name, String price) {
this.name = name;
this.price = price;
}
// setter、getter、toString()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Objects.equals(name, car.name) && Objects.equals(price, car.price);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
public class TestLink {
public static void main(String[] args) {
Set set = new LinkedHashSet();
set.add(new Car("小汽车","30"));
set.add(new Car("小玩偶","40"));
set.add(new Car("小汽车","30"));
System.out.println("set="+set);
}
}