Java集合详解4一文读懂HashMap和HashTable的区别以及常见面试题

11次阅读

共计 14652 个字符,预计需要花费 37 分钟才能阅读完成。

《Java 集合详解系列》是我在完成夯实 Java 基础篇的系列博客后准备开始写的新系列。

这些文章将整理到我在 GitHub 上的《Java 面试指南》仓库,更多精彩内容请到我的仓库里查看

https://github.com/h2pl/Java-…

喜欢的话麻烦点下 Star、fork 哈

文章首发于我的个人博客:

www.how2playlife.com

今天我们来探索一下 HashMap 和 HashTable 机制与比较器的源码。

本文参考 http://cmsblogs.com/?p=176

HashMap

HashMap 也是我们使用非常多的 Collection,它是基于哈希表的 Map 接口的实现,以 key-value 的形式存在。在 HashMap 中,key-value 总是会当做一个整体来处理,系统会根据 hash 算法来来计算 key-value 的存储位置,我们总是可以通过 key 快速地存、取 value。下面就来分析 HashMap 的存取。

定义

HashMap 实现了 Map 接口,继承 AbstractMap。其中 Map 接口定义了键映射到值的规则,而 AbstractMap 类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实 AbstractMap 类已经实现了 Map,这里标注 Map LZ 觉得应该是更加清晰吧!

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

构造函数

  HashMap 提供了三个构造函数:HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。

在这里提到了两个参数:初始容量,加载因子。

这两个参数是影响 HashMap 性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。

对于使用链表法的散列表来说,查找一个元素的平均时间是 O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为 0.75,一般情况下我们是无需修改的。

HashMap 是一种支持快速存取的数据结构,要了解它的性能必须要了解它的数据结构。

数据结构

我们知道在 Java 中最常用的两种结构是数组和模拟指针 (引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap 也是如此。实际上 HashMap 是一个“链表散列”,如下是它的数据结构:

HashMap 数据结构图

下图的 table 数组的每个格子都是一个桶。负载因子就是 map 中的元素占用的容量百分比。比如负载因子是 0.75,初始容量(桶数量)为 16 时,那么允许装填的元素最大个数就是 16*0.75 = 12,这个最大个数也被成为阈值,就是 map 中定义的 threshold。超过这个阈值时,map 就会自动扩容。

存储实现:put(key,vlaue)

  首先我们先看源码

public V put(K key, V value) {
        // 当 key 为 null,调用 putForNullKey 方法,保存 null 与 table 第一个位置中,这是 HashMap 允许为 null 的原因
        if (key == null)
            return putForNullKey(value);
        // 计算 key 的 hash 值,此处对原来元素的 hashcode 进行了再次 hash
        int hash = hash(key.hashCode());                  ------(1)
        // 计算 key hash 值在 table 数组中的位置
        int i = indexFor(hash, table.length);             ------(2)
        // 从 i 出开始迭代 e, 找到 key 保存的位置
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 判断该条链上是否有 hash 值相同的 (key 相同)
            // 若存在相同,则直接覆盖 value,返回旧 value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;    // 旧值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     // 返回旧值
            }
        }
        // 修改次数增加 1
        modCount++;
        // 将 key、value 添加至 i 位置处
        addEntry(hash, key, value, i);
        return null;
    }

通过源码我们可以清晰看到 HashMap 保存数据的过程为:首先判断 key 是否为 null,若为 null,则直接调用 putForNullKey 方法。

若不为空则先计算 key 的 hash 值,然后根据 hash 值搜索在 table 数组中的索引位置,如果 table 数组在该位置处有元素,则通过比较是否存在相同的 key,若存在则覆盖原来 key 的 value,== 否则将该元素保存在链头(最先保存的元素放在链尾)==。

若 table 在该处没有元素,则直接保存。这个过程看似比较简单,其实深有内幕。有如下几点:

1、先看迭代处。此处迭代原因就是为了防止存在相同的 key 值,若发现两个 hash 值(key)相同时,HashMap 的处理方式是用新 value 替换旧 value,这里并没有处理 key,这就解释了 HashMap 中没有两个相同的 key。

