欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

深入解析HashMap、ConcurrentHashMap丶HashTable丶ArrayList

程序员文章站 2024-03-15 09:17:11
...

Java集合类是个非常重要的知识点,HashMap、HashTable、ConcurrentHashMap等算是集合类中的重点,可谓“重中之重”,首先来看个问题,如面试官问你:HashMap和HashTable有什么区别,一个比较简单的回答是: 
1、HashMap是非线程安全的,HashTable是线程安全的。 
2、HashMap的键和值都允许有null值存在,而HashTable则不行。 
3、因为线程安全的问题,HashMap效率比HashTable的要高

能答出上面的三点,简单的面试,算是过了,但是如果再问:Java中的另一个线程安全的与HashMap极其类似的类是什么?同样是线程安全,它与HashTable在线程同步上有什么不同?能把第二个问题完整的答出来,说明你的基础算是不错的了。带着这个问题深入解HashMap和HashTable类应用而生

一、HashMap的内部存储结构 
Java中数据存储方式最底层的两种结构,一种是数组,另一种就是链表,数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。而链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。有没有一种数据结构来综合一下数组和链表,以便发挥他们各自的优势?答案是肯定的!就是:哈希表。哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”,我们可以深入解析HashMap、ConcurrentHashMap丶HashTable丶ArrayList理解为“链表的数组”,如下图: 
从上图中,我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。它的内部其实是用一个Entity数组来实现的,属性有key、value、next。接下来我会从初始化阶段详细的讲解HashMap的内部结构。

1、初始化

首先来看三个常量: 
static final int DEFAULT_INITIAL_CAPACITY = 16; 初始容量:16 
static final int MAXIMUM_CAPACITY = 1 
<< 30; 最大容量:2的30次方:1073741824 
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
装载因子,后面再说它的作用 
来看个无参构造方法,也是我们最常用的:

[java] view plain copy
public HashMap() {  
        this.loadFactor = DEFAULT_LOAD_FACTOR;  
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
        table = new Entry[DEFAULT_INITIAL_CAPACITY];  
        init();  
    }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

loadFactor、threshold的值在此处没有起到作用,不过他们在后面的扩容方面会用到,此处只需理解table=new Entry[DEFAULT_INITIAL_CAPACITY].说明,默认就是开辟16个大小的空间。另外一个重要的构造方法:

ublic 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);  

        // Find a power of 2 >= initialCapacity  
        int capacity = 1;  
        while (capacity < initialCapacity)  
            capacity <<= 1;  

        this.loadFactor = loadFactor;  
        threshold = (int)(capacity * loadFactor);  
        table = new Entry[capacity];  
        init();  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

就是说传入参数的构造方法,我们把重点放在:while (capacity <

initialCapacity)  
           capacity <<= 1; 
  • 1
  • 2

上面,该代码的意思是,实际的开辟的空间要大于传入的第一个参数的值。举个例子: 
new HashMap(7,0.8),loadFactor为0.8,capacity为7,通过上述代码后,capacity的值为:8.(1 << 2的结果是4,2 << 2的结果为8<此处感谢网友wego1234的指正>)。所以,最终capacity的值为8,最后通过new Entry[capacity]来创建大小为capacity的数组,所以,这种方法最红取决于capacity的大小。 
2、put(Object key,Object value)操作 
当调用put操作时,首先判断key是否为null,如下代码1处:

<p>public V put(K key, V value) {  
        if (key == null)  
            return putForNullKey(value);  
        int hash = hash(key.hashCode());  
        int i = indexFor(hash, table.length);  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }</p><p>        modCount++;  
        addEntry(hash, key, value, i);  
        return null;  
    }</p>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

如果key是null,则调用如下代码:

private V putForNullKey(V value) {  
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
            if (e.key == null) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
        modCount++;  
        addEntry(0, null, value, 0);  
        return null;  
    }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

就是说,获取Entry的第一个元素table[0],并基于第一个元素的next属性开始遍历,直到找到key为null的Entry,将其value设置为新的value值。 
如果没有找到key为null的元素,则调用如上述代码的addEntry(0, null, value, 0);增加一个新的entry,代码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {  
    Entry<K,V> e = table[bucketIndex];  
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
        if (size++ >= threshold)  
            resize(2 * table.length);  
    } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

