JDK源代码学习-ArrayList、LinkedList、HashMap

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.

 */

transientObject[] elementData; // non-private to simplify nested class access


/**

 * The size of the ArrayList (the number of elements it contains).

 *

 * @serial

 */

privateintsize;

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

Fail-Fast 机制

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

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

/**

 * The number of times this list has been structurally modified.

 * 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.

 *

 *

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

 * fail-fast behavior, rather than non-deterministic behavior in

 * the face of concurrent modification during iteration.

 *

 *

Use of this field by subclasses is optional. 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.

 */

protectedtransientintmodCount = 0;

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

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

publicclassArrayListRemoveTest {

  publicstaticvoidmain(String[] args) {

    List lstString = newArrayList();

    lstString.add("hello");


    Iterator iterator = lstString.iterator();

    while(iterator.hasNext()) {

      String item = iterator.next();

      if(item.equals("hello")) {

        lstString.remove(item);

      }

    }

  }

}

运行后会抛出异常:

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

finalvoidcheckForComodification() {

    if(modCount != expectedModCount)

        thrownewConcurrentModificationException();

}

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

LinkedList

链表

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

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

transientintsize = 0;

transientNode first; //链表的头指针

transientNode last; //尾指针

//存储对象的结构 Node, LinkedList的内部类

privatestaticclassNode {

    E item;

    Node next; // 指向下一个节点

    Node prev; //指向上一个节点


    Node(Node prev, E element, Node 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。

欢迎工作一到五年的Java工程师朋友们加入Java架构开发: 854393687

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

你可能感兴趣的:(JDK源代码学习-ArrayList、LinkedList、HashMap)