2、在看(1)、(2)处。这里是 HashMap 的精华所在。首先是 hash 方法,该方法为一个纯粹的数学计算,就是计算 h 的 hash 值。

static int hash(int h) {h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

我们知道对于 HashMap 的 table 而言,数据分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。计算 hash 值后,怎么才能保证 table 元素分布均与呢?我们会想到取模,但是由于取模的消耗较大,HashMap 是这样处理的:调用 indexFor 方法。

static int indexFor(int h, int length) {return h & (length-1);
    }

HashMap 的底层数组长度总是 2 的 n 次方,在构造函数中存在:capacity <<= 1; 这样做总是能够保证 HashMap 的底层数组长度为 2 的 n 次方。当 length 为 2 的 n 次方时,h&(length – 1) 就相当于对 length 取模,而且速度比直接取模快得多,这是 HashMap 在速度上的一个优化。至于为什么是 2 的 n 次方下面解释。

== 对 length 取模来得到 hash 是常用的 hash 索引方法,这里采用位运算的话效率更高。==

我们回到 indexFor 方法,该方法仅有一条语句:h&(length – 1),这句话除了上面的取模运算外还有一个非常重要的责任:均匀分布 table 数据和充分利用空间。

这里我们假设 length 为 16(2^n) 和 15,h 为 5、6、7。

当 n =15 时,6 和 7 的结果一样,这样表示他们在 table 存储的位置是相同的,也就是产生了碰撞,6、7 就会在一个位置形成链表,这样就会导致查询速度降低。诚然这里只分析三个数字不是很多,那么我们就看 0 -15。

而当 length = 16 时,length – 1 = 15 即 1111,那么进行低位 & 运算时,值总是与原来 hash 值相同,而进行高位运算时,其值等于其低位值。所以说当 length = 2^n 时,不同的 hash 值发生碰撞的概率比较小,这样就会使得数据在 table 数组中分布较均匀,查询速度也较快。

这里我们再来复习 put 的流程:当我们想一个 HashMap 中添加一对 key-value 时,系统首先会计算 key 的 hash 值,然后根据 hash 值确认在 table 中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其 key 的 hash 值。

如果两个 hash 值相等且 key 值相等 (e.hash == hash && ((k = e.key) == key || key.equals(k))), 则用新的 Entry 的 value 覆盖原来节点的 value。如果两个 hash 值相等但 key 值不等,则将该节点插入该链表的链头。具体的实现过程见 addEntry 方法,如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
        // 获取 bucketIndex 处的 Entry
        Entry<K, V> e = table[bucketIndex];
        // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 
        table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
        // 若 HashMap 中元素的个数超过极限了,则容量扩大两倍
        if (size++ >= threshold)
            resize(2 * table.length);
    }

这个方法中有两点需要注意:

 后面添加的 entry 反而会接到前面。

一、是链的产生。

这是一个非常优雅的设计。系统总是将新的 Entry 对象添加到 bucketIndex 处。如果 bucketIndex 处已经有了对象,那么新添加的 Entry 对象将指向原有的 Entry 对象,形成一条 Entry 链,但是若 bucketIndex 处没有 Entry 对象,也就是 e ==null, 那么新添加的 Entry 对象指向 null,也就不会产生 Entry 链了。

二、扩容问题。

随着 HashMap 中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响 HashMap 的速度,为了保证 HashMap 的效率,系统必须要在某个临界点进行扩容处理。

该临界点在当 HashMap 中元素的数量等于 table 数组长度 * 加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新 table 数组中的位置并进行复制处理。所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。

JDK1.8 的 hashmap:put 方法

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;
                // 如果 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
                        // 当链表节点数超过 8 个,则直接进行红黑树化。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;
    }

JDK1.8 在链表长度超过 8 时会转换为红黑树。
转换方法如下:

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // 如果节点数变小小于红黑树的节点数阈值时,调整空间
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
            // 该方法直接返回一个红黑树结点。TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                // 从链表头开始依次插入红黑树
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
    
        // For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {return new TreeNode<>(p.hash, p.key, p.value, next);
}

