Java集合(四)、继承自Collection接口的Set接口

一、Set接口的特点

  一个不包含重复元素的collection。更确切地讲,Set不包含满足e1.equals(e2)的元素对 e1和e2,并且最多包含一个null元素。

  Set集合由Set接口和Set接口的实现类组成,Set接口继承了Collection接口,因此包含了Collection接口的所有方法。其主要实现类有HashSet和TreeSet,在HashSet的基础上又延伸出了LinkedHashSet。

HashSet和TreeSet的不同就在于如何判断两个数是否相同的方法上。

  1.HashSet判断两个对象是否相同的方法时继承自Object类的equals方法

    (public boolean equals(Object o)方法只可以比较是否相等,相等返回true,反之返回false)。

  2.TreeSet判断两个对象是否相同的方法则是Comparable接口中的compareTo()方法

    (public void int compareTo(Object o)方法不仅可以比较是否相等,还可以比较大小,如果相等返回0,调用者大于参数则返回正数,否则返回负数)

所以可以得到添加到TreeSet中的对象必须实现Comparable接口。同时如果使用HashSet则最好重写equals()方法。

二、HashSet

HashSet实现了Set接口,基于HashMap进行存储。遍历时不保证顺序,并不保证下次遍历的顺序和之前一样。HashSet中允许null值。

进入到HashSet源码中我们发现,所有数据存储在

private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();

意思就是HashSet的集合其实就是HashMap的key的集合,然后HashMap的val默认都是PRESENT。HashMap的定义即是key不重复的集合。使用HashMap实现,这样HashSet就不需要再实现一遍。

所以所有的add,remove等操作其实都是HashMap的add、remove操作。遍历操作其实就是HashMap的keySet的遍历,举例如下

...
public Iterator<E> iterator(){
    return map.keySet().iterator();
}

public boolean contains(Object o) {
    return map.containsKey(o);
}

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

public void clear() {
    map.clear();
}
...

1.HashSet底层实际上是一个HashMap,HashMap底层采用了哈希表数据结构。

2.哈希表又叫散列表,哈希表底层是一个数组,这个数组中每一个元素是一个单向链表,每个单向链表都有一个独一无二的hash值,代表数组的下表。在某个单向链表中的每个节点上的hash值是相同的。hash值实际上是key调用hashCode方法,再通过“hash function”转换成的值。

3.如何向哈希表中添加元素?

  先调用存储的key的hashCode方法,经过 某个算法得到hash值,如果这个哈希表中不存在这个hash值,则直接加入元素。如果该hash值已经存在,继续调用key之间的equals方法,如果equals方法返回false,则将钙元素添加。如果equals方法返回true,则放弃添加该元素 ,即元素重复。

  HashMap和HashSet的初始化容量是16,默认加载因子是0.75。

代码举例

public class Student{
    private Integer id;

    private String name;

    public Student(Integer id, String name){
        this.id = id;
        this.name = name;
    }

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

测试类

import java.util.*;

public class test{
    public static void main(String[] args) {
        Set<Student> set = new HashSet<>();
        Student stu1 = new Student(1001, "leslie");
        Student stu2 = new Student(1001, "leslie");
        set.add(stu1);
        set.add(stu2);

        set.forEach(stu -> System.out.println(stu));
    }
}

运行程序,得到如下结果

看到这个结果应该很惊讶,我们期望的是Set中存放的数据不可重复,而代码里的两个Student对象,学号与姓名都相同,插入到HashSet中后应该只保留一个对象。

 这是因为Set集合中所说的不允许重复,这个重复是指对象的重复。何为同一个对象?即在内存中的编号是一致的。内存中的编号是什么?就是哈希码(哈希码一般是 类名 和 对象所在内存地址的十六进制数字表示 的组合)。

改造我们定义的Student类,为Student类增加hashCode()与equals()方法。

public class Student{
    private Integer id;

    private String name;

    public Student(Integer id, String name){
        this.id = id;
        this.name = name;
    }

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

    @Override
    public boolean equals(Object o){
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        Student stu = (Student) o;
        return id.equals(stu.id);
    }

    @Override
    public int hashCode(){
        return id.hashCode();
    }
}

再次运行测试程序,结果如下:

此时,HashSet中只有一个对象。

另外,HashSet完全继承了Set或者Collection里的方法实现add、addAll、clear、isEmpty、size、contains、iterator、remove等

简单示例:

import java.util.*;

public class test{
    public static void main(String[] args) {
        HashSet<Integer> hashSet = new HashSet<>();
        for (int i = 0; i < 10; i++){
            hashSet.add((int)(Math.random()*100));
        }
        System.out.println(hashSet.size());
        //打印
        Iterator<Integer> it = hashSet.iterator();
        while(it.hasNext()){
            System.out.println(it.next() + " ");
        }
        //判断是否包含重复元素
        System.out.println(hashSet.contains(8));
    }
}

输出

三、TreeSet

TreeSet集合:可以对Set集合中的元素进行排序。是不同步的。

但是TreeSet集合的存储是有序的,即:存储到集合中的元素是按自然顺序存储的。

判断元素唯一性的方式:

