java中的Collections集合类源码分析与相关特性和性能

随着1998年JDK 1.2的发布,同时新增了常用的Collections集合类,包含了Collection和Map接口。而Dictionary类是在1996年JDK 1.0发布时就已经有了。它们都可以在rt.jar这个基础类库包中找到。全文以JDK8为例,尝试介绍Collections集合类的相关内容。

《java中的Collections集合类源码分析与相关特性和性能》

它们的关系如上图所示,标蓝的为抽象类,实线全箭头指的是extends(继承),虚线全箭头表示implement(实现),虚线半箭头依赖指的是这个类里面有依赖接口或者类的成员变量,比如HashSet类,继承AbstractSet抽象类,它里面又定义了HashMap的成员变量:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map; ... }

 

一、Collection接口下的List和Set

首先Collection接口继承Iterable接口,主要定义了size、isEmpty、contains、toArray、add、remove、clear、equals、hashCode等方法

List接口继承Collection接口,新增了sort、get、set、indexof、lastindexof、subList等方法,Set接口也继承Collection接口,但没有List这些新增方法

先从语义上看,List表达列表的意思,Set表示集合的意思。两者区别在于:List元素有序、不唯一,Set元素无序、唯一。

List

List接口下有LinkedList、ArrayList、Vector实现类:

LinkedList在JDK1.7之后,从单向链表结构Entry变成了双向链表节点Node数据结构实现的,一个Node节点有前后节点,JDK源码如下(为节省篇幅,去掉了相关注释,挑了重点的成员变量和成员函数,后同

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;
transient Node<E> first;
transient Node<E> last; 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++; } 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++; } public boolean add(E e) { linkLast(e); return true; } public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } 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; } } ... }

ArrayList和Vector使用变长数组实现的,但ArrayList非线程安全,达到数组长度时每次扩大50%:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10; transient Object[] elementData; // non-private to simplify nested class access private int size;
public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0 : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); } private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } ... }

代码稍长,主要看add增加元素时调用ensureCapacityInternal,最后进入grow方法里

int newCapacity = oldCapacity + (oldCapacity >> 1);

这里newCapacity就是oldCapacity的1.5倍,再往下判断一下是否仍比传过来的新的长度minCapacity小(1增长到2时出现),再往下与最大整型Integer.MAX_VALUE相比进行处理,ArrayList数组最长也只能是Integer.MAX_VALUE。

Vector线程安全,达到数组长度时每次扩大一倍。除了public函数全部用synchronized修饰外,与ArrayList主要的不同的就是grow函数

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

可以看到,如果我们没有定义capacityIncrement 增长步长的话,newCapacity就是oldCapacity+oldCapacity,就是原数组长度的两倍。

ArrayList和Vector每次插入元素都需要System.arraycopy或Arrays.copyOf一次,每次数组长度不足时就要扩长一次。所以,网上通常的说法是:

1. ArratList主要消耗在扩大数组长度和每次的copy上,随机插入删除的效率较低,但查询速度较快;

2. Vector由于线程安全效率更低,插入删除查询都比较慢;

3. LinkedList由于每个Node只知道前后Node,所以随机查询效率较低,每次都需要从first或last开始遍历查询,但插入删除效率较高,只需改变引用;

事实真的是这样吗?实际测试过才知道。

import java.util.*;

public class Main {

    public static void main(String[] args) {
        List arrayList = new ArrayList();
        Long start_time = System.currentTimeMillis();
        for(int i = 0; i < 50000; i++){
            arrayList.add(0,i);
        }
        Long end_time = System.currentTimeMillis();
        System.out.println("ArrayList add time " + (end_time - start_time));

        List linkedList = new LinkedList();
        start_time = System.currentTimeMillis();
        for(int i = 0; i < 50000; i++){
            linkedList.add(0,i);
        }
        end_time = System.currentTimeMillis();
        System.out.println("LinkedList add time " + (end_time - start_time));

        start_time = System.currentTimeMillis();
        for(int i = 0; i < 50000; i++){
            arrayList.get(i);
        }
        end_time = System.currentTimeMillis();
        System.out.println("ArrayList get time " + (end_time - start_time));

        start_time = System.currentTimeMillis();
        for(int i = 0; i < 50000; i++){
            linkedList.get(i);
        }
        end_time = System.currentTimeMillis();
        System.out.println("LinkedList get time " + (end_time - start_time));
    }
}
ArrayList add time 281
LinkedList add time 0
ArrayList get time 16
LinkedList get time 1500

