java集合类学习

以下基于jdk1.8

一、 集合类关系图

  1. 接口关系图

  2.集合中的类,(不包含线程安全的)

 

二、ArrayList

1.类定义

/**
 * 用“可伸缩数组”来实现List接口。实现了所有List接口中的方法,并且允许存放所有元素,包括Null。
 * 除了实现了List接口,本类还是提供操作数组大小的方法。(本类和Vector类似,只是本类是非同步的)
 *
 * size、isEmpty、get、set、iterator、listIterator 这些操作用的时间是常量,(也就是说这些操作与元素的个数无关,操作的时间为o(1))。
 * add操作花费恒定分摊时间,也就是说插入n的元素的时间为o(n),其实分摊之后,也就相当于插入一个元素的时间为o(1)。
 * 粗略的来说本类的其他操作都能在线性的时间内完成。(也就是说这些操作与元素的个成线性关系,操作的时间复杂度o(n))
 *
 * 每个ArrayList实例都有一个容量。这个容量也就是用来存储元素的数组的大小,它至少等于list大小(list大小就是数组实际存放元素的个数)。
 * 当一个元素被添加到集合中,这个集合的容量会自动增长。除了要求添加一个元素的效率为“恒定分摊时间”,对于具体实现的细节没有特别的要求。
 *
 * 在大批量插入元素前,使用ensureCapacity操作来增加集合的容量。这或许能够减少扩容之后新数组的大小。
 *
 * 此类是非同步的。如果多个线程同时操作ArrayList实例,至少一个线程结构性的修改,必须要保证线程的同步。
 * (结构性修改:增加或删除元素,或者调整数组大小,仅仅修改属性的值不属于结构性修改)
 * 典型的实现是同步操作数组。
 *
 *如果这种对象不存在,又想同步集合,可以这样写:
 *   List list = Collections.synchronizedList(new ArrayList(...));</pre>
 *
 * fail-fast 机制是java集合(Collection)中的一种错误机制。见2
 */

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 共享空常量数组,用于空的实例对象
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 与上面的区别在于当第一个元素被加入进来的时候它知道如何扩张;在源码中函数add(E e)中的第一行代码中的所在的函数就是这句话的实现。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 实际用来存储元素的地方,ArrayList的底层数据结构
     * 该数组的大小即为ArrayList的容量
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 元素个数
     */
    private int size;

2.fail-fast简介

  fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
  例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

  2.2  fail-fast解决办法

    fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。
    所以,本例中只需要将ArrayList替换成java.util.concurrent包下对应的类即可。
    例如:将代码

private static List<String> list = new ArrayList<String>();

    替换为

private static List<String> list = new CopyOnWriteArrayList<String>();

   2.3   原理剖析

      产生fail-fast事件,是通过抛出ConcurrentModificationException异常来触发的。
      那么,ArrayList是如何抛出ConcurrentModificationException异常的呢?

      我们知道,ConcurrentModificationException是在操作Iterator时抛出的异常。ArrayList的Iterator是在父类AbstractList.java中实现的。

