Java集合(六) Set详解

  前面咱们学习了List集合。咱们知道List集合表明一个元素有序、可重复的集合,集合中每一个元素都有对应的顺序索引。今天咱们要学习的是一个注重独一无二性质的集合:Set集合。咱们能够根据源码上的简介对它进行初步的认识:安全

/*
 * A collection that contains no duplicate elements.  More formally, sets
 * contain no pair of elements <code>e1</code> and <code>e2</code> such that
 * <code>e1.equals(e2)</code>, and at most one null element.  As implied by
 * its name, this interface models the mathematical <i>set</i> abstraction.
 */
复制代码

  这一段说明了Set这个接口的做用,是一个不包含重复元素的集合。这里的重复指,若是元素e1.equals(e2)是true,就不能包含两个。并且最多也只包含一个null元素。 bash

  从上面Set的类结构图能够看出,Set接口并无对Collection作任何扩展。

对象的相等性

  引用到堆上同一个对象的两个引用是相等的。若是对两个引用调用hashCode方法,会获得一样的结果,若是对象所属的类没有覆盖Object的hashCode方法的话,hashCode会返回每一个对象特有的序号(Java是依据对象的内存地址计算出来此序号),因此两个不一样的对象的hashCode是不可能相等的。
  若是想要让两个不一样的Person对象视为相等的,就必须重写从Object继承下来的hashCode方法和equals方法,由于Object的hashCode方法返回的是该对象的内存地址,因此必须重写,才能保证两个不一样的对象具备相同的hashCode,同时也须要两个不一样对象比较equals方法会返回true。ide

Set集合

特色

  • Set集合中的元素是惟一的,不可重复(取决于hashCode和equals方法),也就是说具备惟一性。
  • Set集合中元素不保证存取顺序,并不存在索引。

继承关系

Collection
  |--Set:元素惟一,不保证存取顺序,只能够用迭代器获取元素。
    |--HashSet:哈希表结构,线程不安全,查询速度较快。元素惟一性取决于hashCode和equals方法。
      |--LinkedHashSet:带有双向链表的哈希表结构,线程不安全,保持存取顺序,保持了查询速度较快特色。
  |--TreeSet:平衡排序二叉树(红黑树)结构,线程不安全,按天然排序或比较器存入元素以保证元素有序。元素惟一性取决于ComparaTo方法或Comparator比较器。
  |--EnumSet:专为枚举类型设计的集合,所以集合元素必须是枚举类型,不然会抛出异常。有序,其顺序就是Enum类内元素定义的顺序。存取的速度很是快,批量操做的速度也很快。函数

HashSet

  源码对于HashSet的介绍简洁明了:这个类实现了Set接口,由哈希表支持(其实是一个HashMap实例)。它不保证集合的迭代顺序;特别是它不能保证随着时间的推移,顺序保持不变。这个类容许使用null元素。这个类是线程不安全的。
  因此说看看经常使用的源码注释仍是很是有必要的。性能

HashSet的equals和hashCode

  哈希表里存放的是哈希值。HashSet存储元素的顺序并非按照存入时的顺序,是按照哈希值来存的,因此取数据也是按照哈希值取的。
  元素的哈希值是经过元素的hashCode方法来获取的,HashSet首先判断两个元素的哈希值,若是哈希值同样,接着会比较equals方法,若是equals结果为true,HashSet就视为同一个元素,只存储一个(重复元素没法放入)。若是equals为false就不是同一元素。学习

基于HashMap实现

  HashSet存储的对象都被做为HashMap的key值保存到了HashMap中。测试

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

  咱们知道HashMap是不容许有重复的key值(至于为何,你们能够先查找资料),因此,这也保证了HashSet存储的惟一性。ui

LinkedHashSet

  照个旧,先看一下源码对LinkedHashSet的定义:由哈希表和链表实现,能够预知迭代顺序。这个实现与HashSet的不一样之处在于,LinkedHashSet维护着一个运行于全部条目的双向链表。这个链表定义了迭代顺序,按照元素的插入顺序进行迭代。
  能够理解为:HashSet集合具备的优势LinkedHashSet集合都具备。并且LinkedHashSet集合在HashSet查询速度快的前提下,可以保持元素存取顺序。this

LinkedHashSet特征总结

  LinkedHashSet是HashSet的一个子类,LinkedHashSet也根据HashCode的值来决定元素的存储位置,但同时它还用一个链表来维护元素的插入顺序,插入的时候既要计算hashCode还要维护链表,而遍历的时候只须要按照链表来访问元素。
  经过LinkedHashSet的源码能够知道,LinkedHashSet没有定义任何方法,只有四个构造方法。再看父类,能够知道LinkedHashSet本质上也是基于LinkedHashMap实现的。LinkedHashSet全部方法都继承于HashSet,而它能维持元素的插入顺序的性质则是继承于LinkedHashSet。spa

TreeSet

  来继续看TreeSet的定义:基于TreeMap实现的NavigableSet。根据元素的天然顺序进行排序,或根据建立Set时提供的Comparator进行排序,具体取决于使用的构造方法。
  TreeSet实现了SortedSet接口(NavigableSet接口继承了SortedSet接口),顾名思义这是一种排序的Set集合,根据源码能够知道底层使用TreeMap实现的,本质上是一个红黑树原理。也正由于它排了序,因此相对HashSet来讲,TreeSet提供了一些额外的根据排序位置访问元素的方法。例如:first(),last(),lower(),higher(),subSet(),headSet(),tailSet()。
  TreeSet的排序分两种类型,一种是天然排序;一种是定制排序;

