Set集合保证元素不重复的原理&TreeSet集合与排序

Set集合保证元素不重复的原理&Java中的排序接口

  • 阅读本文您可以了解到以下内容:
    • 常用集合的继承体系&常用Set体系集合的特点;
    • Set体系集合保证元素唯一性的原因;
    • 重写hashCode()方法的原因和原则; 重写equals()方法的原因和原则;
    • TreeSet集合如何对元素进行排序;
    • 自然排序,比较器排序,Comparable接口,Comparator接口;

1. Set体系集合的继承关系和特点

在这里插入图片描述

  • Set体系常用集合的特点
    • HashSet 的特点:无序、不可重复;
      • LinkedHashSet 的特点:有序、不可重复;
    • TreeSet 的特点:能够使元素按照某种规则排序、不可重复;
  • Set体系常用集合的数据结构
    • HashSet 的底层数据结构:哈希表。增删改都快,哈希表能保证元素的唯一性。
      • LinkedHashSet 的底层数据结构:哈希表+链表。哈希表保证元素唯一,链表保证元素有序。
    • TreeSet 的底层数据结构:红黑树。该数据结构能保证元素的唯一和排序。

2. HashSet如何保证元素的唯一性

2.1 HashSet集合的简介和HashSet#add()方法

  • HashSet特点
    • 元素的存入顺序和取出顺序不同
    • 不能存储重复元素
      • 多次调用add()方法添加同样的数据, 除第一次外都会添加失败。
        因此保证元素唯一性的源码肯定在HashSet#add()方法中。
      • HashSet底层数据结构是哈希表, 哈希表能保证元素的唯一性。
  • HashSet#add()方法的源码的简要分析
//添加相同元素会失败,体现出储存元素唯一性,应该看HashSet#add()方法源码了解为何添加相同元素将失败
interface Collection {
    //...
}

interface Set extends Collection {
    //...
}

class HashSet implements Set {
    private static final Object PRESENT = new Object();
    private transient HashMap<E, Object> map;

    //由HashSet集合的构造可知,HashSet的底层是HashMap
    public HashSet() {
        map = new HashMap<>();
    }

    public boolean add(E e) {
        //可见HashSet#add()方法底层调用了HashMap#put()方法,参数传入了一个E和一个Object类型的静态常量PRESENT
        return map.put(e, PRESENT) == null;
    }
}

class HashMap implements Map {
    public V put(K key, V value) {
        hashCode();
        equals();
    }
}
/*
小结:
HashSet的底层是HashMap,HashSet调用add()方法实际上底层是调用HashMap的put()方法。
put()方法的底层依赖hashCode()方法和equals()方法来保证添加元素的唯一性。

Object#hashCode():返回对象的哈希码值,对象的地址值经过计算得到的值。

Java泛型中的E,就是Element的意思,一般代表放入集合中的元素。
*/
  • HashSet#add()方法保证添加元素唯一性的细节
    1. 用add()方法欲添加一个元素,先用hashCode()方法算出该元素的哈希值,和哈希表中的哈希值进行比较。
    2. 如果哈希表中无此哈希值,那么说明该元素没被添加过,就添加这个元素。
    3. 如果哈希表中有该哈希值,那么比较欲添加元素和同哈希值元素的地址值并使用equals()方法比较欲添加元素和同哈希值元素的内容是否相同。如果地址值或equals()其中之一不同,就添加这个元素。
  • 小结
    • 使用HashSet#add()方法添加元素,先比较哈希值,哈希值不同,添加该元素;哈希值相同,比较地址值或equals(),其中之一不同就添加该元素。
  • 辅助记忆上述内容
    • 哈希表就是字典,哈希值就是页码,欲添加的元素就是字典上的字。
      字的页码是根据字和页码算法计算出来的(元素的哈希值是根据元素的地址值和哈希算法计算出来的)。
    • 如果一个字(元素)的页码(哈希值)在字典(哈希表)中找不到,说明这个字(元素)没有被添加过,那么就添加这个元素。
    • 如果一个字(元素)的页码(哈希值)能在字典(哈希表)中找到,但是该页码(哈希值)下有很多字(元素) (对应不同java对象可能有相同的hashCode),如果该字(元素)和该页码(哈希值)下所有字(元素)的位置(地址值)和笔画顺序(内容)其中之一不同, 说明这个字(元素)没被添加过, 那么就添加这个元素。
  • 综上所属,如果想让HashSet#add()保证添加元素的唯一性, 必须重写集合中元素对应类的hashCode()和equals()。