  根据比较方法的返回值来判断。是0(零)就存入集合,不是0就不存。因为Set集合是不能有重复的元素,无序。

TreeSet要注意的事项:

1.往TreeSet里面添加元素时候,如果元素本具备自然顺序特性,那么就按照元素的自然顺序排序存储.

2.往TreSet里面添加元素时候,如果元素不具备自然顺序特性,那么该元素就必须要实现Comparable接口,把元素的比较规则定义在compareTo(T o)方法中

3.如果比较元素的时候,compareTo返回的是0,那么该元素被视为重复元素,不允许添加 (注意:TreeSet与HashCode,equals没有任何关系)

4.往TreeSet里面添加元素时候,如果元素本身不具备自然自然顺序特性,而且元素所属类也没有实现Comparable接口,那么我们必须要在创建TreeSet的时候传入一个比较器.

自定义比较器

 自定一个比较器只需要实现接口 Comparator<T>即可,把元素与元素之间的比较规则定义在compare方法内即可

自定义比较器的格式:

class 类名 implements Comparator<T>{
    
}

Java中的TreeSet是Set的一个子类,TreeSet集合是用来对象元素进行排序的,同样他也可以保证元素的唯一。TreeSet如何保证元素唯一呢,他是怎么样进行排序的呢?

首先来看一段代码

public class Person{
    private Integer age;

    private String name;

    public Person(Integer age, String name){
        this.age = age;
        this.name = name;
    }
}

import java.util.*;

public class test{
    public static void main(String[] args) {
        TreeSet<Person> ts = new TreeSet<>();
        ts.add(new Person(23, "张三"));
        ts.add(new Person(13, "李四"));
        ts.add(new Person(13, "周七"));
        ts.add(new Person(43, "王五"));
        ts.add(new Person(33, "赵六"));

        System.out.println(ts);
    }
}

出错,会抛出一个异常:java.lang.ClassCastException显然是出现了类型转换异常。原因在于我们需要告诉TreeSet如何来进行比较元素,如果不指定,就会抛出这个异常

如何解决:

如何指定比较的规则,需要在自定义类(Person)中实现```Comparable```接口,并重写接口中的compareTo方法 

public class Person implements Comparable<Person>{
    private Integer age;

    private String name;

    public Person(Integer age, String name){
        this.age = age;
        this.name = name;
    }

    public int CompareTo(Person o){
        return 0;     //当compareTo方法返回0的时候集合中只有一个元素
        return 1;    //当compareTo方法返回正数的时候集合会怎么存就怎么取
        return -1;    //当compareTo方法返回负数的时候集合会倒序存储
    }

为什么返回0,只会存一个元素,返回-1会倒序存储,返回1会怎么存就怎么取呢?

原因在于TreeSet底层其实是一个二叉树机构,且每插入一个新元素(第一个除外)都会调用```compareTo()```方法去和上一个插入的元素作比较,并按二叉树的结构进行排列。

1. 如果将```compareTo()```返回值写死为0,元素值每次比较,都认为是相同的元素,这时就不再向TreeSet中插入除第一个外的新元素。所以TreeSet中就只存在插入的第一个元素。

2. 如果将```compareTo()```返回值写死为1,元素值每次比较,都认为新插入的元素比上一个元素大,于是二叉树存储时,会存在根的右侧,读取时就是正序排列的。

3. 如果将```compareTo()```返回值写死为-1,元素值每次比较,都认为新插入的元素比上一个元素小,于是二叉树存储时,会存在根的左侧,读取时就是倒序序排列的。

利用上诉规则,我们就可以按照年龄来排序了。

主要代码:

    public int CompareTo(Person o){
        int num = this.age - o.age;        //年龄是比较的主要天骄
        return num == 0 ? this.name.compareTo(o.name) : num; //姓名是比较的次要条件
    }

完整代码:

import java.util.*;


class Person implements Comparable<Person>{
    Integer age;
    String name;
    public Person (Integer age, String name){
        this.age = age;
        this.name = name;
    }

    //重写接口中的方法
    //要重写比较规则
    @Override
    public int compareTo(Person o){
        int num = this.age - o.age;
        return num == 0 ? this.name.compareTo(o.name) : num;
    }

