1 汇合概述

Java中有很多汇合类,例如ArrayList,LinkedList,HashMap,TreeMap等。汇合的性能就是包容多个对象,它们就像容器一样(实际上,间接称为容器也没有故障,C++就是这样称说的),当须要的时候,能够从外面拿进去,十分不便。在Java5提供了泛型机制之后,使容器有了在编译期做类型查看的能力,应用起来更加平安、不便。

Java中的汇合类次要是有两个接口派生进去的:Collection和Map,如下所示:

[](https://imgchr.com/i/i3JEbd)

[

](https://imgchr.com/i/i3JEbd)

[](https://imgchr.com/i/i3JZVA)

[

](https://imgchr.com/i/i3JZVA)

Collection又次要有Set,Queue,List三大接口,在此基础上,又有多个实现类。Map接口下同样有总多的实现类,例如HashMap,EnumMap,HashTable等。接下来我将会筛选几个罕用的汇合类来具体探讨探讨。

2 ArrayList和LinkedList

List汇合能够说是最罕用的汇合了,比HashMap还罕用,个别咱们写代码的时候一旦遇到须要存储多个元素的状况,就优先想到应用List汇合,至于应用的是ArrayList实现类还是LinkedList实现类,那就具体情况具体分析了。
《2020最新Java根底精讲视频教程和学习路线!》

2.1 ArrayList

ArrayList实现了List接口,继承了AbstractList抽象类,AbstractList抽象类实现了绝大部分List接口定义的形象办法,所以咱们在ArrayList源码中看不到大部分List接口中定义的形象办法的实现。ArrayList的外部应用数组来存储对象,这也是ArrayList这个名字的由来,其各种操作,例如get,add等都是基于数组操作的,上面是add办法的源码:

public void add(int index, E element) {    //先查看index是否在一个正当的范畴内    rangeCheckForAdd(index);        //保障数组的容量足够退出新的元素,发现不足够的话会进行扩容操作    ensureCapacityInternal(size + 1);  // Increments modCount!!    //进行一次数组拷贝,这里的elementData就是保留对象的Object数组    System.arraycopy(elementData, index, elementData, index + 1,                     size - index);    //往数组中退出元素    elementData[index] = element;    //批改size大小    size++;}复制代码

解释在正文中给出了,get办法也非常简单,就不浪费时间了。

2.2 LinkedList

LinkedList继承了AbstractSequentialList类,AbstractSequentialList类又继承了AbstractList类,同时LinkedList也当然有实现List接口的,而且还实现了Deque接口,这就比拟有意思了,阐明LinkedList不仅仅是List,还是一个Queue。下图示意其继承体系:

[](https://imgchr.com/i/i3JrqJ)

[

](https://imgchr.com/i/i3JrqJ)

LinkedList的基于链表实现的List,这是和ArrayList最大的区别。LinkedList有一个Node外部类,用来示意节点,如下所示:

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;    }}复制代码

该类有next指针和prev指针,可见是一个双向链表。接下来看看LinkedList的add操作:

public void add(int index, E element) {    //查看index    checkPositionIndex(index);        //如果index和size相等,阐明曾经到最初了,间接在last节点后插入节点接口    if (index == size)        linkLast(element);    else //否则就在index地位插入节点        linkBefore(element, node(index));}复制代码

在linkLast()和linkBefore()办法里会波及到链表的操作,其中LinkLast()的实现比较简单,linkBefore()略微简单一些,但只有学过数据结构的敌人,看这些源码应该没什么问题,在此不贴出源码了,比拟本文定位不是源码解析。

2.3 ArrayList和LinkedList的区别

在后面的介绍中其实有说到过,在这里总结一下:

  1. ArayyList是基于数组实现的,LinkedList是基于链表实现的,因为实现不同,他们的效率之间必定是有差异的,ArrayList的随机拜访效率较高,但插入操作会波及到数组拷贝,所以效率插入效率不高。LinkedList的插入效率可高可低,如果是在尾部插入,因为有一个last节点,所以尾部插入的速度十分快,但在其余地位的插入效率并不高,对于随机拜访来说,因为须要从头开始遍历节点,所以随机拜访的效率并不高。
  2. 他们的继承体系略微有些区别,LinkedList还实现了Deque接口,这是比拟有特点的。

3 SynchronizedList和Vector

之所以把他们俩发在一起是因为它们是线程平安的列表汇合。SynchronizedList是Collections工具类里的一个外部动态类,实现了List接口,继承了SynchronizedCollection类,Vector是JDK晚期的一个同步的List,和ArrayList的继承体系齐全一样,而且也是基于数组实现的,只是他的各种办法都是同步的。

3.1 SynchronizedList

SynchronizedList类是一个Collections类中包级公有的动态外部类,咱们在编写代码的时候无奈间接调用这个类,只能通过Collection.synchronizedList()办法并传入一个List来应用它,这个办法实际上就是帮咱们将原来没有同步措施的一般List包装成了SynchronizedList,使其领有线程平安的个性,对其进行操作就是对原List的操作,如下所示:

public void add(int index, E element) {    synchronized (mutex) {list.add(index, element);}}复制代码

3.2 Vector

Vector是JDK1.0就有的类,算是一个远古期间的类了,在过后因为没有比拟好的同步工具,所以在并发场景下会应用到这个类,但当初随着并发技术的提高,有了更好的同步工具类,所以Vector曾经快成为半废除状态了。为什么呢?次要还是因为同步效率太低,同步伎俩太粗犷了,粗犷到间接将绝大多数办法弄成同步办法(在办法上退出synchronized关键字),连clone办法都没放过:

public synchronized Object clone() {    try {        @SuppressWarnings("unchecked")            Vector<E> v = (Vector<E>) super.clone();        v.elementData = Arrays.copyOf(elementData, elementCount);        v.modCount = 0;        return v;    } catch (CloneNotSupportedException e) {        // this shouldn't happen, since we are Cloneable        throw new InternalError(e);    }}复制代码

这样做尽管能确保线程平安,但效率切实太低了啊,尤其是在竞争强烈的环境下,效率可能还不如单线程。相比之下,SynchronizedList就好很多,只是在必要的中央进行加锁而已(不过实际上效率还是挺低的)。基于它的CRUD操作就不多说了,和ArrayList没什么大的区别。

SynchronizedList和Vector的区别

他们最大区别就在同步效率,Vector的同步伎俩过于粗犷以至于效率太低,SynchronizedList的同步伎俩没那么粗犷,只是在有必要的中央进行同步而已,效率较Vector会好一些,但实际上也不会太好,比拟同步伎俩比拟繁多,只是用内置锁一种计划而已。

4 HashMap、HashTable和ConcurrentHashMap

当咱们想要存储键值对或者说是想要表白某种映射关系的时候,都会用到HashMap这个类,HashTable则是HashMap的同步版本,是线程平安的,但效率很低,ConcurrentHashMap是JDK1.5之后代替HashTable的类,效率较高,所以当初在并发环境下个别不再应用HashTable,而是应用ConcurrentHashMap。

顺便说一下,ConcurrentHashMap是在J.U.C包下的,该包的次要作者是Doug Lea,这位大佬简直一个人撑起了Java并发技术。

4.1 HashMap

HashMap的内部结构是数组(称作table)+链表(达到阈值会转换成红黑树)的模式。数组和链表存储的元素都是Node,如下所示:

static class Node<K,V> implements Map.Entry<K,V> {    final int hash;    final K key;    V value;    Node<K,V> next;    Node(int hash, K key, V value, Node<K,V> next) {        this.hash = hash;        this.key = key;        this.value = value;        this.next = next;    }    public final K getKey() { return key; }    public final V getValue() { return value; }    public final String toString() { return key + "=" + value; }    public final int hashCode() {        return Objects.hashCode(key) ^ Objects.hashCode(value);    }    public final V setValue(V newValue) {        V oldValue = value;        value = newValue;        return oldValue;    }    public final boolean equals(Object o) {        if (o == this)            return true;        if (o instanceof Map.Entry) {            Map.Entry<?,?> e = (Map.Entry<?,?>)o;            if (Objects.equals(key, e.getKey()) &&                Objects.equals(value, e.getValue()))                return true;        }        return false;    }}复制代码

当向HashMap插入键值对的时候,会先拿key进行hash运算,失去一个hashcode,而后依据hashcode来确定该键值对(最终的模式上其实是Node)应该搁置在table的哪个地位,这个过程中如果有Hash抵触,即table中该地位曾经有了Node节点A,那么就会将这个新的键值对插入到以A节点为头节点的链表中(此尾插法,在JDK1.8中改为头插法),如果在遍历链表的途中遇到key雷同的状况,那么就间接用新的value值替换到原来的值,这种状况就不再创立新的Node了,如果在途中没有遇到的话,就在最初创立一个Node节点,并将其插入到链表开端。

对于HashMap更多的内容,例如什么并发扩容导致的问题,以及扩容因子对性能的影响等等,倡议网上搜寻,网上这样的文章十分十分多,多到关上一个社区,都TM是将HashMap的文章.....

4.2 HashTable

HashTable的算法实现和HashMap并没有太多区别,能够简略把HashTable了解成HashMap的线程平安版本,HashTable实现线程平安的伎俩也是十分粗犷的,和Vector简直一样,间接将绝大多数办法设置成同步办法,如下所示:

public synchronized boolean contains(Object value) {    if (value == null) {        throw new NullPointerException();    }    Entry<?,?> tab[] = table;    for (int i = tab.length ; i-- > 0 ;) {        for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {            if (e.value.equals(value)) {                return true;            }        }    }    return false;}复制代码

所以,其效率能够说是非常低的,个别很少用了,而是应用接下来要讲到的ConcurrentHashMap代替。

4.3 ConcurrentHashMap

该类位于java.util.concurrent(简称J.U.C)包下,是一款优良的并发工具类。ConcurrentHashMap外部元素的存储构造和HashMap简直一样,都是数组+链表(达到阈值会转换成红黑树)的构造。不同的是,CouncurrentHashMap是线程平安的,但并不像HashTable那样粗犷的在每个办法上退出synchronized内置锁。而是采纳一种叫做“分段锁”的技术,将一整个table数组分成多个段,每个段有不同的锁,每个锁只能影响到本人所在的段,对其余段没有影响,也就是说,在并发环境下,多个线程能够同时对ConcurrentHashMap的不同的段进行操作。成果就是吞吐量进步了,效率也比HashTable高很多,但麻烦的是一些全局性变量不太好保障一致性,例如size。

对于ConcurrentHashMap更多的内容,还是倡议自行查找材料,网上有很多剖析ConcurrentHashMap的优良文章。

4.4 HashMap、HashTable和ConcurrentHashMap的区别

其实下面几个大节都始终有比拟,就在这里总结一下:

  1. HashTable是HashMap的同步版本,但因为同步伎俩太粗犷,效率较低,ConcurrentHashMap在JDK1.5之后呈现,是HashTable的代替类,在此之前,如果想要保障HashMap的线程平安,要么应用HashTable,要么应用Collections.synchronizedMap来包装HashMap,但这两个计划的效率都比拟低。
  2. 他们三者的实现形式简直一样,外部存储构造并没有什么差异。
  3. HashTable简直处于半废除的状态,不倡议在新我的项目中应用了,举荐应用ConcurrentHashMap。

5 Java8中Stream对汇合类的加强

Java8中除了lambda表达式,最大的个性就是Stream流了。Stream API能够将汇合看做流,汇合中的元素看做一个一个的流元素。这样的形象能够将对汇合的操作变得很简略、清晰,例如在以前要想合并两个汇合,就不得不做创立一个新的汇合,而后遍历两个汇合将元素放入到新的汇合中,但用流API的话就非常简单了,只须要将两个汇合看做两个流,间接将两个流合成一个流即可。

Stream API还提供了很多高阶函数用于操作流元素,流入map,reduce,filter等,上面是一个应用Stream API的示例:

public void streamTest() {    Random random = new Random();    List<Integer> integers = IntStream.generate(() -> random.nextInt(100))            .limit(100).boxed()            .collect(Collectors.toList());    integers.stream().map(num -> num * 10)            .filter(num -> num % 2 == 0)            .forEach(System.out::println);}复制代码

就这几行代码,实际上只能算是三行代码,就实现了随机生成元素放入list中,并且做了一个map操作和filter操作,还顺带遍历了一下List。如果要用以前的办法,就不得不这样写:

public void originTest() {    Random random = new Random();    List<Integer> integers = new ArrayList<>();    for (int i = 0; i < 100; i++) {        integers.add(random.nextInt(100));    }    for (int i = 0; i < 100; i++) {        integers.set(i, integers.get(i) * 10); //map    }    for (int i = 0; i < 100; i++) {        if (integers.get(i) % 2 == 0)  //filter            System.out.println(integers.get(i)); //foreach    }}复制代码

这三个for循环看起来切实是难看。这就是Stream API的长处,简洁,不便,形象水平高,但可读性会差一些,如果对lambda和Stream不相熟的敌人第一次看到可能会比拟吃力(但实际上,这是很简略的代码)。

那是不是当前对汇合的操作都应用Stream API呢?别那么极其,Stream API的确简洁,但可读性很差,Debug难度十分高,更多的时候是靠人肉Debug,而且性能上可能会低于传统的办法,也有可能高,所以,我的倡议是:在应用之前,最初先测试一下,将两种计划比照一下,最终依据测试后果筛选一个比拟好的计划。

6 小结

汇合类是Java开发者必须要把握的,通过浏览源码了解它们比看文章详解来的更加粗浅。本文只是简略的讲了几个罕用的汇合类,还有很多其余的例如TreeMap,HashSet,TreeSet,Stack都没有波及,不是说这些汇合类不重要,只是受篇幅限度,无奈一一道来,心愿读者能好好认真看看这些类的源码,看源码的时候不须要从头看到尾,能够先看几个罕用的办法,例如get,put等,而后一步一步跟进去,也能够应用调试器单步跟踪代码。