Java面试必知:HashMap与Hashtable的源码浅析

HashMap与Hashtable的源码浅析

学习一门技术就要把它学通,学的深入一点,Java中的集合类源码解析是面试中经常会问到的问题,所以今天就来带大家一起解析下JDK源码。如若发现任何不妥的地方,欢迎大家fadeback。

HashMap源码解析

首先看下JDK中是如何定义HashMap的:

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

它是继承自AbstractMap<K, V>这个抽象类,实现了Map<K,V>CloneableSerializable这三个接口。值得注意的是AbstractMap<K, V>也实现了Map<K, V>接口。

HashMap存储键值对的成员变量——table

HashMap内声明了一个成员变量用于存储部分键值对,它是一个数组,源码如下:

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K,V>[] table;

table被transient关键字修饰,也就是说table里面存储的数据不会被序列化,它保证了用户敏感信息在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中不会被传输。
注意:上面之所以说存储部分Node是因为它只会存储Key的Hash码首次在该HashMap中出现的Node,其他的将以链表的形式存储在next上。如下。。

HashMap中的静态内部类——Node

Node是HashMap用来存储键值对的最小粒度,是一个静态内部类,它类似于链表,实现了Map接口的内部接口Entry<K, V>,关键代码如下:

/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
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;
    }
 }

Node的next存储了与当前Key的hash码相同的下一个Node对象。也就是说当使用HashMap存储一个键值对时,它会首先检查这个Key的hash值在table数组对应的位置上是否为null,若为null则直接赋值,若不为null,则会依次检验next对象,直到next为null后,然后给next赋值。

那么HashMap是怎么存储一个新的键值对的呢?

我们从HashMap的put方法看起(注释略):

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

它调用了一个putVal方法:

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) //若table为空,调用resize()方法初始化table数组。
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) //当前key的hash对应的位置为空,直接赋值
            tab[i] = newNode(hash, key, value, null);
        else { //否则获取当前Node对象,检验并赋值
            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;
    }

源码中本来没有这些注释,但是为了方便理解,我直接在源码里添加了注释,只是大致的叙述了下,细节地方还需要读者细细品味。不过个人觉得理解了大致是怎么回事就够了,没有必要深究哈。

HashMap小总结

HashMap就研究到这里啦,总的来说,HashMap是采用数组加链表的方式存储数据的,通过key对应的hash值来快速定位到指定的数组位置上,每一个hash值对应的实体类都可以看做是一个链表的起始位置(不同于表头,因为它是有数据的),通过这个实体类可以找到其他以此hash值为key的实体类。

值得注意的地方是:当使用HashMap存储数据时,最好不要使用一些连续的数字去作为key,最好使用字符串,因为连续的数字的hash值都是相同的,这样会导致数据都存储在一个hash值上,影响效率。另外HashMap是非线程安全的,当发布HashMap时要特别小心。

Hashtable源码分析

首先,看一下JDK中是如何定义Hashtable的:

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

它的继承方式与HashMap一致。
其次,在Hashtable中也找到了内部类Node<K, V>

/**
 * Hashtable bucket collision list entry
 */
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();
    }
}

它的实现方式跟HashMap.Node基本上是一致的,只是方法放置的上下顺序不同,有的个别的方法代码略有不同,但是结果是相同的。如:

//HashMap.Node的toString方法
public final String toString() { return key + "=" + value; }

//Hashtable.Node的toString方法
public String toString() {
    return key.toString()+"="+value.toString();
}

虽然功能是一样的,但是为什么不直接复制呢?哈哈,也许有我不理解的一面吧。

在Hashtable中也找到了table数组,如下:

/**
 * The hash table data.
 */
 private transient Entry<?,?>[] table;

看到这里我们就能明白,Hashtable的实现也是数组加链表的实现的,不同的是Hashtable是线程安全类,而HashMap则是非线程安全的。

而且Hashtable与HashMap中的其他方法上下顺序也是不同的(有些方法实现也是不同的),从这里可以看出从HashMap到Hashtable的实现并不是直接复制然后给方法加锁。

我们再来看几个具体需要加锁的方法实现:
首先是put方法

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

可以看到它是直接在定义方法时直接synchronized ,这样加锁性能会大大的降低,在方法内部没有任何的优化,也就是说只要是调用put方法就会加锁。
我们再来看看其它方法:

public synchronized V remove(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>)tab[index];
    for(Entry<K,V> 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;
}

remove方法也是直接在定义的时候synchronized。

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

get方法也是一样,Hashtable并没有想象中的对锁进行相应优化,比如缩小锁的范围,使用CAS等。

Hashtable小总结

Hashtable对加锁的优化很粗糙,只是单纯的在外部使用synchronized,所以我们在并发较大的情况下尽量不要使用Hashtable,这将会造成严重的性能问题。另外提醒大家注意下Java中的Hashtable中table的首字母是不大写的,粗心的同学要稍微注意下。稍后我会带来ConcurrentHashMap的源码浅析,希望大家多多关注。

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