天天看点

java集合Collection集合Map集合

java集合主要有两类:Collection和Map。Collection接口主要有三个派生接口:Queue,List,Set,这三个派生接口又派生出很多实现类,总体来说,java集合的种类有这四个Queue,List,Set,Map。以下是这它们的关系图:

java集合Collection集合Map集合
java集合Collection集合Map集合
  • 目录:
  • Collection集合

    一. List

    1. ArrayList
    2. LinkedList
    3. Stack
    二. Queue
    1. ArrayDeque
    2. LinkedList
    三. Set
    1. HashSet
    2. TreeSet
    3. LinkedHashSet
  • Map集合

    一.HashMap

    二.LinkedHashMap

    三.TreeMap

    四.HashTable

Collection集合

Iterable接口定义了iterator()方法,Queue,List,Set接口的实现类均实现了此方法,因此,Collection下所有集合都可以通过此方法获取Iterator 对象,从而遍历集合中的元素。

Iterable接口:

public interface Iterable<T> {
    Iterator<T> iterator();
}
           

Iterator 遍历元素:

    Iterator it = collection.iterator(); // 获得一个迭代子
    while(it.hasNext()) {
      Object obj = it.next(); // 得到下一个元素
    }
           

一. List

List接口下的具体实现类大都实现了 Cloneable和java.io.Serializable接口,因此它们都拥有克隆和序列化的能力。

查看List接口提供的方法,可知List的一大特点是:可以通过下标来存取元素(索引存取)

E get(int index);
 E set(int index, E element);
 void add(int index, E element);
 E remove(int index);
           

1. ArrayList

ArrayList内部定义一个数组,用于保存元素。如果插入的时候不指定下标,将会按照数组下标顺序插入。

//无参构造函数。创建一个空数组,在第一次add元素时,会判断是否需要扩容,此时会设置默认容量大小为10,再执行arraycopy的操作,也就创建了容量为10的数组。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final int DEFAULT_CAPACITY = ;

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//有参构造函数,创建指定容量的数组
public ArrayList(int initialCapacity) {
    if (initialCapacity > ) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == ) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
//不指定下标插入。判断是否需要扩容,再插入数组的末尾。
public boolean add(E e) {
    ensureCapacityInternal(size + );  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
//指定下标插入。判断是否需要扩容,再拷贝移动元素并插入。
public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + );  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + , size - index);
    elementData[index] = element;
    size++;
}
//判断是否需要扩容,如果此时elementData仍然为空(首次创建无参ArrayList时),设置默认容量大小为DEFAULT_CAPACITY = 10
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > )
        grow(minCapacity);
}
//扩容,重新计算容量大小(之前容量的1.5倍),重新创建一个新的数组,并把之前数组中的元素拷贝到新的数组中。
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> );
    if (newCapacity - minCapacity < )
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > )
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
//ArrayList中数组容量的最大值设定为MAX_VALUE = 2^(32-1)-1
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < ) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - ;
public E get(   int index) {
    rangeCheck(index);
    return elementData(index);
}
           

Arrays.java

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, , copy, ,
                     Math.min(original.length, newLength));
    return copy;
}
           

查看以上源码,ArrayList在插入元素时,会先检查ArrayList的容量是否足够,如果容量不足,将会进行扩容。扩容时会重新创建一个新的数组,并把之前数组中的元素拷贝到新的数组中。插入指定下标的元素时,会拷贝并移动元素。跟扩容时一样,使用了System.arraycopy()进行了拷贝和移动。

ArrayList的时间复杂度:

get(int index),add(E e),时间复杂度为O(1)

add(int index, E element),remove(int index),remove(Object o),indexOf(Object o),时间复杂度为O(n)

2. LinkedList

LinkedList继承自List和Queue,它是一个双向队列的集合。LinkedList的实现方式不再是操作数组,而是通过双向链表实现。每个元素的插入都生成一个Node对象保存,并各自指向前后两个Node对象。可以使用LinkedList来实现队列,双向队列,堆栈等数据结构。另外,由于LinkedList也继承自List,它也包含了List的特点,可以通过下标存取元素。

//定义Node类。item用来保存元素,Node的引用prev,next用来指向上一个节点,下一个节点。
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;
    }
}

public boolean add(E e) {
    linkLast(e);
    return true;
}

