前面咱们学习了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
引用到堆上同一个对象的两个引用是相等的。若是对两个引用调用hashCode方法,会获得一样的结果,若是对象所属的类没有覆盖Object的hashCode方法的话,hashCode会返回每一个对象特有的序号(Java是依据对象的内存地址计算出来此序号),因此两个不一样的对象的hashCode是不可能相等的。
若是想要让两个不一样的Person对象视为相等的,就必须重写从Object继承下来的hashCode方法和equals方法,由于Object的hashCode方法返回的是该对象的内存地址,因此必须重写,才能保证两个不一样的对象具备相同的hashCode,同时也须要两个不一样对象比较equals方法会返回true。ide
Collection
|--Set:元素惟一,不保证存取顺序,只能够用迭代器获取元素。
|--HashSet:哈希表结构,线程不安全,查询速度较快。元素惟一性取决于hashCode和equals方法。
|--LinkedHashSet:带有双向链表的哈希表结构,线程不安全,保持存取顺序,保持了查询速度较快特色。
|--TreeSet:平衡排序二叉树(红黑树)结构,线程不安全,按天然排序或比较器存入元素以保证元素有序。元素惟一性取决于ComparaTo方法或Comparator比较器。
|--EnumSet:专为枚举类型设计的集合,所以集合元素必须是枚举类型,不然会抛出异常。有序,其顺序就是Enum类内元素定义的顺序。存取的速度很是快,批量操做的速度也很快。函数
源码对于HashSet的介绍简洁明了:这个类实现了Set接口,由哈希表支持(其实是一个HashMap实例)。它不保证集合的迭代顺序;特别是它不能保证随着时间的推移,顺序保持不变。这个类容许使用null元素。这个类是线程不安全的。
因此说看看经常使用的源码注释仍是很是有必要的。性能
哈希表里存放的是哈希值。HashSet存储元素的顺序并非按照存入时的顺序,是按照哈希值来存的,因此取数据也是按照哈希值取的。
元素的哈希值是经过元素的hashCode方法来获取的,HashSet首先判断两个元素的哈希值,若是哈希值同样,接着会比较equals方法,若是equals结果为true,HashSet就视为同一个元素,只存储一个(重复元素没法放入)。若是equals为false就不是同一元素。学习
HashSet存储的对象都被做为HashMap的key值保存到了HashMap中。测试
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
复制代码
咱们知道HashMap是不容许有重复的key值(至于为何,你们能够先查找资料),因此,这也保证了HashSet存储的惟一性。ui
照个旧,先看一下源码对LinkedHashSet的定义:由哈希表和链表实现,能够预知迭代顺序。这个实现与HashSet的不一样之处在于,LinkedHashSet维护着一个运行于全部条目的双向链表。这个链表定义了迭代顺序,按照元素的插入顺序进行迭代。
能够理解为:HashSet集合具备的优势LinkedHashSet集合都具备。并且LinkedHashSet集合在HashSet查询速度快的前提下,可以保持元素存取顺序。this
LinkedHashSet是HashSet的一个子类,LinkedHashSet也根据HashCode的值来决定元素的存储位置,但同时它还用一个链表来维护元素的插入顺序,插入的时候既要计算hashCode还要维护链表,而遍历的时候只须要按照链表来访问元素。
经过LinkedHashSet的源码能够知道,LinkedHashSet没有定义任何方法,只有四个构造方法。再看父类,能够知道LinkedHashSet本质上也是基于LinkedHashMap实现的。LinkedHashSet全部方法都继承于HashSet,而它能维持元素的插入顺序的性质则是继承于LinkedHashSet。spa
来继续看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集合也是有序的,其顺序就是Enum类内元素定义的顺序。EnumSet存取的速度很是快,批量操做的速度也很快。EnumSet主要提供如下方法,allOf, complementOf, copyOf, noneOf, of, range等。注意到EnumSet并无提供任何构造函数,要建立一个EnumSet集合对象,只须要调用allOf等方法。
EnumSet用的很是少,元素性能是全部Set元素中性能最好的,可是它只能保存Enum类型的元素。
主要介绍了Set的结构,实现原理。Set只是Map的一个马甲,主要逻辑都交给Map实现。东西很少,咱们在后面Map的学习中对实现原理再深刻研究。再提一嘴: