从源码角度认识 ArrayList,LinkedList,HashMap

2,368 阅读20分钟

本文会从源码(JDK 1.8)的角度来分析以下几个Java中常用的数据结构,主要会分析原理与实现,以及每个数据结构所支持的常用操作的复杂度。

  • ArrayList
  • LinkedList
  • HashMap

在对以上数据结构进行具体分析时,我们主要会从以下三个角度来切入:

  • Why:为什么要使用这个数据结构?这个数据结构是为解决什么问题而出现的?
  • What:这个数据结构的原理与实现是什么?所支持的各项操作的复杂度如何?
  • How:如何使用这个数据结构?

ArrayList

定义

快速了解ArrayList究竟是什么的一个好方法就是看JDK源码中对ArrayList类的注释,大致翻译如下:

/** 
  * 实现了List的接口的可调整大小的数组。实现了所有可选列表操作,并且允许所有类型的元素,
  * 包括null。除了实现了List接口,这个类还提供了去动态改变内部用于存储集合元素的数组尺寸
  * 的方法。(这个类与Vector类大致相同,除了ArrayList是非线程安全外。)size,isEmpty,
  * get,set,iterator,和listIterator方法均为常数时间复杂度。add方法的摊还时间复杂度为
  * 常数级别,这意味着,添加n个元素需要的时间为O(n)。所有其他方法的时间复杂度都是线性级别的。
  * 常数因子要比LinkedList低。
  * 每个ArrayList实例都有一个capacity。capacity是用于存储ArrayList的元素的内部数组的大小。
  * 它通常至少和ArrayList的大小一样大。当元素被添加到ArrayList时,它的capacity会自动增长。
  * 在向一个ArrayList中添加大量元素前,可以使用ensureCapacity方法来增加ArrayList的容量。
  * 使用这个方法来一次性地使ArrayList内部数组的尺寸增长到我们需要的大小提升性能。需要注意的
  * 是,这个ArrayList实现是未经同步的。若在多线程环境下并发访问一个ArrayList实例,并且至少
  * 一个线程对其作了结构型修改,那么必须在外部做同步。(结构性修改指的是任何添加或删除了一个或
  * 多个元素的操作,以及显式改变内部数组尺寸的操作。set操作不是结构性修改)在外部做同步通常通
  * 过在一些自然地封装了ArrayList的对象上做同步来实现。如果不存在这样的对象,ArrayList应
  * 使用Collections.synchronizedList方法来包装。最好在创建时就这么做,以防止对ArrayList
  * 无意的未同步访问。(List list = Collections.synchronizedList(new ArrayList(...));)
  * ArrayList类的iterator()方法以及listIterator()方法返回的迭代器是fail-fast的:
  * 在iterator被创建后的任何时候,若对list进行了结构性修改(以任何除了通过迭代器自己的
  * remove方法或add方法的方式),迭代器会抛出一个ConcurrentModificationException异常。
   * 因此,在遇到并发修改时,迭代器马上抛出异常,而不是冒着以后可能在不确定的时间发生不确定行为
  * 的风险继续。需要注意的是,迭代器的fail-fast行为是不能得到保证的,因为通常来说在未同步并发
  * 修改面前无法做任何保证。fail-fast迭代器会尽力抛出ConcurrentModificationException异常。
  * 因此,编写正确性依赖于这个异常的程序是不对的:fail-fast行为应该仅仅在检测bugs时被使用。
  * ArrayList类是Java集合框架中的一员。
*/

根据源码中的注释,我们了解了ArrayList用来组织一系列同类型的数据对象,支持对数据对象的顺序迭代与随机访问。我们还了解了ArrayList所支持的操作以及各项操作的时间复杂度。接下来我们来看看这个类实现了哪些接口。

public class ArrayList extends AbstractList       
    implements List, RandomAccess, Cloneable, java.io.Serializable

我们可以看到,它实现了4个接口:List、RandomAccess、Cloneable、Serializable。
官方文档对List接口的说明如下:List是一个有序的集合类型(也被称作序列)。使用List接口可以精确控制每个元素被插入的位置,并且可以通过元素在列表中的索引来访问它。列表允许重复的元素,并且在允许null元素的情况下也允许多个null元素。
List接口定义了以下方法:

ListIterator listIterator();
void add(int i, E element);
E remove(int i);
E get(int i);
E set(int i, E element);
int indexOf(Object element);

我们可以看到,add、get等方法都是我们在使用ArrayList时经常用到的。
在ArrayList的源码注释中提到了,ArrayList使用Object数组来存储集合元素。我们来一起看下它的源码中定义的如下几个字段:

/** * 默认初始capacity. */
private static final int DEFAULT_CAPACITY = 10;
/** * 供空的ArrayList实例使用的空的数组实例 */
private static final Object[] EMPTY_ELEMENTDATA = {};
/** * 供默认大小的空的ArrayList实例使用的空的数组实例。
     * 我们把它和EMPTY_ELEMENTDATA区分开来,一边指导当地一个元素被添加时把内部数组尺寸设为
     * 多少
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/** * 存放ArrayList中的元素的内部数组。
     * ArrayList的capacity就是这个内部数组的大小。
     * 任何elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList在第一个元素
     * 被添加进来时,其capacity都会被扩大至DEFAULT_CAPACITYhe 
*/
transient Object[] elementData; // non-private to simplify nested class access
/** *ArrayList所包含的元素数 */
private int size;

通过以上字段,我们验证了ArrayList内部确实使用一个Object数组来存储集合元素。
那么接下来我们看一下ArrayList都有哪些构造器,从而了解ArrayList的构造过程。

ArrayList的构造器

首先我们来看一下我们平时经常使用的ArrayList的无参构造器的源码:

