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


    随着1998年JDK 1.2的发布,同时新增了常用的Collections集合类,包含了Collection和Map接口。而Dictionary类是在1996年JDK 1.0发布时就已经有了。它们都可以在rt.jar这个基础类库包中找到。全文以JDK8为例,尝试介绍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("
    " + 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涉及弱引用,待了解后再补充吧。

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

  • 相关阅读:
    C语言变长数组data[0]总结
    常见网络摄像机默认使用的端口,RTSP地址
    目前使用过的各大厂商rtsp取流的url
    Fix "Unable to lock the administration directory (/var/lib/dpkg/)" in Ubuntu
    笔记整理--C语言
    笔记整理--LibCurl开发
    Linux下Socket连接超时的一种实现方法(转载)
    笔记整理--Linux守护进程
    笔记整理--Linux编程
    笔记整理--Linux平台MYSQL的C语言
  • 原文地址:https://www.cnblogs.com/Jeffscnblog/p/8485217.html
Copyright © 2020-2023  润新知