//从First端插入元素
public void addFirst(E e) {
    linkFirst(e);
}
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}
//从Last端插入元素:
public void addLast(E e) {
    linkLast(e);
}
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
//从First端获取第一位元素
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
//从Last端获取第一位元素
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}
//获取指定下标的元素
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
//在指定下标插入元素
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
           

我们知道链表里面一般是没有下标(索引)的概念,那么这里是怎么实现下标存取的呢? 关键就在这个node()方法,在get()和set()方法中,都使用这个node()方法来定位元素。node()方法通过遍历链表元素,并计数索引的方法,来支持LinkedList中的索引存取。如下:

Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> )) {
        Node<E> x = first;
        for (int i = ; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - ; i > index; i--)
            x = x.prev;
        return x;
    }
}
           

LinkedList的时间复杂度:

getFirst(),getLast(),removeFirst(),removeLast(),addFirst(E e),addLast(E e),时间复杂度为O(1)

add(int index, E element),set(int index, E element),remove(int index),get(int index),时间复杂度为O(n)

3. Stack

Stack继承自Vector,它的功能实现依赖于vector。先来看看vector与ArrayList的区别:

  • vector扩容时是原来容量的2倍。ArrayList扩容时是原来容量的1.5倍。
  • vector中的多数方法都加了synchronized来修饰,是线程安全的。而ArrayList是线程不安全的。
  • vector是已经过时的类,为了支持老的程序,vector中有大量功能重复的冗余方法

除了这几点,vector中功能的现实基本和ArrayList一样,因此可以把vector当成ArrayList来看。vector中也是定义一个数组,来保存元素,如果插入的时候不指定下标,将会按照数组下标顺序插入。

Stack继承了Vector的线程安全特性,因此,在性能上比较低,如果不考虑线程安全的问题的话,不建议使用Stack来实现堆栈,应该使用LinkedList来替代。

Stack在取元素的时候,总是从数组保存的最后一个元素取。见pop()方法。

public E push(E item) {
    addElement(item);
    return item;
}
public synchronized void addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + );
    elementData[elementCount++] = obj;
}

public synchronized E pop() {
    E       obj;
    int     len = size();
    obj = peek();
    removeElementAt(len - );
    return obj;
}
public synchronized E peek() {
    int     len = size();
    if (len == )
        throw new EmptyStackException();
    return elementAt(len - );
}
public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                 elementCount);
    }
    else if (index < ) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - ;
    if (j > ) {
        System.arraycopy(elementData, index + , elementData, index, j);
    }
    elementCount--;
    elementData[elementCount] = null; /* to let gc do its work */
}
           

Stack的时间复杂度:

push(E item), pop(), peek(),时间复杂度为O(1)

search(Object o),时间复杂度为O(n)

二. Queue

1. ArrayDeque

ArrayDeque也是一个双向队列的集合。不同于LinkedList,它的实现方式是使用数组。通过操作下标,把分配到的数组当成一个循环的数组来插入和删除元素。

transient int head;
transient int tail;

public boolean add(E e) {
    addLast(e);
    return true;
}
//从First端添加新元素(其实就是数组末尾),如果head和tail下标相同,说明数组已经使用完,需要扩容
public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - ) & (elements.length - )] = e;
    if (head == tail)
        doubleCapacity();
}
//从Last端添加新元素(其实就是数组开头),如果head和tail下标相同,说明数组已经使用完,需要扩容
public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + ) & (elements.length - )) == head)
        doubleCapacity();
}
//扩容,重新计算容量(之前容量的两倍)创建一个新的数组,并把之前数组的元素拷贝到新数组中
private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << ;
    if (newCapacity < )
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, , r);
    System.arraycopy(elements, , a, r, p);
    elements = a;
    head = ;
    tail = n;
}
//从First端(数组末尾)取元素
public E getFirst() {
    @SuppressWarnings("unchecked")
    E result = (E) elements[head];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}
//从Last端(数组开头)取元素
public E getLast() {
    @SuppressWarnings("unchecked")
    E result = (E) elements[(tail - ) & (elements.length - )];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}
           

循环数组的理解,我们可以重点看addFirst()方法和addLast()方法,head和tail两个下标在ArrayDeque对象创建的时候并没有初始化,只是进行了定义,那么默认值都为0,如果一直使用addFirst()插入数据,那么addFirst()中head下标的变化为:

