欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java基础---集合(4)Set接口及其实现类

程序员文章站 2022-05-23 16:26:34
...

一、Set接口

       Set接口继承自Collection接口,是一个包含没有重复元素的集合,即Set接口实现类可以实现元素存储的唯一性。继承关系图如下:

    Java基础---集合(4)Set接口及其实现类

二、HashSet实现类

 1、概述

   HashSet类通过一个哈希表支持(实际上是一个HashMap实例)实现了Set接口,它并没有保证集合的迭代顺序(是指集合元素取出的顺序和存储的顺序不同,如按 A,B,C,D 的顺序存储,打印出的结果可能是 B,C,A,D ),特别的是,它不能保证随着时间的推移,它的顺序将保持不变。

     HashSet类也可以实现元素存储的唯一性。

     继承关系图:

Java基础---集合(4)Set接口及其实现类

2、验证元素存储的无序性、唯一性

   案例:

   

import java.util.HashSet;

public class Test1 {
    public static void main(String[] args) {
        HashSet<Integer> list = new HashSet<>();  //例1
        list.add(8);
        list.add(2);
        list.add(1);
        list.add(6);
        list.add(5);
        list.add(1);
        list.add(2);
        list.add(9);
        System.out.println(list);
        System.out.println("==============================");
        //例2
        HashSet<String> strings = new HashSet<>();
        strings.add("aaa");
        strings.add("ddd");
        strings.add("aaaddhe");
        strings.add("ddd");
        strings.add("aaa");
        strings.add("dchhd");
        System.out.println(strings);
        System.out.println("===============================");
        //例3
        HashSet<Student> students = new HashSet<>();
        students.add(new Student("Alice",25));
        students.add(new Student("Bob",25));
        students.add(new Student("Alice",18));
        students.add(new Student("Jack",19));
        students.add(new Student("Lily",25));
        students.add(new Student("Bob",25));
        for(Student stu:students){
            System.out.println(stu);
        }
    }


}


  //运行结果

Java基础---集合(4)Set接口及其实现类

   //分析

 唯一性是靠元素重写HashCode()方法和equals()方法来保证的,如果自定义的类不重写,则不能保证,String和Integer默认重写这两个方法,所以上述例1和例2是有序输出的。而自定义的类的对象 Student 输出并没有保证顺序。


  我们来看看Student对象的add()方法:

  1)

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

  2)   

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

  3) 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }


  以上三个方法可不必详细解读,只需要得出元素存储的唯一性由HashCode()方法和equals()方法 决定,所以我们要重写这两个方法,达到存储的唯一性效果


   现在,我们来重写Student类的HashCode()方法和equals()方法,实现存储的唯一性,在Student类中重写,如下:

  

 @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);
    }

  现在重新运行,就发现确实实现了唯一性。

Java基础---集合(4)Set接口及其实现类


3、理解元素存储唯一性的原理


Java基础---集合(4)Set接口及其实现类

  上图中复写的hashCode()方法为:

    @Override
    public int hashCode() {

        return O;
    }

  但是这并不是最优化的结果,这种返回0的做法,会大大的减缓运行,访问比较方法equals()次数很多,我们不妨来运行测试一下:

 

@Override
    public boolean equals(Object o) {
        System.out.println("调用equals方法"+this.name+"=="+this.age+"======="+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);
        return 0;
    }


 //运行结果

Java基础---集合(4)Set接口及其实现类

 

  所以,最终我们使用系统生成的最优化的重写方法:(前面有)

  重新运行,得到:

Java基础---集合(4)Set接口及其实现类


三、LinkedHashSet实现类

   LinkedHashSet类也是实现了Set接口,它的底层数据结构是链表和哈希表。

   链表保证元素有序(集合元素取出的顺序和存储的顺序不同),哈希表保证元素唯一。

   继承关系图:

Java基础---集合(4)Set接口及其实现类

   简单示例:

  

import java.util.LinkedHashSet;

public class MyTest2 {
    public static void main(String[] args) {

        LinkedHashSet<Integer> list = new LinkedHashSet<>();
        list.add(1);
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        System.out.println(list);
    }
}

  //输出结果与存储结果顺序一致,且唯一

Java基础---集合(4)Set接口及其实现类                          


四、TreeSet实现类

   TreeSet类的继承关系图:

Java基础---集合(4)Set接口及其实现类


 1、TreeSet集合的特点   

     底层数据结构是二叉树,元素唯一,且能排序。

     案例演示: TreeSet存储Integer类型的元素并遍历,

               存储下列元素:  20 , 18 , 23 , 22 , 17 , 24, 19 , 18 , 24

   

import java.util.TreeSet;

public class Test {
    public static void main(String[] args) {
        TreeSet<Integer> integers = new TreeSet<>();
        // 20 , 18 , 23 , 22 , 17 , 24, 19 , 18 , 24
        integers.add(20);
        integers.add(18);
        integers.add(23);
        integers.add(22);
        integers.add(17);
        integers.add(24);
        integers.add(19);
        integers.add(18);
        integers.add(24);
        System.out.println(integers);

    }
}

  //运行结果

 Java基础---集合(4)Set接口及其实现类     



