集合框架
Posted baoziy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了集合框架相关的知识,希望对你有一定的参考价值。
- Java集合介绍
Java集合主要包含四部分:list、set、Map、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)
- Collection
a) Collection是一个接口 ,里边定义了关于集合的基本操作和属性。
b) Collection中包含了List和Set两个接口,这两个接口都继承于Collection,但是他们的实现类都是继承于AbstractCollection抽象类:
i. List是一个有序的集合,里边的元素是有序排列的,每一个元素都能通过索引值进行查找。List的实现类主要包括:ArrayList、LinkedList、Vector、Stack
ii. Set是一个不允许有重复元素的集合。Set的主要实现类包括:Hash Set和TreeSet,其中HashSet是通过HashMap实现的,而TreeSet是通过TreeMap实现的。
- Map
是一个映射接口,即key-value键值对。AbstractMap是个抽象类,它实现了Map接口中的大部分API。而HashMap,TreeMap,WeakHashMap都是继承于AbstractMap。Hashtable虽然继承于Dictionary,但它实现了Map接口。SortedMap 是继承于Map的接口。SortedMap中的内容是排序的键值对,排序的方法是通过比较器(Comparator)。
NavigableMap 是继承于SortedMap的接口。相比于SortedMap,NavigableMap有一系列的导航方法;如"获取大于/等于某对象的键值对"、“获取小于/等于某对象的键值对”等等。
Iterator迭代器是遍历集合的工具,之所以说Collection依赖于Iterator,是因为Collection的实现类都要实现iterator()函数,返回一个Iterator对象。ListIterator是专门为遍历List而存在的。
Enumeration,它是JDK 1.0引入的抽象类。作用和Iterator一样,也是遍历集合;但是Enumeration的功能要比Iterator少。在上面的框图中,Enumeration只能在Hashtable, Vector, Stack中使用。
- Arrays和Collections
它们是操作数组、集合的两个工具类。
- ArrayList
a) ArrayIist可以理解为一个动态数组,它的容量是可以动态扩展的。ArrayList继承于AbstractList(继承于AbstractCollection),实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
b) ArrayList相较于Vector是非线程安全的,一般只能在单线程中使用,多线程中一般使用Vector或者 CopyOnWriteArrayList。
c) ArrayList的内部实现细节:
i. ArrayList内部定义了一个Object类型的数组,存储添加到ArrayList中的元素。我们可以通过在定义ArrayList对象的时候指定容量大小,不指定的话默认容量是10.
ii. ArrayList中数组容量的大小是可以动态增长的,具体的增长是通过ensureCapacity(参数是需要的实际容量值)方法实现的。每次进行添加操作都会先确定当前数组的容量大小,保证能够装下新添加元素。若ArrayList的容量不足以容纳当前的全部元素,设置 新的容量=“(原始容量x3)/2 + 1”,判断此时的容量是否能够比ensureCapacity()传入的参数要大,不行的化就直接把ensureCapacity()中传入的参数作为数组的容量大小,然后在把原数组中的全部数据通过Arrays.copyOf进行拷贝。
d) ArrayList有三种遍历访问元素的方法:可以通过迭代器、可以进行随机访问、也可以使用for循环。一般随机访问的效率是最高的,效率最差的是使用迭代器。
e) ArrayList提供了2个toArray()函数:调用 toArray() 函数会抛出“java.lang.ClassCastException”异常,但是调用 toArray(T[] contents) 能正常返回 T[]。toArray() 会抛出异常是因为 toArray() 返回的是 Object[] 数组,将 Object[] 转换为其它类型会抛出“java.lang.ClassCastException”异常,因为Java不支持向下转型。解决该问题的办法是调用 <T> T[] toArray(T[] contents) , 而不是 Object[] toArray()。
- LinkedList
a) LinkedList是一个继承于AbstractSequentialList(该抽象类继承于AbstractList)本质是一个双向链表。所以它可以作为一个堆栈、队列或者双端队列来使用。LinkedList可以作为FIFO(先进先出)的队列, LinkedList可以作为LIFO(后进先出)的栈
b) LinkedList实现了Cloneable ,java.io.Serializable,List接口,但是没有实现randomAccess接口。依然支持随机访问,但它的随机访问是通过别的方法实现的,效率比较低。由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。
c) LinkedList本质是一个双向链表,所以不存在容量不足的问题,他有两个重要的成员变量:header是双向链表的头节点;size用来表示双向链表中节点的个数。双向链表中的每一个节点都是Entry类型的数据结构,该数据结构中包含三部分内容:指向前一个节点的指针、指向下一个节点的指针、节点值。
d) 它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
e) LinkedList虽然是一个双向链表,但是依然支持随机访问,那么它是如何实现的呢?它是通过一个计数索引值来实现的。例如,当我们调用get(int location)时,首先会比较“location”和“双向链表长度的1/2”;若前者大,则从链表头开始往后查找,直到location位置;否则,从链表末尾开始先前查找,直到location位置。这就是“双线链表和索引值联系起来”的方法。
f) LinkedList有多种访问遍历元素的方式:可以使用迭代器进行遍历、for循环、随机访问、还有作为队列的方法方式、以及作为栈的方法方式。不管如何尽量不要使用随机访问的方式,是几种遍历方法中效率最低的。
- Vector
a) Vector 是矢量队列,继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。但是没有实现serialization不支持序列化
b) Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。
c) 和ArrayList不同,Vector中的操作是线程安全的。
d) Vector的内部实现细节:
i. Vector的数据结构和ArrayList差不多,它包含了3个成员变量:elementData , elementCount, capacityIncrement。
ii. elementData 是"Object[]类型的数组",保存添加到Vector中的元素。elementData是个动态数组,初始化Vector时没指定动态数组大小,使用默认大小10。随着Vector中元素的增加,Vector的容量也会动态增长,capacityIncrement是与容量增长相关的增长系数,具体的增长方式由ensureCapacity()函数确定。
iii. elementCount 是动态数组的实际大小。
iv. capacityIncrement 是动态数组的增长系数。如果在创建Vector时,指定了capacityIncrement的大小;则每次当Vector中动态数组容量增加时,增加的大小都是capacityIncrement。
v. 具体的容量增加办法:每次添加元素之前都需要ensureCapacity(参数是需要的实际容量大小)函数确定容量的大小,当需要容量扩充的时候先判断capacityIncrement这个增长系数是否存在,如果存在就直接原来容量加上增长系数作为新数组的长度,没指定的话就直接原来容量*2作为新数组容量,然后在判断是否大于实际需要的容量,如果不满足,则直接把实际需要的容量作为新数组的长度。
e) 遍历方法有四种:迭代器、随机访问、for循环、enumeration枚举,其中使用索引的方法最快,迭代器是最慢的。
f) Vector本身是一个list,如果使用list来声明它,使用iterator进行迭代,此时的Vector就不是线程安全的啦。但是使用Vector的elements进行迭代就不会出现线程安全性问题
- HashMap
a)
b) HashMap 是一个散列表,它存储的内容是键值对(key-value)映射,实现了Map、Cloneable、java.io.Serializable接口。HashMap 的实现不是同步的,它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
c) HashMap中有两个参数影响其性能:“初始容量” 和 “加载因子”。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但也增加了查询成本,还要最大限度地减少 rehash 操作次数。
d) HashMap实现了Map、clonable、Serializable接口。
e) HashMap的内部实现细节:
i. HashMap内部存储键值对的是一个Entry[]数组(transient Entry[] table),其中每一个数据节点都是一个Entry(这是一个类,可以理解为是一个单链表,里面包含有k,v,hash值以及指向下一个的next)。
ii. 比较两个entry是否相等:先比较两个entry的K值是否相等,相等之后在比较两个entry的V值是否相等
iii. HashMap实际上就是一个散列表,通过“拉链法”解决哈希冲突。
iv. HashMap的默认容量是16,并且容量必须是2的幂,最大不能操作2的30次方。如果最大值传入过大,那么直接用最大值进行替换,如果指定的容量大小不是2的幂次方,那么就要找到比指定容量值大的最小的2的幂次方值作为容量值。
v. threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值="容量*加载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍
vi. 获取K和V的hashcode值都是直接使用Object中的hashcode值,但是在存入HashMap的时候还要经过一个Hash()方法的计算才能作为entry节点真正Hash值。
vii. 通过K获得V的过程:如果传入的K为null,这样的键值对指定存在Entry[]的第0个位置上,如果K不为null,那么计算出K的hashcode值还要经过一个hash方法进行处理,相当于是定位到某一个单链表,然后遍历找到对应的entry节点的hash值相等并且K也相等,然后返回当前节点的v值。
viii. HashMap中有put方法和PutForCreat方法,前一个是对外操作的方法,后一个是供内部调用的方法,这是一般创建HashMap对象的时候可能传入一个map的对象作为实参,使用内部方法直接添加到HashMap中。
ix. hashMap实现遍历时是通过先便利Entry[]第一个K不为空的entry开始先便利当前的entry(单链表),知道next为空的时候在指向下一个entry节点。
x. clear() 的作用是清空HashMap。它是通过将所有的元素设为null来实现的。
xi. 遍历方法分为不同的情况:遍历K的集合返回一个set集合,遍历键值对返回一个Set集合,遍历V的集合返回一个collections的集合。
- TreeMap
a) TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
b) TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。reeMap 实现了Cloneable接口,意味着它能被克隆。TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时指定的 Comparator(比较器)进行排序,具体取决于使用的构造方法。
c) TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
d) 另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
e) TreeMap的本质是R-B Tree(红黑树),它包含几个重要的成员变量: root, size, comparator。root 是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的,size是红黑数中节点的个数。
f) R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
a) 红黑树的特性:
i. 每个节点非红即黑。
ii. 根节点一定是黑色。
iii. 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
iv. 如果一个节点是红色的,则它的子节点必须是黑色的。
v. 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。(确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。)
b) 红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。例如,Java集合中的TreeSet和TreeMap,以及Linux虚拟内存的管理,都是通过红黑树去实现的。
c) 红黑树的时间复杂度为O(lgn),一棵含有n个节点的红黑树的高度至多为2log(n+1).
d) 将一个节点插入到红黑树中,首先将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。
e) 将红黑树内的某一个节点删除:首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。
- weakHashMap
a) WeakHashMap 继承于AbstractMap,实现了Map接口。
b) 和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。
c) WeakHashMap的源码发现,Entry继承了WeakReference类,并且实例化Entry对象时,所有的key都会通过调用super(key,queue)方法保存成对实际对象的弱引用。实际上,弱引用在构造时也需要传入一个对象的强引用作为参数。tomcat的源码里,实现缓存时会用到WeakHashMap,tomcat中是使用ConcurrentHashMap和WeakHashMap做了分代的缓存。
d) WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。换句话说就是对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。
e) 这个“弱键”的原理呢?大致上就是,通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。实现步骤是:
新建WeakHashMap,将“键值对”添加到WeakHashMap中。实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
i. 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
ii. 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。这就是“弱键”如何被自动从WeakHashMap中删除的步骤了。
f) WeakHashMap和HashMap都是通过"拉链法"实现的散列表
g) WeakHashMap是“弱键”实现的哈希表。它这个“弱键”的目的就是:实现对“键值对”的动态回收。当“弱键”不再被使用到时,GC会回收它,WeakReference也会将“弱键”对应的键值对删除。“弱键”是一个“弱引用(WeakReference)”,在Java中,WeakReference和ReferenceQueue 是联合使用的。在WeakHashMap中亦是如此:如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 接着,WeakHashMap会根据“引用队列”,来删除“WeakHashMap中已被GC回收的‘弱键’对应的键值对”。
- HashTable
a) 和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。和Hashmap一样,Hashtable也是一个散列表,它也是通过“拉链法”解决哈希冲突的
b) Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
c) Hashtable 的函数都是同步的是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。
d) Hashtable 的实例有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。加载因子是对哈希表在其容量自动增加之前可以达到多满的一个尺度。初始容量和加载因子这两个参数只是对该实现的提示。关于何时以及是否调用 rehash 方法的具体细节则依赖于该实现。
e) 通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查找某个条目的时间。
f) Hashtable的内部实现细节
a) HashMap内部存储键值对的是一个Entry[]数组(transient Entry[] table),其中每一个数据节点都是一个Entry(这是一个类,可以理解为是一个单链表,里面包含有k,v,hash值以及指向下一个的next)。
b) 比较两个entry是否相等:先比较两个entry的K值是否相等,相等之后在比较两个entry的V值是否相等
c) Hashtable中的每一个方法都是使用synchonized关键是修饰的方法,也就是每一个方法都是线程安全的。
d) 在调整hashtable的大小时候,原来的容量*2+1,然后再将原来的元素全部添加到新的entry[]数组中
e) 遍历方法分为不同的情况:遍历K的集合返回一个set集合,遍历键值对返回一个Set集合,遍历V的集合返回一个collections的集合。
f) 可以使用entrySet遍历hashtable中的键值对;可以使用迭代器遍历键、值的集合;也可以使用枚举方法遍历键、值的集合。
- HashSet
HashSet 是一个没有重复元素的集合。它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素。HashSet是非同步的。如果多个线程同时访问一个hashset,而其中至少一个线程修改了该 set,那么它必须保持外部同步。这通常是通过对自然封装该 hashSet的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装”set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问
hashSet实现了Set接口,clonable接口和serializable接口
- TreeSet
a) TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, java.io.Serializable接口。
b) TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
c) TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
d) 另外,TreeSet是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。
14:Iterator和Enumeration比较
a) 函数接口包含的方法不同
i. Enumeration只有2个函数接口。通过Enumeration,我们只能读取集合的数据,而不能对数据进行修改。
ii. Iterator只有3个函数接口。Iterator除了能读取集合的数据之外,也能数据进行删除操作。
b) Iterator支持fail-fast机制,而Enumeration不支持。
i. Enumeration接口使用它的函数包括Vector、Hashtable等类, Enumeration存在的目的就是为它们提供遍历接口。
ii. Enumeration本身并没有支持同步,而在Vector、Hashtable实现Enumeration时,添加了同步。
15、fail-fast机制
a) fail-fast机制的介绍:fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件
b) fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。在多线程环境下建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。
c) 实际例子说明fail-fast机制:
i. 这种问题出现在多个线程对同一个集合进行操作的情况,当某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
ii. 原因如下:在Arraylist中有两个变量 “modCount,expectedModCount”,当前数据如果被修改那么就会使modCount的值加一,当我们遍历list的时候这两个值是相等的,并且每次便利一个元素都会检查这两个值是否相等,但是遍历过程中另一个线程修改了list中的内容,那么在遍历的线程继续遍历下一个元素的时候发现这两个值不相等了,这样,便抛出ConcurrentModificationException异常,产生fail-fast事件
iii. 解决办法(拿ArrayList举例):和ArrayList继承于AbstractList不同,CopyOnWriteArrayList没有继承于AbstractList,它仅仅只是实现了List接口;ArrayList的iterator()函数返回的Iterator是在AbstractList中实现的;而CopyOnWriteArrayList是自己实现Iterator;ArrayList的Iterator实现类中调用next()时,会“调用checkForComodification()比较‘expectedModCount’和‘modCount’的大小”;但是,CopyOnWriteArrayList的Iterator实现类中,没有所谓的checkForComodification(),更不会抛出ConcurrentModificationException异常!
以上是关于集合框架的主要内容,如果未能解决你的问题,请参考以下文章