elements[head = (head - 1) & (elements.length - 1)] = e;

假定定义一个elements.length = 6的数组,
head = 0;
head = (0-1) & (6-1) = -1 & 5 = 5
head = (5-1) & (6-1) = 4 & 5 = 4
head = (4-1) & (6-1) = 3 & 5 = 3
head = (3-1) & (6-1) = 2 & 5 = 2
head = (2-1) & (6-1) = 1 & 5 = 1
head = (1-1) & (6-1) = 0 & 5 = 0
           

其实addFirst()就是从数组末尾执行的插入。

同理,addLast()方法中tail下标的变化为:

if ( (tail = (tail + 1) & (elements.length - 1)) == head)

tail = 0;
tail = (0+1) & (6-1) = 1 & 5 = 1
tail = (1+1) & (6-1) = 2 & 5 = 2
tail = (2+1) & (6-1) = 3 & 5 = 3
tail = (3+1) & (6-1) = 4 & 5 = 4
tail = (4+1) & (6-1) = 5 & 5 = 5
           

addLast()就是从数组0下标开始插入元素。

如果同时在两端插入数据,那head和tail下标总会有相等的时候,说明数组容量不够了,这时候就需要扩容了。另外,ArrayDeque虽然使用了数组来存储元素,但是并没有提供索引存取的功能。

ArrayDeque的时间复杂度:

addFirst(E e),addLast(E e),pollFirst(),pollLast(),getFirst(),getLast() ,removeFirst(),removeLast(),由于ArrayDeque为循环数组,并没有提供索引存取的功能,因此,这些方法的时间复杂度都为O(1)

remove(Object o),contains(Object o),时间复杂度为O(n)

ArrayDeque和LinkedList都可以实现队列的数据结构,如果只是在首尾两端插入删除查询元素,ArrayDeque的效率要高一些。ArrayDeque不能使用索引,这个应该要注意。

2. LinkedList

…..见List中的介绍

三. Set

Set的实现多数需要依赖于Map。Map的设计是使用键值对存取,对于很多应用场景,我们并不需要这个结构来存储数据,设计Set的初衷就是为了可以简化Map,并且可以使用迭代器Iterator。比如

  • HashSet,借助于HashMap散列表的设计,Hashset可以用来去重。
  • TreeSet借助于TreeMap实现红黑树的数据结构。
  • LinkedHashSet借助于LinkedHashMap实现了HashSet的排序。

以上Set的子类都实现了Map中的功能,但是又不需要键值对这种麻烦的数据存取形式,使用起来相当的简便。个人理解。

1. HashSet

为快速查找而设计的Set,因为设定的是快速查找,所有并没有获取HashSet内元素的直接方法get(),必须通过Iterator迭代器来获取元素。

HashSet的现实依赖于HashMap,从HashSet中存入的元素被当做HashMap中的key健值,因此必须要重写hashCode()方法和equals()方法。如果HashSet中存入相同的元素(hashCode()和equals()相同),则会发生元素覆盖。

public HashSet() {
    map = new HashMap<>();
}
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
public boolean contains(Object o) {
    return map.containsKey(o);
}
public Iterator<E> iterator() {
    return map.keySet().iterator();
}
           

2. TreeSet

保持次序的Set,底层为树形结构。因为要保持元素的有序,因此元素必须要实现Comparable接口,并重写compareTo()方法,否则将会抛出ClassCastException异常。

TreeSet的实现依赖于TreeMap。

public TreeSet() {
    this(new TreeMap<E,Object>());
}
public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}
public E first() {
    return m.firstKey();
}
public E last() {
    return m.lastKey();
}
           

3. LinkedHashSet

具有HashSet的查询速度,且内部使用链表维护元素的插入顺序。

LinkedHashSet的实现依赖于LinkedHashMap。