以上是网上一般的测试例子。我们看结果确实是这样的,ArrayList比LinkedList插入慢,比LinkedList查询快。但是这里我有一个疑问,为什么要使用add(index,element)方法呢?而不直接用我们常用的add(element)方法?如果换成add(element)又是怎样的呢?

import java.util.*;

public class Main {

    public static void main(String[] args) {
        List arrayList = new ArrayList();
        Long start_time = System.currentTimeMillis();
        for(int i = 0; i < 50000; i++){
            arrayList.add(i);
        }
        Long end_time = System.currentTimeMillis();
        System.out.println("ArrayList add time " + (end_time - start_time));

        List linkedList = new LinkedList();
        start_time = System.currentTimeMillis();
        for(int i = 0; i < 50000; i++){
            linkedList.add(i);
        }
        end_time = System.currentTimeMillis();
        System.out.println("LinkedList add time " + (end_time - start_time));
    }
}
ArrayList add time 0
LinkedList add time 16

换成add(element)的时候,ArrayList比LinkedList快,那么ArrayList的add(index,element)是什么处理逻辑呢?我们再来看看ArrayList里的源码:

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

    public void add(int index, E element) {
        rangeCheckForAdd(index);

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

可以看到,add(index,element)比add(e)主要多了System.arraycopy一句,这句的意思是把elementData数组在index下标后面的元素全部后移一位,我们例子里index是0,那么就是每次新增元素时,都要使eletmentData[0]后面的元素后移一位,然后把新元素放到eletmentData[0]上。

再来看看LinkedList的源码

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

    public void add(int index, E element) {
        checkPositionIndex(index);

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

    ...

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

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

可以看到,主要性能消耗是linkBefore(element, node(index))这句node(index),需要从first元素或者last元素(index与size相比,看index是在数组前半部分还是后半部分)找到特定index的node,拿到了这个node,修改前后元素的引用,修改引用消耗不大。所以如果index越靠近双向链表的中间,Linked的消耗越大。

这就很流氓了!add(0,element)是把ArrayList最坏的情况跟LinkedList最好的情况比较。我们回过头来看网上的说法,强调的是随机,那么我们再试试随机插入:

import java.util.*;

public class Main {

    public static void main(String[] args) {
        Random random = new Random();
        List arrayList = new ArrayList();
        Long start_time = System.currentTimeMillis();
        for(int i = 0; i < 50000; i++){
            arrayList.add((arrayList.size() > 0) ? random.nextInt(arrayList.size()) : i, i);
        }
        Long end_time = System.currentTimeMillis();
        System.out.println("ArrayList add time " + (end_time - start_time));

        List linkedList = new LinkedList();
        start_time = System.currentTimeMillis();
        for(int i = 0; i < 50000; i++){
            linkedList.add((linkedList.size() > 0) ? random.nextInt(linkedList.size()) : i,i);
        }
        end_time = System.currentTimeMillis();
        System.out.println("LinkedList add time " + (end_time - start_time));
}
ArrayList add time 141
LinkedList add time 4187

以上例子按照ArrayList和LinkedList现有长度随机一个index去插入,我执行了多次,实际效果都是ArrayList比LinkedList快。

所以我的结论是:如果必须使用add(0,element)的时候,那就请使用LinkedList吧。

Set

Set下面主要有HashMap和TreeMap,先看看HashMap的源码

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; } ... }

可以看到,HashSet实际就是利用HashMap存放元素的,将元素放入HashMap的Key里,利用HashMap的Key自动哈希散列来保证元素的唯一性,后面再详细聊聊HashMap如何进行存取元素的。

TreeSet目前没使用过,就先不说。后面研究过再补上。简要的理解是,TreeSet利用TreeMap实现,元素是有序的,和HashSet一样元素是唯一的。

回到上面所说的,List元素有序、不唯一,Set元素无序、唯一。唯一性大家应该能够理解,有序无序的意思是,LinkedList、ArrayList可以预知和控制元素排序,但HashSet根据元素哈希值存放的,不能根据插入的先后顺序控制。但这句话其实也是有问题的,因为TreeSet是有序的,估计因为不常用被忽略了吧。HashSet元素无序例子如下

import java.util.*;

public class Main {

    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        Random random = new Random();
        for(int i = 0; i < 10; i++){
            int r = random.nextInt(10000);
            System.out.print(r + " ");
            hashSet.add(r);
        }
        System.out.println("\n" + hashSet);
    }
}
6192 4913 4415 6384 6593 1136 8666 2021 7117 8972 
[6192, 6384, 1136, 4913, 6593, 2021, 8666, 8972, 7117, 4415]

 

