在尽可能短的篇幅里,将所有集合与并发集合的特征、实现方式、性能捋一遍。适合所有"精通Java",其实还不那么自信的人阅读。 【转自:花钱的年华

期望能不止用于面试时,平时选择数据结构,也能考虑一下其成本与效率,不要看着API合适就用了。

1.List

1.1 ArrayList

以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组。因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。

按数组下标访问元素-get(i)、set(i,e) 的性能很高,这是数组的基本优势。

如果按下标插入元素、删除元素-add(i,e)、 remove(i)、remove(e),则要用System.arraycopy()来复制移动部分受影响的元素,性能就变差了。

越是前面的元素,修改时要移动的元素越多。直接在数组末尾加入元素-常用的add(e),删除最后一个元素则无影响。

1.2 LinkedList

以双向链表实现。链表无容量限制,但双向链表本身使用了更多空间,每插入一个元素都要构造一个额外的Node对象,也需要额外的链表指针操作。

按下标访问元素-get(i)、set(i,e) 要悲剧的部分遍历链表将指针移动到位 (如果i>数组大小的一半,会从末尾移起)。

插入、删除元素时修改前后节点的指针即可,不在需要复制移动。但还是要部分遍历链表的指针才能移动到下标所指的位置。

只有在链表两头的操作-add()、addFirst()、removeLast()或用iterator()上的remove()倒能省掉指针的移动。

Apache Commons 有个TreeNodeList,里面是棵二叉树,可以快速移动指针到位。

1.3 CopyOnWriteArrayList

并发优化的ArrayList。基于不可变对象策略,在修改时先复制出一个数组快照来修改,改好了,再让内部指针指向新数组。

因为对快照的修改对读操作来说不可见,所以读读之间不互斥,读写之间也不互斥,只有写写之间要加锁互斥。但复制快照的成本昂贵,典型的适合读多写少的场景。

虽然增加了addIfAbsent(e)方法,会遍历数组来检查元素是否已存在,性能可想像的不会太好。

1.4 遗憾

无论哪种实现,按值返回下标contains(e), indexOf(e), remove(e) 都需遍历所有元素进行比较,性能可想像的不会太好。

没有按元素值排序的SortedList。

除了CopyOnWriteArrayList,再没有其他线程安全又并发优化的实现如ConcurrentLinkedList。凑合着用Set与Queue中的等价类时,会缺少一些List特有的方法如get(i)。如果更新频率较高,或数组较大时,还是得用Collections.synchronizedList(list),对所有操作用同一把锁来保证线程安全。


2.Map

2.1 HashMap

以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。

插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),我们称之为哈希冲突。

JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。查找哈希值为17的key时,先定位到哈希桶,然后链表遍历桶里所有元素,逐个比较其Hash值然后key值。

在JDK8里,新增默认为8的閥值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。

当然,最好还是桶里只有一个元素,不用去比较。所以默认当Entry数量达到桶数量的75%时,哈希冲突已比较严重,就会成倍扩容桶数组,并重新分配所有原来的Entry。扩容成本不低,所以也最好有个预估值。