2.2 重写hashCode()方法的原则

  • hashCode()方法是Object类中的方法。
    该方法的作用是:返回对象的哈希码值。
    该方法的源码是:public native int hashCode();
    native 关键字代表该方法底层是用c/c++实现的。
  • 重写该方法的原则:使成员变量值相同的对象的哈希值相同。
  • 重写该方法的细节
    • 将对象的成员变量值相加得出一个值A即可, 如此一来, 值A相同的对象就是成员变量值相同的对象, 就视为相同的对象;
    • 如果成员变量是引用类型, 就加哈希值; 如果成员变量是基本类型, 就加变量值;
  • 重写该方法的示例
// String name;       int age;

@Override
public int hashCode() {
    return this.name.hashCode() + this.age * 任意数字;
}

/*
 * 为什么要乘以任意数字呢 ? 
 * A对象:name.hashCode()=40, age=30.    40+30=70;
 * B对象:name.hashCode()=50, age=20.    50+20=70;
 * 若出现以上情况,尽管成员变量值都不同,但是最后相加后结果都是相同的,
 * 因此我们将其中一个成员变量值做算数运算,保证了最后相加值的唯一性。
*/
  • 注意
    • 如果换一个重写hashCode()的方式,比如将hashCode()重写为永远返回一个固定的数值的方法(例如将hashCode()的方法体重写为“return 1;”)也能实现Set体系集合的唯一性。
      因为比较完哈希值是否相同,还会再用equals()比较,当equals()返回false时才添加元素,但是这样做欲添加的第1000个元素需要和以前添加的999个元素做hashCode()比较和equals()比较才能判断是否有相同元素, 效率低。所以我们想直接比较哈希值,哈希值不同就添加,相同就不添加,这就需要不同元素哈希值不同, 相同元素哈希值相同, 因此我们选择成员变量相加的方式重写hashCode().。
      HashSet集合的底层是哈希表结构,哈希表就靠hashCode()和equals()实现元素的唯一性。

2.3 重写equals()方法的原则

  • equals()方法是Object类中的方法。
    该方法的作用是:判断两个变量是否相同。
    Object类中的equals()方法是用"=="实现的。

  • equals()和==的区别

    • ==比较基本数据类型变量,比较的是栈中该变量的值是否相同。
    • ==比较引用数据类型变量,比较的是栈中该对象的引用变量是否相同,即比较的是两个对象的地址值是否相同。
    • equals()不能比较基本数据类型。因为equals()是Object类中的方法,基本数据类型不能调用方法。
    • equals()比较引用数据类型,比较的是对象的内容是否相同。
    • 由于equals()是Object类中的方法,该方法底层是用“==”比较的。因此我们一般需要重写该方法。有些类已经帮我们重写了该方法,例如String类。
  • 重写equals()方法的原则:只要对象的所有属性值都不同,就视为不同对象。因此重写equals()只要写一个比较两对象所有属性值是否相同的方法即可。

  • 我们只需要知道为什么要重写hashCode()方法和equals()方法,不需要手动重写这两个方法。
    使用IDE工具的快捷键能快速重写这两个方法,并且重写的方法的逻辑比我们的缜密。

3. TreeSet与排序

3.1 TreeSet集合的简介和TreeSet#add()方法

  • TreeSet集合的特点
    • 不能添加重复元素
    • 能够对元素按照某种规则排序
      • 排序方式有两种,具体用哪一种方式取决于创建TreeSet时使用哪个构造方法。
        1. 自然排序 :需要使用TreeSet的无参构造。
        2. 比较器排序(通过创建set时提供的Comparator接口进行排序) :需要使用TreeSet的有参构造。
  • TreeSet是如何保证唯一并对元素排序的
    • 因为TreeSet数据结构是二叉树,因此元素能排序并唯一。即底层结构是红黑树,此数据结构能保证元素的唯一性和排序。类似分析HashSet,我们也要分析TreeSet的add()方法。
      通过观察TreeSet的add(),我们知道最终要看TreeMap的put()。TreeSet底层是TreeMap()。TreeSet的add()的底层是TressMap的put()。
  • TreeSet集合的数据结构

在这里插入图片描述

3.2 自然排序

  • 概述
    • 用TreeSet无参构造创建对象时才能用此排序方式。
      集合中元素对应的类A必须实现Comparable<T>接口(自然排序接口),在类A中必须重写int compareTo(T o)方法,方法的内容就写排序的规则。要充分考虑排序的主要条件和次要条件。
  • TreeSet集合使用自然排序步骤
    1. 使用无参构造创建TreeSet集合的对象
    2. 把要传入集合的元素对应的类实现Comparable<T>接口,泛型填传入集合的元素类型
    3. 在要传入集合的元素对应的类中实现Comparable<T>接口的int compareTo(T o)方法
    4. 方法内容就是比较的规则,要全面考虑主要规则和次要规则。
  • 自然排序示例01