先获取第一个元素table[bucketIndex],传给e对象,新建一个entry,key为null,value为传入的value值,next为获取的e对象。如果容量大于threshold,容量扩大2倍。 
如果key不为null,这也是大多数的情况,重新看一下源码:

public V put(K key, V value) {  
        if (key == null)  
            return putForNullKey(value);  
        int hash = hash(key.hashCode());//---------------2---------------  
        int i = indexFor(hash, table.length);  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//--------------3-----------  
            Object k;  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }//-------------------4------------------  
        modCount++;//----------------5----------  
        addEntry(hash, key, value, i);-------------6-----------  
        return null;  
    }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

看源码中2处,首先会进行key.hashCode()操作,获取key的哈希值,hashCode()是Object类的一个方法,为本地方法,内部实现比较复杂,我们 
会在后面作单独的关于Java中Native方法的分析中介绍。hash()的源码如下

static int hash(int h) {  
        // This function ensures that hashCodes that differ only by  
        // constant multiples at each bit position have a bounded  
        // number of collisions (approximately 8 at default load factor).  
        h ^= (h >>> 20) ^ (h >>> 12);  
        return h ^ (h >>> 7) ^ (h >>> 4);  
    } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

int i = indexFor(hash, table.length);的意思,相当于int i = hash % Entry[].length;得到i后,就是在Entry数组中的位置,(上述代码5和6处是如果Entry数组中不存在新要增加的元素,则执行5,6处的代码,如果存在,即Hash冲突,则执行 3-4处的代码,此处HashMap中采用链地址法解决Hash冲突。此处经网友bbycszh指正,发现上述陈述有些问题)。重新解释:其实不管Entry数组中i位置有无元素,都会去执行5-6处的代码,如果没有,则直接新增,如果有,则将新元素设置为Entry[0],其next指针指向原有对象,即原有对象为Entry[1]。具体方法可以解释为下面的这段文字:(3-4处的代码只是检查在索引为i的这条链上有没有key重复的,有则替换且返回原值,程序不再去执行5-6处的代码,无则无处理) 
上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。如, 第一个键值对A进来,通过计算其key的hash得到的i=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其i也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,i也等于0,那么C.next = B,Entry[0] = C;这样我们发现i=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起,也就是说数组中存储的是最后插入的元素。 
到这里为止,HashMap的大致实现,我们应该已经清楚了。当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个i的链就会很长,会不会影响性能?HashMap里面设置一个因素(也称为因子),随着map的size越来越大,Entry[]会以一定的规则加长长度。

2、get(Object key)操作 
get(Object key)操作时根据键来获取值,如果了解了put操作,get操作容易理解,先来看看源码的实现:

public V get(Object key) {  
        if (key == null)  
            return getForNullKey();  
        int hash = hash(key.hashCode());  
        for (Entry<K,V> e = table[indexFor(hash, table.length)];  
             e != null;  
             e = e.next) {  
            Object k;  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))//-------------------1----------------  
                return e.value;  
        }  
        return null;  
    }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

意思就是:1、当key为null时,调用getForNullKey(),源码如下:

private V getForNullKey() {  
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
            if (e.key == null)  
                return e.value;  
        }  
        return null;  
    } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

当key不为null时,先根据hash函数得到hash值,在更具indexFor()得到i的值,循环遍历链表,如果有:key值等于已存在的key值,则返回其value。如上述get()代码1处判断。 
总结下HashMap新增put和获取get操作:

//存储时:  
int hash = key.hashCode();  
int i = hash % Entry[].length;  
Entry[i] = value;  

//取值时:  
int hash = key.hashCode();  
int i = hash % Entry[].length;  
return Entry[i]; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

理解了就比较简单。 
此处附一个简单的HashMap小算法应用:

package com.xtfggef.hashmap;  

import java.util.HashMap;  
import java.util.Map;  
import java.util.Set;  

/** 
 * 打印在数组中出现n/2以上的元素 
 * 利用一个HashMap来存放数组元素及出现的次数 
 * @author erqing 
 * 
 */  
public class HashMapTest {  

