【JAVA入门】Day30 - 单列集合 —— Set 系列
文章目录
Set 集合系列是单列集合体系结构的另一条分支,有别于 List 集合的有序、可重复、有索引,Set 集合的特征是无序、不重复、无索引。
- 无序:存取顺序不一致。
- 不重复:可以去除重复数据。
- 无索引:没有带索引的方法,所以不能使用普通 for 循环遍历,也不能通过索引来获取元素。
Set 集合有三个实现类,它们分别是:
- HashSet:无序、不重复、无索引。
- LinkedHashSet:有序、不重复、无索引。
- TreeSet:可排序、不重复、无索引。
Set 本身是一个接口,它里面的方法基本上和 Collection 的 API 一致。
一、Set 集合的遍历
Set 集合也可以用常用的那三种方式遍历,即:迭代器、增强 for 、Lambda 表达式。
package Set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.function.Consumer;
public class SetDemo1 {
public static void main(String[] args) {
/*
遍历Set
*/
//1.创建一个Set集合的对象
//由于Set是一个接口,所以只能创建其实现类对象,可以利用多态实现调用Set中的方法
Set<String> s = new HashSet<>();
//2.添加元素
//add方法的返回值是一个布尔类型,用于判断元素是否添加成功
//如果已经有重复元素,添加就会失败
s.add("aaa");
s.add("bbb");
System.out.println(s.add("bbb"));
s.add("ccc");
//3.打印集合
System.out.println(s);
//迭代器遍历
Iterator<String> it = s.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
//增强for
for (String str : s) {
System.out.println(str);
}
//lambda表达式
s.forEach(str -> System.out.println(str));
}
}
二、HashSet
HashSet 集合在底层采取哈希表存储数据。
哈希表是一种对于增删改查数据性能都良好的结构。
在 JDK8 以前,哈希表的底层是数组+链表;在 JDK8 以后,哈希表的底层是数组+链表+红黑树。
2.1 哈希值
哈希值是对象的整数表现形式。
哈希表在底层是有一个数组存在的,这个数组存储数据时,存储在哪个位置上的索引,是有公式要求的:
int index = (数组长度 - 1) & 哈希值;
公式中的这个哈希值,是 Java 根据 hashCode() 方法计算出来的 int 类型的整数,这个方法定义在 Object 类中,所以所有对象都可以调用,默认使用对象的地址值进行计算。
但是,一般情况下,我们在使用时会改写 hashCode 方法,利用对象内部的属性值来计算哈希值。因此:
- 如果没有重写 hashCode() 方法,不同对象计算出的哈希值一定是不同的。
- 如果已经重写 hashCode() 方法,不同的对象只要属性值相同,计算出的哈希值就是相同的。
- 小概率事件(哈希碰撞):不同的属性值或者不同的地址值计算出来的哈希值出现了一样的情况。
比如我们创建一个学生类的 Javabean,在里面重写 hashCode() 方法,格式如下。
@Override
public int hashCode() {
return Objects.hash(name, age);
}
重写的 hashCode() 方法是根据属性值计算哈希值的,因此如果两个学生对象的 name 和 age 完全一致,其计算出的哈希值就会完全一样。
package Set;
public class HashSetDemo1 {
public static void main(String[] args) {
//创建学生类对象
Student s1 = new Student("小王", 30);
Student s2 = new Student("小王", 30);
Student s3 = new Student("老王", 19);
//计算hash值
System.out.println(s1.hashCode()); //23565827
System.out.println(s2.hashCode()); //23565827
System.out.println(s3.hashCode()); //32408938
}
}
2.2 HashSet 存储底层原理
HashSet 底层在这之前是通过数组+链表存储的。
当创建一个 HashSet 对象时:
HashSet<String> hm = new HashSet<>();
第一步,系统会创建一个数组,里面有一些默认参数值。
第二步,开始往集合中放元素,会根据元素的哈希值跟数组的长度计算出应存入的位置,计算索引公式是这个:
int index = (数组长度 - 1) & 哈希值;
第三步,判断当前索引位置是否为 null,如果是 null ,直接存入。
第四步,如果当前位置不为 null,表示已经有元素存入,则调用 equals() 方法比较其属性值。
第五步,此时判断该元素是否能存入数组,如果 equals 返回 true,代表已经有一模一样的元素在了,所以不存;如果不一样,就存,但是要和老元素一起形成链表。
随着元素存储越来越多,数组变得不够大,需要扩容。此时会根据这个公式计算:
数组长度 × 加载因子 = 数组存储元素个数上限
默认16 × 0.75 = 12,当存储12个元素时,数组会扩容成原来的两倍。
而当链表长度大于8且数组长度大于等于64时,当前链表就会自动转化为红黑树,从而提高了查找效率。
两个注意点:
1.JDK8 以后,当链表长度超过8,而且数组长度大于等于64时,自动把链表转换为红黑树。
2.如果集合中存储的是自定义对象,必须要重写 hashCode 和 equals 方法。
2.3 HashSet 集合的特点
1.HashSet 存和取的顺序不一样。
HashSet 在读数据时,从数组0索引开始,一条链表一条链表地遍历,这和我们添加元素时的顺序不一样,因此 HashSet 存和取的顺序是不一样的。
2.HashSet 没有索引。
HashSet 在底层是数组+链表+红黑树组成,没有办法用一个索引值来找寻一个元素。
3.HashSet 为什么能数据去重?
HashSet 就是利用 hashCode() 方法和 equals() 方法来去重的。在添加元素前,先用 hashCode() 方法算出元素应该存到数组的哪个位置,如果这个位置上有元素了,再调用 equals() 方法,判断这个元素和要添加的元素是不是完全一样,如果一样,就不用添加了。因此完成了数据去重。
【练习】使用 HashSet 去除重复元素。
package Set;
import java.util.Objects;
public class Student {
private String name;
private int age;
public Student() {
}
public Student(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 boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Student 中重写 hashCode() 方法和 equals() 方法。
package Set;
import java.util.HashSet;
public class HashSetDemo2 {
/*
利用集合存储学生对象,然后遍历,在创建时如果有两个学生成员变量值相同,我们认为他们相同
*/
public static void main(String[] args) {
//1.创建三个学生对象
Student s1 = new Student("zhangsan", 23);
Student s2 = new Student("lisi", 24);
Student s3 = new Student("wangwu", 23);
Student s4 = new Student("zhangsan", 23);
//2.创建集合来添加学生
HashSet<Student> hs = new HashSet<>();
//3.添加元素
System.out.println(hs.add(s1)); //true
System.out.println(hs.add(s2)); //true
System.out.println(hs.add(s3)); //true
System.out.println(hs.add(s4)); //false
//4.打印集合
System.out.println(hs); //[Student{name='lisi', age=24}, Student{name='zhangsan', age=23}, Student{name='wangwu', age=23}]
}
}
注意:如果集合里要放的是自定义对象,那就必须重写 hashCode() 和 equals() 方法;但是如果你要放的是 String 或者 Integer 这样本来就有的类,就不需要重写那两个方法了,Java 已经自动帮我们重写好了。
三、LinkedHashSet
LinkedHashSet 是继承自 HashSet 的一种类。它的特点是:有序、不重复、无索引。
这里的有序保证的是数据的存储和取出顺序是完全一致的。
这是因为这个类的底层数据结构是哈希表,又额外添加了一个双链表的机制来记录数据存储的顺序。
存储完毕时,数组里的元素之间会形成一条双向链表,这样在遍历时,就会按照这个双向链表进行遍历,通过遍历这个双向链表得到的数据顺序就和添加时的顺序是一致的,这样就能让元素变为有序。
LinkedHashSet 正适合于要求去重且存取数据有序的情况。
四、TreeSet
TreeSet 是 Set 体系中的一种集合类。它的特点是:不重复、无索引、可排序。
TreeSet 可排序,且按照元素的默认规则(由小到大)排序。它的底层是基于红黑树的数据结构实现排序的,增删改查性能都较好。
4.1 TreeSet 对象排序
【例1】利用 TreeSet 存储整数并进行排序。
package Set;
import java.util.TreeSet;
public class TreeSetDemo1 {
public static void main(String[] args) {
/*
需求:利用TreeSet存储整数并排序
*/
//1.创建TreeSet集合对象
TreeSet<Integer> ts = new TreeSet<>();
//2.添加元素
ts.add(4);
ts.add(1);
ts.add(2);
ts.add(3);
ts.add(5);
//3.打印
System.out.println(ts); //[1, 2, 3, 4, 5]
//4.三种遍历
Iterator<Integer> it = ts.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
for (Integer t : ts) {
System.out.println(t);
}
ts.forEach(i -> System.out.println());
}
}
通过打印我们发现,在添加完元素后,TreeSet 自动就把我们添加的 Integer 类型元素按照从小到大排好序了。经过三种遍历,我们发现,它们确实是按照从小到大的顺序遍历的。
TreeSet 集合对于数值类型:Integer,Double,默认都是按照从小到大排列的。
而对于字符、字符串类型:按照字符在 ASCII 码表中的数字升序进行排序。比如 “a” 是 97,“A” 是 65。
如果字符串内容比较多,它会从首字母开始挨个比较,此时和字符串的长度没有任何关系。比如:“aaa” 就大于 “ab” ,因为第一个字母一样,比较第二个字母,“a” > “b” ,已经可以确定大小关系,因此不再比较后面的字母。
那么如何使用 TreeSet 排序自定义对象?
TreeSet 的比较方式有两种:
- 其一是默认排序 / 自然排序:利用 Javabean 类实现 Comparable 接口指定比较规则。
//在Student类中重写Comparable接口中的抽象方法
@Override
public int compareTo(Student o) {
//指定排序规则
//只看年龄,按照年龄升序排列
//TreeSet内部是红黑树结构,Student不需要重写其equals和hashCode方法
//this:表示当前要添加的元素
//o:表示已经在红黑树存在的元素
//返回值:负数:认为当前要添加的元素是小的,就要存左边
//正数:认为要添加的元素是大的,就要存右边
//0:认为当前要添加的元素已经存在,舍弃不存
System.out.println("-------------------------");
System.out.println("this:" + this);
System.out.println("o:" + o);
return this.getAge() - o.getAge();
- 其二是比较器排序:在创建 TreeSet 对象的时候,传递比较器 Comparator 指定规则(参数是一个 Comparator 接口的匿名实现类)。传递 Comparator 接口的匿名实现类(可以用Lambda表达式),然后在内部重写 compare 方法,指定比较规则。
package Set;
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetDemo3 {
public static void main(String[] args) {
/*
存入四个字符串,"c","ab","df","qwer" 按照长度排序,如果一样长就按照首字母排序
比较器排序:创建集合时传入一个比较器对象
*/
//1.创建集合
//参数传递一个Comparator的匿名实现类,重写compare方法
/* TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//按照长度排序
int i = o1.length() - o2.length();
//如果一样长就按照首字母排序,否则按照长度排序
i = i == 0 ? o1.compareTo(o2) : i;
return i;
}
});*/
//改写为Lambda表达式
TreeSet<String> ts = new TreeSet<>((o1, o2) -> {
//按照长度排序
int i = o1.length() - o2.length();
//如果一样长就按照首字母排序,否则按照长度排序
//compareTo默认是按照字符串首字母排序的
i = i == 0 ? o1.compareTo(o2) : i;
return i;
}
);
//2.添加元素
ts.add("c");
ts.add("ab");
ts.add("df");
ts.add("qwer");
System.out.println(ts);
}
}
【练习】使用 TreeSet 对象自定义规则排序。
需求:
package Set;
import java.util.Comparator;
import java.util.Objects;
public class Student2 implements Comparable<Student2> {
private String name;
private int age;
private int chinese;
private int math;
private int english;
public Student2() {}
public Student2(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 boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student2 student2 = (Student2) o;
return age == student2.age && Objects.equals(name, student2.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Student2{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public int getChinese() {
return chinese;
}
public void setChinese(int chinese) {
this.chinese = chinese;
}
public int getMath() {
return math;
}
public void setMath(int math) {
this.math = math;
}
public int getEnglish() {
return english;
}
public void setEnglish(int english) {
this.english = english;
}
@Override
public int compareTo(Student2 o) {
/* 需求:创建5个学生对象
属性:(姓名,年龄,语文成绩,数学成绩,英语成绩),
按照总分从高到低输出到控制台
如果总分一样,按照语文成绩排
如果语文一样,按照数学成绩排
如果数学成绩一样,按照英语成绩排
如果英文成绩一样,按照年龄排
如果年龄一样,按照姓名的字母顺序排
如果都一样,认为是同一个学生,不存。*/
//总分比较
int sum1 = this.getChinese() + this.getMath() + this.getEnglish();
int sum2 = o.getChinese() + o.getMath() + o.getEnglish();
int i = sum1 - sum2;
//如果总分一样按语文排
i = i == 0 ? this.getChinese() - o.getChinese() : i;
//如果语文一样按数学排
i = i == 0 ? this.getMath() - o.getMath() : i;
//如果数学一样按英语排
i = i == 0 ? this.getEnglish() - o.getEnglish() : i;
//如果英语一样按年龄排(可以省略不写)
i = i == 0 ? this.getAge() - o.getAge() : i;
//如果年龄一样按姓名的字母顺序排
i = i == 0 ? this.getName().compareTo(o.getName()) : i;
}
}
五、单列集合的使用场景
- 如果想要集合中的元素可重复:使用 ArrayList 集合,基于数组实现。
- 如果想对集合中的元素去重:使用 HashSet 集合,基于哈希表实现。
- 如果集合中的元素可重复,且当前的增删操作明显多于查询:使用 LinkedList 集合,基于链表实现。
- 如果集合中的元素去重,且要保证存取有序:使用 LinkedHashSet 集合,基于哈希表和双链表实现,但效率要略低于 HashSet。
- 如果相对集合中的元素进行排序:使用 TreeSet 集合,基于红黑树实现。