package cn.king.demo01;

import org.junit.Test;
import java.util.TreeSet;

class User implements Comparable<User> {
    private String name;
    private int age;
    
    @Override
    public int compareTo(User o) {
        /*
         * 返回什么, 要根据我们的排序规则来决定;
         * 按照年龄排序-->主要条件;
         * 按照年龄排序是主要条件,需分析出次要条件;
         * 年龄相同时,判断姓名是否相同-->次要条件;
         * 如果年龄和姓名都不相同时,才是同一个元素;
         */

        //比较年龄大小--主要条件
        int num = this.age - o.age;
        //如果年龄相同,就比较姓名,否则还是比较年龄--次要条件
        int num2 = num == 0 ? this.name.compareTo(o.name) : num;
        return num2;
    }
    
    // getter...   setter...  toString...  全参构造...   无参构造...
    
}

/**
 * 排序方式:自然排序--按照年龄从小到大排序
 * 排序规则:
 * 注意:如果类A中的对象要进行自然排序,类必须实现自然排序接口;并在类A中实现compareTo()方法。
 */
public class Demo01 {
    @Test
    public void test01() {
        TreeSet<User> users = new TreeSet<>();
        User user01 = new User("张三", 20);
        User user02 = new User("张三", 30);
        User user03 = new User("王五", 4);
        User user04 = new User("李四", 50);
        User user05 = new User("张三", 20);
        User user06 = new User("赵六", 20);
        users.add(user01);
        users.add(user02);
        users.add(user03);
        users.add(user04);
        users.add(user05);
        users.add(user06);

        users.forEach(System.out::println);

        /*  输出:
            User{name='王五', age=4}
            User{name='张三', age=20}
            User{name='赵六', age=20}
            User{name='张三', age=30}
            User{name='李四', age=50}
         */
    }
}
  • 自然排序示例02
package cn.king.demo01;

import org.junit.Test;
import java.util.TreeSet;

class User implements Comparable<User> {
    private String name;
    private int age;

    @Override
    public int compareTo(User o) {
        // 比较姓名长度 --> 主要条件
        int num = this.name.length() - o.name.length();
        // 姓名长度相同, 比较姓名内容 --> 次要条件
        int num2 = num == 0 ? this.name.compareTo(o.name) : num;
        // 姓名长度和内容相同, 比较年龄 --> 次要条件
        int num3 = num2 == 0 ? this.age - o.age : num2;
        return num3;
    }

    // getter...   setter...  toString...  全参构造...   无参构造...
}

/**
 * 排序方式:自然排序--按照年龄从小到大排序
 * 排序规则:
 * 注意:如果类A中的对象要进行自然排序,类必须实现自然排序接口;并在类A中实现compareTo()方法。
 */
public class Demo01 {
    @Test
    public void test01() {
        TreeSet<User> users = new TreeSet<>();
        User user01 = new User("张三", 20);
        User user02 = new User("张三", 30);
        User user03 = new User("王五", 4);
        User user04 = new User("李四", 50);
        User user05 = new User("张三", 20);
        User user06 = new User("赵六", 20);
        User user07 = new User("张三丰", 40);
        users.add(user01);
        users.add(user02);
        users.add(user03);
        users.add(user04);
        users.add(user05);
        users.add(user06);
        users.add(user07);

        users.forEach(System.out::println);

        /*
        输出:
        User{name='张三', age=20}
        User{name='张三', age=30}
        User{name='李四', age=50}
        User{name='王五', age=4}
        User{name='赵六', age=20}
        User{name='张三丰', age=40}
         */
    }
}

3.3 比较器排序

  • 概述:用TreeSet<E>的有参构造创建对象时提供的Comparator进行排序。
  • TreeSet集合使用比较器排序步骤
    1. 用能使用比较器的有参构造创建TreeSet集合对象
    2. 构造方法的参数传入比较器Comparator<T>接口的实现子类A的对象
    3. 自定义一个比较器实现子类A,实现比较器接口,泛型填要比较的元素类型
    4. 在比较器实现子类中实现接口的compare方法
    5. 方法内容写自定义的比较规则
  • 比较器排序示例01
package cn.king.demo01;

import org.junit.Test;
import java.util.Comparator;
import java.util.TreeSet;

class Student {
    private String name;
    private int age;
    // getter...   setter...  toString...  全参构造...   无参构造...
}

// 自定义的比较器
class MyComparator implements Comparator<Student> {
    @Override
    public int compare(Student s1, Student s2) {
        // 姓名长度
        int num = s1.getName().length() - s2.getName().length();
        // 姓名内容
        int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
        // 年龄
        int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
        return num3;
    }
}

