Java深入理解集合框架Map

前言

Java集合框架中Map接口主要包括HashMap、HashTable、TreeMap,下面依次介绍

HashMap

主要方法

HashMap增加查询删除数据的方法为put get remove

构造方法

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;


//存储数据的结构
transient Node[] table;

transient Set> entrySet;

transient int size;

transient int modCount;

int threshold;

final float loadFactor;

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

HashMap内部使用数组+链表的形式储存数据,当插入一个新值时,会首先新建一个节点,然后使用key的hash值对数组大小取余,得到的值就是数组插入位置的Index,在插入位置处,由于实际保存的是链表,因此会将新节点插入到链表末尾,因此,HshMap中每一条链上的节点的key的hash值对数组大小取余,都应该等于这条链表在数组的索引

由于HashMap要通过key值来确定在数组和链表中的唯一位置,因此HashMap不允许key值有重复,如果新增加一个数据时,数据key值和已有数据的key值相等,会执行替换操作,即将老数据替换为新数据

当链表的节点数大于8时,HashMap为了提高检索效率会将链表转换为红黑树。

HashMap内部比较重要的成员变量,table存储数据的结构,以数组形式表示,类型为Node,使用Node能形成一个单向链表,内部包含hashkeyvalue值,以及指向下一个节点的用next。数组的初始大小为DEFAULT_INITIAL_CAPACITY(16)
Node结构如下:

static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;
}

