Java集合框架——Java Collections Framework

集合框架

JCF(Java collections framework),也就是java集合框架

包括:

  • 集合(Collection)
    • 列表(List)
      • ArrayList
      • LinkedList
      • Vector
      • Stack
      • CopyOnWriteArrayList
    • 队列(Queue)
      • ArrayBlockingQueue
      • LinkedBlockingQueue
      • PriorityBlockingQueue
      • DelayQueue
      • PriorityQueue
      • ConcurrentLinkedQueue
    • Set集合
      • HashSet
      • LinkedHashSet
      • TreeSet
  • 映射(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的最大值。

遍历方式:

  1. 随机访问for (int i=0; list.size();<size; i++) {list.get(i);}:3 ms
  2. foreach循环for (Integer integ:list):5 ms
  3. 迭代器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()

遍历方式:
十万条数据,耗时时间从低到高:

  1. pollFirst()、pollLast()、removeFirst()、removeLast() 边读边删,4 ms
  2. 迭代器遍历, 6 ms
  3. Foreach遍历 ,6 ms
  4. get()方法随机遍历 ,3414 ms

所以,LinkedList不要使用get()随机遍历。

列表3——矢量列表(Vector)

Vector的结构与ArrayList差不多,内部实现也是一个动态数组。
差别在于:

  1. Vector是线程安全的
  2. 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实现的栈对比:

  1. Stack是线程安全的,LinkedList是非线程安全
  2. Stack基于数组实现,LinkedList基于链表实现。
  3. 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转换为数组下标的过程
拉链法解决"哈希冲突"

有可能两个不同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空转。

在并发场景,有几种解决方法

  1. 使用SynchronizedMap
    Map<String,String> m = Collections.synchronizedMap(new HashMap<String,String>());
  2. 使用Hashtable代替HashMap
  3. 使用ConcurrentHashMap代替HashMap
java8 HashMap

Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。
因为 Java7 HashMap使用链表解决哈希冲突,当链表变大时,查找时间复杂度取决于链表的长度,为 O(n),在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

图片.png

映射2——LinkedHashMap

参考:https://www.cnblogs.com/xiaoxi/p/6170590.html

LinkedHashMap与HashMap

  • LinkedHashMap是HashMap的子类。
  • LinkedHashMap与HashMap的区别在于,HashMap是无序的,LinkedHashMap是有序的
  • LinkedHashMap比HashMap,增加了时间和空间上的开销,它为了保证元素迭代的有序性,内部维护了一个双向链表,可以控制迭代顺序按照插入顺序或者是访问顺序。
  • LinkedHashMap继承HashMap,并通过多态来实现维护元素次序的功能。

LinkedHashMap定义了两个属性:

  1. Entry<K,V> header:双向链表的头结点,这个双向链表就是用来维护元素的次序。
  2. boolean accessOrder:迭代顺序控制属性,true表示最近最少使用次序,false表示插入顺序。

HashMap的数组+链表的结构,再增加一个双向链表,形成了LinkedHashMap的结构,如下图:

LinkedHashMap结构图

LinkedHashMap遍历图


LinkedHashMap遍历图

LinkedHashMap有两种遍历顺序

  1. 当accessOrder=false,元素按插入的顺序排序,最先插入的元素排在第一位。
  2. 当accessOrder=true,元素按最近最少使用的顺序排序,最近使用的元素排在第一位,利用LinkedHashMap的这个功能可以实现LRU算法缓存,LRUCache实现缓存LRU功能都是源自LinkedHashMap的,当缓存满了,会优先淘汰那些最近最不常访问的数据。

映射3——HashTable

HashTable已经不建议使用,被ConcurrentHashMap替代。
HashTable和HashMap的区别

  • HashTable与HashMap的实现原理几乎一样
  • HashTable不允许key和value为null,而HashMap允许
  • HashTable是线程安全的
  • HashTable为了线程安全,性能非常差
HashTable的全表锁
图片.png

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(数组+链表)。

图片.png

ConcurrentHashMap 几个初始化参数

  • concurrencyLevel:并发级别,也是Segment 的数量,默认16个,这个值一旦初始化不会改变,这时,ConcurrentHashMap最多可以同时支持 16 个线程并发写。
  • initialCapacity:初始容量,这个值指的是整个ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。
  • loadFactor:负载因子,这个负载因子是给每个 Segment 内部的数组使用的。
分段锁

Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,而不是整个哈希表,这叫分段锁,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。这个设计让ConcurrentHashMap 比HashTable性能提升很多倍。


图片.png
java8的ConcurrentHashMap

java8对ConcurrentHashMap的改动非常大,完全不用java7那套Segment的主干结构,变得更像java8的HashMap,也使用了数组+链表+红黑树的数据结构,链表转红黑树的临界点也是8(注意:当数组长度小于64时会先通过扩容解决链表过长问题),并且采用final + volatile + CAS + synchronized来保证并发安全进行实现。


图片.png

java 8的ConcurrentHashMap实现极其细致:

  1. 它的大部分变量都是volatile和final类型,保证可见性。
  2. 使用了大量的本地CAS方法+循环,乐观锁的经典使用。
  3. 使用synchronized来保证线程同步,java8的内置锁已经经过优化,性能可以与ReentrantLock媲美。

具体实现:

  1. initTable初始化方法使用CAS乐观锁
  2. transfer扩容方法支持多线程同时扩容,每个线程负责stride个节点的扩容。使用volatile类型的transferIndex变量,通过cas计算transferIndex=transferIndex-stride来安全分配每个线程所负责的hash桶。
  3. put方法使用synchronized加锁,但是锁定的只是数组的一个节点,粒度比Segment更小。
  4. get方法不需要加锁。因为Node的val是volatile修饰的,所以其他线程的修改对当前线程是可见的。

映射5——TreeMap

要了解TreeMap,需要先了解红黑树
参考:https://www.cnblogs.com/CarpenterLee/p/5503882.html

图片.png

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中移除。

关键属性:

  1. queue,是一个引用队列(ReferenceQueue),用来保存的是“已被GC清除”的“弱引用的键”。
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
  1. 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来实现弱键功能。

  1. 整个哈希表的主干是table(Entry数组),每个Entry中存储了key和value,而key是一个WeakReference弱引用
  2. 当key引用的对象不再被使用到时(不存在强引用),垃圾回收器会回收它,Java虚拟机就会把这个弱引用(key)加入到与之关联的引用队列(queue)中。
  3. 接着,下次操作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接口)