扩容

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 如果原容量大于最大空间,则让阈值为最大值。因为不能再扩容了,最大容量就是整数最大值。if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 两倍扩容,阈值也跟着变为两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {oldTab[j] = null;
                    if (e.next == null)
                        // 当后面没有节点时,直接插入即可 // 每个元素重新计算索引位置,此处的 hash 值并没有变,只是改变索引值
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                    // 否则,就从头到尾依次将节点进行索引然后插入新数组,这样插入后的链表顺序会和原来的顺序相反。Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> 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;
    }

读取实现:get(key)

        相对于 HashMap 的存而言,取就显得比较简单了。通过 key 的 hash 值找到在 table 数组中的索引处的 Entry,然后返回该 key 对应的 value 即可。public V get(Object key) {
        // 若为 null,调用 getForNullKey 方法返回相对应的 value
        if (key == null)
            return getForNullKey();
        // 根据该 key 的 hashCode 值计算它的 hash 码  
        int hash = hash(key.hashCode());
        // 取出 table 数组中指定索引处的值
        for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            // 若搜索的 key 与查找的 key 相同,则返回相对应的 value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

在这里能够根据 key 快速的取到 value 除了和 HashMap 的数据结构密不可分外,还和 Entry 有莫大的关系,在前面就提到过,HashMap 在存储过程中并没有将 key,value 分开来存储,而是当做一个整体 key-value 来处理的,这个整体就是 Entry 对象。

同时 value 也只相当于 key 的附属而已。在存储的过程中,系统根据 key 的 hashcode 来决定 Entry 在 table 数组中的存储位置,在取的过程中同样根据 key 的 hashcode 取出相对应的 Entry 对象。

在 java 中与有两个类都提供了一个多种用途的 hashTable 机制,他们都可以将可以 key 和 value 结合起来构成键值对通过 put(key,value) 方法保存起来,然后通过 get(key) 方法获取相对应的 value 值。

HashTable

一个是前面提到的 HashMap,还有一个就是马上要讲解的 HashTable。对于 HashTable 而言,它在很大程度上和 HashMap 的实现差不多,如果我们对 HashMap 比较了解的话,对 HashTable 的认知会提高很大的帮助。他们两者之间只存在几点的不同,这个后面会阐述。

定义

  HashTable 在 Java 中的定义如下:public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable
      从中可以看出 HashTable 继承 Dictionary 类,实现 Map 接口。其中 Dictionary 类是任何可将键映射到相应值的类(如 Hashtable)的抽象父类。每个键和每个值都是一个对象。在任何一个 Dictionary 对象中,每个键至多与一个值相关联。Map 是 "key-value 键值对" 接口。HashTable 采用 "拉链法" 实现哈希表,它定义了几个重要的参数:table、count、threshold、loadFactor、modCount。table:为一个 Entry[] 数组类型,Entry 代表了“拉链”的节点,每一个 Entry 代表了一个键值对,哈希表的 "key-value 键值对" 都是存储在 Entry 数组中的。count:HashTable 的大小,注意这个大小并不是 HashTable 的容器大小,而是他所包含 Entry 键值对的数量。threshold:Hashtable 的阈值,用于判断是否需要调整 Hashtable 的容量。threshold 的值 ="容量 * 加载因子"。loadFactor:加载因子。modCount:用来实现“fail-fast”机制的(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出 ConcurrentModificationException 异常,而不是等到迭代完成之后才告诉你(你已经出错了)。

构造方法

  在 HashTabel 中存在 5 个构造函数。通过这 5 个构造函数我们构建出一个我想要的 HashTable。public Hashtable() {this(11, 0.75f);
    }
      默认构造函数,容量为 11,加载因子为 0.75。public Hashtable(int initialCapacity) {this(initialCapacity, 0.75f);
    }
      用指定初始容量和默认的加载因子 (0.75) 构造一个新的空哈希表。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,获得大小为 initialCapacity 的 table 数组
        table = new Entry[initialCapacity];
        // 计算阀值
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        // 初始化 HashSeed 值
        initHashSeedAsNeeded(initialCapacity);
    }

用指定初始容量和指定加载因子构造一个新的空哈希表。其中 initHashSeedAsNeeded 方法用于初始化 hashSeed 参数,其中 hashSeed 用于计算 key 的 hash 值,它与 key 的 hashCode 进行按位异或运算。这个 hashSeed 是一个与实例相关的随机值,主要用于解决 hash 冲突。

private int hash(Object k) {return hashSeed ^ k.hashCode();
    }

构造一个与给定的 Map 具有相同映射关系的新哈希表。

public Hashtable(Map<? extends K, ? extends V> t) {
        // 设置 table 容器大小,其值 ==t.size * 2 + 1
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

主要方法

HashTable 的 API 对外提供了许多方法,这些方法能够很好帮助我们操作 HashTable,但是这里我只介绍两个最根本的方法:put、get。

  首先我们先看 put 方法:将指定 key 映射到此哈希表中的指定 value。注意这里键 key 和值 value 都不可为空。public synchronized V put(K key, V value) {
        // 确保 value 不为 null
        if (value == null) {throw new NullPointerException();
        }

        /*
         * 确保 key 在 table[] 是不重复的
         * 处理过程:* 1、计算 key 的 hash 值,确认在 table[] 中的索引位置
         * 2、迭代 index 索引位置,如果该位置处的链表中存在一个一样的 key,则替换其 value,返回旧值
         */
        Entry tab[] = table;
        int hash = hash(key);    // 计算 key 的 hash 值
        int index = (hash & 0x7FFFFFFF) % tab.length;     // 确认该 key 的索引位置
        // 迭代,寻找该 key,替换
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {  // 如果容器中的元素数量已经达到阀值,则进行扩容操作
            rehash();
            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // 在索引位置处插入一个新的节点
        Entry<K,V> e = tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        // 容器中元素 +1
        count++;
        return null;
    }

put 方法的整个处理流程是:计算 key 的 hash 值,根据 hash 值获得 key 在 table 数组中的索引位置,然后迭代该 key 处的 Entry 链表(我们暂且理解为链表),若该链表中存在一个这个的 key 对象,那么就直接替换其 value 值即可,否则在将改 key-value 节点插入该 index 索引位置处

在 HashTabled 的 put 方法中有两个地方需要注意:

1、HashTable 的扩容操作,在 put 方法中,如果需要向 table[] 中添加 Entry 元素,会首先进行容量校验,如果容量已经达到了阀值,HashTable 就会进行扩容处理 rehash(),如下:

protected void rehash() {
        int oldCapacity = table.length;
        // 元素
        Entry<K,V>[] oldMap = table;

        // 新容量 = 旧容量 * 2 + 1
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {if (oldCapacity == MAX_ARRAY_SIZE)
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }

        // 新建一个 size = newCapacity 的 HashTable
        Entry<K,V>[] newMap = new Entry[];

        modCount++;
        // 重新计算阀值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        // 重新计算 hashSeed
        boolean rehash = initHashSeedAsNeeded(newCapacity);

        table = newMap;
        // 将原来的元素拷贝到新的 HashTable 中
        for (int i = oldCapacity ; i-- > 0 ;) {for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;

                if (rehash) {e.hash = hash(e.key);
                }
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = newMap[index];
                newMap[index] = e;
            }
        }
    }

在这个 rehash() 方法中我们可以看到容量扩大两倍 +1,同时需要将原来 HashTable 中的元素一一复制到新的 HashTable 中,这个过程是比较消耗时间的,同时还需要重新计算 hashSeed 的,毕竟容量已经变了。

这里对阀值啰嗦一下:比如初始值 11、加载因子默认 0.75,那么这个时候阀值 threshold=8,当容器中的元素达到 8 时,HashTable 进行一次扩容操作,容量 = 8 2 + 1 =17,而阀值 threshold=170.75 = 13,当容器元素再一次达到阀值时,HashTable 还会进行扩容操作,依次类推。

下面是计算 key 的 hash 值,这里 hashSeed 发挥了作用。

private int hash(Object k) {return hashSeed ^ k.hashCode();
    }

相对于 put 方法,get 方法就会比较简单,处理过程就是计算 key 的 hash 值,判断在 table 数组中的索引位置,然后迭代链表,匹配直到找到相对应 key 的 value, 若没有找到返回 null。


public synchronized V get(Object key) {Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {if ((e.hash == hash) && e.key.equals(key)) {return e.value;}
        }
        return null;
    }

HashTable 与 HashMap 的异同点

HashTable 和 HashMap 存在很多的相同点,但是他们还是有几个比较重要的不同点。

第一:我们从他们的定义就可以看出他们的不同,HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是什么?它是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的骨干实现,它以最大限度地减少实现此接口所需的工作。

第二:HashMap 可以允许存在一个为 null 的 key 和任意个为 null 的 value,但是 HashTable 中的 key 和 value 都不允许为 null。如下:

当 HashMap 遇到为 null 的 key 时,它会调用 putForNullKey 方法来进行处理。对于 value 没有进行任何处理,只要是对象都可以。

if (key == null)
            return putForNullKey(value);
      而当 HashTable 遇到 null 时,他会直接抛出 NullPointerException 异常信息。
if (value == null) {throw new NullPointerException();
}

第三:Hashtable 的方法是同步的,而 HashMap 的方法不是。所以有人一般都建议如果是涉及到多线程同步时采用 HashTable,没有涉及就采用 HashMap,但是在 Collections 类中存在一个静态方法:synchronizedMap(),该方法创建了一个线程安全的 Map 对象,并把它作为一个封装的对象来返回,所以通过 Collections 类的 synchronizedMap 方法是可以我们你同步访问潜在的 HashMap。这样君该如何选择呢???

面试题:HashMap 和 HashTable 的区别

HashMap 线程不安全,HashTable 是线程安全的。HashMap 内部实现没有任何线程同步相关的代码,所以相对而言性能要好一点。如果在多线程中使用 HashMap 需要自己管理线程同步。HashTable 大部分对外接口都使用 synchronized 包裹,所以是线程安全的,但是性能会相对差一些。

二者的基类不一样。HashMap 派生于 AbstractMap,HashTable 派生于 Dictionary。它们都实现 Map, Cloneable, Serializable 这些接口。AbstractMap 中提供的基础方法更多,并且实现了多个通用的方法,而在 Dictionary 中只有少量的接口,并且都是 abstract 类型。

key 和 value 的取值范围不同。HashMap 的 key 和 value 都可以为 null,但是 HashTablekey 和 value 都不能为 null。对于 HashMap 如果 get 返回 null,并不能表明 HashMap 不存在这个 key,如果需要判断 HashMap 中是否包含某个 key,就需要使用 containsKey 这个方法来判断。

算法不一样。HashMap 的 initialCapacity 为 16,而 HashTable 的 initialCapacity 为 11。HashMap 中初始容量必须是 2 的幂, 如果初始化传入的 initialCapacity 不是 2 的幂,将会自动调整为大于出入的 initialCapacity 最小的 2 的幂。HashMap 使用自己的计算 hash 的方法 (会依赖 key 的 hashCode 方法),HashTable 则使用 key 的 hashCode 方法得到。

参考文章

http://cmsblogs.com/?p=176

http://mini.eastday.com/mobil…

https://blog.csdn.net/lihua54…

https://www.cnblogs.com/aeoli…

微信公众号

Java 技术江湖

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java 技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点 Docker、ELK,同时也分享技术干货和学习经验,致力于 Java 全栈开发!

Java 工程师必备学习资源: 一些 Java 工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。

个人公众号:黄小斜

黄小斜是跨考软件工程的 985 硕士,自学 Java 两年,拿到了 BAT 等近十家大厂 offer,从技术小白成长为阿里工程师。

作者专注于 JAVA 后端技术栈,热衷于分享程序员干货、学习经验、求职心得和程序人生,目前黄小斜的 CSDN 博客有百万 + 访问量,知乎粉丝 2W+,全网已有 10W+ 读者。

黄小斜是一个斜杠青年,坚持学习和写作,相信终身学习的力量,希望和更多的程序员交朋友,一起进步和成长!关注公众号【黄小斜】后回复【原创电子书】即可领取我原创的电子书《菜鸟程序员修炼手册:从技术小白到阿里巴巴 Java 工程师》

程序员 3T 技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取。

本文由博客一文多发平台 OpenWrite 发布!

正文完
 0