    @Override
    public String toString(){
        return "Person{" + "age=" + age + ", name='" + name + '\'' + '}';
    }
}


public class test{
    //需求:将字符创安装长度排序
    public static void main(String[] args) {
        TreeSet<Person> ts = new TreeSet<>(); 
        
        ts.add(new Person(23, "张三"));
        ts.add(new Person(13, "李四"));
        ts.add(new Person(13, "周七"));
        ts.add(new Person(43, "王五"));
        ts.add(new Person(33, "赵六"));

        System.out.println(ts);
    }
}

按照姓名排序(依据Unicode编码大小),主要代码如下:

    public int CompareTo(Person o){
        int num = this.name.compareTo(o.name);        //姓名是比较的主要条件
        retrun num == 0 ? this.age - o.age : num; //年龄是比较的次要条件
    }

完整代码:

import java.util.*;


class Person implements Comparable<Person>{
    Integer age;
    String name;
    public Person (Integer age, String name){
        this.age = age;
        this.name = name;
    }

    //重写接口中的方法
    //要重写比较规则
    @Override
    public int compareTo(Person o){
        int num = this.name.compareTo(o.name);
        return num == 0 ? this.age - o.age : num;
    }

    @Override
    public String toString(){
        return "Person{" + "age=" + age + ", name='" + name + '\'' + '}';
    }
}


public class test{
    //需求:将字符创安装长度排序
    public static void main(String[] args) {
        TreeSet<Person> ts = new TreeSet<>(); 
        
        ts.add(new Person(23, "张三"));
        ts.add(new Person(13, "李四"));
        ts.add(new Person(13, "周七"));
        ts.add(new Person(43, "王五"));
        ts.add(new Person(33, "赵六"));

        System.out.println(ts);
    }
}

按照姓名长度排序,代码如下:

    public int CompareTo(Person o){
        int length = this.name.length() - o.name.length(); //比较长度为主要条件
        int num = length == 0? this.name.compareTo(o.name) : length; //比较内容为此要条件
        return num == 0 ? this.age - o.age : num; //比较年龄为次要条件
    }

完整代码:

import java.util.*;


class Person implements Comparable<Person>{
    Integer age;
    String name;
    public Person (Integer age, String name){
        this.age = age;
        this.name = name;
    }

    //重写接口中的方法
    //要重写比较规则
    @Override
    public int compareTo(Person o){
        int length = this.name.length() - o.name.length();
        int num = length == 0 ? this.name.compareTo(o.name) : length;
        return num == 0 ? this.age - o.age : num;
    }

    @Override
    public String toString(){
        return "Person{" + "age=" + age + ", name='" + name + '\'' + '}';
    }
}


public class test{
    //需求:将字符创安装长度排序
    public static void main(String[] args) {
        TreeSet<Person> ts = new TreeSet<>(); 
        
        ts.add(new Person(23, "张三呀"));
        ts.add(new Person(13, "李"));
        ts.add(new Person(13, "周七"));
        ts.add(new Person(43, "王颜王五"));
        ts.add(new Person(33, "赵leslie六"));

        System.out.println(ts);
    }
}

以上是TreeSet如何比较自定义对象,接下来我们再来看一下TreeSet如何利用比较器比较元素。

需求:现在要制定TreeSet中按照String长度比较String。

import java.util.*;

//定义一个类,实现Comparator接口,并重写compare()方法
class CompareByLen implements Comparator<String>{
    @Override
    public int compare(String s1, String s2){    //按照字符串的长度比较
        int num = s1.length() - s2.length();    //长度为主要条件
        return num == 0 ? s1.compareTo(s2) : num;  //内容为次要条件
    }
}



public class test{
    //需求:将字符创安装长度排序
    public static void main(String[] args) {
        TreeSet<String> ts = new TreeSet<>(new CompareByLen()); 
        //CompareByLen c = new CompareByLen();

        ts.add("aaaaaaaa");
        ts.add("z");
        ts.add("wc");
        ts.add("nba");
        ts.add("cba");

        System.out.println(ts);
    }
}

总结:

1.特点

  TreeSet是用来排序的,可以指定一个顺序,对象存入之后会按照指定的顺序排列

2.使用方式

  (1)自然顺序(Comparable)

      TreeSet类的add()方法会把存入的对象提升为Comparable类型

      调用对象的comparaTo()方法和集合中的对象比较

      根据comparaTo()方法返回的结果进行存储

  (2)比较器顺序

      创建TreeSet的时候可以指定一个Comparator

      如果传入了Comparator的子类对象,那么TreeSet就会按照比较器的顺序排序。

      add()方法内部会自动调用Comparator接口中的compare()方法排序

      调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数

  (3)两种方式的区别

      TreeSet构造函数什么都不传,默认按照类中Comparable的顺序(没有就报错ClassCastException)

      TreeSet如果传入COmparator,就有先按照Comparator

猜你喜欢

转载自www.cnblogs.com/lixiansheng/p/11349379.html