天天看点

JAVA学习笔记——Set集合

1、Collection集合主要包括:

    List:有序(存储顺序和取出顺序一样),可重复

    Set:无序(存储顺序和取出顺序不一样),唯一

2、Set集合是一个接口,主要使用其子类hashSet和TreeSet

3、hashSet

  1)hashSet如何保证值的唯一?

  通过查看add()方法的源码,可以发现该方法底层依赖两个方法:hashCode()和equals()

  步骤:

    首先通过hashCode()方法得到对象的哈希值并比较

     如果哈希值相同,则比较地址值或者调用equals()方法比较

     如果不同,则直接添加到集合中

  按照方法的步骤来说:

    先调用hashCode(),比较其值

      相同,调用equals()方法

        返回true:说明元素重复,就不添加

        返回false:说明元素不重复,添加到集合

      不同则直接添加到集合

  注意:如果类没有重写这两个方法,默认使用Object(),一般来说不相同。

  而String重写了hashCode()方法和equals()方法,所以可以把相同的字符串过滤掉,只留下不同的。

  

  2)自定义类如何保证用hashSet()存储的唯一性?

  通常我们创建对象的时候,当对象的成员变量相同的时候我们就默认这两个对象是同一个,但是在内存中并不是,因此在使用hashSet()存储的时候,我们就需要重写hashCode()方法和equals()方法。

  方法一:直接让hashCode()的返回值为0,并且重写equals()方法。

  这样可以让所有对象的hash值都相同,这样程序会直接继续调用equals()方法,这样就能实现比较对象的成员变量来判断。

  (注意:哈希表是一种元素为链表的数组。综合了数组和链表的优点。)

  但是这种方法会导致把所有的对象成员都存储在哈希表的第一个链表元素中,虽然解决了重复问题但是效率不高,这种方法并不好。

  方法二:

    如何优化第一种方法呢?

    我们可以让每个对象的哈希值尽可能不同。由于对象的哈希值是与它们的成员变量相关的,因此我们可以让对象的哈希值等于多个成员变量的哈希值的和。其中:

    如果是基本引用类型,就直接加值。

    如果是引用类型,就加哈希值。

    通过这种方式来重写hashCode()方法

    例如:

public int hashCode(){
    return this.name.hashCode()+this.age ;
}
           

    但是即使这样,返回值依然有可能是相同的,例如:20+30和30+20最终还是相等的。

  方法三:最终方案,为了尽可能的区分,我们可以让基本类型的成员变量直接乘以一个数,以此来尽可能的产生不同的结果。例如:

public int hashCode(){
    return this.name.hashCode() + this.age * ;
}
           

    如上,我们也可以直接用eclipse直接生成。

  由上我们可知,hashSet集合的底层是哈希表结构,而哈希表结构主要依赖hashCode()和equals()方法,如果想让自定义类的对象是根据成员变量相同即为同一个对象的话,就应该重写这两个方法。

  3)LinkedHashSet:底层数据结构由哈希表和链表组成

           哈希表保证元素唯一性

           链表保证有序(存储和取出)

4.TreeSet:能够对集合元素按照某种规则排序

  TreeSet集合两个特点:排序和唯一

   唯一性:根据比较的返回结果是否为0

   排序分为两种:

     A:自然排序(元素具备比较性):让要排序的元素的类自身实现Comparable接口

     B:比较器排序(集合具备比较性):让集合的构造方法接收一个比较器接口的子类对象Comparator

     

  TreeSet真正的比较是基于元素的compareTo()方法,而这个方法是定义在Comparable里面的,因此要重写compareTo()方法,就必须先继承Comparable接口。这个接口表示的就是自然顺序。

  TreeSet集合使用哪种排序主要取决于Set集合使用的构造方法。

    无参构造使用的是自然排序

    

  2)TreeSet如何保证元素唯一性和有序?

  如下:

  

JAVA学习笔记——Set集合

   通过这种方式,TreeSet集合既保证元素的唯一性,又能保证其有序。以上是自然排序,需要继承Comparable接口并且实现compareTo()方法,在实现compareTo()方法的时候需要注意通过需求分析出排序的主要条件和次要条件,例如:根据学生年龄排序,那么compareTo()方法重写方式如下:

   

public int compareTo(Student s){
        //主要条件:年龄
        //返回结果正数就排在后面,负数就排在前面,0就说明两个相等
        int result = this.age - s.age ; 
        //如果出现年龄相同的不同学生,这时候就需要思考什么作为次要条件

        //次要条件:姓名
        //如果年龄相等,就比较姓名,如果不相等,就直接按照年龄的比较结果来排序。
        result = result ==  ? this.name.compareTo(s.name) : result ;
        //name作为String类型自身实现了compareTo()方法,可以直接调用。
        return result ;
    }
           

   接下来,如何通过排序器实现排序?

   我们知道,当使用Set的无参构造的时候使用的是自然排序,那么当使用形参为比较器接口的构造函数时,使用的就是比较类排序。

public TreeSet(Comparable<? super E> comparator) ;
           

使用该构造方法时,需要传一个Comparable的实例,但是Comparable是一个接口,也就是说,这里传的应该是一个Comparable接口的实现类的对象,这样我们就需要定义一个Comparable接口的实现类,并且实现compareTo()方法。为了排序而创建类太过于浪费,我们可以通过匿名内部类来实现。

TreeSet<Student> ts= new  TreeSet<Student>(new Comparator<Student>() {

            @Override
            public int compare(Student s1, Student s2) {
                 //主要条件:年龄
                int result = s1.getAge() - s2.getAge() ; 

                //次要条件:姓名
                result = result ==  ? s1.getName().compareTo(s2.getName()) : result ;

                return result ;
            };
        }) ;
           

以上是比较器排序方式,但是我们不常用,更常用自然排序。