2、理解TreeSet类对象存储数据的原理

   二叉树的数据结构 :先存入一个根结点, 分两个分支,分别是左孩子结点、右孩子结点;
   存储元素时: 跟根结点比较, 小的放在左边, 大的放在右边;
                如果相等就不存储
   取元素的时候:按照中序遍历来读取元素,实现从小到大排序(即按照左中右的顺序来取) 


   如下图:

Java基础---集合(4)Set接口及其实现类


3、TreeSet集合的排序功能:

   排序分为:自然排序、比较器排序,到底使用哪一种排序,取决于集合对象的构造方法的形式。

   案例:分别用这两种排序实现

         按照姓名的长度进行排序;

         主要条件是姓名的长度;然后是姓名;然后是年龄

1)自然排序:

   要求:

       创建对象使用无参构造;

     必须实现Comparable接口,然后重写里面的CompareTo()方法,根据自己的排序方式,返回正数、负数、或者0.

   案例:

  //Student类

  

public class Student implements Comparable<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 +
                '}';
    }

    //重写compareTo()方法

    @Override
    public int compareTo(Student s1) {
        //先比较名字的长短
        int num_len=this.getName().length()-s1.getName().length();
        if(num_len==0){  //如果长度相等
            int num_name = this.getName().compareTo(s1.getName());//比较姓名是否相同
            if(num_name==0) {
                int num_age=this.getAge()-s1.getAge();//比较年龄
                if (num_age == 0) {
                    return 0;
                } else {
                    return num_age; }
            }else{
                return num_name; }
        }
        else{
            return num_len; }
    }
}


 

  上述重写的CompareTo()方法,可以用三元表达式写,比较简单明了,如下:

 

@Override
        public int compareTo(Student s1) {
            //先比较名字长短
            int num=this.getName().length()-s1.getName().length();
            //比较名字是否相同
            int num1= num==0 ? this.name.compareTo(s1.name):num;
            //比较年龄
            int res=num1==0?this.age-s1.age:num1;
            return res;
        }



  //测试类

import java.util.TreeSet;

public class Test1 {
    public static void main(String[] args) {
        //自然排序
        TreeSet<Student> students = new TreeSet<>();   //创建对象:无参构造
        students.add(new Student("Lucy",25));
        students.add(new Student("Alice",25));
        students.add(new Student("Jack",18));
        students.add(new Student("LiMing",19));
        students.add(new Student("WangFang",25));
        students.add(new Student("Lily",25));

        for(Student stu:students){
            System.out.println(stu);
        }


    }
}


 //运行结果

Java基础---集合(4)Set接口及其实现类


2) 使用比较器排序

   要求:创建对象使用带参构造,构造方法需要一个比较器;

         比较器实现Compartor接口,并重写里面的Compare()方法,根据自定义的排序方式返回 正数、负数、0

 

   方法1:创建一个MyClass类(任意类),实现Compartor接口,并重写Compare(),创建对象时,传递该类对象即可;

  //Student类

  

import java.util.Comparator;

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 +
                '}';
    }
}


//MyClass类,实现Comparator接口,并重写Compare()

 

import java.util.Comparator;

public class MyClass implements Comparator<Student> {

    @Override
    public int compare(Student s1, Student s2) {
        int num=s1.getName().length()-s2.getName().length();
        //比较名字是否相同
        int num1= num==0 ? s1.getName().compareTo(s2.getName()):num;
        //比较年龄
        int res=num1==0?s1.getAge()-s2.getAge():num1;
        return res;
    }
}

  //测试类

 

import java.util.TreeSet;

public class Test {
    public static void main(String[] args) {
        //比较器排序,方法1
        TreeSet<Student> students=new TreeSet<Student>(new MyClass());
        students.add(new Student("Lucy",25));
        students.add(new Student("Alice",25));
        students.add(new Student("Jack",18));
        students.add(new Student("LiMing",19));
        students.add(new Student("WangFang",25));
        students.add(new Student("Lily",25));

        for(Student stu:students){
            System.out.println(stu);
        }
    }
}



 方法2:不需要重新创建一个类来实现Comparator接口,只需要在构造对象时传递一个匿名内部类,类中重写Compare()方法,即可,(Student类和上面相同)如下:

//测试类

import java.util.Comparator;
import java.util.TreeSet;

public class Test2 {
    public static void main(String[] args) {
        //匿名内部类
        TreeSet<Student> students = new TreeSet<>(new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                int num=s1.getName().length()-s2.getName().length();
                //比较名字是否相同
                int num1= num==0 ? s1.getName().compareTo(s2.getName()):num;
                //比较年龄
                int res=num1==0?s1.getAge()-s2.getAge():num1;
                return res;
            }
        });
        
        students.add(new Student("Lucy",25));
        students.add(new Student("Alice",25));
        students.add(new Student("Jack",18));
        students.add(new Student("LiMing",19));
        students.add(new Student("WangFang",25));
        students.add(new Student("Lily",25));

        for(Student stu:students){
            System.out.println(stu);
        }
    }
}