Java -集合总结

1、List

1.1、ArrayList

ArrayList 底层通过数组实现,默认长度为10,超出容量的时候以容量的50%扩容,并通过System.arraycopy()赋值到新数组;所以在使用ArrayList的时候要预估好容量,尽量避免扩容。

由于采用数组实现,自然继承了数组的特点,通过下标来查找元素时效率很高,例如get(index);

但是在插入和删除元素时,会使用System.arraycopy()来复制被影响的部分,这样的操作很影响性能,越是操作下标靠前的元素,效率越差。

ArrayList是jdk1.2中实现的,它实现了List接口,是线程非安全的,在多个线程同时操作的时候可能会出现线程安全问题。

 

1.2、LinkedList

LinkedList是一个双向链表结构。链表容量无限,但双向链表本身占用了更多空间,每插入一个元素都要构造一个额外的Node对象,并且也需要额外的链表指针操作。

在根据下标获取元素时,要遍历部分链表来移动指针,如果i>数组长度的一半,会从末尾移起。

插入或者删除元素时,只需要修改前后节点的指针即可,不需要复制移动,但还是要遍历部分链表来移动指针。只有在头尾操作的时候才不需要移动指针。

LinkedList也是在jdk1.2开始才提供的,它也实现了List接口,同样是线程非安全的集合。

1.3、CopyOnWriteArrayList

并发优化的ArrayList,基于不可变对象策略,在修改的时候先复制出一个数组快照来修改,改好了再让内部指针指向新数组,因为对快照的修改对于读来说不可见,所以读与读之间不互斥,读与写之间也不互斥,只有写与写之间要加锁互斥,但复制快照的成本昂贵,适应于读多写少的场景,在增加元素的时候仍然会遍历数组来检查元素是否存在。性能可想而知。

2、Map

2.1、HashMap

以Entity[]数组的形式形式实现的Hash桶数组,用key的hash值(除留取模法)来计算得到元素的下标,从而找到元素在位置,查找元素效率高。

在插入元素的时候,如果两个元素的取模后的下标相同,称之为hash冲突,jdk的做法是复制该位置的元素到Entity,并通过entity的属性next指向该元素,然后将新值复制到原来的位置。

在取元素的时候,先定位到hash桶,然后链表遍历所有的元素,再根据hash值取出key值。

在jdk1.8新增了阈值8,当链表的长度超过8的时候,不在以单向链表的方式来存储,而是以红黑树的方式来存储,从而提高读取的效率。

当桶的容量达到了桶容量x负载因子(默认为0.75),此时的hash冲突比较严重,会成倍的扩展桶容量,会重新打散Entity,进行Map重组。所以扩容的成本比较高,建议在使用Map之前要预估好容量,默认容量为16,当你设置的容量不是2的幂次方的时候,map会调整为比设置的容量大的,最接近的2的幂次方容量。

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。

在JDK5里的经典设计,默认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层楼,其中的元素从稀疏到密集,每个元素有往右与往下的指针。从第一层楼开始遍历,如果右端的值比期望的大,那就往下走一层,继续往前走。

 

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

它的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.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.3.3 PriorityBlockingQueue

无界的PriorityQueue,也是基于数组存储的二叉堆(见前)。一把公共的锁实现线程安全。因为无界,空间不够时会自动扩容,所以入列时不会锁,出列为空时才会锁。

 

4.3.4 DelayQueue

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

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

4.4 同步队列

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

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

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值