// 测试
public class Demo01 {
    @Test
    public void test01() {
        // 创建集合对象用到的构造: public TreeSet(Comparator comparator)
        TreeSet<Student> ts = new TreeSet<>(new MyComparator());
        // 创建元素并 添加元素
        ts.add(new Student("linqingxia", 27));
        ts.add(new Student("zhangguorong", 29));
        ts.add(new Student("wanglihong", 23));
        ts.add(new Student("linqingxia", 27));
        ts.add(new Student("liushishi", 22));
        ts.add(new Student("wuqilong", 40));
        ts.add(new Student("fengqingy", 22));
        ts.add(new Student("linqingxia", 29));
        // 遍历
        ts.forEach(System.out::println);
    }
}
  • 比较器排序示例02 – 使用匿名内部类
package cn.king.demo01;

import org.junit.Test;
import java.util.Comparator;
import java.util.TreeSet;

class Student {
    private String name;
    private int age;
    // getter...   setter...  toString...  全参构造...   无参构造...
}

public class Demo01 {
    @Test
    public void test01() {
        // 创建集合对象用到的构造: public TreeSet(Comparator comparator)
        TreeSet<Student> ts = new TreeSet<>(new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                // 姓名长度
                int num = s1.getName().length() - s2.getName().length();
                // 姓名内容
                int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
                // 年龄
                return num2 == 0 ? s1.getAge() - s2.getAge() : num2;
            }
        });
        // 创建元素并 添加元素
        ts.add(new Student("linqingxia", 27));
        ts.add(new Student("zhangguorong", 29));
        ts.add(new Student("wanglihong", 23));
        ts.add(new Student("linqingxia", 27));
        ts.add(new Student("liushishi", 22));
        ts.add(new Student("wuqilong", 40));
        ts.add(new Student("fengqingy", 22));
        ts.add(new Student("linqingxia", 29));
        // 遍历
        ts.forEach(System.out::println);
    }
}
  • 比较器排序示例03 – 使用Lambda表达式
package cn.king.demo01;

import org.junit.Test;
import java.util.Comparator;
import java.util.TreeSet;

class Student {
    private String name;
    private int age;
    // getter...   setter...  toString...  全参构造...   无参构造...
}

public class Demo01 {
    @Test
    public void test01() {
        // 创建集合对象用到的构造: public TreeSet(Comparator comparator)
        TreeSet<Student> ts = new TreeSet<>((s1, s2) -> {
            // 姓名长度
            int num = s1.getName().length() - s2.getName().length();
            // 姓名内容
            int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
            // 年龄
            return num2 == 0 ? s1.getAge() - s2.getAge() : num2;
        });
        // 创建元素并 添加元素
        ts.add(new Student("linqingxia", 27));
        ts.add(new Student("zhangguorong", 29));
        ts.add(new Student("wanglihong", 23));
        ts.add(new Student("linqingxia", 27));
        ts.add(new Student("liushishi", 22));
        ts.add(new Student("wuqilong", 40));
        ts.add(new Student("fengqingy", 22));
        ts.add(new Student("linqingxia", 29));
        // 遍历
        ts.forEach(System.out::println);
    }
}
  • 比较器排序示例04 – 我们可以在条件允许的情况下使用方法引用来简化Lambda的书写
public class Demo01 {
    // 原来的匿名内部类
    @Test
    public void fun1() {
        Comparator<Integer> comparator = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return Integer.compare(o1, o2);
            }
        };
        TreeSet<Integer> treeSet = new TreeSet<>(comparator);
    }

    // lambda表达式.
    @Test
    public void fun2() {
        Comparator<Integer> comparator = (o1, o2) -> Integer.compare(o1, o2);
        TreeSet<Integer> treeSet = new TreeSet<>(comparator);
    }

    // 方法引用
    @Test
    public void fun3() {
        Comparator<Integer> comparator = Integer::compare;
        TreeSet<Integer> treeSet = new TreeSet<>(comparator);
    }

    // 上述的fun1 fun2 fun3 功能相同.
}

关于Lambda表达式和方法引用的使用,详见我得另一篇博客: Lambda表达式入门

3.4 小结

  • 总结自然排序和比较器排序
    • 自然排序(元素具备比较性):让元素所属的类实现自然排序接口 Comparable
    • 比较器排序(集合具备比较性):让集合的构造方法接收一个比较器接口的子类对象 Comparator
    • 自然排序和比较器排序都能达到同样的目的,若只使用一次,建议使用比较器器排序,因为比较器排序能用匿名内部类或Lambda,更灵活。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值