JavaSE(八)集合之Set

今天这一篇把之前没有搞懂的TreeSet中的比较搞得非常的清楚,也懂得了它的底层实现。希望博友提意见!

一、Set接口

1.1、Set集合概述

  Set集合:它类似于一个罐子,程序可以依次把多个对象 “丢进” Set 集合,而 Set 集合通常不能记住元素的添加的顺序,也就是说Set 集合是无序的。

        Set 集合与 Colleaction 基本相同,没有提供额外的方法,实际上 Set 就是 Collection,只是行为略有不同(Set 不允许包含重复元素)。

1.2、Set类型集合特点

  集合中的元素不可重复,无索引,有没有序要看Set接口具体的实现类是谁。 

  Set接口常见的实现类有:HashSet、LinkedHashSet     HashSet集合中元素的特点 :无序不可重复     LinkedHashSet集合中元素的特点:有序不可重复

二、HashSet

2.1、HashSet概述

    Set 接口的典型实现类,HashSet是基于HashMap实现,大多数时候使用 Set 集合时就是使用这个实现类。HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取和查找性能。数据结构是哈希表。线程是非同步的。  

     保证元素唯一性的原理:判断元素的hashCode值是否相同。

                           如果相同,还会继续判断元素的equals方法,是否为true。

2.2、HashSet特点

不能保证元素的排列顺序,顺序可能和添加的顺序不同,顺序也有可能发生变化。

    HashSetf不是同步的,如果多个线程同时来访问一个 HashSet,假设有两个或者两个以上线程同时修改了HashSet 集合时,则必须通过代码来保证其同步。

    集合元素值可以是 null。

2.3、HashSet如何保证元素唯一性的原理

  当我们想要创建一个集合,该集合里面的元素都具有唯一性时。会遇到两种情况:

  1)元素为String类型,可以直接用Hashset<String>集合来创建

  2)String类重写了hashCode()和equals()方法,所以,它就可以把内容相同的字符串去掉,只留下一个。

    思路图:

  3)HashSet保证元素唯一性的原理      

    我们使用Set集合都是需要去掉重复元素的, 如果在存储的时候逐个equals()比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用equals()方法的次数     当HashSet调用add()方法存储对象的时候, 先调用对象的hashCode()方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象     如果没有哈希值相同的对象就直接存入集合       如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行equals()比较,比较结果为false就存入, true则不存。

  4)将自定义类的对象存入HashSet去重复 

    类中必须重写hashCode()和equals()方法       hashCode(): 属性相同的对象返回值必须相同, 属性不同的返回值尽量不同(提高效率)       equals(): 属性相同返回true, 属性不同返回false,返回false的时候存储。

    在开发中并不要我们去写,比如使用eclipse开发中,在类上面 Alt+Shift+s ,再点h,就能生成相对应的重写的hashCode()和equls()方法了。那我们就来分析一下,该怎么写的。

