ArrayList、LinkedList、HashMap是Java开发中非常常见的数据类型。它们的区别也非常明显的,在Java中也非常具有代表性。在Java中,常见的数据结构是:数组、链表,其他数据结构基本就是这两者的组合。

  复习一下数组、链表的特征。

  数组:在内存中连续的地址块,查找按照下标来寻址,查找快速。但是插入元素和删除元素慢,需要移动元素。

  链表:内存中逻辑上可以连接到一起的一组节点。每个节点除了存储本身,还存储了下一个元素的地址。查找元素需要依次找找各个元素,查找慢,插入和删除元素只需要修改元素指向即可。

结合这两种数据结构的特征,就不难理解ArrayList、LinkedList、HashMap的各种操作了。

ArrayList

数组

ArrayList的底层实现就是数组,根据数组的特征就很好理解ArrayList的各个特性了。

下面是ArrayList中最基本的两个变量:存储对象的数组和数组大小。

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

在执行add操作前,会首先检查数组大小是否足以容纳新的元素,如果不够,就进行扩容,扩容的公式是:新的数组大小=(老的数组大小*3)/2 + 1,例如初始时数组大小为10,第一次扩容后,数组大小就为16,再扩容一次变为25。

Fail-Fast 机制

在操作元素的方法中,例如add方法和remove方法中,会看到modCount++操作。这个modCount变量是记录什么的?

查看modCount的定义,modCount是在AbstractList中定义的,其说明如下:

    /**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     *
     * <p>This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     */
    protected transient int modCount = 0;

 modCount记录是List的结构变化次数,就是List大小变化的次数,如果在遍历List的时候,发现modCount发生变化,则抛出异常ConcurrentModificationException。

例如下面的代码,定义了一个Array List,向其中增加元素,然后遍历元素,在遍历元素过程中,删除了一个元素。

public class ArrayListRemoveTest {
  public static void main(String[] args) {
    List<String> lstString = new ArrayList<String>();
    lstString.add("hello");

    Iterator<String> iterator = lstString.iterator();
    while (iterator.hasNext()) {
      String item = iterator.next();
      if (item.equals("hello")) {
        lstString.remove(item);
      }
    }
  }
}

 运行后会抛出异常:

根据报错堆栈,next方法会调用checkForComodification方法,在checkForComodification方法中抛出异常。

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

代码中会比较当前的modCount和expectedModCount的值,expectedModCount的值是在执行Iterator<String> iterator = lstString.iterator();时,在Itr的构造函数中赋值的,是原始的List结构变化次数。在执行remove方法后,List的大小发生了变化,则modCount发生了变化,两次modCount不同,抛出异常。做这个检查的原因,是要保持单线程的唯一操作。这就是Fail-Fast机制。

LinkedList

链表

LinkedList的底层实现就是链表,插入和删除只需要改变节点指向,效率高。随机访问需要依次找到各个节点,慢。

LinkedList在类中包含了 first 和 last 两个指针(Node)。Node 中包含了上一个节点和下一个节点的引用,这样就构成了双向的链表。

    transient int size = 0;
    transient Node<E> first; //链表的头指针
    transient Node<E> last; //尾指针
    //存储对象的结构 Node, LinkedList的内部类
    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;
        }
    }

在新增节点时,只需要创建一个Node,指向这个Node即可。删除节点,修改上一个节点的prev指向即可。

HashMap

数组+链表

  HashMap是Java数据结构中两大结构数组和链表的组合。其结构图如下:

 可以看出,HashMap底层是数组,数组中的每一项又是一个链表。

  当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 在数组中的存储位置,即数组下标。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同(即碰撞)。再调用equals,如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖,就是value替换。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部。

  简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,再根据 equals 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该Entry。

版权声明:本文为leanfish原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/leanfish/p/9779513.html