目录
HashMap
概述:
HashMap 是Map接口的一个实现类。本质是一种用key-value键值对来存储数据的table,并允许使用 null 值和 null 键。无序的,线性线性非安全。
区分1.7 ,1.8 分别介绍
数据结构:
JDK1.7:
HashMap是一个由数组和链表的结合形成的“链表散列”的数据结构。当新建一个 HashMap 的时候,就会初始化一个数组。数组中的每一个元素就是一个Entry static class,组成的Entry数组,每个 Entry 包含一个 key-value 对,它持有一个指向下一个元素的引用,数组中同一位置的多个Entry连起来就形成了Entry数组即构成了Entry链表。
JDK1.8:
Put 存储:
=====================================================================
get 读取:
Hash Map通过get获取元素时, 通过key 的 hashCode() +数组长度计算该 Entry 的存储位置,然后通过 key 的 equals 方法在对应位置的链表中找到需要的元素。
扩容和性能参数:
当 HashMap 中的元素越来越多的时候,table数组的长度是固定的,数据存不下,就要对 HashMap 的数组进行扩容。扩容后table 数组的长度变化了,经过key的hashcode 和 table length进行 计算,得到的各个Entry链表在table数组中的索引位置也就变了,要重新计算。这是最消耗性能的问题。HashMap的扩容由两个核心参数决定:初始容量(默认16)和负载因子(默认0.75)决定。16*0.75=12,即HashMap 中table数组元素个数超过 16*0.75=12 的时候,就把数组的大小扩展为 2*16=32 ,即扩大一倍,然后重新计算每个元素在数组中的位置。
线性非安全问题解决方案:
java.util.HashMap 不是线程安全的,因此高并发情况下会出现先行安全问题。此时可用fail-fast 机制来检测错误(议使用“java.util.concurrent 包下的类”去 取代“java.util 包下的类”。
HashSet
由于 HashMap 的 put() 方法添加 key-value 对时,当新放入 HashMap 的 Entry 中 key 与集合中原有 Entry的 key 相同(hashCode()返回值相等,通过 equals 比较也返回 true),新添加的 Entry 的 value 会将覆盖原来 Entry 的 value(HashSet 中的 value 都是 PRESENT ),但 key 不会有任何改变,因此如果向 HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入 HashMap中,原来的元素也不会有任何改变,这也就满足了 Set 中元素不重复的特性。
HashTable
ArrayList
ArrayList 是List 接口的实现类,功能上可以理解为动态数组,底层也是使用数组保存所有元素(包括null 元素),默认的数组容量是10,随着向 ArrayList 中不断添加元素,其容量也自动增长(每次数组容量的增长大约是其原容量的 1.5 倍)。自动增长会带来数据向新数组的重新拷贝,该动作很耗时和耗费性能,因此,如果可预知数据量的多少,可在构造 ArrayList 时指定其容量。或者在在添加大量元素前,应用程序也可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量,这样可以减少递增式再分配的数量。ArrayList不是线程安全的。也使用了FailFast 机制。
LinkedList
LinkedList 和 ArrayList 一样,都实现了 List 接口,但其内部的数据结构有本质的不同,LinkedList 是双向链表结构,它允许插入包括 null的所有元素, 该链表中包含了 除 first 和 last 两个标记开始和结束的指针(Node)的外的存储数据的各个节点。这些结点不仅存储着数据,还存储着一个指向前驱结点的前指针和指向后继节点的后指针。LinkedList 它是线程不同步的。
ArrayList和LinkedList都实现了List接口,有以下的不同点:
1、数据查询,ArrayList 时间复杂度o(1)快于LinkedList o(n)
ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
2、数据插入,删除,LinkedList 不需要像ArrayList 那样重新计算大小甚至更新索引,LinkedList更快。
相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。
3、消耗内存上,LinkedList因为每个节点上除了存数据还存了指向前一个和后一个节点的引用而更占内存。
LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
属性
/链表的节点个数 transient int size = 0; //指向头节点的指针 transient Node<E> first; //指向尾节点的指针 transient Node<E> last;
结点结构
Node 是在 LinkedList 里定义的一个静态内部类,它表示链表每个节点的结构,包括一个数据域 item,一个后置指针 next,一个前置指针 prev。
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
添加元素
对于链表这种数据结构来说,添加元素的操作无非就是在表头/表尾插入元素,又或者在指定位置插入元素。因为 LinkedList 有头指针和尾指针,所以在表头或表尾进行插入元素只需要 O(1) 的时间,而在指定位置插入元素则需要先遍历一下链表,所以复杂度为 O(n)。
在表头添加元素的过程如下:
当向表头插入一个节点时,很显然当前节点的前驱一定为 null,而后继结点是 first 指针指向的节点,当然还要修改 first 指针指向新的头节点。除此之外,原来的头节点变成了第二个节点,所以还要修改原来头节点的前驱指针,使它指向表头节点,源码的实现如下
private void linkFirst(E e) { final Node<E> f = first; //当前节点的前驱指向 null,后继指针原来的头节点 final Node<E> newNode = new Node<>(null, e, f); //头指针指向新的头节点 first = newNode; //如果原来有头节点,则更新原来节点的前驱指针,否则更新尾指针 if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; }
在表尾添加元素跟在表头添加元素大同小异,如图所示
当向表尾插入一个节点时,很显然当前节点的后继一定为 null,而前驱结点是 last指针指向的节点,然后还要修改 last 指针指向新的尾节点。此外,还要修改原来尾节点的后继指针,使它指向新的尾节点,源码的实现如下:
最后,在指定节点之前插入,如图所示:
当向指定节点之前插入一个节点时,当前节点的后继为指定节点,而前驱结点为指定节点的前驱节点。此外,还要修改前驱节点的后继为当前节点,以及后继节点的前驱为当前节点,源码的实现如下:
void linkBefore(E e, Node<E> succ) { // assert succ != null; //指定节点的前驱 final Node<E> pred = succ.prev; //当前节点的前驱为指点节点的前驱,后继为指定的节点 final Node<E> newNode = new Node<>(pred, e, succ); //更新指定节点的前驱为当前节点 succ.prev = newNode; //更新前驱节点的后继 if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
LinkedHashMap
HashMap和LinkedList合二为一即是LinkedHashMap。所谓LinkedHashMap,其本质是HashMap,所以LinkedHashMap自然会拥有HashMap的所有特性。只是它结合和LinkedList的双链特征,将HashMap的所有Entry节点以双链表的形式连接起来,虽然增加了时间和空间上的开销,但是记录了插入顺序,也就能保证迭代顺序,该迭代顺序可以是插入顺序,也可以是访问顺序。当然由于双链表的存在,LinkedHashMap在实现HashMap的细节实现上会与HashMap稍有不同。此外,LinkedHashMap可以很好的支持LRU算法。
与HashMap比数据结构变化
数据结构
升级后的HashMap
每个Entry内部的数据结构
与HashMap比类中结构变化
成员变量变化
与HashMap相比,LinkedHashMap增加了两个属性用于保证迭代顺序,分别是 双向链表头结点header 和 标志位accessOrder (值为true时,表示按照访问顺序迭代;值为false时,表示按照插入顺序迭代),默认是false。
/** * The head of the doubly linked list. */ private transient Entry<K,V> header; // 双向链表的表头元素 /** * The iteration ordering method for this linked hash map: <tt>true</tt> * for access-order, <tt>false</tt> for insertion-order. * * @serial */ private final boolean accessOrder; //true表示按照访问顺序迭代,false时表示按照插入顺序
Entry变化
LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了Entry。LinkedHashMap中的Entry增加了两个指针 before 和 after,它们分别用于维护双向链接列表。特别需要注意的是,next用于维护HashMap各个桶中Entry的连接顺序,before、after用于维护Entry插入的先后顺序的,源代码如下:
private static class Entry<K,V> extends HashMap.Entry<K,V> { // These fields comprise the doubly linked list used for iteration. Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); } ... }
LRU(Least recently used,最近最少使用)算法
LinkedHashMap的构造方法中可以自定义传入的accessOrder的值,也就是说可以指定双向循环链表中Entry的排序规则(包括在put 时指定,和get时指定)。特别地,当我们要用LinkedHashMap实现LRU算法时,就需要调用该构造方法并将accessOrder置为true。
LinkedHashMap重写了HashMap中的recordAccess方法(HashMap中该方法为空),当调用父类的put方法时,在发现key已经存在时,会调用该方法;当调用自己的get方法时,也会调用到该方法。该方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部。也就是说,当accessOrder为true时,get方法和put方法都会调用recordAccess方法使得最近使用的Entry移到双向链表的末尾;当accessOrder为默认值false时,从源码中可以看出recordAccess方法什么也不会做。多次操作后,双向链表前面的Entry便是最近没有使用的。当LRU算法的数据结构缓存的Entry 个数大于最大缓存值个数6时,会自动删除最近没有被使用的ntry,因为它就是最近最少使用的Entry。
ConcurrentHashMap
ConcurrentHashMap 可以看做是一个segment类型的数组( final Segmen t<K,V>[] segments; )segments。每个Segment 包含了一个table数组,table数组的每一个元素(每一个桶)是一个HashEntry组成的链表。HashEntry类似于HashMap 的Entry,包含了 key 和 value 以及 HashEntry 的next 指针。
ConcurrentHashMap 默认有16个Segment ,在put一个元素时会选择一个Segment,并对其 加锁,此时该Segment不能被put 操作,而其他Segment还可以正常put操作,也即其他线程可以继续对该ConcurrentHashMap进行并发的写操作。另外,因为CurrentHashMap 对put 的值采用了 volatile修饰,保证值变量在线程间的内存可见性。所以Segment 无论是否被锁,Segment都支持读操作。相较于HashTable 支持单线程的Synchronized锁,ConcurrentHashMap 具有优秀的并发性能。
CopyOnWriteArrayList
CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的写操作需加锁的ArrayList,写操作则通过创建底层数组的一个副本来实现,是一种读写分离的并发策略的一种实现,这种容器已被称为"写时复制器"。
原理:
CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。写操作时,先将当前容器复制一份,然后在副本上执行写操作,此时副本加锁,结束之后再将原容器的引用指向新容器。
优点:
读操作性能很高,无需加锁,线性安全,比较适用于读多写少的并发场景。
缺点:
一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;
二是无法保证实时性,CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。