package java.util;

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

    ...

    // AbstractList中唯一的属性
    // 用来记录List修改的次数:每修改一次(添加/删除等操作),将modCount+1
    protected transient int modCount = 0;

    // 返回List对应迭代器。实际上,是返回Itr对象。
    public Iterator<E> iterator() {
        return new Itr();
    }

    // Itr是Iterator(迭代器)的实现类
    private class Itr implements Iterator<E> {
        int cursor = 0;

        int lastRet = -1;

        // 修改数的记录值。
        // 每次新建Itr()对象时,都会保存新建该对象时对应的modCount;
        // 以后每次遍历List中的元素的时候,都会比较expectedModCount和modCount是否相等;
        // 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size();
        }

        public E next() {
            // 获取下一个元素之前,都会判断“新建Itr对象时保存的modCount”和“当前的modCount”是否相等;
            // 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
            checkForComodification();
            try {
                E next = get(cursor);
                lastRet = cursor++;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public void remove() {
            if (lastRet == -1)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

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

    ...
}
View Code

      从中,我们可以发现在调用 next() 和 remove()时,都会执行 checkForComodification()。若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。

      要搞明白 fail-fast机制,我们就要需要理解什么时候“modCount 不等于 expectedModCount”!
      从Itr类中,我们知道 expectedModCount 在创建Itr对象时,被赋值为 modCount。通过Itr,我们知道:expectedModCount不可能被修改为不等于 modCount。所以,需要考证的就是modCount何时会被修改。

      接下来,我们查看ArrayList的源码。

 

      从中,我们发现:无论是add()、remove(),还是clear(),只要涉及到修改集合中的元素个数时,都会改变modCount的值。

      接下来,我们再系统的梳理一下fail-fast是怎么产生的。步骤如下:
        (01) 新建了一个ArrayList,名称为arrayList。
        (02) 向arrayList中添加内容。
        (03) 新建一个“线程a”,并在“线程a”中通过Iterator反复的读取arrayList的值
        (04) 新建一个“线程b”,在“线程b”中删除arrayList中的一个“节点A”。
        (05) 这时,就会产生有趣的事件了。
           在某一时刻,“线程a”创建了arrayList的Iterator。此时“节点A”仍然存在于arrayList中,创建arrayList时,expectedModCount = modCount(假设它们此时的值为N)。
           在“线程a”在遍历arrayList过程中的某一时刻,“线程b”执行了,并且“线程b”删除了arrayList中的“节点A”。“线程b”执行remove()进行删除操作时,在remove()中执行了“modCount++”,此时modCount变成了N+1
       “线程a”接着遍历,当它执行到next()函数时,调用checkForComodification()比较“expectedModCount”和“modCount”的大小;而“expectedModCount=N”,“modCount=N+1”,这样,便抛出      ConcurrentModificationException异常,产生fail-fast事件。

      至此,我们就完全了解了fail-fast是如何产生的!
        即,当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

  2.4解决fail-fast的原理      

    上面,说明了“解决fail-fast机制的办法”,也知道了“fail-fast产生的根本原因”。接下来,我们再进一步谈谈java.util.concurrent包中是如何解决fail-fast事件的。
    还是以和ArrayList对应的CopyOnWriteArrayList进行说明。我们先看看CopyOnWriteArrayList的源码

package java.util.concurrent;
import java.util.*;
import java.util.concurrent.locks.*;
import sun.misc.Unsafe;

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    ...

    // 返回集合对应的迭代器
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

    ...
   
    private static class COWIterator<E> implements ListIterator<E> {
        private final Object[] snapshot;

        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            // 新建COWIterator时,将集合中的元素保存到一个新的拷贝数组中。
            // 这样,当原始集合的数据改变,拷贝数据中的值也不会变化。
            snapshot = elements;
        }

        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        public boolean hasPrevious() {
            return cursor > 0;
        }

        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        public void add(E e) {
            throw new UnsupportedOperationException();
        }
    }
  
    ...

}
View Code

    从中,我们可以看出:

      (01) 和ArrayList继承于AbstractList不同,CopyOnWriteArrayList没有继承于AbstractList,它仅仅只是实现了List接口。
      (02) ArrayList的iterator()函数返回的Iterator是在AbstractList中实现的;而CopyOnWriteArrayList是自己实现Iterator。
      (03) ArrayList的Iterator实现类中调用next()时,会“调用checkForComodification()比较‘expectedModCount’和‘modCount’的大小”;但是,CopyOnWriteArrayList的Iterator实现类中,没有所谓的checkForComodification(),更不会抛出ConcurrentModificationException异常! 

 3、扩容

  添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。

  扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;    // 记录集合的修改次数,因为ArrayList是非同步的,在迭代器遍历数组的同时修改数组,应当会抛出异常,
//所以AbstractList通过modcount和expectedModCount进行简单的比较判断,出错时抛出ConcurrentModificationException异常
    //overflow-conscious code
if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(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); }

4. 删除元素

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

5. 序列化

  ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。

  保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。

  ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
View Code

  序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是           ObjectInputStream 的 readObject() 方法,原理类似。

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);

三、LinkedList

1、类定义

/**
 * 使用“双向链表”来实现List与Deque接口。 实现了所有List接口中的方法,并且允许存放所有元素,包括Null。
 *
 * 所有的操作都可通过双向链表完成。通过从开头或者结尾遍历集合,去接近要操作的那个元素。
 *
 * 注意,这个实现不是同步的。如果多个线程同时访问一个链表,并且至少其中一个线程从结构上修改列表,它必须保证线程同步。
 * (结构性修改:增加或删除元素,或者调整数组大小,仅仅修改属性的值不属于结构性修改)
 * 这通常通过自然地同步某些对象来完成封装列表。
 *
 * 如果这种对象不存在,又想同步集合,可以这样写:
 *   List list = Collections.synchronizedList(new LinkedList(...));</pre>
 *
 * 迭代器
 * fail-fast
 */