天然排序

  TreeSet会调用compareTo方法比较元素大小,而后按升序排序。因此天然排序中的元素对象,都必须实现了Comparable接口。否则就会抛出异常。对于TreeSet判断元素是否重复的标准,也是调用元素从Comparable接口继承的compareTo方法,若是返回0就是重复元素(返回一个 -1,0,或1表示这个对象小于、等于或大于指定对象。)。其实Java常见的类基本已经实现了Comparable接口。举个例子吧:

public class Person implements Comparable {
    public String name;
    public int age;
    public String gender;

    public Person() {

    }

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

    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", gender=" + gender
                + "]\r\n";
    }

    @Override
    public int compareTo(@NonNull Object o) {
        Person p = (Person) o;
        if (this.age > p.age) {
            return 1;
        }
        if (this.age < p.age) {
            return -1;
        }
        return this.name.compareTo(p.name);
    }
}
复制代码

  这边咱们先建立一个Person类,实现Comparable接口,重写了compareTo方法。排序条件是,先按照年龄进行排序,年龄相同的状况下,再比较姓名。咱们再测试一下:

public class TreeSetTest {
    public static void main(String args[]) {
        TreeSet ts = new TreeSet();
        ts.add(new Person("A", 24, "男"));
        ts.add(new Person("B", 23, "女"));
        ts.add(new Person("C", 18, "男"));
        ts.add(new Person("D", 18, "女"));
        ts.add(new Person("D", 20, "女"));
        ts.add(new Person("D", 20, "女"));

        System.out.println(ts);
        System.out.println(ts.size());
    }
}
复制代码

  结果以下:

[Person [name=C, age=18, gender=男]
, Person [name=D, age=18, gender=女]
, Person [name=D, age=20, gender=女]
, Person [name=B, age=23, gender=女]
, Person [name=A, age=24, gender=男]
]
5
复制代码

  很是直观的能够看出,排序是先根据年龄再根据姓名排序的。并且根据元素个数和结果,知道TreeSet去了重。

定制排序

  TreeSet另一种排序就是定制排序,也叫自定义比较器。这种通常是在元素自己不具有比较性,或者元素自己具有的比较性不知足要求,这个时候就只能让容器自身具有。定制排序,须要关联一个Comparator对象,由Comparator提供逻辑。
  通常步骤为,定义一个类实现Comparator接口,重写compare方法。而后将该接口的子类对象做为参数传递给TreeSet的构造方法。举个例子:

public class TreeSetTest {
    public static void main(String args[]) {
        TreeSet ts = new TreeSet(new MyComparator());
        ts.add(new Person("A", 24, "男"));
        ts.add(new Person("B", 23, "女"));
        ts.add(new Person("C", 18, "男"));
        ts.add(new Person("D", 18, "女"));
        ts.add(new Person("D", 20, "女"));
        ts.add(new Person("D", 20, "女"));

        System.out.println(ts);
        System.out.println(ts.size());
    }

    class MyComparator implements Comparator {

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

            if (p1.age < p2.age) {
                return 1;
            }
            if (p1.age > p2.age) {
                return -1;
            }
            return p1.name.compareTo(p2.name);
        }

    }
}
复制代码

  此次排序规则是年龄先按照从大到小(倒序),而后再根据姓名的天然排序进行元素的整体排序。Person类没变,依然实现Comparable接口,在两种排序都有的状况下,咱们以为结果会是怎样的呢?

[Person [name=A, age=24, gender=男]
, Person [name=B, age=23, gender=女]
, Person [name=D, age=20, gender=女]
, Person [name=C, age=18, gender=男]
, Person [name=D, age=18, gender=女]
]
5
复制代码

  能够看出,当Comparable比较方式,及Comparator比较方式同时存在,以Comparator比较方式为主。其余的都没有疑问。

异同

  Comparable是由对象本身实现的,一旦一个对象封装好了,compare的逻辑就肯定了,若是咱们须要对同一个对象增长一个字段的排序就比较麻烦,须要修改对象自己。好处是对外部不可见,调用者不须要知道排序的逻辑,只要调用排序就能够。
  而Comparator由外部实现,比较灵活,对于须要增长筛选条件,只要新增一个Comparator便可。缺点是全部排序逻辑对外部暴露,须要对象外部实现。(这里的外部指对象的外部,咱们能够封装好全部的Comparator,对调用者隐藏内部逻辑。)优势是很是灵活,随时能够增长排序方法,只要对象内部字段支持,相似动态绑定。

EnumSet

  EnumSet顾名思义就是专为枚举类型设计的集合,所以集合元素必须是枚举类型,不然会抛出异常。EnumSet集合也是有序的,其顺序就是Enum类内元素定义的顺序。EnumSet存取的速度很是快,批量操做的速度也很快。EnumSet主要提供如下方法,allOf, complementOf, copyOf, noneOf, of, range等。注意到EnumSet并无提供任何构造函数,要建立一个EnumSet集合对象,只须要调用allOf等方法。
  EnumSet用的很是少,元素性能是全部Set元素中性能最好的,可是它只能保存Enum类型的元素。

总个结吧

  主要介绍了Set的结构,实现原理。Set只是Map的一个马甲,主要逻辑都交给Map实现。东西很少,咱们在后面Map的学习中对实现原理再深刻研究。再提一嘴:

  • 看到array,就要想到角标。
  • 看到link,就要想到first,last。
  • 看到hash,就要想到hashCode,equals。
  • 看到tree,就要想到两个接口。Comparable,Comparator。
相关文章
相关标签/搜索