KEEP ON CODING......

JDK源码学习之 集合实现类

一、HashMap

(1) 简介:java1.8版本之前HashMap的结构图如下:

数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。
但这就存在一个问题:在一个链表中查找一个节点时,将会花费O(n)的查找时间,会有很大的性能损失,所以在JDK1.8后,当同一个hash值的节点数不小于8时,不再采用单链表形式存储,而是采用红黑树,结构图如下:

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

(2)  Fields

/* ---------------- Fields -------------- */

/**
 * 存储Node类的数组
 */
transient Node<K,V>[] table;

/**
 * entrySet()缓存
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * 记录hashmap中存储键-值对的数量 
 **/
transient int size;

/**
 * hashmap结构被改变的次数,fail-fast机制
(modCount值记录修改次数,对HashMap内容的修改都将增加这个值。迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常) */ transient int modCount; /** * 扩容的门限值,当size大于这个值时,table数组进行扩容  */ int threshold; /** * The load factor for the hash table. * * @serial */ final float loadFactor; /** * 默认初始化数组大小为16,必须是2的倍数 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** 最大数组容量 MAXIMUN_CAPCITY <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认装载因子, */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** 链表的最大长度,当大于这个长度时,链表转化为红黑树 */ static final int TREEIFY_THRESHOLD = 8;

 (3) 构造函数: 可自定义初始容量和加载因子

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

    // 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();
}

  (4) 主要方法分析:

  a) put(K key, V value)

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;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
// HashMap最多允许且仅允许一个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;
}

  b) get(Object key)

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)))
            return e.value;
    }
    return null;
}
//如果存在key=null,返回key对应的value,否则直接返回null
private V getForNullKey() {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

  c) resize(int newCapacity)

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
}

  当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

  d) Fail-Fast机制:

   HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

     这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount

HashIterator() {
    expectedModCount = modCount;
    if (size > 0) { // advance to first entry
    Entry[] t = table;
    while (index < t.length && (next = t[index++]) == null)
        ;
    }
} 

  在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map。(modCount声明为volatile,保证了线程之间修改的可见性)

 

二、 HashTable

  1. Hashtable是基于哈希表的Map接口的同步实现,不允许使用null值和null键。
  2. 底层使用数组实现,数组中每一项是个单链表,即数组和链表的结合体
  3. Hashtable在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
  4. synchronized是针对整张Hash表的,即每次锁住整张表让线程独占。

 

三、 ConcurrentHashMap
  1. ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同段进行的修改,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个并发发生在不同的段上,它们就可以并发进行。
  2. ConcurrentHashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
  3. 与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)

  (注:JDK 1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现。)
  4. ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。

 

四、 HashSet
  1. HashSet由哈希表(实际上是一个HashMap实例)支持,不保证set的迭代顺序,并允许使用null元素。
  2. 基于HashMap实现,API也是对HashMap的行为进行了封装,可参考HashMap。

 

五、LinkedHashMap
  1. LinkedHashMap继承于HashMap,底层使用哈希表和双向链表来保存所有元素,并且它是非同步,允许使用null值和null键。
  2. 基本操作与父类HashMap相似,通过重写HashMap相关方法,重新定义了数组中保存的元素Entry,来实现自己的链接列表特性。该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而构成了双向链接列表。


六、LinkedHashSet
  继承了HashSet、又基于LinkedHashMap来实现的。LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同。


七、 ArrayList
  1. ArrayList是List接口的可变数组非同步实现,并允许包括null在内的所有元素。
  2. 底层使用数组实现,该集合是可变长度数组,数组扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量增长大约是其容量的1.5倍,这种操作的代价很高。
  3. 采用了Fail-Fast机制,面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险
  4. remove方法会让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便GC。

  5. 实现:

  a)  底层使用数组实现:

private transient Object[] elementData;  

  b)构造方法: ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列。

public ArrayList() {
    this(10);
}

public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    this.elementData = new Object[initialCapacity];
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

  c) 存储&获取&移除(这里列出了较为常用的几种): 

  set(int index, E element)

//用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。
public E set(int index, E element) {
    RangeCheck(index);

    E oldValue = (E) elementData[index];
    elementData[index] = element;
    return oldValue;
}
private void RangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(
	"Index: "+index+", Size: "+size);
}

  add(E e)

// 将指定的元素添加到此列表的尾部。
public boolean add(E e) {
    ensureCapacity(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

  add(int index, E element)

// 将指定的元素添加到此列表的指定位置。
public void add(int index, E element) {
   if (index > size || index < 0)
       throw new IndexOutOfBoundsException(
	"Index: "+index+", Size: "+size);

    ensureCapacity(size+1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
		 size - index);
   elementData[index] = element;
   size++;
}

  addAll(Collection<? extends E> c)

//按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。 
public boolean addAll(Collection<? extends E> c) {
       Object[] a = c.toArray();
       int numNew = a.length;
       ensureCapacity(size + numNew);  // Increments modCount
       System.arraycopy(a, 0, elementData, size, numNew);
       size += numNew;
       return numNew != 0;
}    

  get(int index)

// 获取指定位置上的元素
public E get(int index) {
    RangeCheck(index);
    return (E) elementData[index];
}

  remove(Object o)

//移除此列表中首次出现的指定元素(如果存在, 针对null做特殊处理)
public boolean remove(Object o) {
    if (o == null) {
           for (int index = 0; index < size; index++)
	if (elementData[index] == null) {
	    fastRemove(index);
	    return true;
	}
    } else {
        for (int index = 0; index < size; index++)
	    if (o.equals(elementData[index])) {
	        fastRemove(index);
	        return true;
	    }
           }
    return false;
}

  ensureCapacity(int minCapacity) : 调整数组容量

  每当向数组中添加元素时,都要检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 

public void ensureCapacity(int minCapacity) {
    modCount++;
    int oldCapacity = elementData.length;
    if (minCapacity > oldCapacity) {
        Object oldData[] = elementData;
        int newCapacity = (oldCapacity * 3)/2 + 1;
   	    if (newCapacity < minCapacity)
	    newCapacity = minCapacity;
           // minCapacity is usually close to size, so this is a win:
           elementData = Arrays.copyOf(elementData, newCapacity);
    }
}

  数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

   ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:

public void trimToSize() {
    modCount++;
    int oldCapacity = elementData.length;
    if (size < oldCapacity) {
           elementData = Arrays.copyOf(elementData, size);
    }
}

  ArrayList同样也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

八、 LinkedList

  1. LinkedList是List接口的双向链表非同步实现,并允许包括null在内的所有元素。
  2. 底层的数据结构是基于双向链表的,该数据结构我们称为节点
  3. 双向链表节点对应的类Node的实例,Node中包含成员变量:prev,next,item。其中,prev是该节点的上一个节点,next是该节点的下一个节点,item是该节点所包含的值。
  4.它的查找是分两半查找,先判断index是在链表的哪一半,然后再去对应区域查找,这样最多只要遍历链表的一半节点即可找到。

 

参考资料:
http://blog.csdn.net/qq_25868207/article/details/55259978

posted @ 2018-02-23 16:05  Cecil2020  阅读(219)  评论(0编辑  收藏  举报