文章目录
一、Set 接口
Java 用 Set 接口来描述集合(集合不允许有重复值)。
Set 不保存重复的元素。Set 中最常被使用的是测试归属性,可以很容易地查询某个对象是否存在于某个 Set 中。因此,Set 最重要的操作就是查找。通常选择 Set 的 HashSet 实现,因为 HashSet 对快速查找进行了优化。
Set 接口继承自 Collection 接口,实际上是与 Collection 完全一样的接口,只是行为不同。因此,Set 并没有额外的功能。
Set 中,元素是无序的且不可重复。除此之外,Set 与 List 大致相同,方法基本一致。
二、Set 的分类
-
HashSet(散列集)——使用散列函数存储元素,因此元素是无序的,但查找速度快;
-
TreeSet(树集)——将元素存储在红黑树数据结构中,并且能够以某种规则对元素进行排序;
-
LinkedHashSet——也使用散列函数存储元素保证较快的查找速度,但内部使用链表维护元素的顺序(插入的次序)。
三、HashSet 测试
1、集合中的有序、无序以及排序的概念区分
Java 集合中,有序、无序指的是在进行插入操作时,插入位置的顺序性。
先插入的位置在前,后插入的位置在后,则为有序,反之为无序。也就是说,插入数据时和取出数据时的顺序是一样的,即为有序。
有序和排序容易混淆,排序是指集合内的元素是按照升序或降序来排列的。
2、关于 HashSet 无序的测试
代码一:
public static void main(String[] args) {
Set set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);
System.out.println(set);
}
测试结果:
[1, 2, 3]
代码二:
public static void main(String[] args) {
Set set = new HashSet<>();
set.add(9);
set.add(5);
set.add(558);
set.add(7);
System.out.println(set);
}
测试结果:
[5, 7, 9, 558]
对比上面两段代码,代码一是有序的(巧合),代码二是无序的,并且两个结果都是排好序的结果。
解释: 一般来说,HashSet 是无序的,它既不能保证存储和取出的顺序一致,也不能保证自然顺序(排序)。
分析:
上面两段代码中,所创建的 HashSet 是 Integer 类型的,这也是最巧的一点。因为 HashSet 在存储元素的时候,将元素用 hashCode() 运算后得到自己在数组中的位置,而 Integer 类型 hashCode() 的返回值就是其 int 值本身,所以在这一步其实已经实现了排序功能。
这是因为,JDK7 到 JDK8,其内部发生了一些变化,不同版本的 JDK 下运行结果不同。按照 JDK8 的 hash 算法,数据在经过 HashMap.hash() 运算后仍然是自己本身的值,且没有发生哈希冲突。
对于有序、无序,是说元素存入集合的顺序。若元素存储顺序和取出顺序一致,就是有序,否则就是无序。因此,在使用 for 循环添加元素时,无论是随机添加还是有序添加,遍历的结果都是按照值的大小排序输出。
代码二中随机添加元素,存储和取出的顺序不一样,所以我们说 HashSet 是无序的。代码一的情况只是一种巧合。
所以说,HashSet 只是不保证有序,并不是保证无序。
3、HashSet 存储自定义类
HashSet 存储 int、String 等基本类型的数据时,使用默认的 hashCode() 和 equals() 方法对数据进行计算后存储。而存储自定义类时,需要重写 hashCode() 和 equals() 方法,用自定义的规则去除掉重复的元素。示例如下:
// 球员类
class Player{
String name;// 姓名
int number;// 号码
Player(String name, int number){
this.name = name;
this.number = number;
}
@Override
// 重写hashCode()
public int hashCode(){
return name.hashCode() + number;
}
@Override
// 重写equals()
public boolean equals(Object obj){
// 若equals()返回true,则表示插入元素的内容与集合中某元素相同,就不再插入这个元素了
return this.name.equals(((Player)obj).name) &&
this.number == ((Player)obj).number;
}
}
public class SetTest {
public static void main(String[] args) {
Set<Player> set = new HashSet<>();
set.add(new Player("kobe", 24));
set.add(new Player("curry", 30));
set.add(new Player("jordan", 23));
set.add(new Player("kobe", 24));
Iterator<Player> it = set.iterator();
while(it.hasNext()){
Player p = (Player)it.next();
System.out.println(p.name + ", " + p.hashCode() + ", " + p.number);
}
}
}
测试结果:
jordan, -1154271045, 23
kobe, 3297471, 24
curry, 95027365, 30
四、TreeSet 测试
- TreeSet 虽然是有序的,但是并没有具体的索引,所以在插入数据时,TreeSet 中原有的数据可能需要重新排序,导致 TreeSet 插入和删除的效率低;
- 并且由于没有具体的索引,TreeSet 没有 get() 方法来获取指定位置的元素( HashSet 也没有),所以它们只能通过迭代器来获取指定位置的元素;
- TreeSet 是一个有序集合,其元素按照升序排列,默认是按照自然顺序排列,也就是说 TreeSet 中的对象元素需要实现 Comparable 接口来实现自比较功能;
- 使用 TreeSet 存储自定义类的对象时,自定义类要实现 Comparable 接口并重写 compareTo 方法,以提供比对形式,TreeSet 则会根据自定义的比对方式对元素进行排序。
1、TreeSet 存储 Java 已有的类——自然排序
存储 Java 已有的类对象时,如 Integer、String 等,使用默认的自然排序方式。代码如下:
Set<Integer> set = new TreeSet<>();
set.add(5);
set.add(2);
set.add(78);
set.add(6);
for(Integer i : set){
System.out.print(i + " ");
}
测试结果:
2 5 6 78
2、TreeSet 存储自定义类——使用 Comparable 接口
TreeSet 存储自定义类时,自定义类要实现 Comparable 接口并重写 compareTo() 方法,在 compareTo() 方法中定义比较规则,决定排序的方式,这种排序方式称为内排序。测试如下:
/*
* 学生类,姓名和成绩两个属性;
* 重写hashcode()方法和equals()方法,去除重复元素;
* 实现Comparable接口,重写compareTo()方法,定义排序规则。
*/
class Student implements Comparable<Student>{
private String name;
private int score;
public Student(){ super();}
public Student(String name, int score){
super();
this.name = name;
this.score = score;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getScore() { return score; }
public void setScore(int score) { this.score = score; }
@Override
// 重写hashCode()
public int hashCode(){
return name.hashCode();
}
@Override
// 重写equals()
public boolean equals(Object obj){
// 若equals()返回true,则表示插入元素的内容与集合中某元素相同,就不再插入这个元素了
return this.name.equals(((Student)obj).name) &&
this.score == ((Student)obj).score;
}
public String toString(){
return "[" + name + ", " + score + "]";
}
@Override
public int compareTo(Student o) {
// 利用String类的compareTo()方法进行比较
// 用成绩进行排序
return String.valueOf(this.score).compareTo(String.valueOf(o.score));
}
}
public class TreeSetTest1 {
public static void main(String[] args) {
TreeSet<Student> set = new TreeSet<>();
set.add(new Student("张三", 98));
set.add(new Student("李四", 95));
set.add(new Student("王麻子", 97));
set.add(new Student("王麻子", 97));
System.out.println(set);
}
}
测试结果:
[[李四, 95], [王麻子, 97], [张三, 98]]
由结果看出,TreeSet 实现了自定义的按成绩排序的功能,并且在添加元素过程中滤除了重复元素。
3、TreeSet 存储自定义类——使用 Comparator 接口
TreeSet 存储自定义类时,也可以自定义一个外比较器,实现 Comparator 接口并重写 compare() 方法,这种排序方式称为外排序。测试如下:
// 学生类,重写hashcode()方法和equals()方法,去除重复元素
class Student{
private String name;
private int score;
public Student(){ super();}
public Student(String name, int score){
super();
this.name = name;
this.score = score;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getScore() { return score; }
public void setScore(int score) { this.score = score; }
@Override
// 重写hashCode()
public int hashCode(){
return name.hashCode();
}
@Override
// 重写equals()
public boolean equals(Object obj){
// 若equals()返回true,则表示插入元素的内容与集合中某元素相同,就不再插入这个元素了
return this.name.equals(((Student)obj).name) &&
this.score == ((Student)obj).score;
}
public String toString(){
return "[" + name + ", " + score + "]";
}
}
// 自定义比较器,定义排序规则
class Student_Comparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
// 先比较成绩,如果成绩相同,再比较姓名
if(o1.getScore() == o2.getScore()){
// 调用String类的compareTo()方法
return o1.getName().compareTo(o2.getName());
}else{
return o1.getScore() - o2.getScore();
}
}
}
public class TreeSetTest2 {
public static void main(String[] args) {
// 创建Set时,要将比较器传给容器对象
TreeSet<Student> set = new TreeSet<>(new Student_Comparator());
set.add(new Student("d", 98));
set.add(new Student("b", 95));
set.add(new Student("e", 97));
set.add(new Student("e", 97));
set.add(new Student("f", 97));
System.out.println(set);
}
}
测试结果:
[[b, 95], [e, 97], [f, 97], [d, 98]]
由结果看出,TreeSet 实现了自定义的先成绩再姓名的排序功能,并且在添加元素过程中滤除了重复元素。
五、Set 的遍历
Set 没有 get() 方法,不能用 for 循环。
方式一:foreach 循环
Set<Integer> set = new TreeSet<>();
set.add(5);
set.add(2);
set.add(78);
set.add(6);
for(Integer i : set){
System.out.print(i + " ");
}
方式二:迭代器
上面的用 HashSet 存储自定义的 Player 类对象,就是使用迭代器进行遍历。如下:
Set<Player> set = new HashSet<>();
set.add(new Player("kobe", 24));
set.add(new Player("curry", 30));
set.add(new Player("jordan", 23));
set.add(new Player("kobe", 24));
Iterator<Player> it = set.iterator();
while(it.hasNext()){
Player p = (Player)it.next();
System.out.println(p.name + ", " + p.hashCode() + ", " + p.number);
}