JavaSE——Day17——Set、HashSet、LinkedHashSet、TreeSet详解

本文详细介绍了Java中Set集合的实现类,包括HashSet、LinkedHashSet和TreeSet。解释了它们的特性和使用场景,如元素的唯一性、排序以及如何实现自定义对象的存储。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Set集合概述

Set继承于Collection接口,但是Set接口并不像List接口那样对Collection接口进行了大量的扩充,而是简单的继承了Collection接口。也就是说,Set里面并没有提供使用get()方法根据索引取得保存数据的操作。Set主要的实现类有:

  • HashSet——散列存放数据
  • LinkedHashSet
  • TreeSet——有序存放数据

在判断重复元素的时候,Set集合会调用hashCode()和equal()方法来实现。

HashSet的概述及特点

此类实现 Set 接口,由哈希表(实际上是一个 Has
hMap 实例)支持。 它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。 此类允许使用 null 元素。

  • 不能保证元素的排列顺序,顺序有可能发生变化
  • 不是同步的
  • 集合元素可以是null,但只能放入一个null

构造方法

HashSet()
构造一个新的空集合; 背景HashMap实例具有默认初始容量(16)和负载因子(0.75)。

HashSet(Collection<? extends E> c)
构造一个包含指定集合中的元素的新集合。

HashSet(int initialCapacity)
构造一个新的空集合; 背景HashMap实例具有指定的初始容量和默认负载因子(0.75)。

HashSet(int initialCapacity, float loadFactor)
构造一个新的空集合; 背景HashMap实例具有指定的初始容量和指定的负载因子。

  • 初始容量 就是创建时默认的容量
  • 加载因子 是对其容量自动增加之前可以达到多满的一个尺度。

也就是说,HashSet()默认实例化时的初始容量是16,当元素达到16*0.75=12时,它就会自动增加。

HashSet元素的唯一性与无序性

  • HashSet 底层数据结构是哈希表元素无序,且唯一
import java.util.HashSet;

public class TestDefaultLength {
    public static void main(String[] args) {        
        //演示hashset的唯一性和无序性
        HashSet<String> hashSet = new HashSet<>();
        //初始容量是16,加载因子是0.75
        hashSet.add("张三");
        hashSet.add("张三");
        hashSet.add("李四");
        hashSet.add("李四");
        hashSet.add("王五");
        hashSet.add("王五");
        for (String s : hashSet) {
            System.out.println(s);
        }  
              
    }
}
// 运行后在控制台输出
3
李四
张三
王五
// 分析:
// 1. 唯一性:我们存了6个元素,有一半的重复元素,但是真正存进去的只有三个元素
// 2. 无序性:遍历这三个元素,显示它是随机输出的,既没有先进先出,也没有先进后出
// 3. 我们存入的类型都是String类型,之所以保证了唯一性是因为String类重写了hashCode()和equals()方法。
  • 哈希表HashTable:
    一个元素为链表的数组,综合了数组和链表的优点 (像新华字典一样) (JDK1.7之前)(JDK1.8 数组+链表+红黑树)

  • 当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值决定该对象在 HashSet 中的存储位置。

  • HashSet 集合判断两个元素相等的标准:

    1. 取得哈希码,调hashCode()方法,先判断对象的hash码是否相同,依靠hash码取得一个对象的内容。
    2. 再调用equals()方法,将对象的属性依次比较。
  • hash算法:一般称为散列,无序。利用二进制的计算结果来设置保存的空间,计算结果不同,最终保存空间的位置也不同,用hash算法保存的集合都是无序的,但是查找速度快。

  • 结论:
    HashSet 保证元素唯一性是靠元素重写hashCode()和equals()方法来保证的,如果不重写则无法保证。上面我们举的例子,往hashSet里添加的元素都是String类型,而String类型重写了hashCode()和equals()方法,所以保证了元素的唯一性。

HashSet存储自定义对象保证元素唯一性

按照上面的思路,如果我们要在hashSet里面存放自定义对象同时保证唯一性,那么我们也要重写hashCode()和equals()方法这两个方法。这两个方法你可以自己来写,要尽可能的保证能够判断出两个对象是否相同。一般我们利用工具直接生成。

  • 举例:
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 String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    
    // 重写hashCode和equals方法保证用HashSet存储该类对象的唯一性
    @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);
    }
}


import java.util.HashSet;

public class Test {
    public static void main(String[] args) {
        HashSet<Student> students = new HashSet();
        students.add(new Student("张三", 19));
        students.add(new Student("张三", 19));
        students.add(new Student("李四", 19));
        students.add(new Student("李四", 19));
        students.add(new Student("王五", 19));
        students.add(new Student("王五", 19));
        for (Student student : students) {
            System.out.println(student.toString());
        }

    }
}
// 运行结果:
Student{name='王五',age=19}
Student{name='张三',age=19}
Student{name='李四',age=19}