public LinkedHashSet() {
    super(, f, true);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
           

Map集合

HashMap,LinkdeHashMap,TreeMap,HashTable均实现了 Cloneable和java.io.Serializable接口,因此它们都拥有克隆和序列化的能力。

Map使用了键值对的形式来保存元素。

由于Map接口没有继承Iterable接口,因此,它不能使用迭代器来遍历集合。但是,Map可以通过Set来使用迭代器。如下:

Iterator iterator = map.entrySet().iterator();//map.keySet().iterator()
while (iterator.hasNext()){
      Objects obj = iterator.next();
 }
           

其原理是使用了内部类机制。这里有两个内部类AbstractSet和Iterator,在keySet()被调用时,keySet 被初始化,并获得了外部类Map的引用(内部类可以访问外部类的所有数据成员和方法)。在迭代器Iterator的方法被调用时,匿名内部类Iterator也获得外部类Map和keySet 中的引用,可以访问map和set中的所有数据成员和方法,接下来执行的next()涉及的Entry其实就是Map中的数据,里面包含了指向下一个对象的引用,遍历即可得到所有的键值对。

public Set<K> keySet() {
        if (keySet == null) {
            keySet = new AbstractSet<K>() {
                public Iterator<K> iterator() {
                    return new Iterator<K>() {
                        private Iterator<Entry<K,V>> i = entrySet().iterator();

                        public boolean hasNext() {
                            return i.hasNext();
                        }

                        public K next() {
                            return i.next().getKey();
                        }

                        public void remove() {
                            i.remove();
                        }
                    };
                }

                public int size() {
                    return AbstractMap.this.size();
                }

                public boolean isEmpty() {
                    return AbstractMap.this.isEmpty();
                }

                public void clear() {
                    AbstractMap.this.clear();
                }

                public boolean contains(Object k) {
                    return AbstractMap.this.containsKey(k);
                }
            };
        }
        return keySet;
    }
           

一.HashMap

之前的集合要么是使用数组来实现,要么是使用链表来实现。HashMap集合的实现既使用了数组,同时还使用了链表,能结合这两种一起使用的方式就是散列。

散列:

假如键没有按照一定的顺序进行保存,那么查询的时候就只能按照顺序进行线性查询,然而,线性查询是最慢的查询方式。所以,将键值按照一定的顺序排序,并且使用二分查找能购有效的提升速度。散列在此之上,更近一步,他将键保存在数组中(数组的查询速度最快),用数组来表示键的信息,但是由于Map的容量是可变的,而数组的容量是不变的。要解决这个问题,数组中存的并不是键本身,而是键对象生成的一个数字,将其作为数组的下标,这个数字就是散列码hashcode。

不同的健对象,有可能会产生相同的散列码,这通常也叫做哈希冲突(碰撞)。如下:

java集合Collection集合Map集合

产生哈希冲突后,多个键值对(Node节点)会被保存同一个hashcode所代表的链表里面,将会影响这个Map集合的性能。如果每个hashcode所代表的链表只保存了一个键值对(数组中的Node节点),那么这个Map集合的性能将会是最完美的。所以,当我们需要把自己设计的类当成健来保存键值对,必须要重写hashCode()和equals()方法。equals()方法的重写需要满足以下条件:

  • 自反性 x.equals(x) 一定返回true
  • 对称性 x.equals(y)返回true,则y.equals(x) 也返回true
  • 传递性 x.equals(y)返回true,y.equals(z)返回true,则x.equals(y)返回true
  • 一致性 如果对象中的信息没有改变,x.equals(y)要么一直返回true,要么一直返回false
  • 对任何不是null的x,想x.equals(null)一定返回false

重写hashCode()方法,最重要的因素就是:

  • 无论何时,对同一个对象调用hashCode()都应该生成同样的值。
  • hashCode()返回的值应该尽量分布均匀。

HashMap中定义了Node和TreeNode节点来保存键值对,为了能成功散列,Node中还定义了一个int类型的hash变量来保存散列码。一般数组是通过下标来存取元素,这里使用了散列码来替代下标的功能。根据健对象生成的散列码来确定数组中的位置。

transient Node<K,V>[] table; //Node节点的数组
final float loadFactor;//负载因子
//默认的负载因子为DEFAULT_LOAD_FACTOR = 0.75
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//保存键值对。HashMap的构造方法中并没有初始化table数组,table的初始化在第一次put()时由resize()方法执行。
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //table为null,说明HashMap对象刚创建,调用resize()进行table数组的初始化。
    if ((tab = table) == null || (n = tab.length) == )
        n = (tab = resize()).length;
    //根据hash值找到在数组table中的对应位置,如果此位置没有值,说明没有保存过此key的键值对,保存此键值对。
    if ((p = tab[i = (n - ) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //根据hash值找到在数组table中的对应位置,但是此位置已经存有值。说明发生了哈希碰撞,需要判断是保存到单链表中还是红黑树中。
    else {
        Node<K,V> e; K k;
        //发生了哈希碰撞,且在table数组中发现hash值,健值相同,说明相同key的键值对已经存在,直接覆盖。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        /*发生了哈希碰撞,且检查到table数组中保存的是TreeNode类型,说明此后的元素都是以红黑树的形式保存,接下来要遍历红黑树。
        * 如果在红黑树中找到了hash值,健值相同的元素,直接覆盖。如果遍历结束后,仍然没有找到hash值,健值相同的元素,说明没有保存过此
        * key的键值对,添加到红黑树中。这个过程的实现在putTreeVal()方法中,不再细讲。
        */
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        /*发生了哈希碰撞,且检查到table中保存的不是TreeNode类型,那么就只能是单链表结构了。检查单链表,如果相同key的键值对
        *已经存在,直接覆盖,如果相同key的键值对不存在,则添加。
        */
        else {
            //遍历单链表
            for (int binCount = ; ; ++binCount) {
                if ((e = p.next) == null) { //遍历至链表尾部
                    p.next = newNode(hash, key, value, null); //元素封装到Node对象中,加入到单链表结构
                    //红黑树的性能要比单链表高,因此,如果单链表的长度大于TREEIFY_THRESHOLD - 1的时候,单链表转化成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - ) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //hash值,健值相同,说明相同key的键值对已经存在,直接覆盖。
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //相同key的键值对已经存在,返回value值。
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //threshold的值由负载因子loadFactor决定。判断是否需要扩容。
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
/*给table数组初始化,并判断是否需要扩容。如果需要扩容,则需要保存旧的table数组的元素,然后table数组进行两倍扩容,
*再把旧table数组的元素保存到新table数组中,并且,旧table数组中所指向的单链表,红黑树结构要链接到新的table数组中。
*/
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ?  : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = ;
    if (oldCap > ) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << ) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << ; // double threshold
    }
    else if (oldThr > ) // 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 == ) {
        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 = ; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - )] = 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) == ) {
                            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;
}
//获取指定key的键值对。如果可以从table数组中获取,则直接返回,如果table数组中获取不到,则需要遍历相应hashcode下的单链表或者红黑树。
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) >  &&
        (first = tab[(n - ) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
           

负载因子:

空表的负载因子是0,而半满表的负载因子是0.5,HashMap中的默认负载因子是0.75,也就是四分之三满。如果HashMap的容量不足时(table数组的容量),会自动进行扩容,并重新将现有的对象分布到新的table集中,这个称之为再散列。是否需要扩容的判断依赖于负载因子,更高的负载因子可以降低所需的空间,但是会增加查找的代价。默认的负载因子0.75在时间和空间代价之间达到了平衡。

resize():
if (newThr == ) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;

putVal():
if (++size > threshold)
    resize();
           

二.LinkedHashMap

LinkedHashMap的特点是可以得到可以通过迭代它获取有序的HashMap集合。重点是有序。

LinkedHashMap继承自HashMap,重写了get(),afterNodeAccess(),newTreeNode()和newNode()方法(重要),put()方法则是直接继承自HashMap。当我们new一个LinkedHashMap对象后,使用LinkedHashMap对象的引用调用put()方法时,putVal()方法中调用的newNode()方法并不是HashMap中的方法,而是LinkedHashMap中的newNode()方法,这里使用了多态的机制。putVal()方法参见HashMap中的介绍,不再列出。

afterNodeAccess()在HashMap中也有定义,不过并没有具体的实现。afterNodeAccess()在putVal()中也被调用了,如下:

往HashMap中插入键值对时,如果key相同的键值对已经存在,直接覆盖此键值对,并把此键值对移动到LinkedHashMap维护的双向链表中(注意,不要跟HashMap中的单链表和红黑树混淆),并返回value值。由于HashMap中并没有定义afterNodeAccess(),如果是HashMap对象调用此方法,则什么都不会发生,这里的afterNodeAccess()主要是扩展给实现此方法的子类使用的:

if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
           

accessOrder = false : 按插入顺序排序(LRU)

accessOrder = true : 按访问顺序排序

//头尾引用,用来构建双向链表
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder; //排序方式,默认值为false

public LinkedHashMap() {
    super();
    accessOrder = false;
}
//重写newNode()方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
//重写newTreeNode()方法
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    linkNodeLast(p);
    return p;
}
//链接双向节点
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}
/*根据key获取键值对时,如果accessOrder = true(按访问顺序排序),则把此键值对移动至LinkedHashMap维护的双向链表的末尾,
*并返回此键值对。如果accessOrder = false(按插入顺序排序),直接返回此键值对。
*/
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
//把传入的键值对移动至链表的尾部
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
           

LinkedHashMap主要功能就是维护自己的双向链表,使得HashMap中的键值对变得有序。在维护双向链表时,head固定在首个插入的键值对上,tail按照插入键值对的顺序,逐个移动。所以,不需要做多余的操作,从head开始逐个取出键值对就可以达到”按插入顺序排序”的效果,因此accessOrder = false。查看以下取出所有LinkedHashMap中的键值对方法:

Iterator iterator = linkedHashMap.entrySet().iterator();
while (iterator.hasNext()){
      Objects obj = iterator.next();
 }
           

使用迭代器获取所有的key信息:LinkedKeyIterator

使用迭代器获取所有的value信息:LinkedValueIterator

final class LinkedKeyIterator extends LinkedHashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().getKey(); }
}