threshold用于判断HashMap是否需要扩容,当Table的节点数大于threshold时,会进行扩容操作,loadFactor指载因子,通过当前容量和loadFactor来决定threshold的大小。
在new一个HashMap时,我们可以选择无参构造,或者传入初始容量,加载因子,也可以传入一个Map。需要注意的是不管是传入初始容量还是传入Map都会调用tableSizeFor

    /**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

这是由于HashMap设计者将HashMap的数组大小设置为2的幂,因此通过这个函数能让我们在创建新数组时,保证数组大小是2的幂,那为什么HashMap的数组大小要设计成为2的幂呢?前面说到插入新节点时,要找对应在数组中的索引时是通过key的hash值对数组大小取余hash%length,但是这样运算的话效率低,所以改为了hash&(length-1),另外有工程师发现如果length是2的幂,效率会更高,因此HashMap大小就设计成为了2的幂

增加数据&&扩容机制

HashMap通过put增加数据,put会再次调用putVal,我们注意到,HashMap是自己封装了一个函数hash()来求key的值,而不是直接使用key.hashCode()作为hash值,这里我们也可以看出HashMap允许keynull,但是HashMap内部不会有重复的Key值,重复的话会替换数据。

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

HashMap首先通过Hsh值找到对应的数组索引,然后再得到链表,最后将新节点插入到链表尾。

putVal里面需要注意的主要有扩容操作resize和树形化操作treeifyBin,为什么要进行树形化呢,主要是单独对链表进行遍历的时间复杂度为O(n),由于随着链表的扩大,效率会很慢,采用红黑树的话,时间复杂度为O(log2n),因此效率得到了提高。

树形化treeifyBin里面基本上都是红黑树的知识,不理解的话可以先看一下二叉搜索树、红黑树的知识,另外红黑树貌似是在JDK1.8的源码里才增加的


    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        //判断是不是第一次增加数据
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//调用resize函数扩容
        //判断要插入的位置的链表头是否为null,是null的话直接将头指向新节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//hash值相同,key值也相同,后面会进行替换操作
            else if (p instanceof TreeNode)
                //如果链表的节点数大于8之后,会转换为红黑树,节点类型也会变化为TreeNode,因此增加数据的方法会有一些区别
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //找到链表尾,然后使用key value值新建节点,并加入链表尾
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//节点数大于8之后,将链表树形化
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;//找到了相同的key值,后面要进行替换操作
                    p = e;
                }
            }
            //这里如果为true的话,表示hashmap中有相同的key值,那么执行替换操作
            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;
    }

下面看一下扩容操作resize,在HashMap插入数据时putVal时,最后会执行判断操作if (++size > threshold),然后执行resize,在这个函数中,每次扩容都会将数组大小扩展为原始数组大小的2倍,主要是为了保证数组的大小是2的幂

final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //之前table不为空
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //数组扩大为原始大小的2倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            //如果初始化HashMap手动传入了初始数组大小或者传入了Map会走到这,newCap表示的是要形成的新数组的大小
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //初始化HashMap时采用无参构造方法,且未进行任何数据的操作
            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[] newTab = (Node[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            //在扩容时我们需要将old数组的数据拷贝到新数组中,每个节点的位置会根据新数组的大小变化
            for (int j = 0; j < oldCap; ++j) {
                Node 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)e).split(this, newTab, j, oldCap);
                    else { // preserve order

                        Node loHead = null, loTail = null;

                        Node hiHead = null, hiTail = null;
                        Node 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;
    }

查询数据

HashMap中通过get方法来查询数据,首先通过key的hash值来获取所在链表在数组中的索引,找到对应的链表后,再遍历链表找到key值相同的数据

    public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node getNode(int hash, Object key) {
        Node[] tab; Node 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)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;
    }

删除数据

HashMap通过remove删除数据,本质上就是链表删除数据

多线程

HashMap在多线程下是线程不安全的,因为没有进行同步操作

HashTable

主要方法

HashTable增加查询删除数据的方法为put get remove

构造方法

    private transient HashtableEntry[] table;
    private int threshold;
    private float loadFactor;
    private transient int modCount = 0;
    //默认数组大小为11
    public Hashtable() {
        this(11, 0.75f);
    }
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }
    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new HashtableEntry[initialCapacity];
        // Android-changed: Ignore loadFactor when calculating threshold from initialCapacity
        // threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        threshold = (int)Math.min(initialCapacity, MAX_ARRAY_SIZE + 1);
    }
    public Hashtable(Map t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

HashTable内部也是采用与HashMap类似的结构来存储数据table,不同于HashMap的是数组的初始为11,同时内部不存在红黑树,节点的类定义如下,依旧包含key hash value next

private static class HashtableEntry<K,V> implements Map.Entry<K,V> {
    // END Android-changed: Renamed Entry -> HashtableEntry.
        final int hash;
        final K key;
        V value;
        HashtableEntry next;
}

在HashTable中,hash值得求法跟HashMap不同,那就是这里hash值直接调用key.hashCode(),也就是说HashTable不允许KEY值为NULL,否则的话会报空指针异常

增加数据&&扩容机制

HashTbale通过put来添加数据,首先我们可以发现HashTable不允许value值为null,当然就如上所讲,key也不允许为null,否则会报空指针错误。

在获取数组的索引index值时也与HashMap不同,int index = (hash & 0x7FFFFFFF) % tab.length;

在插入数据之前首先会遍历链表,如果有重复的key的话,会用新value替换老数据

    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.
        HashtableEntry tab[] = table;
        int hash = key.hashCode();
        //找到数组索引
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        HashtableEntry entry = (HashtableEntry)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;
    }

我们看到插入新数据是通过addEntry来实现的,下面分析一下这个函数

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

        HashtableEntry 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")
        HashtableEntry e = (HashtableEntry) tab[index];
        //直接将节点插入到链表尾
        tab[index] = new HashtableEntry<>(hash, key, value, e);
        count++;
    }

函数很简单,主要是判断当前数据的数目是否大于threshold,如果大于的话就要进行扩容rehash,否则的直接将新节点插入到链表尾,下面分析rehash

@SuppressWarnings("unchecked")
    protected void rehash() {
        int oldCapacity = table.length;
        HashtableEntry[] oldMap = table;

        // overflow-conscious code
        //新大小为原来的两倍再加1
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        HashtableEntry[] newMap = new HashtableEntry[newCapacity];

        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;

        for (int i = oldCapacity ; i-- > 0 ;) {
            for (HashtableEntry old = (HashtableEntry)oldMap[i] ; old != null ; ) {
                HashtableEntry e = old;
                old = old.next;
                //遍历数组里的每个链表,将每个节点加入到新数组中
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (HashtableEntry)newMap[index];
                newMap[index] = e;
            }
        }
    }

rehash中,数组容量为原始数组容量2倍加1 : int newCapacity = (oldCapacity << 1) + 1;,同时在拷贝原数组数据到新数组时,挨个遍历数组的没个位置的链表的元素,根据运算之后的hash值对数组大小取余之后,放到合适的位置

查询数据

查询数据时,先得到hash值,然后做了运算之后对数组大小取余得到数组索引,然后再遍历相应的链表,找到key值相同的节点

    @SuppressWarnings("unchecked")
    public synchronized V get(Object key) {
        HashtableEntry tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (HashtableEntry e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

删除数据

删除数据时先找到数据所在链表在数组中的索引,然后就是链表的基本操作,不再赘述

public synchronized V remove(Object key) {
        HashtableEntry tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        HashtableEntry e = (HashtableEntry)tab[index];
        for(HashtableEntry prev = null ; e != null ; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                V oldValue = e.value;
                e.value = null;
                return oldValue;
            }
        }
        return null;
    }

多线程

put add remove方法都有synchronized修饰,因此在多线程下操作数据是线程安全的,但是数据效率比HashMap低,而且链表未像HashMap那样就行优化(树形化)

TreeMap

主要方法

TreeMap增加查询删除数据的方法为put get remove

构造方法

TreeMap内部采用类似红黑树的结构来保存数据,为什么说类似呢,因为传统红黑树一般默认左子节点的key值小于当前节点key值,右子节点key值大于当前节点,在TreeMap中,会有一个comparator来比较key的大小,也就是说具体怎么比较key值可以由我们自己规定,而不是定死的。

TreeMap有4中构造方法如下:

    private final Comparatorsuper K> comparator;

    private transient TreeMapEntry root;

    /**
     * The number of entries in the tree
     */
    private transient int size = 0;

    /**
     * The number of structural modifications to the tree.
     */
    private transient int modCount = 0;

    public TreeMap() {
        comparator = null;
    }
    public TreeMap(Comparatorsuper K> comparator) {
        this.comparator = comparator;
    }
    public TreeMap(Map m) {
        comparator = null;
        putAll(m);
    }
    public TreeMap(SortedMap m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }

增加数据&&扩容机制

TreeMap使用put添加数据,大致流程就是从根节点开始,挨个通过comparator比较当前节点和要插入节点的通过key值大小,如果要插入节点的key值小于当前节点的key值,那么就将当前节点的左子节点再与要插入节点比较,否则将当前节点的右子节点与要插入节点比较,知道找到了树的叶节点,然后再将新节点放到叶节点下面(至于是左节点还是右节点通过key值判断),在插入完成后,通过fixAfterInsertion去修正红黑树

    public V put(K key, V value) {
        TreeMapEntry t = root;
        if (t == null) {

            if (comparator != null) {
                if (key == null) {
                    comparator.compare(key, key);
                }
            } else {
                if (key == null) {
                    throw new NullPointerException("key == null");
                } else if (!(key instanceof Comparable)) {
                    throw new ClassCastException(
                            "Cannot cast" + key.getClass().getName() + " to Comparable.");
                }
            }
            // END Android-changed: Work around buggy comparators. http://b/34084348
            root = new TreeMapEntry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        TreeMapEntry parent;
        // split comparator and comparable paths
        Comparatorsuper K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparablesuper K> k = (Comparablesuper K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        TreeMapEntry e = new TreeMapEntry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

查询数据

红黑树操作,不再赘述

删除数据

红黑树操作,不再赘述

多线程

线程不安全

总结

类别 HashMap HashTable TreeMap
内部实现 内部通过数组+链表的形式保存数据,当链表节点数大于8时,会将链表转换为红黑树 内部通过数组+链表的形式保存数据 红黑树
初始大小 数组的初始大小为16 数组的初始大小为11 0
Hash值 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) key.hashCode() 无hash值
key是否能为null 允许key值为null,此时数组索引为0 不允许 如果我们在构造器初始化时传入了comparator,那么key可以为null,否则不允许为null
value是否能为null 允许 不允许 允许
扩容机制 数组大小变为原数组的两倍,始终保证数组大小是2的幂 数组大小为原数组大小的2倍再加1 树,挨个添加节点,再修正
多线程 不同步 同步 不同步
插入速度 比较快 比较快 比较快
查询速度 查询时间复杂度最差为O(n),最好O(log2n) 查询时间复杂度恒为O(n 查询时间复杂度O(log2n)

你可能感兴趣的:(Java)