public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    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;
        }
    }
    //大小
    transient int size = 0;

    /**
     * 头指针
     */
    transient Node<E> first;

    /**
     * 尾指针
     */
    transient Node<E> last;
View Code

2. 与 ArrayList 的比较

  • ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
  • ArrayList 支持随机访问,LinkedList 不支持;
  • LinkedList 在任意位置添加删除元素更快

 四、Vector和CopyOnWriteArrayList

1.vector(年代比较久远,现在一般不用)

  1. 1 同步

    它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

    1.2. 与 ArrayList 的比较

      • Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
      • Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。

    1.3. 替代方案

       可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

                      List<String> list = new ArrayList<>();
                      List<String> synList = Collections.synchronizedList(list);       
       也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

2、CopyOnWriteArrayList

  2.1读写分离

    写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。

    写操作需要加锁,防止并发写入时导致写入数据丢失。

    写操作结束之后需要把原始数组指向新的复制数组。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}
View Code
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}
View Code

  2.2 适用场景

    CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

    但是 CopyOnWriteArrayList 有其缺陷:

      • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
      • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

    所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

五、HashMap

  1.类定义

/**
 * 基于哈希表实现的Map接口。这实现提供所有可选的map操作,允许null的value值和null的key键。
 * (HashMap类大致相当于Hashtable,只是它是不同步,允许为空。)
 * 这个类不能保证map的次序;特别是,它不能保证次序会随着时间保持不变。
 *
 * 此实现提供了一定时间的性能为基本操作(<tt>get</tt> and <tt>put</tt>),
 * 假设哈希函数将元素适当地分散到桶中。迭代收集视图所需的时间成比例的HashMap实例(桶的数量)加上它的大小(key-value的数量)capacity容量与
 * *键值映射)。因此,不设置初始值是非常重要的
 * *如果迭代性能过高,则容量过高(或负载因子过低)
 * *重要。
 *
 * 一个HashMap的实例有两个参数影响它性能:初始容量,负载因数。
 * capacity是哈希表中桶的数量,初始值容量就是创建哈希表时的容量。
 * load factor 在哈希表的容量被自动增加之前,哈希表被允许填满的程度的度量指标。
 * 当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表被rehash(即重新构建内部数据结构),这样哈希表的桶数大约是桶数的两倍。
 *
 * 一般来说,默认的负载因子(.75)在时间和空间成本之间提供了很好的权衡。
 * 较高的值减少了空间开销,但增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。
 * 在设置map的初始容量时,应该考虑map中预期条目的数量及其负载因子,从而最小化rehash操作的数量。
 * 如果初始容量大于最大条目数除以负载因子,则不会发生任何重哈希操作。
 *
 * 如果要将许多映射存储在HashMap实例中,那么使用足够大的容量创建映射将比让映射根据需要执行自动哈希来增长表更有效地存储映射。
 * 注意,使用多个具有相同{@code hashCode()}的键肯定会降低任何散列表的性能。
 * 为了改善影响,当键是{@link Comparable}时,该类可以使用键之间的比较顺序来帮助断开连接。
 *
 * 注意,这个实现不是同步的。
 * 如果多个线程同时访问一个散列映射,并且至少有一个线程从结构上修改了映射,则必须在外部同步。
 * (结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已经包含的键关联的值不是结构修改。)
 * 这通常是通过在一些自然封装映射的对象上进行同步来实现的。
 *
 * 可以实现以下方式进行同步操作
 *   Map m = Collections.synchronizedMap(new HashMap(...));
 *
 * fail-fast
 *
 */
public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;