    public static void main(String[] args) {  

        int [] a = {2,3,2,2,1,4,2,2,2,7,9,6,2,2,3,1,0};  

        Map<Integer, Integer> map = new HashMap<Integer,Integer>();  
        for(int i=0; i<a.length; i++){  
            if(map.containsKey(a[i])){  
                int tmp = map.get(a[i]);  
                tmp+=1;  
                map.put(a[i], tmp);  
            }else{  
                map.put(a[i], 1);  
            }  
        }  
        Set<Integer> set = map.keySet();//------------1------------  
        for (Integer s : set) {  
            if(map.get(s)>=a.length/2){  
                System.out.println(s);  
            }  
        }//--------------2---------------  
    }  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

此处注意两个地方,map.containsKey(), 
理解了HashMap的上面的操作,其它的大多数方法都很容易理解了。搞清楚它的内部存储机制,一切OK!

二、HashTable的内部存储结构 
HashTable和HashMap采用相同的存储机制,二者的实现基本一致,不同的是: 
1、HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都是synchronized。 
2、HashTable不允许有null值的存在。 
在HashTable中调用put方法时,如果key为null,直接抛出NullPointerException。其它细微的差别还有,比如初始化Entry数组的大小等等,但基本思想和HashMap一样。 
三、HashTable和ConcurrentHashMap的比较 
如我开篇所说一样,ConcurrentHashMap是线程安全的HashMap的实现。同样是线程安全的类,它与HashTable在同步方面有什么不同呢? 
之前我们说,synchronized关键字加锁的原理,其实是对对象加锁,不论你是在方法前加synchronized还是语句块前加,锁住的都是对象整体,但是ConcurrentHashMap的同步机制和这个不同,它不是加synchronized关键字,而是基于lock操作的,这样的目的是保证同步的时候,锁住的不是整个对象。事实上,ConcurrentHashMap可以满足concurrentLevel个线程并发无阻塞的操作集合对象。关于concurrentLevel稍后介绍。 
1、构造方法 
为了容易理解,我们先从构造函数说起。ConcurrentHashMap是基于一个叫Segment数组的,其实和Entry类似,如下:

public ConcurrentHashMap()  
  {  
    this(16, 0.75F, 16);  
  } 
  • 1
  • 2
  • 3
  • 4

默认传入值16,调用下面的方法:

public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  
  {  
    if ((paramFloat <= 0F) || (paramInt1 < 0) || (paramInt2 <= 0))  
      throw new IllegalArgumentException();  

    if (paramInt2 > 65536) {  
      paramInt2 = 65536;  
    }  

    int i = 0;  
    int j = 1;  
    while (j < paramInt2) {  
      ++i;  
      j <<= 1;  
    }  
    this.segmentShift = (32 - i);  
    this.segmentMask = (j - 1);  
    this.segments = Segment.newArray(j);  

    if (paramInt1 > 1073741824)  
      paramInt1 = 1073741824;  
    int k = paramInt1 / j;  
    if (k * j < paramInt1)  
      ++k;  
    int l = 1;  
    while (l < k)  
      l <<= 1;  

    for (int i1 = 0; i1 < this.segments.length; ++i1)  
      this.segments[i1] = new Segment(l, paramFloat);  
  }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

你会发现比HashMap的构造函数多一个参数,paramInt1就是我们之前谈过的initialCapacity,就是数组的初始化大小,paramfloat为loadFactor(装载因子),而paramInt2则是我们所要说的concurrentLevel,这三个值分别被初始化为16,0.75,16,经过:

while (j < paramInt2) {  
      ++i;  
      j <<= 1;  
    }  
  • 1
  • 2
  • 3
  • 4

后,j就是我们最终要开辟的数组的size值,当paramInt1为16时,计算出来的size值就是16.通过: 
this.segments = Segment.newArray(j)后,我们看出了,最终稿创建的Segment数组的大小为16.最终创建Segment对象时:

this.segments[i1] = new Segment(cap, paramFloat);  
  • 1

需要cap值,而cap值来源于:

int k = paramInt1 / j;  
  if (k * j < paramInt1)  
    ++k;  
  int cap = 1;  
  while (cap < k)  
    cap <<= 1;  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
public V put(K paramK, V paramV)  
  {  
    if (paramV == null)  
      throw new NullPointerException();  
    int i = hash(paramK.hashCode());  
    return segmentFor(i).put(paramK, i, paramV, false);  
  }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

与HashMap不同的是,如果key为null,直接抛出NullPointer异常,之后,同样先计算hashCode的值,再计算hash值,不过此处hash函数和HashMap中的不一样:

private static int hash(int paramInt)  
  {  
    paramInt += (paramInt << 15 ^ 0xFFFFCD7D);  
    paramInt ^= paramInt >>> 10;  
    paramInt += (paramInt << 3);  
    paramInt ^= paramInt >>> 6;  
    paramInt += (paramInt << 2) + (paramInt << 14);  
    return (paramInt ^ paramInt >>> 16);  
  }  


[java] view plain copy
final Segment<K, V> segmentFor(int paramInt)  
  {  
    return this.segments[(paramInt >>> this.segmentShift & this.segmentMask)];  
  } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

根据上述代码找到Segment对象后,调用put来操作:

V put(K paramK, int paramInt, V paramV, boolean paramBoolean)  
{  
  lock();  
  try {  
    Object localObject1;  
    Object localObject2;  
    int i = this.count;  
    if (i++ > this.threshold)  
      rehash();  
    ConcurrentHashMap.HashEntry[] arrayOfHashEntry = this.table;  
    int j = paramInt & arrayOfHashEntry.length - 1;  
    ConcurrentHashMap.HashEntry localHashEntry1 = arrayOfHashEntry[j];  
    ConcurrentHashMap.HashEntry localHashEntry2 = localHashEntry1;  
    while ((localHashEntry2 != null) && (((localHashEntry2.hash != paramInt) || (!(paramK.equals(localHashEntry2.key)))))) {  
      localHashEntry2 = localHashEntry2.next;  
    }  

    if (localHashEntry2 != null) {  
      localObject1 = localHashEntry2.value;  
      if (!(paramBoolean))  
        localHashEntry2.value = paramV;  
    }  
    else {  
      localObject1 = null;  
      this.modCount += 1;  
      arrayOfHashEntry[j] = new ConcurrentHashMap.HashEntry(paramK, paramInt, localHashEntry1, paramV);  
      this.count = i;  
    }  
    return localObject1;  
  } finally {  
    unlock();  
  }  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

先调用lock(),lock是ReentrantLock类的一个方法,用当前存储的个数+1来和threshold比较,如果大于threshold,则进行rehash,将当前的容量扩大2倍,重新进行hash。之后对hash的值和数组大小-1进行按位于操作后,得到当前的key需要放入的位置,从这儿开始,和HashMap一样。 
从上述的分析看出,ConcurrentHashMap基于concurrentLevel划分出了多个Segment来对key-value进行存储,从而避免每次锁定整个数组,在默认的情况下,允许16个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。 
在多线程的环境中,相对于HashTable,ConcurrentHashMap会带来很大的性能提升! 
ArrayList工作原理

ArrayList工作原理其实很简单,底层是动态数组,每次创建一个ArrayList实例时会分配一个初始容量(如果指定了初始容量的话),以add方法为例,如果没有指定初始容量,当执行add方法,先判断当前数组是否为空,如果为空则给保存对象的数组分配一个最小容量,这里为10。当添加大容量元素额时候,会先增加数组的大小,以提高添加的效率。 
把ArrayList理解为一个数组就好了 先看看增大容量的方法

privatevoidgrow(int minCapacity) {
       // overflow-conscious code
       int oldCapacity = elementData.length;
       int newCapacity = oldCapacity + (oldCapacity >> 1);
       if (newCapacity - minCapacity < 0)
           newCapacity = minCapacity;
       if (newCapacity - MAX_ARRAY_SIZE > 0)
           newCapacity = hugeCapacity(minCapacity);
       // minCapacity is usually close to size, so this is a win:
       elementData = Arrays.copyOf(elementData, newCapacity);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

当调用add方法的时候,如果ArrayList判断出需要扩容就会调用grow自动为其增加容量。以上黄色部分为扩容量大小,不难看出每次以1.5倍的速度在扩充。我们来总结一下ArrayList的扩充步骤 
1),判断是否需要扩充 
2),计算新容量大小,扩充1.5倍(这里有个实现小细节,是通过移位而不是*1.5来实现的,编程技巧,从点滴学起) 
3),拷贝以前的数组到新的数组; 
通过以上分析我们可以看到,所谓的自动增长并不是毫无代价的,特别是当数据量大的时候,频繁扩容会导致大量数组拷贝,进而影响性能。建议:在使用时尽可能的给一个合理的初始值。