final class LinkedValueIterator extends LinkedHashIterator
    implements Iterator<V> {
    public final V next() { return nextNode().value; }
}
abstract class LinkedHashIterator {
    LinkedHashMap.Entry<K,V> next;
    LinkedHashMap.Entry<K,V> current;
    int expectedModCount;
    //构造方法,next指向了head引用
    LinkedHashIterator() {
        next = head;
        expectedModCount = modCount;
        current = null;
    }

    public final boolean hasNext() {
        return next != null;
    }

    final LinkedHashMap.Entry<K,V> nextNode() {
        LinkedHashMap.Entry<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        current = e;
        next = e.after;
        return e;
    }

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}
           

如果想要达到”按访问顺序排序”的效果,就需要对访问时的键值对做处理,这里是把访问的键值对移动到LinkedHashMap维护的双向链表的末尾。按照上面迭代器的方式获取所有LinkedHashMap中的键值对,由于迭代器是从head开始取值,因此,我们需要在迭代器获得的结果中再反向取值就OK了

三.TreeMap

TreeMap仅使用了链表来实现,其数据结构是红黑树。HashMap根据key健值产生的散列码和key健值equals()方法来确定键值对是否重复,而TreeMap则是使用了Comparable比较器来确定健值是否重复。

TreeMap所得到的结果是经过排序的,其排序规则来自Comparable比较器,用户可以自己定义Comparable比较器,如果不定义,将会使用默认的规则来排序。