HashSet几种用法

来看几种HashSet几种比较有意思的用法:

去重
  • HashSet(Collection<? extends E> c)
    构造一个包含指定集合中的元素的新集合。

HashSet的有参构造要求传入一个集合类对象,那么HashSet就会创造一个新集合,我们已经知道了HashSet存储元素的特点是无序且唯一,那么就可以用这个特点来去重。

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;

/**
 * @Author: 等一次另眼相看
 * @CreateTime: 2019-03-27 18:56
 * @Description: TODO
 */
public class AnotherConstrustor {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("一一一");
        list.add("二二二");
        list.add("三三三");
        list.add("一一一");
        list.add("二二二");
        list.add("三三三");
        HashSet<String> hashSet = new HashSet<>(list);
        //遍历输出一下
        Iterator iterator = hashSet.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

获取重复元素的索引

比如说我现在有一个字符串"abc?123#$xyz%acz",我要得到所有重复字符的索引,就可以new一个HashSet对象,利用add()方法的返回值作为判断条件来获取重复字符的索引:

import java.util.HashSet;

public class UseConstructor {
    public static void main(String[] args) {
        char[] chars = "abc?123#$xyz%acz".toCharArray();
        String indexOfRepeat = getIndexOfRepeat(chars);
        System.out.println(indexOfRepeat);
    }

    public static String getIndexOfRepeat(char[] chars) {
    	// 新建一个HashSet来保存字符
        HashSet<Character> hashSet = new HashSet<>();
        String result = "查询结果:";
        if (chars != null) {
        	// 遍历字符数组,添加到hashSet
            for (int i = 0; i < chars.length; i++) {
            	// hashSet不会把重复字符保存进去
            	//如果add方法返回false,就说明是重复元素
                if (!hashSet.add(chars[i])) {
                    result += "\n重复的元素是:" + chars[i] + "\t索引是:" + i;
                }
            }
        } else {
            result = "字符数组为null";
        }
        return result;
    }
}

//程序执行结果:
查询结果:
重复的元素是:a	索引是:13
重复的元素是:c	索引是:14
重复的元素是:z	索引是:15

LinkedHashSet的概述和特点

LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。

  • LinkedHashSet 是 Set 的一个具体实现,其维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
  • LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。
  • 如果我们需要迭代的顺序为插入顺序或者访问顺序,那么 LinkedHashSet 是需要你首先考虑的。

构造方法

与HashSet类似:

LinkedHashSet()
构造一个具有默认初始容量(16)和负载因子(0.75)的新的,空的链接散列集。

LinkedHashSet(Collection<? extends E> c)
构造与指定集合相同的元素的新的链接散列集。

LinkedHashSet(int initialCapacity)
构造一个具有指定初始容量和默认负载因子(0.75)的新的,空的链接散列集。

LinkedHashSet(int initialCapacity, float loadFactor)
构造具有指定的初始容量和负载因子的新的,空的链接散列集。

LinkedHashSet的特点演示


import java.util.Iterator;
import java.util.LinkedHashSet;

public class LinkedHashSetDemo {
    public static void main(String[] args) {
        LinkedHashSet<Integer> linkedHashSet = new LinkedHashSet<>();
        linkedHashSet.add(1);
        linkedHashSet.add(1);
        linkedHashSet.add(2);
        linkedHashSet.add(2);
        linkedHashSet.add(3);
        linkedHashSet.add(3);
        Iterator<Integer> iterator = linkedHashSet.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
// 程序运行结果:
1
2
3
// 分析:
// 可见LinkedHashSet保存元素的特点是:元素有序 , 并且唯一

TreeSet的概述和特点

TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序比较器排序,其中自然排序为默认的排序方式,即按照字母的升序排列数据。

  1. 自然排序(元素具备比较性)。
    根据元素的自然顺序对元素进行排序 。自然排序需要实现comparable接口中的compareTo()方法,在compareTo方法中定义规则
  2. 构造器排序(集合具备比较性)。
    根据TreeSet的构造方法接收一个比较器接口的子类对象Comparator。

具体情况取决于使用的构造方法。无参就是自然排序,否则就是比较器排序 :

public TreeSet():
构造一个新的空 set,该 set 根据其元素的自然顺序进行排序
public TreeSet(Comparator<? super E> comparator):
构造一个新的空 TreeSet,它根据指定比较器进行排序。

TreeSet保证元素唯一和自然排序的原理


import java.util.Iterator;
import java.util.TreeSet;

public class Demo {
    public static void main(String[] args) {
        TreeSet<Integer> treeSet = new TreeSet<>();
        // 我们给treeSet里面添加数据:乱序且重复
        treeSet.add(5);
        treeSet.add(5);
        treeSet.add(6);
        treeSet.add(2);
        treeSet.add(7);
        treeSet.add(4);
        treeSet.add(2);
        treeSet.add(3);
        treeSet.add(1);
        //treeSet------自然排序,元素唯一
        Iterator<Integer> iterator = treeSet.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
// 程序运行结果:
1
2
3
4
5
6
7

那么TreeSet是怎样对元素进行排序并且保证元素唯一的呢?

  • TreeSet判断两个对象是否相同的方式:
    通过CompareTo()方法比较是否返回0

Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。
obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是 负数,则表明obj1小于obj2。

这里我们需要了解红黑树这种数据结构:

初步了解红黑树:
https://blog.youkuaiyun.com/v_JULY_v/article/details/6105630

  1. 每个结点要么是红的要么是黑的。
  2. 根结点是黑的。
  3. 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
  4. 如果一个结点是红的,那么它的两个儿子都是黑的。
  5. 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。

关于TreeSet存储元素的具体过程:
https://blog.youkuaiyun.com/zhaojie181711/article/details/80494318

TreeSet的添加方法实现了map的put方法,其中key为我们添加的变量值,value为定值Object对象。
第一次在TreeSet中添加值的时候,元素对比的是key自身。
在这里插入图片描述
然后直接新添加一条数据到Entry<k,v>中,并记录size = 1,且记录对树进行结构修改的次数++。
后面再添加数据会依次跟结点比较(依据compareTo()的返回值:< 0 | == 0 | > 1),比结点大的放在结点的右边,比结点小的放在结点的左边。
迭代读取元素的顺序是:左-中-右(相对结点而言)。

在这里插入图片描述

TreeSet存储自定义对象

  1. 自然排序(字典顺序)

1)TreeSet类的add()方法中会把存入的对象提升为Comparable类型
2)调用对象的compareTo()方法和集合已有对象比较
3)根据compareTo()方法返回的结果进行存储

