Java集合总结

目录

HashMap

HashSet

HashTable

ArrayList

LinkedList

 LinkedHashMap

ConcurrentHashMap

CopyOnWriteArrayList


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:

1.  1.7中底层是数组+链表,1.8中底层是数组+链表 或者数组+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和 查询整体效率
2.JDK1.7table 数组中每个元素是Entry 数组(链表),JDK 1.8中换成了node 组成的链表或红黑树
3.默认是数组+链表的数据结构,当某个桶上链表长度 >8 且 整个数组长度 >64时,当前桶中链表转换为红黑树。当红黑树上元素个数 < 6时,对应的桶上红黑树转换为链表。

Put 存储:

=====================================================================

先说HashMap的Put⽅法的⼤体流程:
1. 根据Key通过哈希算法与 与运算得出数组下标
2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(
JDK1.7中是Entry对象,JDK1.8中
是Node对象)并放⼊该位置
3. 如果数组下标位置元素不为空,则要分情况讨论
          a. 如果是JDK1.7,则先判断是否需要扩容,
                       i.如果要扩容就进⾏扩容,
                       ii.如果不⽤扩容就⽣成 Entry 对象,迭代链表,通equal ()依次判断是否 key 相等,相 等则值覆盖,不相等这使⽤头插法添加到当前位置的链表中
           b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表       Node
                       ⅰ. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中     去(当然是尾部),在这个 过程中会判断红⿊树中是否存在当前key,如果存在则  更新value       
                       ⅱ. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node。
同样的先遍历链表,通过equal()方法对比 ,如果相同,则更新,否则表里到链表底部,直接插入(尾插法。 插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转 成红⿊树
                        ⅲ. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩 容,如果需要 就扩容,如果不需要就结束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 ,即扩大一倍,然后重新计算每个元素在数组中的位置

1.7版本
    1. 先⽣成新数组
    2. 遍历⽼数组中的每个位置上的链表上的每个元素
    3. 取每个元素的key,基于key 和 新数组⻓度,计算出每个元素在新数组中的下标
    4. 将元素添加到新数组中去
    5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
    1. 先⽣成新数组
    2. 遍历⽼数组中的每个位置上的链表或红⿊树
    3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
    4. 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置(先不插入)
            a. 统计每个下标位置的元素个数
            b. 如果该位置下的元素个数超过了8,则在新数组中⽣成⼀个新的红⿊树
            c. 如果该位置下的元素个数没有超过8,则在新数组中⽣成⼀个链表
    5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
=====================================================================

线性非安全问题解决方案:

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)。

在表头添加元素的过程如下:

2019111142457099.png

当向表头插入一个节点时,很显然当前节点的前驱一定为 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++;
}

 在表尾添加元素跟在表头添加元素大同小异,如图所示

2019111142546294.png

当向表尾插入一个节点时,很显然当前节点的后继一定为 null,而前驱结点是 last指针指向的节点,然后还要修改 last 指针指向新的尾节点。此外,还要修改原来尾节点的后继指针,使它指向新的尾节点,源码的实现如下:

 最后,在指定节点之前插入,如图所示:

2019111142620872.png

当向指定节点之前插入一个节点时,当前节点的后继为指定节点,而前驱结点为指定节点的前驱节点。此外,还要修改前驱节点的后继为当前节点,以及后继节点的前驱为当前节点,源码的实现如下:

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由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值