/** * Constructs an empty list with an initial capacity of ten. */
public ArrayList() {    
  this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

我们可以看到,无参构造器仅仅是把ArrayList实例的elementData字段赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
接下来,我们再来看一下ArrayList的其他构造器:

/** * Constructs an empty list with the specified initial capacity. 
     * * @param  initialCapacity  the initial capacity of the list 
     * @throws IllegalArgumentException if the specified initial capacity 
     *         is negative 
*/
public ArrayList(int initialCapacity) {    
  if (initialCapacity > 0) {        
    this.elementData = new Object[initialCapacity];    
  } else if (initialCapacity == 0) {        
    this.elementData = EMPTY_ELEMENTDATA;    
  } else {        
    throw new IllegalArgumentException("Illegal Capacity: "+                                           
        initialCapacity);    
  }
}

/** * Constructs a list containing the elements of the specified 
     * collection, in the order they are returned by the collection's * iterator. 
     * * @param c the collection whose elements are to be placed into this list 
     * @throws NullPointerException if the specified collection is null 
*/
public ArrayList(Collection c) {    
  elementData = c.toArray();    
  if ((size = elementData.length) != 0) {        
    // c.toArray might (incorrectly) not return Object[] (see 6260652)        
    if (elementData.getClass() != Object[].class)            
      elementData = Arrays.copyOf(elementData, size, Object[].class);      
  } else {        
    // replace with empty array.        
    this.elementData = EMPTY_ELEMENTDATA;    
  }
}

通过源码我们可以看到,第一个构造器指定了ArrayList的初始capacity,然后根据这个初始capacity创建一个相应大小的Object数组。若initialCapacity为0,则将elementData赋值为EMPTY_ELEMENTDATA;若initialCapacity为负数,则抛出一个IllegalArgumentException异常。

第二个构造器则指定一个Collection对象作为参数,从而构造一个含有指定集合对象元素的ArrayList对象。这个构造器首先把elementData实例域赋值为集合对象转为的数组,而后再判断传入的集合对象是否不含有任何元素,若是的话,则将elementData赋值为EMPTY_ELEMENTDATA;若传入的集合对象至少包含一个元素,则进一步判断c.toArray方法是否正确返回了Object数组,若不是的话,则需要用Arrays.copyOf方法把elementData的元素类型改变为Object。

现在,我们又了解了ArrayList实例的构建过程,那么接下来我们来通过ArrayList的get、set等方法的源码来进一步了解它的实现原理。

add方法源码分析

/** * Appends the specified element to the end of this list. 
     * * @param e element to be appended to this list 
     * @return true (as specified by {@link Collection#add}) 
*/
public boolean add(E e) {    
  ensureCapacityInternal(size + 1);  // Increments modCount!!    
  elementData[size++] = e;    
  return true;
}

我们可以看到,在add方法内部,首先调用了ensureCapacityInternal(size+1),这句的作用有两个:

  • 保证当前ArrayList实例的capacity足够大;
  • 增加modCount,modCount的作用是判断在迭代时是否对ArrayList进行了结构性修改。

然后通过将内部数组下一个索引处的元素设置为给定参数来完成了向ArrayList中添加元素,返回true表示添加成功。

get方法源码分析

/** * Returns the element at the specified position in this list. 
     * * @param  index index of the element to return 
     * @return the element at the specified position in this list 
     * @throws IndexOutOfBoundsException {@inheritDoc} 
*/
public E get(int index) {    
  rangeCheck(index);    
  return elementData(index);
}

首先调用了rangeCheck方法来检查我们传入的index是否在合法范围内,然后调用了elementData方法,这个方法的源码如下:

E elementData(int index) {    
  return (E) elementData[index];
}

set方法源码分析

/** * Replaces the element at the specified position in this list with 
     * the specified element. 
     * * @param index index of the element to replace 
     * @param element element to be stored at the specified position 
    * @return the element previously at the specified position 
    * @throws IndexOutOfBoundsException {@inheritDoc} 
*/
public E set(int index, E element) {    
  rangeCheck(index);    
  E oldValue = elementData(index);    
  elementData[index] = element;    
  return oldValue;
}

我们可以看到,set方法的实现也很简单,首先检查给定的索引是否在合法范围内,若在,则先把该索引处原来的元素存储在oldValue中,然后把新元素放到该索引处并返回oldValue即可。

LinkedList

定义

LinkedList类源码中的注释如下:

/** * 实现了List接口的双向链表。实现了所有可选列表操作,并且可以存储所有类型的元素,包括null。
  * 对LinkedList指定索引处的访问需要顺序遍历整个链表,直到到达指定元素。
  * 注意LinkedList是非同步的。若多线程并发访问LinkedList对象,并且至少一个线程对其做
  * 结构性修改,则必须在外部对它进行同步。这通常通过在一些自然封装了LinkedList的对象上
  * 同步来实现。若不存在这样的对象,这个list应使用Collections.synchronizedList来包装。     
  * 这最好在创建时完成,以避免意外的非同步访问。
  * LinkedList类的iterator()方法以及listIterator()方法返回的迭代器是fail-fast的:
  * 在iterator被创建后的任何时候,若对list进行了结构性修改(以任何除了通过迭代器自己的
  * remove方法或add方法的方式),迭代器会抛出一个ConcurrentModificationException异常。
  * 因此,在遇到并发修改时,迭代器马上抛出异常,而不是冒着以后可能在不确定的时间发生不确定行为
  * 的风险继续。需要注意的是,迭代器的fail-fast行为是不能得到保证的,因为通常来说在未同步并发
  * 修改面前无法做任何保证。fail-fast迭代器会尽力抛出ConcurrentModificationException异常。
  * 因此,编写正确性依赖于这个异常的程序是不对的:fail-fast行为应该仅仅在检测bugs时被使用。
  * LinkedList类是Java集合框架中的一员。 
*/

LinkedList是对链表这种数据结构的实现(对链表还不太熟悉的小伙伴可以参考深入理解数据结构之链表),当我们需要一种支持高效删除/添加元素的数据结构时,可以考虑使用链表。
总的来说,链表具有以下两个优点:

  • 插入及删除操作的时间复杂度为O(1)
  • 可以动态改变大小

链表主要的缺点是:由于其链式存储的特性,链表不具备良好的空间局部性,也就是说,链表是一种缓存不友好的数据结构。

支持的操作

LinkedList主要支持以下操作:

void addFirst(E element);
void addLast(E element);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
boolean add(E e) //把元素e添加到链表末尾
void add(int index, E element) //在指定索引处添加元素

以上操作除了add(int index, E element)外,时间复杂度均为O(1),而add(int index, E element)的时间复杂度为O(N)。

Node类

在LinkedList类中我们能看到以下几个字段:

transient int size = 0;
/** * 指向头结点  */
transient Node first;
/** * 指向尾结点 */
transient Node last;

我们看到,LinkedList只保存了头尾节点的引用作为其实例域,接下来我们看一下LinkedList的内部类Node的源码如下:

private static class Node {    
  E item;    
  Node next;    
  Node prev;    
  Node(Node prev, E element, Node next) {        
    this.item = element;        
    this.next = next;        
    this.prev = prev;    
  }
}

每个Node对象的next域指向它的下一个结点,prev域指向它的上一个结点,item为本结点所存储的数据对象。

addFirst源码分析

/** * Inserts the specified element at the beginning of this list. 
      * * @param e the element to add 
*/
public void addFirst(E e) {    
  linkFirst(e);
}

实际干活的是linkFirst,它的源码如下:

/** * Links e as first element. */
private void linkFirst(E e) {    
  final Node f = first;    
  final Node newNode = new Node<>(null, e, f);    
  first = newNode;    
  if (f == null)        
    last = newNode;    
  else        
    f.prev = newNode;    
  size++;    
  modCount++;
}

首先把头结点引用存于变量f中,而后创建一个新结点,这个新结点的数据为我们传入的参数e,prev指针为null,next指针为f。然后把头结点指针指向新创建的结点newNode。而后判断f是否为null,若为null,说明之前链表中没有结点,所以last也指向newNode;若f不为null,则把f的prev指针设为newNode。最后还需要把size和modCount都加一,modCount的作用与在ArrayList中的相同。

getFirst方法源码分析

/** * Returns the first element in this list. 
     * * @return the first element in this list 
     * @throws NoSuchElementException if this list is empty 
*/
public E getFirst() {    
  final Node f = first;    
  if (f == null)        
    throw new NoSuchElementException();    
  return f.item;
}

这个方法的实现很简单,主需要直接返回first的item域(当first不为null时),若first为null,则抛出NoSuchElementException异常。

removeFirst方法源码分析

/** * Removes and returns the first element from this list. 
     * * @return the first element from this list 
     * @throws NoSuchElementException if this list is empty 
*/
public E removeFirst() {    
  final Node f = first;    
  if (f == null)        
    throw new NoSuchElementException();    
  return unlinkFirst(f);
}

unlinkFirst方法的源码如下:

/** * Unlinks non-null first node f. */
private E unlinkFirst(Node f) {    
  // assert f == first && f != null;    
  final E element = f.item;    
  final Node next = f.next;    
  f.item = null;    
  f.next = null; // help GC    
  first = next;    
  if (next == null)        
    last = null;    
  else        
    next.prev = null;    
  size--;    
  modCount++;    
  return element;
}

add(int index, E e)方法源码分析

/** * Inserts the specified element at the specified position in this list. 
     * Shifts the element currently at that position (if any) and any 
     * subsequent elements to the right (adds one to their indices). 
     * * @param index index at which the specified element is to be inserted 
     * @param element element to be inserted 
     * @throws IndexOutOfBoundsException {@inheritDoc} 
*/
public void add(int index, E element) {    
  checkPositionIndex(index);    
  if (index == size)        
    linkLast(element);    
  else        
    linkBefore(element, node(index));
}

这个方法中,首先调用checkPositionIndex方法检查给定index是否在合法范围内。然后若index等于size,这说明要在链表尾插入元素,直接调用linkLast方法,这个方法的实现与之前介绍的linkFirst类似;若index小于size,则调用linkBefore方法,在index处的Node前插入一个新Node(node(index)会返回index处的Node)。linkBefore方法的源码如下:

/** * Inserts element e before non-null Node succ. */
void linkBefore(E e, Node succ) {    
  // assert succ != null;    
  final Node pred = succ.prev;    
  final Node newNode = new Node<>(pred, e, succ);    
  succ.prev = newNode;    
  if (pred == null)        
    first = newNode;    
  else        
    pred.next = newNode;    
  size++;    
  modCount++;
}

我们可以看到,在知道要在哪个结点前插入一个新结点时,插入操作是很容易的,时间复杂度也只有O(1)。下面我们来看一下node方法是如何获取指定索引处的Node的:

/** * Returns the (non-null) Node at the specified element index. */
Node node(int index) {    
  // assert isElementIndex(index);    
  if (index < (size >> 1)) {        
    Node x = first;        
    for (int i = 0; i < index; i++)            
      x = x.next;        
    return x;    
  } else {        
    Node x = last;        
    for (int i = size - 1; i > index; i--)            
      x = x.prev;        
    return x;    
  }
}

首先判断index位于链表的前半部分还是后半部分,若是前半部分,则从头结点开始遍历,否则从尾结点开始遍历,这样可以提升效率。我们可以看到,这个方法的时间复杂度为O(N)。

HashMap

Map接口

我们先来看下它的定义:

一个把键映射到值的对象被称作一个映射表对象。映射表不能包含重复的键,每个键至多可以与一个值关联。Map接口提供了三个集合视图:键的集合视图、值的集合视图以及键值对的集合视图。一个映射表的顺序取决于它的集合视图的迭代器返回元素的顺序。一些Map接口的具体实现(比如TreeMap)保证元素有一定的顺序,其它一些实现(比如HashMap)则不保证元素在其内部有序。

也就是说,Map接口定义了一个类似于“字典”的规范,让我们能够根据键快速检索到它所关联的值。我们先来看看Map接口定义了哪些方法:

void clear()
boolean containsKey(Object key) //判断是否包含指定键
boolean containsValue(Object value) //判断是否包含指定值
boolean isEmpty()
V get(Object key) //返回指定键映射的值
V put(K key, V value) //放入指定的键值对
V remove(Object key)
int size()
Set> entrySet() 
Set keySet()
Collection values()

HashMap的定义

HashMap是基于哈希表这个数据结构的Map接口具体实现,允许null键和null值(最多只允许一个key为null,但允许多个value为null)。这个类与HashTable近似等价,区别在于HashMap不是线程安全的并且允许null键和null值。由于基于哈希表实现,所以HashMap内部的元素是无序的。HashMap对与get与put操作的时间复杂度是常数级别的(在散列均匀的前提下)。对HashMap的集合视图进行迭代所需时间与HashMap的capacity(bucket的数量)加上HashMap的尺寸(键值对的数量)成正比。因此,若迭代操作的性能很重要,不要把初始capacity设的过高(不要把load factor设的过低)。

(对散列表(哈希表)这种数据结构还不太熟悉的小伙伴请戳这里散列表的原理与实现
有两个因素会影响一个HashMap的性能:intial capacity(初始容量)和load factor(负载因子)。intial capacity就是HashMap对象刚创建时其内部的哈希表的“桶”的数量。load factor等于maxSize / capacity,也就是HashMap所允许的最大键值对数与桶数的比值。增大load factor可以节省空间但查找一个元素的时间会增加,减小load factor会占用更多的存储空间,但是get与put的操作会更快。当HashMap中的键值对数量超过了maxSize(即load factor与capacity的乘积),它会再散列,再散列会重建内部数据结构,桶数(capacity)大约会增加到原来的两
倍。
HashMap默认的load factor大小为0.75,这个数值在时间与空间上做了很好的权衡。当我们清楚自己将要大概存放多少数据时,也可以自定义load factor的大小。
HashMap的常用方法如下:

void clear()
boolean containsKey(Object key)
boolean containsValue(Object value)
V get(Object key)
V put(K key, V value)
boolean isEmpty()
V remove(Object key)
int size()
Collection values()
Set> entrySet()
Set keySet()

HashMap的构造器

HashMap有以下几个构造器:

HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
HashMap(Map m) //创建一个新的HashMap,用m的数据填充

无参构造器的源码如下:

/** * Constructs an empty HashMap with the default initial capacity * (16) and the default load factor (0.75). */
public HashMap() {    
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

这个构造器把loadFactor域设为DEFAULT_LOAD_FACTOR(0.75),其他域都保持默认值。

我们再来看下第三个构造器的源码:

/** * Constructs an empty HashMap with the specified initial 
     * capacity and load factor. 
     * * @param  initialCapacity the initial capacity 
     * @param  loadFactor      the load factor 
     * @throws IllegalArgumentException if the initial capacity is negative 
     *         or the load factor is nonpositive 
*/
public HashMap(int initialCapacity, float loadFactor) {    
  if (initialCapacity < 0)        
    throw new IllegalArgumentException("Illegal initial capacity: " +                                           initialCapacity);    
  if (initialCapacity > MAXIMUM_CAPACITY)        
    initialCapacity = MAXIMUM_CAPACITY;    
  if (loadFactor <= 0 || Float.isNaN(loadFactor))        
    throw new IllegalArgumentException("Illegal load factor: " +                                           loadFactor);    
  this.loadFactor = loadFactor;    
  this.threshold = tableSizeFor(initialCapacity);
}

以上源码中的threshold即为上面提到的maxSize(loadFactor与capacity的乘积)。tableSizeFor方法会根据给定的initialCapacity返回一个值作为maxSize。

基本实现原理

HashMap是基于拉链法处理碰撞的散列表的实现,一个存储整型元素的HashMap的内部存储结构如下图所示:


linked.jpg


我们可以看到,HashMap是采用数组+链表实现的,在JDK 1.8中,对HashMap做了进一步优化,引入了红黑树。当链表的长度大于8时,就会使用红黑树来代替链表。

put方法源码分析

在分析put方法前,我们先来看下HashMap的如下字段:

/** * The table, initialized on first use, and resized as 
     * necessary. When allocated, length is always a power of two. 
     * (We also tolerate length zero in some operations to allow 
     * bootstrapping mechanics that are currently not needed.) 
*/
transient Node[] table;

table字段是一个Node数组,这个数组由链表的头结点组成。我们再来看一下Node的定义:

static class Node implements Map.Entry {    
  final int hash;    
  final K key;    
  V value;    
  Node next;    
  Node(int hash, K key, V value, Node next) {        
    this.hash = hash; //"桶号",即该Node在数组的索引       
    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;    
  }    
  . . . 
}

Node类的hash域为它在Node数组中的索引,next域为它的下一个Node,key、value分别为保存在Node中的键和值。
接下来我们看看put方法的源码:

public V put(K key, V value) {    
  return putVal(hash(key), key, value, false, true);
}

这个方法内部实际上调用了putVal方法来干活,hash方法会返回给定key在HashMap中的桶号(即key所在Node在Node数组中的索引),实际上hash方法的作用是在key的hashCode方法的基础上进一步增加哈希值的随机度。putVal方法的源码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {    
  Node[] tab; 
  Node p; 
  int n, i;    
  //若table为空或table的length为0则需要通过resize方法扩容
  if ((tab = table) == null || (n = tab.length) == 0)        
    n = (tab = resize()).length; 
  //让传入的hash与n-1做与运算从而得到目标Node的索引
  //若该索引处为null,则直接插入包含了key-value pair的new Node   
  if ((p = tab[i = (n - 1) & hash]) == null)        
    tab[i] = newNode(hash, key, value, null);    
  else {        
    //若索引处不为null,则判断key是否存在
    Node e; 
    K k;        
    //若key存在,则直接覆盖value
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
      e = p; 
    //若key不存在,则判断table[i]是否为TreeNode       
    else if (p instanceof TreeNode)            
      //若是的话,说明此处为红黑树,直接插入key-value pair
      e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
      //否则遍历链表        
      else {            
        for (int binCount = 0; ; ++binCount) {                
          if ((e = p.next) == null) {                    
            p.next = newNode(hash, key, value, null);      
            //链表长度大于8则转为红黑树              
            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                        
              treeifyBin(tab, hash);                    
            break;                
          }
          //若key已经存在则直接覆盖value                
          if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))                    
            break;                
          p = e;            
       }        
     }        
     if (e != null) { // existing mapping for key            
       V oldValue = e.value;            
       if (!onlyIfAbsent || oldValue == null)                
         e.value = value;            
       afterNodeAccess(e);            
       return oldValue;        
    }    
  }    
  ++modCount;    
  //若超过maxSize,则扩容
  if (++size > threshold)        
    resize();    
  afterNodeInsertion(evict);    
  return null;
}

以上代码的工作过程可以总结为下图:


put.png


关于HashMap我们还需要知道它的扩容方法resize的时间消耗比较大,因此我们在能够估计到大致需要存储的数据量时,应该为其指定一个合适的初始容量。

get方法源码分析

public V get(Object key) {    
  Node e;    
  return (e = getNode(hash(key), key)) == null ? null : e.value;
}

我们可以看到这个方法内部调用了getNode方法以获取key所在的Node,若成功获取到了,则返回key对应的value,否则返回null。getNode方法的源码如下:

final Node getNode(int hash, Object key) {    
  Node[] tab; 
  Node first, e; 
  int n; 
  K k;    
  //若table不为空且长度大于0且指定索引处Node不为空
  //则进一步进行其他判断,否则直接返回null
  if ((tab = table) != null && (n = tab.length) > 0 
    && (first = tab[(n - 1) & hash]) != null) {
      //指定索引处的Node即为我们要找的Node,直接返回即可        
      if (first.hash == hash && // always check first node            
          ((k = first.key) == key || (key != null && key.equals(k))))            
        return first;
      //我们的目标Node和first处于同一红黑树或同一链表中,
      //位于first之后        
      if ((e = first.next) != null) {            
        //first为红黑树
        if (first instanceof TreeNode)                
          return ((TreeNode)first).getTreeNode(hash, key);              
        //first为链表
        do {                
          if (e.hash == hash &&                    
             ((k = e.key) == key || (key != null && key.equals(k))))                    
            return e;            
        } while ((e = e.next) != null);        
      }    
    }    
    return null;
}

理解了putVal方法,getNode方法的逻辑便很容易理解了。

以上是我从源码角度对ArrayList,LinkedList,HashMap这三种常用数据结构所做的分析,若有不准确或是不清晰的地方,希望大家指出,谢谢大家:)

参考资料

  1. Java Docs
  2. Java 8之重新认识HashMap

长按或扫描二维码关注我们,让您利用每天等地铁的时间就能学会怎样写出优质app。