Java SE基础巩固(四):集合类

1 集合概述

Java中有不少集合类,例如ArrayList,LinkedList,HashMap,TreeMap等。集合的功能就是容纳多个对象,它们就像容器同样(实际上,直接称为容器也没有毛病,C++就是这样称呼的),当须要的时候,能够从里面拿出来,很是方便。在Java5提供了泛型机制以后,使容器有了在编译期作类型检查的能力,使用起来更加安全、方便。java

Java中的集合类主要是有两个接口派生出来的:Collection和Map,以下所示:node

i3JEbd.png

i3JZVA.png

Collection又主要有Set,Queue,List三大接口,在此基础上,又有多个实现类。Map接口下一样有总多的实现类,例如HashMap,EnumMap,HashTable等。接下来我将会挑选几个经常使用的集合类来具体讨论讨论。算法

2 ArrayList和LinkedList

List集合能够说是最经常使用的集合了,比HashMap还经常使用,通常咱们写代码的时候一旦遇到须要存储多个元素的状况,就优先想到使用List集合,至于使用的是ArrayList实现类仍是LinkedList实现类,那就具体状况具体分析了。数组

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。下图表示其继承体系:并发

i3JrqJ.png

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

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操做:dom

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等,而后一步一步跟进去,也可使用调试器单步跟踪代码。

相关文章
相关标签/搜索