/*
     * 实现注意事项。
     *这个映射通常充当一个binned (bucketed)哈希表,但是当箱子变得太大时,它们会被转换成树节点的箱子,每个箱子的结构都类似于TreeMap中的箱子。
     *大多数方法都尝试使用普通的bin,但在适用时中继到TreeNode方法(只需检查节点的instanceof)。
     *树节点的存储箱可以像其他存储箱一样被遍历和使用,但是在过度填充时支持更快的查找。
     *但是,由于在正常使用的箱子中,绝大多数都没有被过度填充,因此在表方法的过程中,检查ree箱子是否存在可能会被延迟。
     *
     * 树垃圾箱(即。,其元素都是treenode的箱子)主要由hashCode排序,但在tie的情况下,
     * 如果两个元素属于相同的“class C implementation Comparable<C>”,则键入它们的compareTo方法来排序。
     *(我们通过反射保守地检查泛型类型来验证这一点——请参见comparableClassFor方法)。
     *树的额外复杂性垃圾箱是值得的在提供最坏的情况下为O(log n)当建有不同的散列或规则,因此,在意外或恶意使用hashCode()方法返回值的差分布,性能优雅的降低
     * 以及许多密钥共享一个hashCode,只要他们也类似。
     *(如果这两种方法都不适用,与不采取预防措施相比,我们可能会浪费大约两倍的时间和空间。
     *但目前所知的唯一案例来自于糟糕的用户编程实践,这些实践已经非常缓慢,以至于没有什么区别。)
     *
     * 因为树节点的大小大约是普通节点的两倍,所以我们只在箱子中包含足够的节点以保证使用时才使用它们(请参阅TREEIFY_THRESHOLD)。
     *当它们变得太小(由于移除或调整大小),就会被转换回普通的垃圾箱。
     *在使用分布良好的用户哈希码时,很少使用树箱。
     *理想情况下,在随机哈希码下,bin中节点的频率遵循泊松分布(http://en.wikipedia.org/wiki/Poisson_distribution),默认大小的平均参数约为0.5
     *阈值为0.75,虽然由于调整粒度而具有较大的方差。
     *忽略方差,列表大小k的预期出现率为(exp(-0.5) * pow(0.5, k) / factorial(k))。
     * 第一个值是:
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
     *
     * 树状容器的根通常是它的第一个节点。但是,有时(目前仅在Iterator.remove之后),根可能在其他地方,
     * 但是可以通过父链接(方法TreeNode.root())恢复。
     *
     * 所有适用的内部方法都接受散列代码作为参数(通常由公共方法提供),允许它们在不重新计算用户散列代码的情况下相互调用。
     *大多数内部方法也接受“tab”参数,这通常是当前表,但在调整大小或转换时可能是新的或旧的。
     *
     *当bin列表被treeified、split或untreeified时,我们将它们保持相同的相对访问/遍历顺序(即为了更好地保存局部,并稍微简化对调用iterator.remove的分割和遍历的处理。
     * 当在插入时使用比较器时,为了保持跨重新平衡的总顺序(或尽可能接近这里的要求),我们将类和identityhashcode作为连接符进行比较。     *
     *
     * 由于LinkedHashMap子类的存在,普通vs树模式之间的使用和转换变得复杂。有关定义在插入、删除和访问时调用invoked方法,
     * 请参见下面,这些方法允许LinkedHashMap内部保持独立于这些机制。(这还要求将map实例传递给一些可能创建新n的实用程序方法
     *
     * 基于并行编程的类似于ssa的编码风格有助于避免所有扭曲指针操作中的混叠错误。
*/

    /**
     * 默认的初始容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大的容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认的加载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     *使用树(而不是列表),设置bin计数阈值。当向至少具有这么多节点的bin添加元素时,bin将转换为树。
     *该值必须大于2,并且应该至少为8,以便与树木移除中关于收缩后转换回普通垃圾箱的假设相吻合。
     * (链表转换为红黑树的阈值)
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 用于在调整大小操作期间反树化(拆分)bin的bin计数阈值。应小于TREEIFY_THRESHOLD,且最多6个孔配合下进行缩孔检测。
     * (红黑树转换为链表的值)
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 最小的表容量,其中的箱子可以treeified。(否则,如果一个bin中有太多节点,则会调整表的大小。)
     * 应至少为4 * TREEIFY_THRESHOLD,以避免调整大小和treeification阈值之间的冲突。
     * (红黑树的最小容量)
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            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;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
View Code

  2. 存储结构

    内部包含了一个 Node 类型的数组 table。(1.8之前为Entry类型)


transient Node<K,V>[] table;

    Node 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Node 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值相同的 Node。

 

  3.put方法

/**
     * 将指定值与此映射中的指定键关联。
     * 如果映射以前包含键的映射,则替换旧值。
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * 计算key.hashCode(),并将(XORs)的散列值由高向低扩展。
     * 由于该表使用了2的幂掩码,因此仅在当前掩码之上以位为单位变化的散列集总是会发生冲突。
     * (已知的例子包括在小表中保存连续整数的浮点键集。)
     * 因此,我们应用一个转换,将更高位的影响向下传播。
     * 位扩展的速度、实用性和质量之间存在权衡。
     * 因为许多常见的散列集已经得到了合理的分布(因此不能从扩展中获益),
     * 因为我们用树来处理大型的碰撞在垃圾箱,我们只是XOR一些改变以最便宜的方式来减少系统lossage,以及将最高位的影响,否则永远不会因为指数计算中使用的表。
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//详情看下
    }
    /**
     * @param onlyIfAbsent 如果为 true, 不改变已经存在的值(默认为false,当key相同时替换为新值)
     * @param evict 如果 false, 表处于创建模式(若 evict 为 false,代表是在创建 hashMap 时调用了这个函数,例如利用上述构造函数3创建 hashMap;若 evict 为true,代表是在创建 hashMap 后才调用这个函数,例如上述的 putAll 函数)
     *
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)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);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    /**
     * 初始化或双精度表大小。如果为空,则按照字段阈值中包含的初始容量目标分配。
     * 否则,因为我们使用的是2的幂展开,所以每个bin中的元素必须保持相同的索引,或者在新表中以2的幂偏移量移动。
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

    /**
     * 要调整大小的下一个大小值(容量*负载因子)。
     * (序列化时javadoc描述为true。
     * 此外,如果没有分配表数组,则执行以下操作
     * 字段保存初始数组容量,或零表示
     * DEFAULT_INITIAL_CAPACITY)。
     * (扩容的阈值)
     */
    int threshold;
