Java Connection集合家庭分析
Java集合大致可以分为Set、List、Queue和Map四种体系,其中Set代表无序、不可重复的集合;List代表有序、重复的集合;而Map则代表具有映射关系的集合,Java 5 又增加了Queue体系集合,代表一种队列集合实现。
Java集合类之间的继承关系
Java的集合类主要由两个接口派生而出:Collection和Map,Collection和Map是Java集合框架的根接口。
Collection家族:
Set集合
Set集合时无序、不可重复的集合
Set集合中包含了三个比较重要的实现类:HashSet、TreeSet和EnumSet。本篇文章将重点介绍这三个类。
1.HashSet是Set接口的典型实现,实现了Set接口中的所有方法,并没有添加额外的方法,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素。因此具有很好的存取和查找性能。HashSet不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。HashSet不是同步的,如果多个线程同时访问一个HashSet,则必须通过代码来保证其同步。同HashTable不同集合元素值可以是null。HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。
2.两个对象比较 具体分为如下四个情况:
.如果有两个元素通过equal()方法比较返回false,但它们的hashCode()方法返回不相等,HashSet将会把它们存储在不同的位置。
.如果有两个元素通过equal()方法比较返回true,但它们的hashCode()方法返回不相等,HashSet将会把它们存储在不同的位置。
.如果两个对象通过equals()方法比较不相等,hashCode()方法比较相等,HashSet将会把它们存储在相同的位置,在这个位置以链表式结构来保存多个对象。这是因为当向HashSet集合中存入一个元素时,HashSet会调用对象的hashCode()方法来得到对象的hashCode值,然后根据该hashCode值来决定该对象存储在HashSet中存储位置。
.如果有两个元素通过equal()方法比较返回true,但它们的hashCode()方法返回true,HashSet将不予添加。
HashSet判断两个元素相等的标准:两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。
注意:HashSet是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。所以如果重写类的equals()方法和hashCode()方法时,应尽量保证两个对象通过hashCode()方法返回值相等时,通过equals()方法比较返回true。
3.HashSet的实质是一个HashMap。HashSet的所有集合元素,构成了HashMap的key,其value为一个静态Object对象。因此HashSet的所有性质,HashMap的key所构成的集合都具备。
从源码中我们可以看到,HashSet的add方法其实就是将值存入HashMap的Key,而Value存储了一个静态Object进行填充。
通过源码我们看到,HashSet其实底层应用的HashMap进行存储,因此HashSet如果Hash碰撞频繁,查询效率也会降低。
4.LinkedHashMap源码
继承与HashSet
默认初始化方式
可以看到默认的初始化方式调用了HashSet的初始化
最终我们得出结论,LinkedHashMap底层由LinkedHashMap进行存储的,从初始化源码中可以看出来,并且继承于HashSet,因此其添加元素的原理同HashSet,key存储了Set的值,value存储了一个静态的站位Object对象。
并且,由于底层是LinkedHashMap,因此它是有序的,并且元素不能重复。当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。但是由于要维护元素的插入顺序,在性能上略低与HashSet,但在迭代访问Set里的全部元素时有很好的性能。
5.TreeSet源码
通过上面源码的截图,可以看出其底层是通过TreeMap进行存储,并且套路和HashSet一样TreeMap的Key用来存TreeSet的值,TreeMap的Value用一个静态Object填充。
TreeSet针对元素的排序,可以根据元素的compareTo方法进行比较,还可以在初始化TreeSet时指定Comparator来对元素进行排序。
注意:如果向TreeSet中添加了一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生了改变,但TreeSet不会再次调整它们。我们通过下面的程序来进行演示:
其中Person实现Comparable接口,将Person对象按照年龄从小到大升序排列。
输出结果:
初始年龄排序
[Person[age=10], Person[age=30], Person[age=40]]
修改p1年龄后集合排序
[Person[age=60], Person[age=30], Person[age=40]]
修改p2年龄后集合排序
[Person[age=60], Person[age=40], Person[age=40]]
可以看到并没有发生变化,而且如果修改后进行元素删除操作可能会不成功,具体比较复杂。总之,推荐不要修改放入TreeSet集合中元素的关键实例变量。
补充:TreeSet也是非线程安全的。
6.EnumSet——专门存放枚举类型元素的Set:
1) EnumSet只能存放一种枚举类型的元素,具体存放什么枚举类型的元素可以通过两种方法指定,一种是显式,一种是隐式;
2) 一旦元素的枚举类型确定那么集合就确定了(即只能存放该种枚举类型的元素,不能同时存放多种枚举类型的元素!!否则就会抛出异常);
3) 枚举集合EnumSet底层并不是直接存放枚举对象的,而是用二进制位向量来存放的,因此存储非常紧凑,并且高效(比如枚举类型A里有4个枚举值a、b、c、d,并且这4个值都是对象,现在一个枚举集合存放a、c、d三个值,由于枚举类型的所有值的个数是有限的,因此可以用二进制序列来唯一表示,这里就用4位二进制数表示,现在只有a、c、d这三个值,因此可以表示为1011,1表示该枚举值在集合中,0表示不在集合种;
!!枚举值在枚举类型中是有序号的,就是按照其定义顺序排列的;
!!而从枚举集合中取出一个枚举值是是根据二进制位的位置还原原本的枚举值的,比如取出第二个二进制位置的枚举值,那么根据枚举类型中枚举值定义的顺序,第二个位置是b,那么取出的就是b,即位置和值是一一对应的;
!!由于是二进制位操作,因此向containsAll、retainAll等方法会非常高效;
4) EnumSet不允许包含null元素,强行添加会抛出异常!但是判空和删除null元素的方法可以正常调用,只不过永远返回null而已(因为不存在null元素,因此也没办法删除);
5) 由于使用二进制位来保存的,重复就更加不用担心的,每个二进位的位置就代表一个枚举值,因此一定不会重复;
7.构造EnumSet:
1) EnumSet并没有提供公开的构造器来构造对象,而是提供了很多静态工具方法来构造EnumSet对象;
2) 下面介绍的都是EnumSet的静态工具方法,用来构造EnumSet对象:共有显式和隐式两种
****显式:显式(手工)指出存放元素所属的枚举类型
i. static EnumSet allOf(Class elementType); // 将elementType所代表的枚举类型的所有枚举值加入到集合中,例如EnumSet es = EnumSet.allOf(Season.class);
!!将Season枚举类的所有枚举值SPRING、SUMMER、FALL、WINNTER加入到集合中
ii. static EnumSet noneOf(Class elementType); // 创建一个空的、只用来存放elementType枚举类型值的集合
****隐式:不直接在参数中指定枚举类型,而是通过参数的类型自动推断枚举类型
i. 用其它枚举集合来构造:
a. static EnumSet complementOf(EnumSet s); // 用s之外的其它枚举值来构造一个集合(比如枚举类型有a、b、c、d 4个值,现在s有a和c,那么构造出来集合只包含b和d,即补集);
b. static EnumSet copyOf(EnumSet s); // s的深复制(不是引用复制)
ii. 直接用枚举值来构造:
a. static EnumSet<E> of(E first, E... rest); // 直接用若干枚举值构建一个枚举集合,例如EnumSet es = EnumSet.of(Season.SPRING, Season.WINNTER);
b. static EnumSet<E> range(E from, E to); // 直接用枚举值区间构建一个集合,包括[from, to]的所有枚举值
注意!
*1. 直接用枚举值构造时枚举值必须都属于同一个类型,否则会报错!
*2. [from, to]是闭区间!而不是左闭右开了,因为枚举类型无法表示最后一个值的后一个值!因此只有这里比较特殊,采用了右边闭合的区间;
*3. 枚举值的顺序就是枚举类型中枚举值定义的顺序;
3) 构造完之后就可以正常调用add等集合操作方法对枚举集合进行操作了;
8. 特殊的构造方式——用另一个集合来构造EnumSet:
1) 原型:static EnumSet copyOf(Collection c);
2) 如果c就是一个EnumSet那该方法就跟static EnumSet copyOf(EnumSet s);没有区别;
3) 如果是c是普通的集合(Set、List等),那就要求c里面存放的必须是同一类型的枚举类型对象,因为它会将c中的全部元素加入到新集合中,如果类型不一致肯定是不行的;
!!注意两个关键点:
i. c中元素的类型必须是枚举类型,否则和EnumSet的本质相违背肯定是会异常的!
ii. c中的元素必须属于同一枚举类型,否则也会违反EnumSet类型一致的规定而异常的!
4) 如果c是一个List,而里面的元素有重复会怎么样呢?没关系,重复元素就相当于add了多次,add的时候会自动判断是否重复的,如果重复则拒绝添加,因此最终产生的EnumSet是绝对不会重复(添加的时候自动去重了);
9. Set各实现类的性能以及该如何选择使用哪种实现类:
1) 就效率而言EnumSet毋庸置疑是最高的,毕竟是用二进制向量保存的,其次HashSet性能好于LinkedHashSet(仅仅多了一个维护插入顺序的链表),而TreeSet性能排最后,因为需要时刻维护红黑树的结构已达到有序状态(大小顺序);
2) 至于碰到一个问题该选择何种Set的实现类就很简单了,关键看你是什么需求,枚举就用EnumSet,仅仅是一个无需集合就用HashSet,需要维护插入顺序就用LinkedHashSet,需要维护大小顺序的就只能用TreeSet了!