Java:集合类的数据结构

本文源自参考《Think in Java》,多篇博文以及阅读源码的总结java

前言

Java的集合其实就是各类基本的数据结构(栈,队列,hash表等),基于业务需求进而演变出的Java特有的数据结构(由于不只仅是基本数据结构)。如今,咱们以数据结构的视角来看看Java的集合究竟是什么样子。并分析他们的性能。数组

一 JAVA集合体系

JAVA的集合体系分为两类,Collection接口和Map接口数据结构

主要分为三种:并发

  • Set。无插入顺序的不重复数据集接口(集合演变而来)
  • List。有插入顺序的数据集接口(队列演变而来)
  • Map。Key-Value的键值对数据集接口(Hash表演变而来)

其中Set和List继承自Collection接口,Map则就是Map接口。性能

接口中都定义了一些基本增删改查的方法。优化

具体继承体系以下图:3d

基本能够从名字知道集合的内部数据结构。blog

  • 看后缀,有Set,List,Map后缀的集合,表明着该集合的基本结构,因此会具备以上所说的特性。
  • 看前缀,前缀每每表明着该数据结构的具体实现方式。通常有这几种:
    1. Hash或者Array,表明着以哈希(基本)数组实现的数据结构。
    2. Linked,表明着集合内各个数据之间存在链表关系。
    3. Tree或者Sorted,表明着内部使用红黑树实现了排序。(须要提供Comparator或者实现Comparable)

下面大略说下每一个集合的数据结构,懒得贴源码了。排序

1.1 List

最经常使用的List就是ArrayList和LinkedList了,在此不讨论并发的List集合。
讨论下底层源码对它们的具体实现。继承

1.1.1 ArrayList

使用JAVA的基本数组实现的动态数组集合,源码底层维护着List的容量与实际长度

由于使用的基本数组,不像哈希数组同样须要考虑哈希碰撞问题,所以负载因子默认为1。当List数组容量不够时才进行扩容,扩容的倍数为1.5倍

经过Arrays.copyOf方法,返回复制的新数组。Arrays.copyOf底层调用的System.arraycopy方法。而在ArrayList初始化时,若是不指定初始数组长度,在JDK1.6以后默认初始长度为0,在JDK1.6以前则默认为10。在JDK1.6后,ArrayList在第一次扩容时,若是扩容长度不足10,则会直接扩容到10。

具体集合怎么使用就不废话了。

1.1.2 LinkedList

这是一个双向链表,其中节点用的是LinkedList的内部类。和数据结构中的链表差很少。能够用它实现栈和队列。

1.1.3 ArrayList与LinkedList比较

很明显,ArrayList是某种程度上的哈希表,适合随机读,可是不适合在集合中间插入和删除(会形成后续数据的位移)。
而LinkedList适合在头尾部插入删除,不适合随机读。

值得一提的是ArrayList随机读的时间复杂度是O(1),LinkedList是O(n)。而ArrayList在中间插入和删除的时间复杂度是O(n),LinekdList在中间插入删除时间复杂度也是O(n)

能够明显看出来ArrayList在插入删除上和LinkedList理论上所用的时间是一个级别的,可是ArrayList慢于LinkedList是由于在修改集合后须要进行其余数组数据的移动,而LinkedList则是查找节点花费了O(n),不须要额外移动数据,因此在一样数据量时,LinkedList进行数据修改优于ArrayList。

1.2 Map

最经常使用的Map就是HashMap和TreeMap。

1.2.1 HashMap

HashMap是底层用哈希数组实现的Map。HashMap就是一个个Entry(Key-Value键值对)存储在一个哈希数组上(Entry是HashMap的内部类)。

哈希数组的使用不可避免的须要考虑哈希碰撞问题,经常使用的解决方案有:

  • 拉链法
  • 再哈希法
  • 开放地址法
  • 创建公共溢出区。

在JDK里,使用的就是拉链法解决的哈希碰撞问题,所以每一个哈希数组上的数组元素(又被称为桶——bucket),都是一个链表的表头。这样基本保证了HashMap的平均查找时间是O(1)。

HashMap的负载因子为0.75

可是当出现频繁哈希碰撞时,会致使某个链表过长进而致使了查找时间会趋近于O(n)。对此JDK本来的解决方案是设置负载因子为0.75。当哈希表总负载量达到0.75时,就会进行扩容,扩容为本来的2倍。这样当数据平均下来后,不太容易出现过长的链表(由于扩容会分解链表从新放入桶中)。

可是这并无解决特殊状况下查找效率的问题,只是让这种特殊状况更难以出现了。

JDK1.8中 HashMap出现了红黑树

所以在JDK1.8中又作出了改进,当某个桶中的链表的长度大于8时。链表会重构成一个红黑树。这样保证了HashMap的最坏时间复杂度也仅仅是O(logn)。同时负载因子引发的扩容也保证了红黑树的重构不会频繁发生,不会由于频繁建树致使过多的性能开销。

HashMap的初始化与扩容

另外值得一说的就是HashMap在不知道初始长度进行初始化时,JDK1.6前默认长度为16,JDK1.6后默认长度为0。基本在JDK1.6中,须要初始化底层容器的集合都作出了这种优化。不会提早构造底层容器形成开销,会等到使用时才进行底层的初始化。

而HashMap默认长度设置为16,而且每次扩容都是2倍。这是为了方便底层的哈希数组进行取模时的运算,能够把取模的除法运算改写成位移运算,提高性能。

而且在JDK1.8中,HashMap关于取模运算还作了另外一个优化。在JDK1.8以前,每次哈希数组扩容时,链表里的数据都会再次进行哈希运算。而在JDK1.8后,不须要再进行运算了,只须要在每一个桶中选择一半数据日后移动oldLength位就行(oldLength是集合在扩容前的容量)。

1.2.2 TreeMap

而另外一个经常使用的Map——TreeMap,底层就是用JAVA写了一个红黑树,感受没什么好说的。有兴趣的能够回去翻翻数据结构的书。

1.2.3 LinkedHashMap

HashMap的每一个Node还会以插入顺序相互关联成为双向链表。

1.3 Set

Set主要是SortedSet和HashSet。打开源码一看,分别new了一个TreeMap和HashMap,而后把数据存在了Key里。嗯,这就是Set的底层实现了。

相关文章
相关标签/搜索