Java提供的众多集合框架由两大接口衍生而来:Collection接口和Map接口
Collection接口定义了一个包含一批对象的集合。接口的主要方法包括:
- size() - 集合内的对象数量
- add(E)/addAll(Collection) - 向集合内添加单个/批量对象
- remove(Object)/removeAll(Collection) - 从集合内删除单个/批量对象
- contains(Object)/containsAll(Collection) - 判断集合中是否存在某个/某些对象
- toArray() - 返回包含集合内所有对象的数组
等
Map接口在Collection的基础上,为其中的每个对象指定了一个key,并使用Entry保存每个key-value对,以实现通过key快速定位到对象(value)。Map接口的主要方法包括:
- size() - 集合内的对象数量
- put(K,V)/putAll(Map) - 向Map内添加单个/批量对象
- get(K) - 返回Key对应的对象
- remove(K) - 删除Key对应的对象
- keySet() - 返回包含Map中所有key的Set
- values() - 返回包含Map中所有value的Collection
- entrySet() - 返回包含Map中所有key-value对的EntrySet
- containsKey(K)/containsValue(V) - 判断Map中是否存在指定key/value
等
在了解了Collection和Map两大接口之后,我们再来看一下这两个接口衍生出来的常用集合:
List
List接口继承了Collection,用于定义以列表形式存储的集合,List接口为集合中的每个对象分配了一个索引(index),标记该对象在List中的位置,并可以通过index定位到指定位置的对象。
List在Collection基础上增加的主要方法包括:
- get(int) - 返回指定index位置上的对象
- add(E)/add(int, E) - 在List末尾/指定index位置上插入一个对象
- set(int, E) - 替换置于List指定index位置上的对象
- indexOf(Object) - 返回指定对象在List中的index位置
- subList(int,int) - 返回指定起始index到终止index的子List对象
等
List接口的常用实现类:
ArrayList
ArrayList基于数组来实现集合的功能,其内部维护了一个可变长的对象数组,集合内所有对象存储于这个数组中,并实现该数组长度的动态伸缩
LinkedList
LinkedList基于链表来实现集合的功能,其实现了静态类Node,集合中的每个对象都由一个Node保存,每个Node都拥有到自己的前一个和后一个Node的引用
- ArrayList的寻址效率更高,基于数组实现的ArrayList可直接定位到目标对象,而LinkedList需要从头Node或尾Node开始向后/向前遍历若干次才能定位到目标对象
- LinkedList的插入、删除和顺序遍历的效率更高,因为LinkedList的每一个Node都拥有上一个和下一个Node的引用。但需要注意,遍历LinkedList时应用iterator方式,不要用get(int)方式,否则效率会很低
Vector
Vector和ArrayList很像,都是基于数组实现的集合,它和ArrayList的主要区别在于
- Vector是线程安全的,而ArrayList不是
- 由于Vector中的方法基本都是synchronized的,其性能低于ArrayList
- Vector可以定义数组长度增长的因子,ArrayList不能
CopyOnWriteArrayList
与 Vector一样,CopyOnWriteArrayList也可以认为是ArrayList的线程安全版,不同之处在于 CopyOnWriteArrayList在写操作时会先复制出一个副本,在新副本上执行写操作,然后再修改引用。这种机制让 CopyOnWriteArrayList可以对读操作不加锁,这就使CopyOnWriteArrayList的读效率远高于Vector。 CopyOnWriteArrayList的理念比较类似读写分离,适合读多写少的多线程场景。但要注意,CopyOnWriteArrayList只能 保证数据的最终一致性,并不能保证数据的实时一致性,执行读操作时是有可能会读到失效的数据的。
- 二者均是线程安全的、基于数组实现的List
- Vector读写均是线程安全的,CopyOnWriteArrayList不能保证读的实时线程安全
- CopyOnWriteArrayList读性能远高于Vector
- CopyOnWriteArrayList占用更多的内存空间
Map
前文已经对Map接口的基本特点进行过描述,我们直接来看一下Map接口的常用实现类
Map接口的常用实现类:
HashMap
前文提到过,Map将每一个key-value对存储在一个Entry对象中。HashMap将Entry对象存储在一个数组中,并通过哈希表来实现对Entry的快速访问:
由每个Entry中的key的哈希值决定该Entry在数组中的位置。以这种特性能够实现通过key快速查找到Entry,从而获得该key对应的value。在不发生哈希冲突的前提下,查找的时间复杂度是O(1)。
如 果两个不同的key计算出的index是一样的,就会发生两个不同的key都对应到数组中同一个位置的情况,也就是所谓的哈希冲突。HashMap处理哈 希冲突的方法是拉链法,也就是说数组中每个位置保存的实际是一个Entry链表,链表中每个Entry都拥有指向链表中后一个Entry的引用。在发生哈 希冲突时,将冲突的Entry追加至链表的末尾。当HashMap在寻址时发现某个key对应的数组index上有多个Entry,便会遍历该位置上的 Entry链表,直到找到目标的Entry。
HashMap的Entry类:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; }
HashMap由于其快速寻址的特点,可以说是最经常被使用的Map实现类
Hashtable
Hashtable 可以说是HashMap的前身(Hashtable自JDK1.0就存在,而HashMap乃至整个Map接口都是JDK1.2引入的新特性),其实现思 路与HashMap几乎完全一样,都是通过数组存储Entry,以key的哈希值计算Entry在数组中的index,用拉链法解决哈希冲突。二者最大的 不同在于,Hashtable是线程安全的,其提供的方法几乎都是同步的。
ConcurrentHashMap
ConcurrentHashMap是HashMap的线程安全版(自JDK1.5引入),提供比Hashtable更高效的并发性能。
Hashtable 在进行读写操作时会锁住整个Entry数组,这就导致数据越多性能越差。而ConcurrentHashMap使用分离锁的思路解决并发性能,其将 Entry数组拆分至16个Segment中,以哈希算法决定Entry应该存储在哪个Segment。这样就可以实现在写操作时只对一个Segment 加锁,大幅提升了并发写的性能。
在进行读操作时,ConcurrentHashMap在绝大部分情况下都不需要加锁,其Entry中的value是volatile的,这保证了value被修改时的线程可见性,无需加锁便能实现线程安全的读操作。
ConcurrentHashMap的HashEntry类:
static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; }
但 是鱼与熊掌不可兼得,ConcurrentHashMap的高性能是有代价的(否则Hashtable就没有存在价值了),那就是它不能保证读操作的绝对 一致性。ConcurrentHashMap保证读操作能获取到已存在Entry的value的最新值,同时也能保证读操作可获取到已完成的写操作的内容,但如果写操作是在创建一个新的Entry,那么在写操作没有完成时,读操作是有可能获取不到这个Entry的。
- 三者在数据存储层面的机制基本一致
- HashMap不是线程安全的,多线程环境下除了不能保证数据一致性之外,还有可能引发Entry链表成环,导致get方法死循环
- Hashtable是线程安全的,能保证绝对的数据一致性,但是由于其粗暴地将所有操作加锁,性能低下
- ConcurrentHashMap 也是线程安全的,使用分离锁和volatile等方法极大地提升了读写性能,同时也能保证在绝大部分情况下的数据一致性。但其不能保证绝对的数据一致性, 在一个线程向Map中加入Entry的操作没有完全完成之前,其他线程有可能读不到新加入的Entry。
LinkedHashMap
LinkedHashMap与HashMap非常类似,唯一的不同在于前者的Entry在HashMap.Entry的基础上增加了到前一个插入和后一个插入的Entry的引用,以实现能够按Entry的插入顺序进行遍历。
TreeMap
TreeMap是基于红黑树实现的Map结构,其Entry类拥有到左/右叶子节点和父节点的引用,同时还记录了自己的颜色:
static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left = null; Entry<K,V> right = null; Entry<K,V> parent; boolean color = BLACK; }
红黑树实际是一种算法复杂但高效的平衡二叉树,具备二叉树的基本性质,即任何节点的值大于其左叶子节点,小于其右叶子节点,利用这种特性,TreeMap能够实现Entry的排序和快速查找。
关于红黑树的具体介绍,可以参考这篇文章,非常详细:http://blog.csdn.net/chenssy/article/details/26668941
TreeMap的Entry是有序的,所以提供了一系列方便的功能,比如获取以升序或降序排列的KeySet(EntrySet)、获取在指定key(Entry)之前/之后的key(Entry)等等。适合需要对key进行有序操作的场景。
ConcurrentSkipListMap
ConcurrentSkipListMap同样能够提供有序的Entry排列,但其实现原理与TreeMap不同,是基于跳表(SkipList)的:
如上图所示,ConcurrentSkipListMap由一个多级链表实现,底层链上拥有所有元素,逐级上升的过程中每个链的元素数递减。在查找时从顶层链出发,按先右后下的优先级进行查找,从而实现快速寻址。
static class Index<K,V> { final Node<K,V> node; final Index<K,V> down;//下引用 volatile Index<K,V> right;//右引用 }
与TreeMap不同,ConcurrentSkipListMap在进行插入、删除等操作时,只需要修改影响到的节点的右引用,而右引用又是volatile的,所以ConcurrentSkipListMap是线程安全的。但ConcurrentSkipListMap与ConcurrentHashMap一样,不能保证数据的绝对一致性,在某些情况下有可能无法读到正在被插入的数据。
- 二者都能够提供有序的Entry集合
- 二者的性能相近,查找时间复杂度都是O(logN)
- ConcurrentSkipListMap会占用更多的内存空间
- ConcurrentSkipListMap是线程安全的,TreeMap不是
Set
Set 接口继承Collection,用于存储不含重复元素的集合。所有的Set实现都是基于同类型Map的,简单地说,Set是阉割版的Map。每一个Set 内都有一个同类型的Map实例,Set把元素作为key存储在自己的Map实例中,value则是一个空的Object。Set的常用实现也包括 HashSet、TreeSet、ConcurrentSkipListSet等,原理和对应的Map实现完全一致,此处不再赘述。
Queue
Queue接口继承Collection接口,实现FIFO(先进先出)的集合。Queue接口的常用方法包括:
- add(E)/offer(E):入队,即向队尾追加元素,二者的区别在于如果队列是有界的,add方法在队列已满的情况下会抛出IllegalStateException,而offer方法只会返回false
- remove()/poll():出队,即从队头移除1个元素,二者的区别在于如果队列是空的,remove方法会抛出NoSuchElementException,而poll只会返回null
- element()/peek():查看队头元素,二者的区别在于如果队列是空的,element方法会抛出NoSuchElementException,而peek只会返回null
Queue接口的常用实现类:
ConcurrentLinkedQueue
ConcurrentLinkedQueue是基于链表实现的队列,队列中每个Node拥有到下一个Node的引用:
private static class Node<E> { volatile E item; volatile Node<E> next; }
由于Node类的成员都是volatile的,所以ConcurrentLinkedQueue自然是线程安全的。能够保证入队和出队操作的原子性和一致性,但在遍历和size()操作时只能保证数据的弱一致性。
LinkedBlockingQueue
与 ConcurrentLinkedQueue不同,LinkedBlocklingQueue是一种无界的阻塞队列。所谓阻塞队列,就是在入队时如果队列 已满,线程会被阻塞,直到队列有空间供入队再返回;同时在出队时,如果队列已空,线程也会被阻塞,直到队列中有元素供出队时再返回。LinkedBlocklingQueue同样基于链表实现,其出队和入队操作都会使用ReentrantLock进行加锁。所以本身是线程安全的,但同样的,只能保证入队和出队操作的原子性和一致性,在遍历时只能保证数据的弱一致性。
ArrayBlockingQueue
ArrayBlockingQueue是一种有界的阻塞队列,基于数组实现。其同步阻塞机制的实现与LinkedBlocklingQueue基本一致,区别仅在于前者的生产和消费使用同一个锁,后者的生产和消费使用分离的两个锁。
二者最大的区别在于ArrayBlockingQueue是有界的,适合实现定长的阻塞队列,LinkedBlocklingQueue是无界的,适合实现不限长度的阻塞队列。
- ConcurrentLinkedQueue是非阻塞队列,其他两者为阻塞队列
- 三者都是线程安全的,但都无法在遍历时保证数据的绝对一致性
- LinkedBlocklingQueue是无界的,适合实现不限长度的队列, ArrayBlockingQueue适合实现定长的队列
SynchronousQueue
SynchronousQueue算是JDK实现的队列中比较奇葩的一个,它不能保存任何元素,size永远是0,peek()永远返回null。向其中插入元素的线程会阻塞,直到有另一个线程将这个元素取走,反之从其中取元素的线程也会阻塞,直到有另一个线程插入元素。
这种实现机制非常适合传递性的场景。也就是说如果生产者线程需要及时确认到自己生产的任务已经被消费者线程取走后才能执行后续逻辑的场景下,适合使用SynchronousQueue。
PriorityQueue & PriorityBlockingQueue
这两种Queue并不是FIFO队列,而是根据元素的优先级进行排序,保证最小的元素最先出队,也可以在构造队列时传入Comparator实例,这样PriorityQueue就会按照Comparator实例的要求对元素进行排序。
PriorityQueue是非阻塞队列,也不是线程安全的,PriorityBlockingQueue是阻塞队列,同时也是线程安全的。
Deque
Deque继承了Queue接口,定义了双端队列,也就是说Deque可以从队头或队尾进行出队/入队操作。它比Queue更加灵活,可以用于实现Queue、Stack等数据结构。Deque在Queue的基础上提供了额外的方法:
- addFirst(E)/addLast(E)/offerFirst(E)/offerLast(E)
- removeFirst()/removeLast()/pollFirst()/pollLast()
- getFirst()/getLast()/peekFirst()/peekLast()
Deque 的实现类包括LinkedList(前文已描述过)、ConcurrentLinkedDeque、LinkedBlockingDeque,其实现机制 与前文所述的ConcurrentLinkedQueue和LinkedBlockingQueue非常类似,此处不再赘述
最后,对本文中描述的常用集合实现类做一个简单总结:
相关推荐
《Data Structures and the Java Collections Framework》这本书可能涵盖了如何有效地使用各种数据结构,如何根据需求选择合适的Java集合框架实现,以及如何通过这些工具解决实际问题。此外,它可能还会讨论并发编程...
Java 集合框架(JCF:Java Collections Framework)是 Java 语言中的一组类库,用于实现集合操作的统一标准。集合是计算机科学中的一种基本概念,来源于数学中的集合论。Java 集合框架的出现,使得程序员在编程时...
在 Java 领域,《Java Collection Framework》这本书被广泛认为是一本优秀的教程,尤其适合初学者了解集合框架的前世今生。通过本书的学习,读者不仅能掌握集合框架的基本概念,还能深入了解其内部结构与实现原理。 ...
- **集合框架**:Java集合框架提供了一系列接口和实现,用于处理集合和映射。 **关键接口:** - `Collection`:所有集合类的根接口。 - `Set`:不允许重复元素的集合。 - `List`:允许重复元素且有序的集合。 - `...
Java 集合框架 Java 集合框架是一个强大的框架,提供了各种集合类和接口,以方便开发者处理数据。它是 Java 语言的核心组件之一,广泛应用于各种应用程序中。 集合框架概述 集合框架是一个泛型系统,提供了一个...
数据结构是组织和存储数据的方式,而Java集合框架则提供了这些数据结构的实现,便于开发者在Java应用程序中使用。 数据结构主要包括数组、链表、栈、队列、树(如二叉树、红黑树)和图等。每种数据结构都有其特定的...
总的来说,Java集合框架是一个强大的工具,它简化了数据存储和操作,通过接口和实现类的层次结构提供了高度的灵活性。了解并熟练掌握这个框架,是任何Java开发者必备的技能之一。通过深入学习和实践,我们可以更好地...
4. **排序与比较**:Java集合框架提供了多种排序方式,例如`Collections.sort()`方法可以对List进行排序。为了支持自定义排序规则,Java还提供了`Comparator`接口,用户可以通过实现该接口来定义比较逻辑。 5. **...
Java集合框架(Java Collection Framework, JCF)正是为了满足这一需求而设计的一套规范和实现。 Java集合框架由一系列接口和其实现类组成,它们共同构成了用于存储和操作对象集合的标准库。Java程序员可以直接使用...
在 Java 中,集合框架(Java Collections Framework)是 Java 语言中的一种数据结构,可以用来存储和操作大量数据。集合框架提供了多种数据结构,如列表、集合、映射等,可以满足不同的应用需求。下面是集合框架的...
Java 作为一种广泛使用的编程语言,在其标准库中提供了丰富的集合框架(Collections Framework),用于处理各种数据结构。本文将基于《Java Collections》这本书中的内容,深入探讨 Java 集合框架中的关键概念和技术...
掌握Java集合框架中的三大类集合的特征和适用场合 掌握ArrayList类的使用 掌握HashMap类的使用 了解HashSet类的使用 掌握Collections类的使用 了解集合框架中的其它集合类 集合框架(Collection Framework) java.util...
在Java集合框架中,`Collections`工具类提供了大量静态方法,用于操作集合,如排序、查找、填充、反转等。而`Arrays`类则针对数组提供了类似的方法。 源码阅读对于深入理解Java集合框架至关重要。通过分析`...
在标题“APress Java Collections”和描述中,我们可以看出,这本书的...通过深入学习这本《APress Java Collections》,读者可以全面掌握Java集合框架的使用技巧和优化方法,提高数据结构处理的能力和软件开发效率。
Java集合框架包括一系列接口(如List、Set、Queue等)和实现这些接口的类(如ArrayList、HashSet、LinkedList等)。框架的设计使得各种集合可以相互转换,提供了丰富的操作集合的方法。 1. **接口**:例如,`List`...