二、Map接口下的HashMap和HashTable

Map接口主要定义了自身是Entry<key, value>这种键值对数据结构,包含size、isEmpty、containsKey、containsValue、get、put、remove、clear、keySet等方法。

Map接口下有AbstractMap抽象类,AbstractMap实现了部分方法,增加了SimpleEntry<key, value>数据结构

AbstractMap下面有HashMap、TreeMap、WeakHashMap这几个实现类。

而HashTable是继承Dictionary的和实现Map接口的。

先来看HashMap的源码:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

 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; } } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; transient int size; final float loadFactor; public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
 } 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) > 0 && (first = tab[(n - 1) & 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; } public boolean containsKey(Object key) { return getNode(hash(key), key) != null; } 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; 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; } public boolean containsValue(Object value) { Node<K,V>[] tab; V v; if ((tab = table) != null && size > 0) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { if ((v = e.value) == value || (value != null && value.equals(v))) return true; } } } return false; } ... }

HashMap实现的复杂度跟前面的类不可同日而语,首先定义了一个静态的内部类Node,Node实现Map接口里的Entry<key, value>接口,并定义了hash变量和指向下一个Node的next变量,所以Node可以是一个单向链表,接着HashMap定义了Node<K,V>[] table 数组用来存放元素。首先看我们常用的存放元素函数put,它主要调用了putVal方法,概要的逻辑是:

1.判断初始化

2.利用元素的哈希值hash与(table长度-1)进行与&运算,得到新元素应该放在table数组的index下标

3.判断是否table数组index下标已经有值,有值则说明产生了碰撞,把新元素放在这个单向列表后端(深度最大为8,超过深度则转变成红黑树)

4.在获取table数组index下标的node时,和遍历单向链表时,如果发现key和已有的旧元素一致,则返回旧元素的值,不改变旧元素

5.判断是否需要扩容

这个putVal方法的详细逻辑是这样的:

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

↑ 第一个if判断table和长度是否为空,为空就初始化扩容,顺便赋了值。

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

↑ 第二个if使用元素的哈希值hash与(table长度-1)进行与&运算,得到一个index,如果这个tab[index]没有值,就直接把新的这个元素放到tab[index]上

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;

如果上面tab[index]上已经有值了,说明多个key的哈希值和(table长度-1)与运算的结果一样,产生了碰撞,需要把这个key放到这个tab[index]上p的单向链表上。else里第一个if判断原来tab[index]元素的hash与新元素的hash是否一样,如果它们的hash一样,那么判断key是不是一样的,如果连key也一样,那么保留为旧元素。

else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

↑ 第二个else if判断tab[index]上的p节点是否是红黑树TreeNode

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

↑ 这个第二层else表示如果不是TreeNode,则是单向链表节点,那么会进入这里,遍历这个单向链表,这里的第一个if判断单向链表tab[index]上的p的next是否为空,为空就直接p.next指向新元素,TREEIFY_THRESHOLD定义深度最大为8,如果达到最大深度,就把这个单向链表转换成红黑树TreeNode,break退出遍历。第二个if判断如果这个单向链表tab[index]上的p不为空,那么如果p的key和新元素的key一样,那么保留为旧元素,break退出遍历。最后如果p.next不为空,Key又不一致,那么p = e继续往下遍历。

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

↑ 外层else最后这个if判断之前是否取出了相同hash和key的node节点,如果有已存在这样的node,就把旧node的value值返回。

++modCount;
if (++size > threshold)
    resize();
afterNodeInsertion(evict);
return null;

↑ 函数最后就是判断是否要扩容了。