取模用与操作(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方, 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。

iterator()时顺着哈希桶数组来遍历,看起来是个乱序。

2.2 LinkedHashMap

扩展HashMap,每个Entry增加双向链表,号称是最占内存的数据结构。

支持iterator()时按Entry的插入顺序来排序(如果设置accessOrder属性为true,则所有读写访问都排序)。

插入时,Entry把自己加到Header Entry的前面去。如果所有读写访问都要排序,还要把前后Entry的before/after拼接起来以在链表中删除掉自己,所以此时读操作也是线程不安全的了。

2.3 TreeMap

以红黑树实现,红黑树又叫自平衡二叉树:

对于任一节点而言,其到叶节点的每一条路径都包含相同数目的黑结点。

上面的规定,使得树的层数不会差的太远,使得所有操作的复杂度不超过 O(lgn),但也使得插入,修改时要复杂的左旋右旋来保持树的平衡。

支持iterator()时按Key值排序,可按实现了Comparable接口的Key的升序排序,或由传入的Comparator控制。可想象的,在树上插入/删除元素的代价一定比HashMap的大。

支持SortedMap接口,如firstKey(),lastKey()取得最大最小的key,或sub(fromKey, toKey), tailMap(fromKey)剪取Map的某一段。

2.4 EnumMap

EnumMap的原理是,在构造函数里要传入枚举类,那它就构建一个与枚举的所有值等大的数组,按Enum. ordinal()下标来访问数组。性能与内存占用俱佳。

美中不足的是,因为要实现Map接口,而 V get(Object key)中key是Object而不是泛型K,所以安全起见,EnumMap每次访问都要先对Key进行类型判断,在JMC里录得不低的采样命中频率。

2.5 ConcurrentHashMap

并发优化的HashMap。

在JDK7里的经典设计,默认16把写锁(可以设置更多),有效分散了阻塞的概率。数据结构为Segment[],每个Segment一把锁。Segment里面才是哈希桶数组。Key先算出它在哪个Segment里,再去算它在哪个哈希桶里。

也没有读锁,因为put/remove动作是个原子动作(比如put的整个过程是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态。

但在JDK8里,Segment[]的设计被抛弃了,改为精心设计的,只在需要锁的时候加锁。

支持ConcurrentMap接口,如putIfAbsent(key,value)与相反的replace(key,value)与以及实现CAS的replace(key, oldValue, newValue)。

2.6 ConcurrentSkipListMap

JDK6新增的并发优化的SortedMap,以SkipList结构实现。Concurrent包选用它是因为它支持基于CAS的无锁算法,而红黑树则没有好的无锁算法。

原理上,可以想象为多个链表组成的N层楼,其中的元素从稀疏到密集,每个元素有往右与往下的指针。从第一层楼开始遍历,如果右端的值比期望的大,那就往下走一层,继续往前走。

aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkJDQzA1MTVGNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkJDQzA1MTYwNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkNDMDUxNUQ2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkNDMDUxNUU2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6p+a6fAAAAD0lEQVR42mJ89/Y1QIABAAWXAsgVS/hWAAAAAElFTkSuQmCC" alt="" data-s="300,640" data-type="jpeg" data-src="http://mmbiz.qpic.cn/mmbiz_jpg/TBsh7UONeksIQq2vh9BS2Tboqic8mFVJJ0rDk6aib5R5ynibKXszFv3GtEia2CFnFKbmB12iaCibFviazyiacp12xecTFw/0?wx_fmt=jpeg" data-ratio="0.40083507306889354" data-w="479" />

典型的空间换时间。每次插入,都要决定在哪几层插入,同时,要决定要不要多盖一层楼。

它的size()同样不能随便调,会遍历来统计。


3.Set

所有Set几乎都是内部用一个Map来实现, 因为Map里的KeySet就是一个Set,而value是假值,全部使用同一个Object即可。

Set的特征也继承了那些内部的Map实现的特征。

HashSet:内部是HashMap。

LinkedHashSet:内部是LinkedHashMap。

TreeSet:内部是TreeMap的SortedSet。

ConcurrentSkipListSet:内部是ConcurrentSkipListMap的并发优化的SortedSet。

CopyOnWriteArraySet:内部是CopyOnWriteArrayList的并发优化的Set,利用其addIfAbsent()方法实现元素去重,如前所述该方法的性能很一般。

好像少了个ConcurrentHashSet,本来也该有一个内部用ConcurrentHashMap的简单实现,但JDK偏偏没提供。Jetty就自己简单封了一个,Guava则直接用java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 实现。


4.Queue

Queue是在两端出入的List,所以也可以用数组或链表来实现。

4.1 普通队列

4.1.1 LinkedList
是的,以双向链表实现的LinkedList既是List,也是Queue。

4.1.2 ArrayDeque
以循环数组实现的双向Queue。大小是2的倍数,默认是16。

为了支持FIFO,即从数组尾压入元素(快),从数组头取出元素(超慢),就不能再使用普通ArrayList的实现了,改为使用循环数组。

有队头队尾两个下标:弹出元素时,队头下标递增;加入元素时,队尾下标递增。如果加入元素时已到数组空间的末尾,则将元素赋值到数组[0],同时队尾下标指向0,再插入下一个元素则赋值到数组[1],队尾下标指向1。如果队尾的下标追上队头,说明数组所有空间已用完,进行双倍的数组扩容。

4.1.3 PriorityQueue
用平衡二叉最小堆实现的优先级队列,不再是FIFO,而是按元素实现的Comparable接口或传入Comparator的比较结果来出队,数值越小,优先级越高,越先出队。但是注意其iterator()的返回不会排序。

平衡最小二叉堆,用一个简单的数组即可表达,可以快速寻址,没有指针什么的。最小的在queue[0] ,比如queue[4]的两个孩子,会在queue[2*4+1] 和 queue[2*(4+1)],即queue[9]和queue[10]。

入队时,插入queue[size],然后二叉地往上比较调整堆。

出队时,弹出queue[0],然后把queque[size]拿出来二叉地往下比较调整堆。

初始大小为11,空间不够时自动50%扩容。

4.2 线程安全的队列

4.2.1 ConcurrentLinkedQueue/Deque
无界的并发优化的Queue,基于链表,实现了依赖于CAS的无锁算法。

ConcurrentLinkedQueue的结构是单向链表和head/tail两个指针,因为入队时需要修改队尾元素的next指针,以及修改tail指向新入队的元素两个CAS动作无法原子,所以需要的特殊的算法。

4.2.2 PriorityBlockingQueue
无界的PriorityQueue,也是基于数组存储的二叉堆。一把公共的锁实现线程安全。虽然实现了BlockingQueue接口,但因为无界,其实没有任何阻塞队列的特征,空间不够时会自动扩容。

4.2.3 DelayQueue
内部包含一个PriorityQueue,同样是无界的。一把公共的锁实现线程安全。元素需实现Delayed接口,每次调用时需返回当前离触发时间还有多久,小于0表示该触发了。

pull()时会用peek()查看队头的元素,检查是否到达触发时间。ScheduledThreadPoolExecutor用了类似的结构。

4.3 线程安全的阻塞队列

BlockingQueue的队列长度受限,用以保证生产者与消费者的速度不会相差太远,避免内存耗尽。队列长度设定后不可改变。当入队时队列已满,或出队时队列已空,不同函数的效果见下表:

  立刻报异常 立刻返回布尔 阻塞等待 可设定等待时间
入队 add(e) offer(e) put(e) offer(e, timeout, unit)
出队 remove() poll() take() poll(timeout, unit)
查看 element() peek()

4.3.1 ArrayBlockingQueue

定长的并发优化的BlockingQueue,也是基于循环数组实现。有一把公共的锁与notFull、notEmpty两个Condition管理队列满或空时的阻塞状态。

4.3.2 LinkedBlockingQueue/Deque

可选定长的并发优化的BlockingQueue,基于链表实现,所以可以把长度设为Integer.MAX_VALUE成为无界无等待的。

利用链表的特征,分离了takeLock与putLock两把锁,继续用notEmpty、notFull管理队列满或空时的阻塞状态。

4.4 同步队列

SynchronousQueue同步队列本身无容量,放入元素时,比如等待元素被另一条线程的消费者取走再返回。JDK线程池里用它。

JDK7还有个LinkedTransferQueue,在普通线程安全的BlockingQueue的基础上,增加一个transfer(e) 函数,效果与SynchronousQueue一样。


5. 参考文档

    • 红黑树: https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/03.01.md

    • 跳表:http://blog.sina.com.cn/s/blog_72995dcc01017w1t.html

    • 二叉堆:http://blog.csdn.net/lcore/article/details/9100073

    • ConcurrentLinkedQueue:http://www.ibm.com/developerworks/cn/java/j-jtp04186/

Java集合的小抄的更多相关文章

  1. 关于Java集合的小抄

    在尽可能短的篇幅里,将所有List.Map.Set.Queue的特征与实现方式捋一遍.适合所有"精通Java"其实还不那么自信的人阅读. List ArrayList 以数组实现. ...

  2. Java集合的小抄 Java初学者必备

    在尽可能短的篇幅里,将所有集合与并发集合的特征,实现方式,性能捋一遍.适合所有”精通Java”其实还不那么自信的人阅读. 不断更新中,请尽量访问博客原文. List ArrayList 以数组实现.节 ...

  3. 关于Java集合的小抄--转

    原文地址:http://calvin1978.blogcn.com/articles/collection.html 在尽可能短的篇幅里,将所有集合与并发集合的特征.实现方式.性能捋一遍.适合所有&q ...

  4. java集合-- arraylist小员工项目

    import java.io.*; import java.util.ArrayList; public class Emexe { public static void main(String[] ...

  5. Java集合框架:HashMap

    转载: Java集合框架:HashMap Java集合框架概述   Java集合框架无论是在工作.学习.面试中都会经常涉及到,相信各位也并不陌生,其强大也不用多说,博主最近翻阅java集合框架的源码以 ...

  6. Java集合关于ArrayList

    ArrayList实现源码分析 2016-04-11 17:52 by 淮左, 207 阅读, 0 评论, 收藏, 编辑 本文将以以下几个问题来探讨ArrayList的源码实现1.ArrayList的 ...

  7. Java基础19:Java集合框架梳理

    更多内容请关注微信公众号[Java技术江湖] 这是一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM.SpringBoot.MySQL.分布式.中间件.集群.Linux ...

  8. Java集合,扑克牌的小项目练习

    Java集合,扑克牌的小项目练习 2小时学完了类与集合,一直二倍加跳过,集合和类的学习我觉得得多实践中去记住,光靠背,永远也背不完,学的时候记一下常用的,特殊的就行了,用的时候再查,多写代码才能会,哈 ...

  9. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

随机推荐

  1. ASP.NET页面与IIS底层交互和工作原理详解(第二回)

    引言 在 Part.1 Http请求处理流程 一文中,我们了解了Http请求的处理过程以及其它一些运作原理.我们知道Http管道中有两个可用接口,一个是IHttpHandler,一个是IHttpMod ...

  2. 树形菜单复选框级联选择HTML

    模块标题 标识符 类别 链接 排序 系统管理   组   1 用户权限设定 Sys_UserModelConfigList 模块 Sys_UserModelConfigList.aspx 1 角色管理 ...

  3. Shell学习笔记 - 正则表达式

    一.正则表达式是什么? 正则表达式是用于描述字符排列和匹配模式的一种语法规则.它主要用于字符串的模式分割.匹配.查找及替换操作. 二.正则表达式与通配符 1. 正则表达式 用来在文件中匹配符合条件的字 ...

  4. 跨域 HTTP 请求

    如果你需要从不同的服务器(不同域名)上获取数据就需要使用跨域 HTTP 请求. 跨域请求在网页上非常常见.很多网页从不同服务器上载入 CSS, 图片,Js脚本等. 在现代浏览器中,为了数据的安全,所有 ...

  5. JAVA IO流的简单总结+收集日志异常信息

    1.字节流: IuputStream 所有字节输出流的超类 . 抽象类 ---- FileInputStream ---- BufferedInputStream:提供数据的读取效率,拓展方法(内部维 ...

  6. ASP.NET,web.config 中SessionState的配置

    web Form 网页是基于HTTP的,它们没有状态, 这意味着它们不知道所有的请求是否来自同一台客户端计算机,网页是受到了破坏,以及是否得到了刷新,这样就可能造成信息的丢失. 于是, 状态管理就成了 ...

  7. Android之图片颜色调节

    package net.hnjdzy.imagecolor; import android.os.Bundle; import android.app.Activity; import android ...

  8. apache Internal Server Error 的几个问题

    Internal Server Error The server encountered an internal error or misconfiguration and was unable to ...

  9. php中simplexml_load_string使用实例

    先用一段代码重现一下问题 乍一看,结果很让人费解: 代码如下 复制代码 <?php $string = <<<EOF <data> <foo><b ...

  10. iOS - 数组(NSArray)

    1. 数组的常用处理方式 //--------------------不可变数组 //1.数组的创建 NSString *s1 = @"zhangsan"; NSString *s ...