//无参构造函数,Comparable对象为null,默认规则排序
public TreeMap() {
    comparator = null;
}
//根据key值获取键值对,遍历左子树,右子树
public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < )
            p = p.left;
        else if (cmp > )
            p = p.right;
        else
            return p;
    }
    return null;
}
public V put(K key, V value) {
    // 如果根节点为空,则该元素置为根节点
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = ;
        modCount++;
        return null;
    }
    // 如果比较器对象不为空,也就是自定义了比较器
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {// 循环比较并确定元素应插入的位置(也就是找到该元素的父节点)
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < )
                t = t.left;
            else if (cmp > )
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    // 如果比较器对象为空,使用默认的比较机制
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {// 同样是循环比较并确定元素应插入的位置(也就是找到该元素的父节点)
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < )
                t = t.left;
            else if (cmp > )
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);// 根据key找到父节点后新建一个节点
    if (cmp < )// 根据比较的结果来确定放在左子树还是右子树
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
           

四.HashTable

HashMap和Hashtable的区别如下:

  • Hashtable是线程安全,其多数的方法都加了synchronized 修饰符。
  • 插入Hashtable中的元素,其key和value值都不能为null,否则会抛出NullPointerException异常。
public Hashtable() {
    this(, f);
}
//key和value值均不能为null,否则抛出NullPointerException异常
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();//key不能为null
    int index = (hash & ) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & ) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}