/*
    注意:这里是一个Student类:里面有name和age属性。
     * 为什么是31?
     * 1,31是一个质数,质数是能被1和自己本身整除的数
     * 2,31这个数既不大也不小
     * 3,31这个数好算,2的五次方-1,2向左移动5位
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)                        //调用的对象和传入的对象是同一个对象
            return true;                        //直接返回true
        if (obj == null)                        //传入的对象为null
            return false;                        //返回false
        if (getClass() != obj.getClass())        //判断两个对象对应的字节码文件是否是同一个字节码
            return false;                        //如果不是直接返回false
        Person other = (Person) obj;            //向下转型
        if (age != other.age)                    //调用对象的年龄不等于传入对象的年龄
            return false;                        //返回false
        if (name == null) {                        //调用对象的姓名为null
            if (other.name != null)                //传入对象的姓名不为null
                return false;                    //返回false
        } else if (!name.equals(other.name))    //调用对象的姓名不等于传入对象的姓名
            return false;                        //返回false
        return true;                            //返回true
    }

2.4、一个案例来说明问题

package com.zyh.domain;

public class Person {
    private String name;
    private int age;

    public Person(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;
    }

    /*
     * 为什么是31?
     * 1,31是一个质数,质数是能被1和自己本身整除的数
     * 2,31这个数既不大也不小
     * 3,31这个数好算,2的五次方-1,2向左移动5位
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)                        //调用的对象和传入的对象是同一个对象
            return true;                        //直接返回true
        if (obj == null)                        //传入的对象为null
            return false;                        //返回false
        if (getClass() != obj.getClass())        //判断两个对象对应的字节码文件是否是同一个字节码
            return false;                        //如果不是直接返回false
        Person other = (Person) obj;            //向下转型
        if (age != other.age)                    //调用对象的年龄不等于传入对象的年龄
            return false;                        //返回false
        if (name == null) {                        //调用对象的姓名为null
            if (other.name != null)                //传入对象的姓名不为null
                return false;                    //返回false
        } else if (!name.equals(other.name))    //调用对象的姓名不等于传入对象的姓名
            return false;                        //返回false
        return true;                            //返回true
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package com.zyh.Collection.set;

import com.zyh.domain.Person;

import java.util.HashSet;

public class HashSetDemo_0010 {
/*
  HashSet集合存储自定义对象并遍历。如果对象的成员变量值相同即为同一个对象
 
     注意了:
         你使用的是HashSet集合,这个集合的底层是哈希表结构。
         而哈希表结构底层依赖:hashCode()和equals()方法。
         如果你认为对象的成员变量值相同即为同一个对象的话,你就应该重写这两个方法。
         如何重写呢?不同担心,自动生成即可。
*/
    public static void main(String[] args) {
        HashSet<Person> hs = new HashSet<>();
        hs.add(new Person("boy",10));
        hs.add(new Person("girl",34));
        hs.add(new Person("girl",34));
        hs.add(new Person("boy",10));
        hs.add(new Person("person",10));
        hs.add(new Person("boy",10));
        hs.add(new Person("person",10));
        hs.add(new Person("boy",10));
        hs.add(new Person("girl",34));

        //遍历集合
        for(Person p : hs){
            System.out.println(p.getName()+":"+p.getAge());
        }
    }
}

三、LinkedHashSet   

3.1、LinkedHashSet概述 

  1)LinkedHashSet是HashSet的子类,LinkedHashSet 集合也是根据元素的 hashCode 值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。

    也就是说,当遍历 LinkedHashSet 将会按照添加元素顺序来访问集合里的元素。

  2)LinkedHashSet是通过用一个链表的实现来扩展HashSet,从而支持了对HashSet中的元素的排序。所以LinkedHashSet可以按照元素插入时的顺序进行提取。

  3)LinkedHashSet 需要维护元素的插入顺序,因此性能略低于 HashSet 的性能,但在迭代访问 Set 里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。

    虽然 LinkedHashSet 使用了链表记录集合元素的添加顺序,但 LinkedhasHSet依然是 HashSet,因此它依然不允许集合元素重复。

  4)LinkedHashSet只能按照先后顺序来进行排序,TreeSet则是按照比较器给的比较规则进行从小到大排序。其实现也就是借助于TreeMap。

3.2、LinkedHashSet的特点

  一是:保证元素唯一。二是:可以保证怎么存就怎么取 

3.3、SortedSet接口与TreeSet类

  SortedSet接口是Set接口的子接口,除了拥有Set集合的一些基本特点之外,还提供了排序的功能。   TreeSet类就是SortedSet接口的实现类

四、TreeSet