  1. 比较器排序

1)创建TreeSet的时候可以指定一个comparator(比较器)
2)如果传入了Comparator的子类对象,那么TreeSet就会按照比较器中的顺序排序
3)add()方法内部会自动调用Comparator接口中compare()方法来排序

  1. 二者的区别:

1)TreeSet构造函数什么都不传,默认按照Comparable的顺序
2)TreeSet如果传入Comparator,就优先按照比较器排序

  1. TreeSet的两种构造方法
public TreeSet():
构造一个新的空 set,该 set 根据其元素的自然顺序进行排序
public TreeSet(Comparator<? super E> comparator):
构造一个新的空 TreeSet,它根据指定比较器进行排序。

自然排序

TreeSet保存的子类可以进行排序,但是其排序是依靠比较器接口(Comparable)实现的,如果要利用TreeSet子类保存自定义类的对象,那么这个对象所属的类必须要实现java.lang.Coparable接口。


public class Book implements Comparable<Book> {
    private String title;
    private double price;

    public Book() {
    }

    public Book(String title, double price) {
        this.title = title;
        this.price = price;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Book{" +
                "title='" + title + '\'' +
                ", price=" + price +
                '}';
    }

    // 排序方法:比较所有属性
    // 先比较价格,价格相等再去比较书名(调用String类的compareTo()方法)
    @Override
    public int compareTo(Book o) {
        if (this.price > o.price) {
            return 1;
        } else if (this.price < o.price) {
            return -1;
        } else {
            return this.title.compareTo(o.title);
        }
    }


}


import java.util.TreeSet;

public class Test {
    public static void main(String[] args) {
        TreeSet<Book> books = new TreeSet<>();
        books.add(new Book("西游记",80));
        books.add(new Book("西游记", 80));
        books.add(new Book("三国演义", 75));
        books.add(new Book("红楼梦", 90));
        books.add(new Book("水浒传", 69));
        System.out.println(books);
    }
}

// 程序运行结果:
[Book{title='水浒传', price=69.0}, 
Book{title='三国演义', price=75.0}, 
Book{title='西游记', price=80.0}, 
Book{title='红楼梦', price=90.0}]

在这个案例中我们首先利用TreeSet子类保存了若干个Book对象,由于Book类实现了Comparable接口,所以会自动将所有保存的Book类对象强制转换成Comparable接口对象,然后调用comparTo()方法进行排序,如果发现比较结果为0则认为是重复元素,将不再进行保存。所以TreeSet数据排序以及重复元素的消除依靠的是Comparable接口。

  • 注意

在TreeSet子类中,由于其不允许保存重复元素(comparTo()方法返回0),如果说你自定义的对象中有5个元素,那么在重写必须comparTo()方法时必须将所有属性进行比较,否则TreeSet会错误的将某些数据认为是重复元素,从而造成数据丢失的情况。

比较器排序

