java set集合详解_Java集合(六) Set详解

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

/*

* A collection that contains no duplicate elements. More formally, sets

* contain no pair of elements e1 and e2 such that

* e1.equals(e2), and at most one null element. As implied by

* its name, this interface models the mathematical set abstraction.

*/

复制代码

这一段说明了Set这个接口的作用,是一个不包含重复元素的集合。这里的重复指,如果元素e1.equals(e2)是true,就不能包含两个。而且最多也只包含一个null元素。2f99824cb09a2c07397017727fdd24de.png

从上面Set的类结构图可以看出,Set接口并没有对Collection做任何扩展。

对象的相等性

引用到堆上同一个对象的两个引用是相等的。如果对两个引用调用hashCode方法,会得到同样的结果,如果对象所属的类没有覆盖Object的hashCode方法的话,hashCode会返回每个对象特有的序号(Java是依据对象的内存地址计算出来此序号),所以两个不同的对象的hashCode是不可能相等的。

如果想要让两个不同的Person对象视为相等的,就必须重写从Object继承下来的hashCode方法和equals方法,因为Object的hashCode方法返回的是该对象的内存地址,所以必须重写,才能保证两个不同的对象具有相同的hashCode,同时也需要两个不同对象比较equals方法会返回true。

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存储的唯一性。

LinkedHashSet

照个旧,先看一下源码对LinkedHashSet的定义:由哈希表和链表实现,可以预知迭代顺序。这个实现与HashSet的不同之处在于,LinkedHashSet维护着一个运行于所有条目的双向链表。这个链表定义了迭代顺序,按照元素的插入顺序进行迭代。

可以理解为:HashSet集合具有的优点LinkedHashSet集合都具有。而且LinkedHashSet集合在HashSet查询速度快的前提下,能够保持元素存取顺序。

LinkedHashSet特征总结

LinkedHashSet是HashSet的一个子类,LinkedHashSet也根据HashCode的值来决定元素的存储位置,但同时它还用一个链表来维护元素的插入顺序,插入的时候既要计算hashCode还要维护链表,而遍历的时候只需要按照链表来访问元素。

通过LinkedHashSet的源码可以知道,LinkedHashSet没有定义任何方法,只有四个构造方法。再看父类,可以知道LinkedHashSet本质上也是基于LinkedHashMap实现的。LinkedHashSet所有方法都继承于HashSet,而它能维持元素的插入顺序的性质则是继承于LinkedHashSet。

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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值