图片.png

队列是一个先入先出(FIFO)的数据结构。
Queue接口的实现:

  • 简单实现,LinkedList实现了Queue接口,可以使用LinkedList创建一个队列对象。

  • 阻塞队列,当队列满时put()元素或者队列为空时take()元素,线程将会阻塞,使用加锁的方式来实现线程安全。

    • ArrayBlockingQueue,基于数组的阻塞队列,有界
    • LinkedBlockingQueue,基于链表的阻塞队列,有界
    • PriorityBlockingQueue,使用优先级取代先入先出的阻塞队列,无界,元素按优先级顺序被移除。
    • DelayQueue,带延迟期的阻塞队列,无界,只有在延迟期满时才能从中提取元素。
  • 非阻塞队列,使用CAS循环的方式来实现线程安全

    • PriorityQueue ,使用优先级取代先入先出的队列,无界,元素按优先级顺序被移除。
    • ConcurrentLinkedQueue,基于链表、线程安全的队列,适用于并发访问,获取队列大小需要遍历。
  • 操作方法

image.png
图片.png

队列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开始。

图片.png

队列3——LinkedBlockingQueue

LinkedBlockingQueue是线程安全的、有界的、阻塞队列(FIFO先入先出)。
LinkedBlockingQueue内部是一个单向链表。
LinkedBlockingQueue的阻塞队列也是通过重入锁ReenterLock和Condition条件队列实现的。

与ArrayBlockingQueue不同的是

  1. LinkedBlockingQueue内部是一个链表
  2. LinkedBlockingQueue默认大小是Integer.MAX_VALUE
  3. 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类的实现结构可以看出迭代器的实现原理。

  1. ArrayList实现了Iterable<T>接口,重写了iterator()方法。
  2. 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的原理:

  1. ArrayList集合有个属性modCount,每次改变集合元素(add、remove、clear)都会改变这个值。
  2. 开始迭代ArrayList集合时,会创建Iterator对象,把modCount值赋给Iterator对象的expectedModCount
  3. 每次迭代调用 next()或remove()前,都会检查expectedModCount是否等于modCount。
  4. 假设线程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的区别如下:

  1. 接口个数不同,Iterator除了有遍历函数,还有删除函数。
  2. 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,即能满足效率也可靠。
  1. equals和==
    在比较对象时,==比较的是两个对象的引用(存储地址)是否相等,如果不重写equals方法,equals和==的作用是一样的,这时候除非两个对象变量是同个引用,不然永远不相等。
  2. 什么时候重写hashCode和equals方法
    一般的地方不需要重写hashCode,只有当类需要放在HashTable、HashMap、HashSet等等hash结构的集合时才会重写。
    而且,如果重写了equals方法,需要同时重写hashCode,因为HashMap在比较对象是否相等时使用了equals+hashCode。
随机访问、Iterator迭代器遍历、foreach遍历、Enumeration枚举
  1. 随机访问:
    通过元素的序号(下标)来获取集合的元素,其实就是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);        
}
  1. Iterator迭代器访问:
    是利用实现的迭代器循环遍历集合的元素,LinkedList是以链表形式存储元素,它不适合get(int index)的随机访问方式,但它适合以next()的方式遍历元素。
Integer value = null;
Iterator iter = list.iterator();
while (iter.hasNext()) {
    value = (Integer)iter.next();
}
  1. foreach遍历:
Integer value = null;
for (Integer integ:list) {
    value = integ;
}
  1. Enumeration枚举方式遍历HashTable
Enumeration enu = table.elements();
while(enu.hasMoreElements()) {
    enu.nextElement();
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容