每天读点java源码——HashMap

读注释

HashMap的类注释有点多额…慢慢品读

Hash table based implementation of the {@code Map} interface. This implementation provides all of the optional map operations, and permits {@code null} values and the {@code null} key. (The {@code HashMap} class is roughly equivalent to {@code Hashtable}, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

这段注释主要讲了三点:①HashMap允许null 作为键和值;②HashMap不是线程同步的;③HashMap不能保证里面的键值对的顺序一直不变

This implementation provides constant-time performance for the basic operations ({@code get} and {@code put}), assuming the hash function disperses the elements properly among the buckets. Iteration over collection views requires time proportional to the “capacity” of the {@code HashMap} instance (the number of buckets) plus its size (the number of key-value mappings). Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

这段注释主要讲了:①如果hash函数能把插入的元素均匀的分散到hash桶,那么get和put元素的时间复杂度为常量;②如果经常要遍历hash表中的元素,就不要初始化的时候把hash表的容量设置的太大,也不要把负载因子设置的太低

An instance of {@code HashMap} has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.

这里主要讲影响hash表性能的两个因素:初始化容量和负载因子。容量是指初始化hash表时表中桶的数量,负载因子是用来衡量hash表满的程度,当表中元素个数>负载因子 * 当前hash表容量,hash表就会重建,容量近似扩大为原来的两倍。

最后还有一些注释就不贴了,总结如下:

  • 负载因子默认为0.75,是经过长期实践,平衡时间和空间复杂度得到的结果
  • HashMap不是线程同步的
  • HashMap实例调用iterator()后不能再对其修改,否则会抛出ConcurrentModificationException异常,这点和ArrayList以及LinkedList相同

读源码

重要的属性

// 初始化不指定容量,默认为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 不指定负载因子,默认为0.75
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;

最后三个属性与是否转为树形结构有关,当一个桶里的元素个数大于TREEIFY_THRESHOLD但是hash表的容量小于MIN_TREEIFY_CAPACITY时,会对hash表进行扩容;当一个桶里的元素个数大于TREEIFY_THRESHOLD且hash表的容量达到MIN_TREEIFY_CAPACITY时,会将超过TREEIFY_THRESHOLD的桶转化为树形结构,如果桶里元素个数小于等于UNTREEIFY_THRESHOLD,会将此桶从树形结构转化为链表结构。

这里有个疑问,为什么桶里元素超过达到8就会将链表转化为树形结构?又为什么桶里元素小于6又会从树形结构转化为链表??

扫描二维码关注公众号,回复: 11639313 查看本文章

元素数据结构(链表)

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

hashMap每个元素的数据结构,了解数据结构能让我们更好的理解数据的特点和存储方式。从这里看出,每个元素是以Node来存储,里面包含了key,value,hash值,以及指向下一个元素的next(如果有的话),这个next也就说明了如果两个元素映射到同一个桶中,就会以链表的方式来解决冲突。但是当链表转为树形结构,这个node肯定是不能用了,毕竟树形结构会有左右节点,继续看树形结构时元素的属性结构又是怎样的…

元素数据结构(树形)

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
}

树形结构时元素的结构,我这里只截取了基本属性定义,还有一些方法省略了…从注释上看,采用的是红黑树结构(红黑树我也只了解基本的概念,不敢说太多…),可以看到每个元素存储了很多指针(话外音,存储一个元素需要更多的空间了…)

构造函数

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

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
}

提供四个构造函数,不过大多情况下我们用的都是无参构造,也就是容量和负载因子采用默认值。最后一个构造函数是利用已有的map来构造一个新的hashMap,这里注意如果已有的map为null,这里会报NullPointerException异常。

常用方法

// 返回hashMap中元素个数
public int size() {  return size;  }
// 判断hashMap中有没有元素
public boolean isEmpty() {  return size == 0;  }
  • 获取键对应的值
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;
}

// 判断map是否包含某一值
public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (Node<K,V> e : tab) {
                for (; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
}

根据key取value步骤:①对key进行hash,和表长减1进行与操作(为啥??)得到对应的桶的位置;②取出节点的key进行对比,如果相等返回对应的value;③不相等就取出当前节点的下一个节点。这里需要判断当前桶下是链表结构还是树形结构,链表的话就依次拿当前节点的next,然后比较key就行了,树形的话…emmm 还在看…
判断map里是否包含某一key,内部使用的方法与get相同,不再赘述。

  • put操作
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;
}

put操作,HashMap里相对复杂的操作,涉及扩容、树化转变等。

移除元素

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
}

// 清除map中所有元素
public void clear() {
        Node<K,V>[] tab;
        modCount++;
        if ((tab = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
}

获取map里key或者value的集合

public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
}

public Collection<V> values() {
        Collection<V> vs = values;
        if (vs == null) {
            vs = new Values();
            values = vs;
        }
        return vs;
}

// key=value集合:[key1=chen, key2=prince]
public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

迭代map

public void forEach(BiConsumer<? super K, ? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (Node<K,V> e : tab) {
                for (; e != null; e = e.next)
                    action.accept(e.key, e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
}

jdk1.8新增,配合函数式接口,可以很方便的对map元素进行迭代处理。

猜你喜欢

转载自blog.csdn.net/chenshufeng115/article/details/95366764