集合框架
JCF(Java collections framework),也就是java集合框架
包括:
- 集合(Collection)
- 列表(List)
- ArrayList
- LinkedList
- Vector
- Stack
- CopyOnWriteArrayList
- 队列(Queue)
- ArrayBlockingQueue
- LinkedBlockingQueue
- PriorityBlockingQueue
- DelayQueue
- PriorityQueue
- ConcurrentLinkedQueue
- Set集合
- HashSet
- LinkedHashSet
- TreeSet
- 列表(List)
- 映射(Map)
- HashMap
- LinkedHashMap
- HashTable
- ConcurrentHashMap
- TreeMap
- WeakHashMap
集合图
各种数据结构的区别
数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)
二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。
列表1——ArrayList
ArrayList的内部实现是一个数组(会自动扩容的动态数组)。
ArrayList是非线程安全的。
几个关键属性:
- DEFAULT_CAPACITY:默认容量,值为10,也就是没有指定容量时,ArrayList内部数组elementData的默认大小;
- elementData:元素数据,Object[]类型,也就是ArrayList内部真正存储元素的数组。
- size:大小,也就是ArrayList的真实大小
扩容:
ArrayList的容量也是elementData数组的大小,默认大小是10,初始化时可以指定大小,如果add()元素时数组大小不够,则数组会自动扩大,新数组大小 = 旧数组大小 * 1.5,数组最大不能超过Integer的最大值。
遍历方式:
- 随机访问
for (int i=0; list.size();<size; i++) {list.get(i);}
:3 ms - foreach循环
for (Integer integ:list)
:5 ms - 迭代器
Iterator iter = list.iterator();while (iter.hasNext()) {}
:8 ms
因为ArrayList是基于数组的集合,适合随机访问(通过元素下标访问),所以3种遍历方式按速度排序如下:
随机访问 > foreach遍历 > 迭代器遍历
列表2——LinkedList
虽然把LinkedList放在列表目录,当时它也实现了Deque队列接口,所以可以认为它是具有列表功能的队列。
LinkedList内部是一个双向、双端链表。
LinkedList实现了List<E>和Deque<E>接口,可以作为列表(随机访问)、队列(FIFO先入先出)、栈(LIFO后入先出)来使用。
关键变量:
- size:大小,即LinkedList包含多少元素
- first:第一个元素,是一个Node<E>实例
- last:最后一个元素,是一个Node<E>实例
Node<E>类:
- item:一个泛型的变量,是真正加入LinkedList的元素
- next:是一个Node<E>实例,指下一个元素
- prev:是一个Node<E>实例,指上一个元素
作为列表(随机访问):
public E get(int index) {}
LinkedList本身是一个链表,而不是数组,那它是怎么实现随机访问(通过元素下标访问)呢?
实际原理非常简单,它就是通过一个计数索引值来实现的。当我们调用get(int index)时,首先会比较“index”和“双向链表长度的1/2”;若前者大,则从链表头first开始往后查找,直到index位置;否则,从链表末尾last开始先前查找,直到index位置。
所以LinkedList随机访问的速度比较慢,不建议使用。
作为栈(LIFO后入先出):
- 入栈方法push(e)或者addFirst(e)
- 出栈方法pop()或者removeFirst()
- 查看栈顶元素方法peek()或者peekFirst()
作为队列(FIFO先入先出):
- 入队方法add(e)或者addLast(e)
- 出队方法poll()或者pollFirst()
- 查看队头元素peek()或者peekFirst()
遍历方式:
十万条数据,耗时时间从低到高:
- pollFirst()、pollLast()、removeFirst()、removeLast() 边读边删,4 ms
- 迭代器遍历, 6 ms
- Foreach遍历 ,6 ms
- get()方法随机遍历 ,3414 ms
所以,LinkedList不要使用get()随机遍历。
列表3——矢量列表(Vector)
Vector的结构与ArrayList差不多,内部实现也是一个动态数组。
差别在于:
- Vector是线程安全的
- Vector扩容与ArrayList不太一样,可以自定义扩容增量大小
几个关键属性:
- elementData:动态数组,存储Vector的元素。
- elementCount:存储的元素个数,也就是Vector的真实大小
- capacityIncrement:扩容增量,是elementData动态数组每次扩容时增加的容量大小;
扩容:
Vector初始默认容量大小是10,当Vector容量不足以容纳全部元素时,Vector的容量会增加。若capacityIncrement>0,则将容量的值增加capacityIncrement;否则,将容量大小增加为旧容量*2。
遍历方式:
随机访问 > Enumeration > foreach > 迭代器遍历
列表4——栈(Stack)
Stack是栈,特性是先进后出(FILO, First In Last Out)。
Stack继承了Vector,跟Vector几乎一样,内部实现也是一个动态数组。
Stack使用数组来实现栈,而非链表。
为什么使用数组而不是链表来实现呢?
其实很简单,因为栈是FILO,对于数组而言,每次入栈和出栈都只要操作最后一个数组元素就可以,不会造成数组重组,所以效率跟链表一样快。
与LinkedList实现的栈对比:
- Stack是线程安全的,LinkedList是非线程安全
- Stack基于数组实现,LinkedList基于链表实现。
- Stack类比较突兀,因为它存储顺序和栈是相反的,LinkedList方式的栈存储顺序和栈是一致的。
列表5——读写分离列表(CopyOnWriteArrayList)
CopyOnWriteArrayList是java.util.concurrent下面的一个类。
CopyOnWriteArrayList的实现原理是读写分离+写时复制,数据结构也是数组。
CopyOnWriteArrayList是线程安全的,适合高并发的场景。
实现原理:
CopyOnWriteArrayList使用了CopyOnWrite容器——即写时复制的容器。
通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
优点:
- CopyOnWriteArrayList只对写操作加锁,在兼顾了线程安全的同时,又提高了并发性,特别适合读多写少的场景。
- CopyOnWriteArrayList性能比Vector提高不少,因为Vector既对写加锁也对读加锁
缺点:
- 内存占用问题:因为在写的时候,需要复制一份列表,会占用两倍的内存,可能会引起频繁的Yong GC和Full GC
- 数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
映射1——HashMap
下面的介绍是java7的实现,java8已经做了改动。
参考:https://www.cnblogs.com/chengxiao/p/6059914.html
我们知道,数据结构的物理存储结构只有两种:顺序存储结构
和链式存储结构
(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。
哈希表(HashMap)采用了链地址法,也就是数组+链表的方式存储数据
HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
Entry数组
在存储一个Entry(key-value键值对)时,首先使用hashcode计算出key的哈希值,进行hash扰乱计算后,再通过和 length-1进行位运算得到数组下标,最后将这个Entry插入该数组下标位置。
拉链法解决"哈希冲突"
有可能两个不同key计算出来的hashcode是一样的,或者不同hashcode转换后的数组下标一样,这时候就存在存储位置冲突,为了解决冲突,HashMap在每个数组存储位置,加入一个链表,用来存储相同下标的Entry。
所以,哈希表的实际存储结构如下:
哈希表扩容
HashMap类有两个属性用于控制哈希表的容量(capacity):阈值(threshold)和负载因子(loadFactor)。
- 容量(capacity),即Entry数组的大小,默认是16,会随着哈希表变大而动态扩容,容量大小是2的n次幂。
- threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值="容量 * 加载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap扩容,新容量=旧容量*2。
- loadFactor就是加载因子,默认值为0.75
扩容需要重建哈希表:
因为Entry的存储位置index = h&(length-1)
是通过数组长度(容量)计算出来的,扩容后数组长度大,所以,需要对哈希表进行 rehash 操作(即重建内部数据结构),将老数组中的数据逐个链表地遍历,计算新的数组下标,扔到新的扩容后的数组中。
因为重建过程很耗资源,因此,构造HashMap时应该设置一个合理的初始容量(initialCapacity),避免哈希表因为扩容而重建
空间换时间
如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。也就是让每个数组存储位置上的链表大小变小,最优情况下一个位置只有一个元素,这样的查找速度是最快的。
解决HashMap的并发问题
HashMap本身是线程非安全的,在并发情况下,会导致数据不一致,有可能会造成环状链表(扩容时可能会发生),导致get操作时,cpu空转。
在并发场景,有几种解决方法
- 使用SynchronizedMap
Map<String,String> m = Collections.synchronizedMap(new HashMap<String,String>()); - 使用Hashtable代替HashMap
- 使用ConcurrentHashMap代替HashMap
java8 HashMap
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。
因为 Java7 HashMap使用链表解决哈希冲突,当链表变大时,查找时间复杂度取决于链表的长度,为 O(n),在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
映射2——LinkedHashMap
参考:https://www.cnblogs.com/xiaoxi/p/6170590.html
LinkedHashMap与HashMap
- LinkedHashMap是HashMap的子类。
- LinkedHashMap与HashMap的区别在于,HashMap是无序的,LinkedHashMap是有序的
- LinkedHashMap比HashMap,增加了时间和空间上的开销,它为了保证元素迭代的有序性,内部维护了一个双向链表,可以控制迭代顺序按照插入顺序或者是访问顺序。
- LinkedHashMap继承HashMap,并通过多态来实现维护元素次序的功能。
LinkedHashMap定义了两个属性:
- Entry<K,V> header:双向链表的头结点,这个双向链表就是用来维护元素的次序。
- boolean accessOrder:迭代顺序控制属性,true表示最近最少使用次序,false表示插入顺序。
HashMap的数组+链表的结构,再增加一个双向链表,形成了LinkedHashMap的结构,如下图:
LinkedHashMap遍历图
LinkedHashMap有两种遍历顺序
- 当accessOrder=false,元素按插入的顺序排序,最先插入的元素排在第一位。
- 当accessOrder=true,元素按最近最少使用的顺序排序,最近使用的元素排在第一位,利用LinkedHashMap的这个功能可以实现LRU算法缓存,LRUCache实现缓存LRU功能都是源自LinkedHashMap的,当缓存满了,会优先淘汰那些最近最不常访问的数据。
映射3——HashTable
HashTable已经不建议使用,被ConcurrentHashMap替代。
HashTable和HashMap的区别
- HashTable与HashMap的实现原理几乎一样
- HashTable不允许key和value为null,而HashMap允许
- HashTable是线程安全的
- HashTable为了线程安全,性能非常差
HashTable的全表锁
HashTable虽然是线程安全,但是线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
映射4——ConcurrentHashMap
下面介绍的是java7的ConcurrentHashMap,java8做了改动。
参考:http://www.importnew.com/28263.html
ConcurrentHashMap是HashTable的替代者。
ConcurrentHashMap是线程安全的,支持并发场景。
ConcurrentHashMap使用的是分段锁,性能比HashTable好。
数据结构
ConcurrentHashMap的结构与HashMap不一样,使用了Segment + HashEntry的数据结构,整个 ConcurrentHashMap由一个个 Segment(段)组成,每个Segment相当一个HashMap(数组+链表)。
ConcurrentHashMap 几个初始化参数
- concurrencyLevel:并发级别,也是Segment 的数量,默认16个,这个值一旦初始化不会改变,这时,ConcurrentHashMap最多可以同时支持 16 个线程并发写。
- initialCapacity:初始容量,这个值指的是整个ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。
- loadFactor:负载因子,这个负载因子是给每个 Segment 内部的数组使用的。
分段锁
Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,而不是整个哈希表,这叫分段锁,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。这个设计让ConcurrentHashMap 比HashTable性能提升很多倍。
java8的ConcurrentHashMap
java8对ConcurrentHashMap的改动非常大,完全不用java7那套Segment的主干结构,变得更像java8的HashMap,也使用了数组+链表+红黑树的数据结构,链表转红黑树的临界点也是8(注意:当数组长度小于64时会先通过扩容解决链表过长问题),并且采用final + volatile + CAS + synchronized来保证并发安全进行实现。
java 8的ConcurrentHashMap实现极其细致:
- 它的大部分变量都是volatile和final类型,保证可见性。
- 使用了大量的本地CAS方法+循环,乐观锁的经典使用。
- 使用synchronized来保证线程同步,java8的内置锁已经经过优化,性能可以与ReentrantLock媲美。
具体实现:
- initTable初始化方法使用CAS乐观锁
- transfer扩容方法支持多线程同时扩容,每个线程负责stride个节点的扩容。使用volatile类型的transferIndex变量,通过cas计算transferIndex=transferIndex-stride来安全分配每个线程所负责的hash桶。
- put方法使用synchronized加锁,但是锁定的只是数组的一个节点,粒度比Segment更小。
- get方法不需要加锁。因为Node的val是volatile修饰的,所以其他线程的修改对当前线程是可见的。
映射5——TreeMap
要了解TreeMap,需要先了解红黑树
参考:https://www.cnblogs.com/CarpenterLee/p/5503882.html
TreeMap是有序的,它实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。
TreeMap底层通过红黑树(Red-Black tree)实现,也就意味着containsKey(), get(), put(), remove()都有着log(n)的时间复杂度,查找key的速度跟二分查找一样快。
TreeMap是线程不安全的,如果需要在并发场景使用,可以使用synchronizedSortedMap:
SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));
映射6——WeakHashMap
参考:http://www.cnblogs.com/skywang12345/p/3311092.html
WeakHashMap是弱键的哈希表。
WeakHashMap和HashMap都是通过"拉链法"实现的散列表,数据结构和java7的HashMap一样使用哈希数组+单向链表。
弱键
意思是:key是弱引用,当key不被其它对象引用时,会被GC回收,然后,它对应的键值对也就从WeakHashMap中移除。
关键属性:
- queue,是一个引用队列(ReferenceQueue),用来保存的是“已被GC清除”的“弱引用的键”。
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
- table,是一个Entry数组,和HashMap一样,保存哈希表所有键值对,不同的是Entry继承了WeakReference类,使得Entry里面的key是弱引用。
Entry<K,V>[] table;
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
}
实现原理:
WeakHashMap使用WeakReference和ReferenceQueue来实现弱键功能。
- 整个哈希表的主干是
table
(Entry数组),每个Entry中存储了key和value,而key是一个WeakReference弱引用 - 当key引用的对象不再被使用到时(不存在强引用),垃圾回收器会回收它,Java虚拟机就会把这个弱引用(key)加入到与之关联的引用队列(
queue
)中。 - 接着,下次操作WeakHashMap时,WeakHashMap会根据引用队列(
queue
)中的key,来删除已被GC回收的弱键
对应的键值对。
Set集合
set集合的特点是有:
- 元素不重复,add()重复的元素会返回false
- 存取无序,集合里面的对象没有什么特定顺序
- set集合的相等使用equals()方法
主要实现类:
- HashSet
- LinkedHashSet
- TreeSet
HashSet
HashSet内部实际是一个HashMap,元素的值就是key,value值是一个空的对象。
所以,HashSet是使用哈希算法来定位元素的存储位置,而判断两个元素相等的标准是两个对象的equals()方法相等且hashCode()方法值相等。
LinkedHashSet
LinkedHashSet内部实际是一个LinkedHashMap。
- LinkedHashSet也是根据元素的hashCode值来决定元素的存储位置,但和HashSet不同的是,它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。
- 当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。
- LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历)
TreeSet
TreeSet是SortedSet接口的实现类。
TreeSet内部实际是一个TreeMap。
TreeSet可以确保集合元素处于排序状态。
队列(Queue接口)
队列是一个先入先出(FIFO)的数据结构。
Queue接口的实现:
简单实现,LinkedList实现了Queue接口,可以使用LinkedList创建一个队列对象。
-
阻塞队列,当队列满时put()元素或者队列为空时take()元素,线程将会阻塞,使用加锁的方式来实现线程安全。
- ArrayBlockingQueue,基于数组的阻塞队列,有界
- LinkedBlockingQueue,基于链表的阻塞队列,有界
- PriorityBlockingQueue,使用优先级取代先入先出的阻塞队列,无界,元素按优先级顺序被移除。
- DelayQueue,带延迟期的阻塞队列,无界,只有在延迟期满时才能从中提取元素。
-
非阻塞队列,使用CAS循环的方式来实现线程安全
- PriorityQueue ,使用优先级取代先入先出的队列,无界,元素按优先级顺序被移除。
- ConcurrentLinkedQueue,基于链表、线程安全的队列,适用于并发访问,获取队列大小需要遍历。
操作方法
队列1——ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个双端单向列表结构、无界、线程安全的、非阻塞队列。
- 双端单向:每个元素都是一个节点,每个节点都包含next指向下一个节点,默认有head结点和tail结点。初始化时,head结点和hail结点都指向同一个空结点,其中head节点存放链表第一个item为null的节点,tail则并不是总指向最后一个节点。
- 无界:因为是列表结构,加上不设限,所以它是无界的。
- 线程安全的:出队、入队的函数都是操作volatile变量:head,tail。所以要保证队列线程安全只需要保证对这两个Node操作的可见性和原子性,由于volatile本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性。比如offer操作,是在tail后面添加元素,也就是调用tail.casNext方法,而这个方法是使用的CAS操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取tail,然后执行casNext方法。
- 非阻塞:不适用加锁的方式,而是使用CAS循环,所以它是非阻塞的。
队列2——ArrayBlockingQueue
ArrayBlockingQueue是线程安全的、有界的、阻塞队列(FIFO先入先出)。
ArrayBlockingQueue内部是一个定长数组。
ArrayBlockingQueue的阻塞队列是通过重入锁ReenterLock和Condition条件队列实现的。
/** 存储元素的对象数组 */
final Object[] items;
/** 出队指针 */
int takeIndex;
/** 入队指针 */
int putIndex;
/** 队列里元素个数 */
int count;
/** 可重入锁 */
final ReentrantLock lock;
/** 阻塞控制-队列空 */
private final Condition notEmpty;
/** 阻塞控制-队列满 */
private final Condition notFull;
/** 构造方法 */
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
从源码可以看出,ArrayBlockingQueue的实现原理。
数据结构
- capacity,构造参数,必填,创建ArrayBlockingQueue时必须指定,表示队列大小,所以ArrayBlockingQueue是有界的。
- items,全局变量,Object[]数组,用来存储队列的元素,所以ArrayBlockingQueue是基于数组的。
- count,全局变量,表示队列的元素个数。
阻塞控制
- lock,全局变量,ReentrantLock可重入锁,用来控制线程阻塞
- fair,构造参数,可选,设置lock的访问策略(公平/不公平)。
- notEmpty,全局变量,用来控制队列为空时阻塞
- notEmpty,全局变量,用来控制队列满时阻塞
出队入队控制
- takeIndex:出队指针,指向ArrayBlockingQueue队列(数组)的队头
- putIndex:入队指针,指向ArrayBlockingQueue队列(数组)的队尾
ArrayBlockingQueue很巧妙地使用了两个指针,让数组实现先入先出的队列结构,每加入一个元素,putIndex就加1,每弹出一个元素,takeIndex就加1,当putIndex或takeIndex递增到等于数组长度时,重新置为0,指针从0开始。
队列3——LinkedBlockingQueue
LinkedBlockingQueue是线程安全的、有界的、阻塞队列(FIFO先入先出)。
LinkedBlockingQueue内部是一个单向链表。
LinkedBlockingQueue的阻塞队列也是通过重入锁ReenterLock和Condition条件队列实现的。
与ArrayBlockingQueue不同的是
- LinkedBlockingQueue内部是一个链表
- LinkedBlockingQueue默认大小是Integer.MAX_VALUE
- LinkedBlockingQueue的入队和出队分别使用两个可重入锁ReenterLock,两个操作是可以并行的,所以吞吐量比ArrayBlockingQueue高。
集合相关
clone
Iterator迭代器
参考:https://www.jianshu.com/p/a16ca1560551
通过集合类,可以了解迭代器的实现原理。
比如ArrayList,可以使用迭代器来遍历所有元素
List<String> list = new ArrayList<String>();
Iterator<String> iterator = list.iterator();
while(it.hasNext()){
String str = it.next();
}
从ArrayList类的实现结构可以看出迭代器的实现原理。
- ArrayList实现了Iterable<T>接口,重写了iterator()方法。
- ArrayList有个内部类实现了Iterator<E>接口,重写了hasNext()和next()方法。
public interface Iterator<E> {
boolean hasNext();
E next();
}
public interface Iterable<T> {
Iterator<T> iterator();
}
public interface Collection<E> extends Iterable<E> {
}
public interface List<E> extends Collection<E> {
}
public class ArrayList<E> implements List<E> {
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
public boolean hasNext() { }
public E next() { }
}
}
fail-fast机制
参考:http://www.cnblogs.com/skywang12345/p/3308762.html
fail-fast 机制是java集合(Collection)中的一种错误机制。
当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
我们举例ArrayList迭代的源码,来分析fail-fast的原理:
- ArrayList集合有个属性modCount,每次改变集合元素(add、remove、clear)都会改变这个值。
- 开始迭代ArrayList集合时,会创建Iterator对象,把modCount值赋给Iterator对象的expectedModCount
- 每次迭代调用 next()或remove()前,都会检查expectedModCount是否等于modCount。
- 假设线程A在迭代集合,这时线程B也在修改集合元素,从而导致modCount变化,线程A在迭代时发现expectedModCount!=modCount,会抛出ConcurrentModificationException异常,产生fail-fast事件。
如何避免fail-fast呢,只需要使用线程安全的集合,就可以。
比如使用java.util.concurrent包下面的CopyOnWriteArrayList,代替java.util.ArrayList
Enumeration枚举类和Iterator迭代器的区别
Enumeration是另外一个遍历集合的接口,在Vector、Hashtable集合类中使用了Enumeration。
public interface Enumeration<E> {
boolean hasMoreElements();
E nextElement();
}
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
Enumeration和Iterator的区别如下:
- 接口个数不同,Iterator除了有遍历函数,还有删除函数。
- Iterator支持fail-fast,而Enumeration不支持
使用Hashtable集合,测试两种遍历方式的速度
- Iterator: 9ms
- Enumeration: 5ms
因为Iterator添加了fail-fast,所以Enumeration速度比Iterator快一点
自然排序Comparator和定制排序Comparable
两个接口定义如下:
public interface Comparable<T> {
public int compareTo(T o);
}
public interface Comparator<T> {
int compare(T o1, T o2);
}
Comparable是排序接口,若一个类实现了Comparable接口,就意味着该类支持排序。
实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序。
此外,实现此接口的对象可以用作有序映射(TreeMap)中的键或有序集合(TreeSet)中的集合,无需指定比较器。Comparator是比较器接口,若一个类要进行排序,但它本身不支持排序(即没有实现Comparable接口),那么我们就可以建立一个实现Comparator接口的比较器来进行排序。
在初始化排序集合类时,可以指定比较器来对集合内的元素进行排序,指定比较器后,本身支持排序的元素的排序规则将会被忽略。
TreeMap<String, String> treeMap = new TreeMap<String, String>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.compareTo(o1);
}
});
注意:如果元素需要使用排序集合类,那么元素要么本身支持排序(实现Comparable接口),要么排序集合类需要指定比较器(实现Comparator接口),不然将会发生不可预测的异常。
equals()和hashcode()
这两个方法都是从object类中继承过来的。
object类的默认实现中,equals()是对两个对象的地址值进行的比较(即比较引用是否相同),而hashCode()是一个本地方法,它的实现是根据本地机器相关的。
比较:
- equals和hashcode都是比较两个对象是否相等。
- hashcode的速度比equals要快,但是equals更加可靠,因为hashcode相等的两个对象不一定相等(哈希碰撞)。
- 所以,在比较相等时,合理的做法是先比较hashcode,如果相等,在比较equals,即能满足效率也可靠。
- equals和==
在比较对象时,==比较的是两个对象的引用(存储地址)是否相等,如果不重写equals方法,equals和==的作用是一样的,这时候除非两个对象变量是同个引用,不然永远不相等。 - 什么时候重写hashCode和equals方法
一般的地方不需要重写hashCode,只有当类需要放在HashTable、HashMap、HashSet等等hash结构的集合时才会重写。
而且,如果重写了equals方法,需要同时重写hashCode,因为HashMap在比较对象是否相等时使用了equals+hashCode。
随机访问、Iterator迭代器遍历、foreach遍历、Enumeration枚举
- 随机访问:
通过元素的序号(下标)来获取集合的元素,其实就是get(int index)方法,ArrayList实现了RandmoAccess接口,这个接口是个空接口,唯一作用就是标识它的实现类支持快速随机访问
,因为ArrayList本身就是一个数组,所以根据下标获取元素速度非常快。
Integer value = null;
int size = list.size();
for (int i=0; i<size; i++) {
value = (Integer)list.get(i);
}
- Iterator迭代器访问:
是利用实现的迭代器循环遍历集合的元素,LinkedList是以链表形式存储元素,它不适合get(int index)的随机访问方式,但它适合以next()的方式遍历元素。
Integer value = null;
Iterator iter = list.iterator();
while (iter.hasNext()) {
value = (Integer)iter.next();
}
- foreach遍历:
Integer value = null;
for (Integer integ:list) {
value = integ;
}
- Enumeration枚举方式遍历HashTable
Enumeration enu = table.elements();
while(enu.hasMoreElements()) {
enu.nextElement();
}