add方法 
add方法重载了多个实现,包括add(E e)和add(int index,E e),由于没有指定插入的位置,每次插入操作会把元素放到数组的末尾,而这个过程只需要保证容量够用就行,先来看看add(E e)方法:

public boolean add(E e) {
    //保证数组的容量始终够用
    ensureCapacityInternal(size + 1);
    //size是elementData数组中元组的个数,初始为0
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    //如果数组没有元素,给数组一个默认大小,会选择实例化时的值与默认大小中较大值
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //保证容量够用
    ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
    //modCount是数组发生size更改的次数
    modCount++;
    // 如果数组长度小于默认的容量10,则调用扩大数组大小的方法
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
下面看看add(int index,E e):
public void add(int index, E element) {
    //判断index的值是否合法,如果大于size或者小于0则将抛出异常
    rangeCheckForAdd(index);
    //保证容量够用,并修改modCount的值
    ensureCapacityInternal(size + 1); 
    //从第index位置开始,将元素往后移动一个位置
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    //把要插入的元素e放在第index位置
    elementData[index] = element;
    //数组元素的个数增加1
    size++;
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

get方法 
get方法最简单,首先判断该位置是否合法,如果合法则直接返回该位置的元素

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}
  • 1
  • 2
  • 3
  • 4

remove方法 
由于删除操作会改变size,所以每次删除都需要把元素向前移动一个位置,然后把原来最后一个位置的元素设置为null,一次删除操作完成。下面看看源码

public E remove(int index) {
    //判断index是否合法
    rangeCheck(index);
    //remove操作会改变size,所以modCount加1
    modCount++;
    //保存待删除位置的元素
    E oldValue = elementData(index);
    //要移动的元素个数
    int numMoved = size - index - 1;
    //如果index不是最后一个元素,则从第index+1到最后一个位置,依次向前移动一个位置
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //元素的size减少1,并把原来末尾位置元素的值设置为null
    elementData[--size] = null; 
    //返回index位置的值
    return oldValue;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

可以注意到源码调用了System.arraycopy方法,该方法是native的,即该代码是其他语言编写,但Java允许与其进行交互(详情请搜索JNI),那么该方法是如何让实现的呢?

//add方法的System.arraycopy()
//把从第i位置的元素开始到最后一个元素,都往后移动一个位置
for(int j = size - 1; j > i; j--){
    elements[j] = elements[j-1];
}
//把第i位置的值改为e
elements[i] = e;

//remove方法的System.arraycopy()方法
//把从第i位置到最后一个位置,都向前移动一个位置
for (int j = i; j < size - 1; j++) {
    elements[j] = elements[j + 1];
}
//把数组的元素个数减少1
elements[--size] = null;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

ArrayList小结 
ArrayList是基于数组实现的,是一个动态数组,其容量能够自动增长 ArrayList不是线程安全的,只能用在单线程环境下,多线程环境需要使用Collections同步方法。Collections.synchronizedList(List l)返回一个线程安全的ArrayList。如果是读写比例比较大的话,则可以考虑CopyOnwriteArrayList。 
默认容量是10 
ArrayList每次增加元素的时候,都需要调用ensureCapacity方法来确保足够的容量。当容量不足的时候,就设置新的容量为旧的的容量的1.5倍加1,如果设置的容量仍然不够,那么直接设置为传入的参数,而后用Arrays.copyOf方法将元素拷贝到新的数组。建议:在能够实现确定元素数量的情况下使用ArrayList,否则使用LinkedList。 
Arrays.copy()方法在方法的内部又创建了一个长度等长的数组,调用System.arraycopy方法完成新数组元素的复制。该方法的实际上调用native方法中C的memmove函数,在复制大数组的时候强烈建议使用该方法进行数组的复制。效率高。 
ArrayList是基于数组实现的,支持随机访问,查找效率高,但是插入删除效率低。 
ArrayList一般应用于查询较多但插入以及删除较少情况,如果插入以及从删除较多则建议使用LinkedList