4.1、TreeSet概述

  1)TreeSet继承与实现关系 

    TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, java.io.Serializable接口。     TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。     TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。     TreeSet 实现了Cloneable接口,意味着它能被克隆。     TreeSet 实现了java.io.Serializable接口,意味着它支持序列化。

    TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator (比较器排序)进行排序。这取决于使用的构造方法。     TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。     另外,TreeSet是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。

   2)存储对象

    TreeSet存储对象的时候, 可以排序, 但是需要指定排序的算法。

    Integer能排序(有默认顺序), String能排序(有默认顺序), 自定义的类存储的时候出现异常(没有顺序)

    如果想把自定义类的对象存入TreeSet进行排序, 那么必须实现Comparable接口       在类上implement Comparable       重写compareTo()方法       在使用TreeSet存储对象的时候, add()方法内部就会自动调用compareTo()方法进行比较, 根据比较结果使用二叉树形式进行存储

  3)Tree的构造方法

  // 默认构造函数。使用该构造函数,TreeSet中的元素按照自然排序进行排列。
  TreeSet()

  // 创建的TreeSet包含collection
  TreeSet(Collection<? extends E> collection)

  // 指定TreeSet的比较器
  TreeSet(Comparator<? super E> comparator)

  // 创建的TreeSet包含set
  TreeSet(SortedSet<E> set)

  4)TreeSet与Collection的关系

    TreeSet继承于AbstractSet,并且实现了NavigableSet接口。

     TreeSet的本质是一个"有序的,并且没有重复元素"的集合,它是通过TreeMap实现的。TreeSet中含有一个"NavigableMap类型的成员变量"m,而m实际上是"TreeMap的实例"。 

 4.2、TreeSet原理

  1)特点     TreeSet是用来排序的, 可以指定一个顺序, 对象存入之后会按照指定的顺序排列   2)使用方式     2.1)自然顺序(Comparable)       TreeSet类的add()方法中会把存入的对象提升为Comparable类型       调用对象的compareTo()方法和集合中的对象比较       根据compareTo()方法返回的结果进行存储     2.2)比较器顺序(Comparator)       创建TreeSet的时候可以制定 一个Comparator       如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的顺序排序       add()方法内部会自动调用Comparator接口中compare()方法排序       调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数     2.3)两种方式的区别       TreeSet构造函数什么都不传, 默认按照类中Comparable的顺序(没有就报错ClassCastException)       TreeSet如果传入Comparator, 就优先按照Comparator

4.3、图解TreeSet的排序原理

  4.3.1、环境

    我们创建了一个Person类和一个测试类TreeSetDemo_0010类

package com.zyh.domain;

public class Person {
    private String name;
    private int age;

    public Person(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;
    }