  1. 构建一个比较器(要实现 Comparator 接口):

import java.util.Comparator;

public class sortByTitle implements Comparator<Book> {

    @Override
    public int compare(Book o1, Book o2) {
        // 先按照书名的长度来排序
        // 如果书名的长度一样,就去比较书名是否相同
        // 如果书名长度和书名都一样,就按照价格排序
        int num = o1.getTitle().length() - o2.getTitle().length();
        int num2 = num == 0 ? o1.getTitle().compareTo(o2.getTitle()): num;
        int num3 = num2 == 0 ? (int) o1.getPrice() - (int) o2.getPrice() : num2;
        return num3;
    }
}

  1. TreeSet的有参构造,用我们新建的比较器来排序:

import java.util.TreeSet;

public class Test2 {

    public static void main(String[] args) {
        TreeSet<Book> books = new TreeSet<Book>(new sortByTitle());
        books.add(new Book("西游记", 80));
        books.add(new Book("西游记", 90));
        books.add(new Book("三国演义", 75));
        books.add(new Book("红楼梦", 80));
        books.add(new Book("水浒传", 69));
        books.add(new Book("文化苦旅", 80));
        System.out.println(books);
    }
}
// 程序运行结果:
[Book{title='水浒传', price=69.0},
 Book{title='红楼梦', price=80.0},
 Book{title='西游记', price=80.0},
 Book{title='西游记', price=90.0}, 
 Book{title='三国演义', price=75.0},
 Book{title='文化苦旅', price=80.0}]

案例

案例演示: 需求:键盘录入3个学生信息(姓名,语文成绩,数学成绩,英语成绩),按照总分从高到低输出到控制台。

  1. 先创建学生类
public class Student {
    private String name;
    private int chineseScore;
    private int mathScore;
    private int englishScore;

    public Student() {
    }

    public Student(String name, int chineseScore, int mathScore, int englishScore) {
        this.name = name;
        this.chineseScore = chineseScore;
        this.mathScore = mathScore;
        this.englishScore = englishScore;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getChineseScore() {
        return chineseScore;
    }

    public void setChineseScore(int chineseScore) {
        this.chineseScore = chineseScore;
    }

    public int getMathScore() {
        return mathScore;
    }

    public void setMathScore(int mathScore) {
        this.mathScore = mathScore;
    }

    public int getEnglishScore() {
        return englishScore;
    }

    public void setEnglishScore(int englishScore) {
        this.englishScore = englishScore;
    }

    public int getTotalScore() {
        return englishScore + mathScore + chineseScore;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", chineseScore=" + chineseScore +
                ", mathScore=" + mathScore +
                ", englishScore=" + englishScore +
                '}';
    }
}



import java.util.Comparator;
import java.util.Iterator;
import java.util.Scanner;
import java.util.TreeSet;

publi class SortGrade {
    public static void main(String[] args) {
        TreeSet<Student> students = new TreeSet<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                // 默认按照总分来排序(加-降序),如果总分一样,就去比较姓名从而确保录入数据的唯一性
                int num = -(o1.getTotalScore() - o2.getTotalScore());
                int num2 = num == 0 ? o1.getName().compareTo(o2.getName()) : num;
                return num2;
            }
        });

        Scanner sc = new Scanner(System.in);
        for (int i = 1; i < 4; i++) {
            System.out.println("请输入第" + i + "个学生的姓名:");
            String name = sc.next();
            System.out.println("请输入第" + i + "个学生的语文成绩:");
            int cS = sc.nextInt();
            System.out.println("请输入第" + i + "个学生的数学成绩:");
            int mS = sc.nextInt();
            System.out.println("请输入第" + i + "个学生的英语成绩:");
            int eS = sc.nextInt();
            Student student = new Student(name, cS, mS, eS);
            students.add(student);
        }

        System.out.println("按照总分来排序:");
        Iterator<Student> iterator = students.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }

    }
}

程序运行结果:

请输入第1个学生的姓名:
张三
请输入第1个学生的语文成绩:
100
请输入第1个学生的数学成绩:
100
请输入第1个学生的英语成绩:
100
请输入第2个学生的姓名:
李四
请输入第2个学生的语文成绩:
95
请输入第2个学生的数学成绩:
86
请输入第2个学生的英语成绩:
69
请输入第3个学生的姓名:
王五
请输入第3个学生的语文成绩:
100
请输入第3个学生的数学成绩:
94
请输入第3个学生的英语成绩:
79
按照总分来排序:
Student{name='张三', chineseScore=100, mathScore=100, englishScore=100}
Student{name='王五', chineseScore=100, mathScore=94, englishScore=79}
Student{name='李四', chineseScore=95, mathScore=86, englishScore=69}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值