Set接口与List接口最大的不同在于Set接口中的内容是不允许重复的。Set接口并没有对Collection接口进行扩充,而List对Collection进行了扩充。因此,在Set接口中没有get()方法。
在Set子接口中有两个常用子类:HashSet(无序存储)和TreeSet(有序存储)。
1.HashSet集合
HashSet是Set接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的。当向HashSet集合中添加一个元素时,首先会调用该元素的hashCode()方法来确定元素的存储位置,然后再调用该元素对象的equals()方法来确保该位置没有重复元素。
示例1:
import java.util.HashSet;
public class TestDemo {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("001");
set.add("002");
set.add("003");
set.add("003");
System.out.println(set);
}
}
运行结果:
解释:
HashSet集合之所以能确保不出现重复的元素,是因为它在存入元素是做了很多的工作。当调用HashSet集合的add()方法存入元素是时,首先调用当前存入元素的hashCode()方法获得对象的哈希值,然后根据对象的哈希值计算出一个存储位置;如果该位置上没有元素,则直接将元素存入;如果该位置上有元素存在,则会调用equals()方法让当前存入的元素以此与该位置上的元素进行比较。如果返回的结果为false就将该元素存入集合;返回为true则说明有重复元素,就将该元素舍弃。
示例2:
将开发者自定义的类型对象存入HashSet,结果如何呢?
import java.util.HashSet;
class Student {
String id;
String name;
public Student(String id, String name) {
this.id = id;
this.name = name;
}
public String toString() {
return id + ":" + name;
}
}
public class TestDemo {
public static void main(String[] args) {
HashSet hs = new HashSet();
Student stu1 = new Student("1", "Bob");
Student stu2 = new Student("2", "Lily");
Student stu3 = new Student("2", "Lily");
hs.add(stu1);
hs.add(stu2);
hs.add(stu3);
System.out.println(hs);
}
}
运行结果:
提问:运行结果出现了两个相同的学生信息“1:Lily”,这样的学生信息应该被视为重复元素,不允许同时出现在HashSet集合中?
之所以没有去掉重复元素是因为在定义Student类时没有重写hashCode()和equals()方法,因此创建的这两个学生对象stu2和stu3所引用的对象地址不同,所以HashSet集合会认为这是两个不同的对象。
示例3:
接下来对Studeng类进行改写,增加重写的hashCode()方法和equals()方法,假设id相同的学生就是同一个学生。
import java.util.HashSet;
class Student {
String id;
String name;
public Student(String id, String name) {
this.id = id;
this.name = name;
}
public String toString() {
return id + ":" + name;
}
// 重写hashCode方法
public int hashCode() {
return id.hashCode();// 返回id属性的哈希值
}
// 重写equals方法
public boolean equals(Object obj) {
if (this == obj) { // 判断是否是同一个对象
return true; // 如果是,直接返回true
}
if (!(obj instanceof Student)) { // 判断对象是否是Studeng类型
return false; // 如果对象不是Student类型,返回false
}
Student stu = (Student) obj; // 将对象强转为Student类型
boolean b = this.id.contentEquals(stu.id);// 判断id值是否相同
return b;// 返回判断结果
}
}
public class TestDemo {
public static void main(String[] args) {
HashSet hs = new HashSet();
Student stu1 = new Student("1", "Bob");
Student stu2 = new Student("2", "Lily");
Student stu3 = new Student("2", "Lily");
hs.add(stu1);
hs.add(stu2);
hs.add(stu3);
System.out.println(hs);
}
}
运行结果:
解释:Student类重写了Object类的hashCode()和equals()方法。在hashCode()方法中返回id属性的哈希值,在equals()方法中比较对象的id值是否相等,并返回结果。当调用HashSet集合的add()方法添加stu3对象时,发现它的哈希值与stu2对象相同,而且stu2.equals(stu3)返回true,HashSet集合认为两个对象相同,因此重复的Student对象被舍弃。
2.TreeSet集合
小试牛刀:(方法应用)
import java.util.TreeSet;
public class TestDemo {
public static void main(String[] args) {
// 创建Set集合
TreeSet ts = new TreeSet();
// 1.向TreeSet集合中添加元素
ts.add(3);
ts.add(9);
ts.add(2);
ts.add(20);
System.out.println("创建的TreeSet集合为:" + ts);
// 2.获取首尾元素
System.out.println("TreeSet集合首元素为:" + ts.first());
System.out.println("TreeSet集合尾元素为:" + ts.last());
// 3.比较并获取元素
System.out.println("集合中小于或等于9的最大的一个元素为:" + ts.floor(9));
System.out.println("集合中大于10的最小的一个元素为:" + ts.higher(10));
System.out.println("集合中大于100的最小的一个元素为:" + ts.higher(100));
// 删除元素
Object first = ts.pollFirst();
System.out.println("删除的第一个元素是:" + first);
System.out.println("删除第一个元素后TreeSet集合变为" + ts);
}
}
运行结果:
这些元素都能按照一定的顺序排列,原因是每次向TreeSet集合中存入一个元素时,就会将该元素与其它元素进行比较,最后将它插入到有序的对象序列中。
集合中的元素在进行比较时,都会调用CompareTo()方法,该方法是Compararble接口中定义的,因此要想对集合中的元素进行排序,就必须实现Comparable接口。Java中大部分的类都实现了Comparable接口,并默认实现了接口中的CompareTo()方法,如:Integer、Double、String。
在实际开发中,除了会向TreeSet集合中存储一些Java中默认的类型数据外,还会存储一些用户自定义的类型数据,如Student类型数据等。由于这些自定义类型的数据没有实现Comparable接口,因此也就无法直接在TreeSet集合中进行排序操作。为了解决这个问题,Java提供了两种TreeSet排序规则,分别为:自然排序和定制排序。
2.1自然排序
自然排序要求向TreeSet集合中存储的元素所在类必须实现Comparable接口,并重写compareTo()方法,然后TreeSet集合就会对该类型元素使用compareTo()方法进行比较,并默认进行升序排序。
import java.util.TreeSet;
class Teacher implements Comparable {
String name;
int age;
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + ":" + age;
}
// 重写CompareTo接口的compareTo()方法
public int compareTo(Object obj) {
Teacher s = (Teacher) obj;
// 定义比较方式,先比较年龄age,再比较name
if (this.age - s.age > 0) {
return 1;
}
if (this.age - s.age == 0) {
return this.name.compareTo(s.name);
}
return -1;
}
}
public class TestDemo {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(new Teacher("Bob", 19));
ts.add(new Teacher("Lily", 18));
ts.add(new Teacher("Tom", 19));
ts.add(new Teacher("Lily", 18));
System.out.println(ts);
}
}
运行结果:
解释:Teacher类实现了Comparable接口,并重写了compareTo()方法。在coppareTo()方法中,首先针对age值进行比较,根据比较结果返回-1和1,当age相同时,再对name进行比较。TreeSet集合会将重复的元素去掉。
2.2定制排序
有时候,用户自定义的类型数据所在的类没有实现Comparable接口或者对于实现了Comparable接口的类不想按照定义的compareTo()方法进行排序。例如希望存储在TreeSet集合中的字符串可以按照长度而不是英文字母的顺序来进行排序,这时,可以通过在创建TreeSet集合时就自定义一个比较器来对元素进行定制排序。
import java.util.Comparator;
import java.util.TreeSet;
class MyComparator implements Comparator {
public int compare(Object obj1, Object obj2) {// 定制排序方式
String s1 = (String) obj1;
String s2 = (String) obj2;
int temp = s1.length() - s2.length();
return temp;
}
}
public class TestDemo {
public static void main(String[] args) {
// 1.创建集合时,传入Comparator接口实现定制排序规则
TreeSet ts = new TreeSet(new MyComparator());
ts.add("Bob");
ts.add("Lily");
ts.add("Tomee");
System.out.println(ts);
// 2.创建集合时,使用Lambda表达式定制排序规则
TreeSet ts2 = new TreeSet((obj1, obj2) -> {
String s1 = (String) obj1;
String s2 = (String) obj2;
return s1.length() - s2.length();
});
ts2.add("Bob");
ts2.add("Lily");
ts2.add("Tomee");
System.out.println(ts2);
}
}
运行结果:
解释:
上例中,使用TreeSet集合的public TreeSet(Comparator<? super E>comparator)有参构造方法,分别传入Comparable接口实现MyComparator以及Lambda表达式两种参数方式创建了定制排序规则的TreeSet集合,当向集合中添加元素时,TreeSet集合就会按照定制的排序规则进行比较,从而使存入TreeSet集合中的字符串按照长度进行排序。
注意:在使用TreeSet集合存储数据时,TreeSet集合会对存入元素进行比较排序,所以为了保证程序的正常运行,一定要保证存入TreeSet集合中的元素是同一种数据类型。
3.重复元素判断(hashCode与equals方法)
在使用TreeSet子类进行数据保存的时候,重复元素的判断依靠的Comparable接口完成的。但是这并不是全部Set接口判断重复元素的方式,因为如果使用的是HashSet子类,由于其跟Comparable没有任何关系,所以它判断重复元素的方式依靠的是Object类中的两个方法:
public native int hashCode();
public boolean equals(Object obj);
equals()的作用是用来判断两个对象是否相等,在Object里面的定义是:
public boolean equals(Object obj){
return (thisobj);
}
**这说明在我们实现自己的equals方法之前,equals等于,而==运算符是判断两个对象是不是同一个对象,即它们的地址是否相等。而覆写equals更多的是追求两个对象在逻辑上的相等,咱可以说是值相等,也可以说是内容相等。**
hashCode用于返回对象的hash值,主要用于查找的快捷性,因为hashCode也是在Object对象中就有的,所以所有Java对象都有hashCode,在Hashtable和HashMap这一类的散列结构中,都是通过hashCode来查找在散列表中的位置的。
如果两个对象equals,那么他们的hashCode必然相等,但是hashCode相等,equals不一定相等。对象判断必须两个方法返回值都相等才判断为相同。
个人建议:
1.保存自定义对象的时候使用List接口;
2.保存系统类信息的时候使用Set接口。