View Code

  4.hash方式,确定桶

    hashmap首先调用自身的hash方法

    hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置.
    key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    在此方法中调用了object的hashCode方法,并通过父类AbstractMap进行重写

    那么hashmap要通过这种方式呢,这个与HashMap中table下标的计算有关。

n = table.length;
index = (n-1) & hash;
      当 length 总是 2 的倍数时,h & (length-1)将是一个非常巧妙的设计:
        假设 h=5,length=16, 那么 h & length - 1 将得到 5;
        如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……
        如果 h=15,length=16, 那么 h & length - 1 将得到 15;
      但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;
      当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……
      这样保证计算得到的索引值总是位于 table 数组的索引之内。
     HashMap的初始大小和扩容都是以2的次方来进行的,换句话说length-1换成二进制永远是全部为1,比如容量为16,则length-1为1111,大家知道位运算的规则是两个1才得1,遇0的0,也就是说length-1中的某一位为1,则对应位置的计算结果才取决于h中的对应位置(h中对应位取0,对应位结果为0,h对应位取1,对应位结果为1。这样就有两个结果),但是如果length-1中某一位为0,则不论h中对应位的数字为几,对应位结果都是0,这样就让两个h取到同一个结果,这就是hash冲突了,恰恰length-1又是全部为1的数,所以结果自然就将hash冲突最小化了
  总之:
    1.length(2的整数次幂)的特殊性导致了length-1的特殊性(二进制全为1)
    2.位运算快于十进制运算,hashmap扩容也是按位扩容

  5.拉链法的工作原理

HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");
  • 新建一个 HashMap,默认大小为 16;
  • 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
  • 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
  • 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。

应该注意到链表的插入是以头插法方式进行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 后面,而是插入在链表头部。

查找需要分成两步进行:

  • 计算键值对所在的桶;
  • 在链表上顺序查找,时间复杂度显然和链表的长度成正比。

  6、扩容

    扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。

    在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。

    假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:

      对于一个 Key,

      • 它的哈希值如果在第 5 位上为 0,那么取模得到的结果和之前一样;
      • 如果为 1,那么得到的结果为原来的结果 +16

  7、计算数组容量

    HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
View Code

六、ConcurrentHashMap、LinkedHashMap、WeakHashMap

  1.concurrentHashMap

 

posted @ 2019-04-21 20:39  JokerQ-  阅读(611)  评论(0编辑  收藏  举报