概述
java.util.Set
接口和java.util.list
接口一样, 同样继承自 Collection 接口. 它与 Collection 接口的方法基本一致, 并没有对 Collection 接口进行功能上的扩充, 只是比 Collection 接口更加严格了. 与 List 接口不同的是, Set 接口中元素无序, 并且都会以某种规则保证存入的元素不出现重复.
Set 集合有多个子类, 这里我们介绍其中的java.util.HashSet
, java.util.LinkedHashSet
这两个集合.
注: Set 集合取出元素的方式可以采用: 迭代器, 增强 for.
HashSet 集合介绍
java.util.HashSet
是 Set 接口的一个实现类. 它所存储的元素是不可重复的, 并且元素都是无序的 (即存取顺序不一致). java.util.HashSet
底层的实现其实是一个java.util.HashMap
支持.
HashSet 是根据对象的哈希值来确定元素在集合中的存储位置, 因此具有良好的存取和查找性能. 保证元素唯一性的方法依赖于: hashCode 与 equals 方法.
我们先来使用一下 Set 集合存储, 看下现象, 在进行原理的讲解.
import java.util.HashSet;
public class Test39 {
public static void main(String[] args) {
// 创建 Set 集合
HashSet<String> set = new HashSet<>();
// 添加元素
set.add(new String("cba"));
set.add("abc");
set.add("bac");
set.add("cba");
// 遍历
for (String name : set){
System.out.println(name);
}
}
}
输出结果:
cba
abc
bac
注: 根据结果我们发现字符串 “cba” 只存储了一个, 也就是说重复的元素 set 集合不存储.
HashSet 集合存储数据的结构 (哈希表)
在 JDK1.8 之前, 哈希表底层采用数组 + 链表实现. 即使用链表处理冲突, 同一 hash 值的链表都存储在一个链表里. 但是当位于一个桶中的元素较多, 即 hash 值相等的元素较多是, 通过 key 值依次查找的效率较低. 而 JDK1.8 中, 哈希表存储采用数组 + 链表 + 红黑树实现. 当链表长度超过阈值 (8) 时, 将链表转换为红黑树, 这样大大减少了查找时间.
简单的来说, 哈希表是由数组 + 链表 + 红黑树 ( JDK1.8 增加了红黑树部分 ) 实现的, 如下图所示:
存储
流程图:
总而言之, JDK1.8 引入红黑树大程度优化了 HashMap 的性能. 那么对于我们来讲保证 HashSet 集合元素的唯一, 其实就是根据对象的 hashCode 和 euqals 方法来决定的. 如果我们往集合中存放自定义的对象, 那么保证其唯一, 就必须重复写 hashCode 和 equals 方法建立属于当前对象的比较方式.
HashSet 存储自定义类型元素
给 HashSet 中存放自定义类型元素时, 需要重写对象中的 hashCode 和 equals 方法. 建立自己的比较方式, 才能保证 HashSet 集合中的对象唯一.
创建自定义 Student 类:
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 instanceof Student)) return false;
Student student = (Student) o;
return getAge() == student.getAge() &&
Objects.equals(getName(), student.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试:
import java.util.HashSet;
public class Test40 {
public static void main(String[] args) {
// 创建集合对象, 集合中存储Student类型对象
HashSet<Student> studentHashSet = new HashSet<>();
// 存储
Student student = new Student("于谦", 18);
studentHashSet.add(student);
studentHashSet.add(new Student("郭德纲",50));
for (Student temp : studentHashSet){
System.out.println(temp);
}
}
}
输出结果:
Student{name='于谦', age=18}
Student{name='郭德纲', age=50}
LinkedHashSet
我们知道 HashSet 保证元素唯一, 可是元素存放进去是没有顺序的. 那么我们要保证有序, 怎么办呢?
在 HashSet 下面有一个子类java.util.LinkedHashSet
, 它是链表和哈希表组合的一个数据存储结构.
演示代码如下:
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
public class Test41 {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<>();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
输出结果:
bbb
aaa
abc
bbc
可变参数
在 JDK1.5 之后, 如果我们定义一个方法需要接受多个参数, 并且多个参数类型一致. 我们可以对其简化成如下格式:
修饰符 返回值类型 方法名(参数类型... 形参名){ }
其实这个书写完全等价与
修饰符 返回值类型 方法名(参数类型[] 形参名){ }
只是后面这种定义, 在调用时必须传递数组, 而前者可以直接传递数据即可.
同样是代表数组, 但是在调用这个带有可变参数的方法时. 不用创建数组 (这就是简单之处), 直接将数组中的元素作为实际参数进行传递. 编译成的 class 文件, 将这些元素封装到一个数组中, 在进行传递. 这些动作都是在编译 .class 文件时, 自动完成了.
代码演示:
public class Test {
public static void main(String[] args) {
int[] arr = { 1, 4, 62, 431, 2 };
int sum = getSum(arr);
System.out.println(sum);
int sum2 = getSum(6, 7, 2, 12, 2121);
System.out.println(sum2);
}
/*
* 完成数组所有元素的求和原始写法
public static int getSum(int[] arr){
int sum = 0;
for(int a : arr){
sum += a;
}
return sum;
}
*/
//可变参数写法
public static int getSum(int... arr) {
int sum = 0;
for (int a : arr) {
sum += a;
}
return sum;
}
}
输出结果:
500
2148
注: 上述 add 方法在同一个类中, 只能存在一个. 因为会发生调用的不确定性.
注意事项: 如果在方法书写时, 这个方法拥有多参数. 参数中包含可变参数, 则可变参数一定要写在参数列表的末尾位置.