    /*
     * 为什么是31?
     * 1,31是一个质数,质数是能被1和自己本身整除的数
     * 2,31这个数既不大也不小
     * 3,31这个数好算,2的五次方-1,2向左移动5位
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)                        //调用的对象和传入的对象是同一个对象
            return true;                        //直接返回true
        if (obj == null)                        //传入的对象为null
            return false;                        //返回false
        if (getClass() != obj.getClass())        //判断两个对象对应的字节码文件是否是同一个字节码
            return false;                        //如果不是直接返回false
        Person other = (Person) obj;            //向下转型
        if (age != other.age)                    //调用对象的年龄不等于传入对象的年龄
            return false;                        //返回false
        if (name == null) {                        //调用对象的姓名为null
            if (other.name != null)                //传入对象的姓名不为null
                return false;                    //返回false
        } else if (!name.equals(other.name))    //调用对象的姓名不等于传入对象的姓名
            return false;                        //返回false
        return true;                            //返回true
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package com.zyh.Collection.set;

import com.zyh.domain.Person;

import java.util.TreeSet;

public class TreeSetDemo_0010 {
    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("王五",43));
        ts.add(new Person("赵六",33));

        for (Person p:ts){
            System.out.println(p);
        }
    }

}

    当我们直接执行的时候,会报错误。

Exception in thread "main" java.lang.ClassCastException: com.zyh.domain.Person cannot be cast to java.lang.Comparable

     分析:Integer能排序(有默认顺序), String能排序(有默认顺序), 自定义的类存储的时候出现异常(没有顺序)

  4.3.2、自然排序(Comparable)

    我们让Person实现Comparable接口,重写compareTo方法

    当我们把返回值设置为1时:

    图解:

    当返回值是0时,张三作为二叉树的根,当我们其他的元素比较时,都返回0表示相同的对象。所以只会存储张三。

    当返回值是-1时,张三作为二叉树的根,李四和它比较时,返回-1说明,李四小,挂在张三的左边。王五一进来也和张三比较返回-1,放在张三左边,在和李四比较返回-1,挂在李四左边,以此类推。

    当返回值是1时。和上面一样的推理。

    2.1)按照年龄排序

      分析:

          张三作为二叉树的根,当李四进来的时候,李四的年龄比张三小,挂在张三的左边。当王五进来的时候,王五的年纪比张三大,所以挂在张三的

         右边。当赵六的进来的时候,和张三比较年龄结果赵六大,挂在张三的右边,在和王五比较结果比王五小,挂在王五的左边。

      结果排序就是:李四、张三、赵六、王五

    2.2)按年龄为主要条件,名字是次要条件  

  4.3.3、比较器顺序(Comparator)

      首先我们查看TreeSet的构造方法发现有一个这样的构造方法:

     // 指定TreeSet的比较器
      TreeSet(Comparator<? super E> comparator)    

      通过查看它的构造方法就知道可以传入一个比较器。

      构造一个新的空TreeSet,它根据指定比较器进行排序。插入到该 set 的所有元素都必须能够由指定比较器进行相互比较:对于 set 中的任意两个元素 e1 和e2,执行 comparator.compare(e1, e2) 都不得抛出 ClassCastException。

      如果用户试图将违反此约束的元素添加到 set 中,则 add 调用将抛出 ClassCastException。

    1)TreeSetd的Comparetor比较器实现的二种方法

      第一种:写个类实现Comparator接口 

  class myComparator implements Comparator{

    @Override
    public int compare(Object o1, Object o2) {
        Person p1 = (Person) o1;
        Person p2 = (Person) o2;

        int num = p1.getName().compareTo(p2.getName());
      // 0的话是两个相同,进行下一个属性比较
        if (num == 0){
            return new Integer(p1.getAge()).compareTo(new Integer(p2.getAge()));
        }

        return num;
    }
}
然后在new Set的时候放进去。如 
TreeSet ts = new TreeSet(new myComparator());

      第二种:写内名内部类方法 

 TreeSet ts = new TreeSet(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            Person p1 = (Person) o1;
            Person p2 = (Person) o2;

            int num = p1.getName().compareTo(p2.getName());

            if (num == 0){
                return new Integer(p1.getAge()).compareTo(new Integer(p2.getAge()));
            }

            return num;

        }
    });

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏郭耀华‘s Blog

剑指offer第三天

21.栈的压入、弹出序列 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,...

2916
来自专栏desperate633

排列类算法问题大总结全排列分析带重复元素的全排列代码下一个排列分析上一个排列分析第k个排列分析排列序号分析排列序号II分析

[ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

931
来自专栏java一日一条

Java ArrayList的不同排序方法

由于其功能性和灵活性,ArrayList是 Java 集合框架中使用最为普遍的集合类之一。ArrayList 是一种 List 实现,它的内部用一个动态数组来存...

1192
来自专栏LanceToBigData

JavaSE集合(八)之Map

前面给大家介绍了集合家族中的Collection家族,这一篇给大家分享的是集合中的另一个家族就是Map家族。以前的时候学习Map的时候没有很认真的去学习,我觉得...

2248
来自专栏desperate633

LintCode 乱序字符串题目分析代码

给出一个字符串数组S,找到其中所有的乱序字符串(Anagram)。如果一个字符串是乱序字符串,那么他存在一个字母集合相同,但顺序不同的字符串也在S中。

1053
来自专栏大闲人柴毛毛

剑指offer代码解析——面试题16反转单链表

本题的详细解析均在代码中注释: /** * 题目:将单链表反转,并输出反转后链表的头结点 * @author 大闲人柴毛毛 */ public class...

40311
来自专栏别先生

Map集合遍历的四种方式理解和简单使用

~Map集合是键值对形式存储值的,所以遍历Map集合无非就是获取键和值,根据实际需求,进行获取键和值 1:无非就是通过map.keySet()获取到值,然后根据...

2096
来自专栏吾爱乐享

java之学习去除ArrayList中重复自定义对象元素

1596
来自专栏机器学习从入门到成神

Java使用增强for循环和迭代器遍历Map集合

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_35512245/articl...

2901
来自专栏Java帮帮-微信公众号-技术文章全总结

Java基础-17(01)总结,TreeSet,LinkHashSet

(3)TreeSet集合 A:底层数据结构是红黑树(是一个自平衡的二叉树) B:保证元素的排序方式 a:自然排序(元素具备比较性) 让元素所属的类实现C...

3256

扫码关注云+社区

领取腾讯云代金券