扩容resize函数也值得研究一下,因为元素hash值与数组长度-1与运算的,一旦数组长度发送改变,那么按道理,原有元素的数组位置应该会发送改变,甚至单向链表、红黑树里的元素都会涉及重新调整位置。这部分后面研究过再写。

接着,我们来分析一下get方法,这个方法主要调用了getNode方法,我们知道了put是怎么把元素放到对应的位置的,那么getNode的时候就相应的把这个位置找到,就可以获取元素了:

1.根据key的hash值与table数组长度-1做与运算,获得数组下标index

2.判断是否和table[index]的元素的key一致,一致就返回

3.判断table[index]上是否是红黑树,是红黑树则去红黑树找这个元素

4.判断table[index]是一个单向链表,遍历单向链表找到这个元素

这个逻辑比putVal简单很多,就不详细一段一段代码分析了。

 

接下来看看HashTable,虽说HashTable实现了Map接口,但是这个类其实在JDK1.0就已经有了,应该只是后来重现实现了而已。

HashTable继承Dictionary抽象类,Dictionary与Map很相似,我的感觉是Dictionary是古老的Map,为了兼容以前的代码,HashTable才仍然继承Dictionary的,毕竟可能很多面向抽象编程这么定义:Dictionary hashTable = new HashTable();

 先来看看HashTable的源码:

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {

    private transient Entry<?,?>[] table;

    private transient int count;

    private int threshold;

    private float loadFactor;

    private transient int modCount = 0;

    public synchronized boolean contains(Object value) {
        if (value == null) {
            throw new NullPointerException();
        }

        Entry<?,?> tab[] = table;
        for (int i = tab.length ; i-- > 0 ;) {
            for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
                if (e.value.equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }

    public synchronized boolean containsKey(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return true;
            }
        }
        return false;
    }

    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % 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;
    }

    private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

    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();
        int index = (hash & 0x7FFFFFFF) % 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;
    }

    private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Entry<K,V> next;

        protected Entry(int hash, K key, V value, Entry<K,V> next) {
            this.hash = hash;
            this.key =  key;
            this.value = value;
            this.next = next;
        }

        @SuppressWarnings("unchecked")
        protected Object clone() {
            return new Entry<>(hash, key, value,
                                  (next==null ? null : (Entry<K,V>) next.clone()));
        }

        // Map.Entry Ops

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }

        public V setValue(V value) {
            if (value == null)
                throw new NullPointerException();

            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        public boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;

            return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
               (value==null ? e.getValue()==null : value.equals(e.getValue()));
        }

        public int hashCode() {
            return hash ^ Objects.hashCode(value);
        }

        public String toString() {
            return key.toString()+"="+value.toString();
        }
    }

    ...
}

可以看到HashTable与HashMap很类似,HashTable的内部类Entry<key,value>也是实现Map的Entry<key,value>接口,也有一个hash成员变量和指向下一个Entry的next成员变量,也是一个单向链表。不同的在于,HashTable的public函数都用synchronized修饰,是线程安全的。

先来看看put函数,我们可以看到,重要的获取元素的数组下标Index的方式与HashMap有区别

index = (hash & 0x7FFFFFFF) % tab.length;

HashTable是这样处理的,0x7FFFFFFF的作用是,如果hash值是负数的话就把它变成正数,然后直接除数组长度tab.length取余,这样既保证index是正数,又保证比数组长度小。另外我们还可以看到,HashTable在put的时候产生碰撞时,并不会产生红黑树的处理。碰撞的时候先检查是否已有的key,如果遍历tab[index]单向链表里都没有,就把新的元素加在原来tab[index]上的元素的前面,tab[index]上就指向了新元素,新元素的next指向原来的元素。HashTable的单向链表没有深度控制。

get函数同样根据hash值找到index,遍历单向链表,找到元素。

TreeMap和WeakHashMap

TreeMap和WeakHashMap我也没有使用过,TreeMap和HashMap的区别主要特定是元素是有序的。WeakHashMap涉及弱引用,待了解后再补充吧。

由于本人知识水平有限,如有遗留错误之处请指正。

    原文作者:Jeff砹
    原文地址: https://www.cnblogs.com/Jeffscnblog/p/8485217.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