OpenJDK 源代码阅读之 HashMap

概要

  • 类继承关系
java.lang.Object
    java.util.AbstractMap<K,V>
        java.util.TreeMap<K,V>
  • 定义
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, Serializable
  • 要点

1) 与 Hashtable 区别在于:非同步,允许 null

2) 不保证次序,甚至不保证次序随时间不变

3) 基本操作 put, get 常量时间

4) 遍历操作 与 capacity+size 成正比

5) HashMap 性能与 capacity 和 load
factor
 相关,load factor 是当前元素个数与capacity 的比值,通常设定为 0.75,如果此值过大,空间利用率高,但是冲突的可能性增加,因而可能导致查找时间增加,如果过小,反之。当元素个数大于 capacity
* load_factor
 时,HashMap 会重新安排 Hash 表。因此高效地使用 HashMap 需要预估元素个数,设置最佳的 capacity 和 load
factor
 ,使得重新安排 Hash 表的次数下降。

实现

  • capacity
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);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    init();
}

注意,HashMap 并不会按照你指定的 initialCapacity 来确定 capacity 大小,而是会找到一个比它大的数,并且是 2的n次方

为什么要是 2 的n次方呢?

  • hash
/**
 * Retrieve object hash code and applies a supplemental hash function to the
 * result hash, which defends against poor quality hash functions.  This is
 * critical because HashMap uses power-of-two length hash tables, that
 * otherwise encounter collisions for hashCodes that do not differ
 * in lower bits. Note: Null keys always map to hash 0, thus index 0.
 */
final int hash(Object k) {
    int h = 0;
    if (useAltHashing) {
        if (k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h = hashSeed;
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

如果 k 是 String 类型,使用了特别的 hash 函数,否则首先得到 hashCode,然后又对 h 作了移位,异或操作,问题:

为什么这里要作移位,异或操作呢?

at 22:
h = abcdefgh
h1 = h >>> 20 = 00000abc
h2 = h >>> 12 = 000abcde
h3 = h1 ^ h2 = [0][0][0][a][b][a^c][b^d][c^e]
h4 = h ^ h3 = [a][b][c][a^d][b^e][a^c^f][b^d^g][c^e^h]
h5 = h4 >>> 4 = [0][a][b][c][a^d][b^e][a^c^f][b^d^g]
h6 = h4 >>> 7 = ([0][:3])[0][0][a][b][c][a^d][b^e][a^c^f]([a^c^f][0])
h7 = h4 ^ h6 = 太凶残了。。。
  • put
/**
 * Associates the specified value with the specified key in this map.
 * If the map previously contained a mapping for the key, the old
 * value is replaced.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with <tt>key</tt>, or
 *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
 *         (A <tt>null</tt> return can also indicate that the map
 *         previously associated <tt>null</tt> with <tt>key</tt>.)
 */
public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

从 put 其实可以看出各个 hash 表是如何实现的,首先取得 hash 值,然后由 indexFor 找到链表头的 index,然后开始遍历链表,如果链表里的一个元素 hash 值与当前 key 的 hash 值相同,或者元素 key 的引用与当前 key 相同,或者 equals 相同,就说明当前 key 已经在 hash 表里了,那么修改它的值,返回旧值。

如果不在表里,会调用 addEntry,将这一 (key,
value)
 对添加进去。

/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

/**
 * Like addEntry except that this version is used when creating entries
 * as part of Map construction or "pseudo-construction" (cloning,
 * deserialization).  This version needn‘t worry about resizing the table.
 *
 * Subclass overrides this to alter the behavior of HashMap(Map),
 * clone, and readObject.
 */
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

可以看出,新增加元素时,可能会调整 hash 表的大小,原因之前已经讨论过。直接的添加在 createEntry 中完成,但是这里并没有体现出如何处理冲突。

Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}

注意这里,将 n 赋值给了 next,这其实就是将新添加的项指向了当前链表头。这一操作在 Entry 的构造函数中完成。

put 操作的基本思路在到这里已经很清楚了,有了这个思路,不难想象 get 是如何动作的。

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}

final Entry<K,V> getEntry(Object key) {
    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

和 put 差不多,只是找到了就会返回相应的 value ,找不到就返回 null

OpenJDK 源代码阅读之 HashMap

时间: 2024-08-04 12:34:47

OpenJDK 源代码阅读之 HashMap的相关文章

OpenJDK 源代码阅读之 TreeMap

概要 类继承关系 java.lang.Object java.util.AbstractMap<K,V> java.util.HashMap<K,V> 定义 public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable 要点 1) 基于 NavigableMap 实现的红黑树 2) 按

OpenJDK 源代码阅读之 Collections

概要 类继承关系 java.lang.Object java.util.Collections 定义 public class Collections extends Object 实现 sort public static <T extends Comparable<? super T>> void sort(List<T> list) { Object[] a = list.toArray(); Arrays.sort(a); ListIterator<T&g

OpenJDK 源代码阅读之 String

概要 类继承关系 java.lang.Object java.lang.String 定义 public final class String extends Object implements Serializable, Comparable<String>, CharSequence 要点 一旦创建就不可改变 实现 storage /** The value is used for character storage. */ private final char value[]; 可以看出

开始OpenJDK源代码阅读

开始OpenJDK源代码阅读 在阅读了一周的 OpenJDK 源代码后,我才写这篇文章.因为除非你已经开始阅读,否则是不知道自己是不是应该读下去的.所以,不要贸然说自己要干嘛,先做一段时间,觉得感觉还好,再决定做下去. 这一周,主要是看 java.util 中和容器相关的几个文件,虽然还没看太多,但是已经有一些收获了.看到了以前学过的数据结构在Java的标准库中是如何被实现的.也明白了平时使用的一些类的原理是什么.另外,由于最近在看 <Java编程思想>,也能把书中讲的和标准库的源代码对应起来

OpenJDK 源代码阅读之 BitSet

概要 类继承关系 java.lang.Object java.util.BitSet 定义 public class BitSet extends Object implements Cloneable, Serializable 要点 BitSet 类用来支持位操作,给它一个 size ,就会返回一个对象,代表 size 个位.可以完成"与或非"操作. 实现 试想一下,long 最多也就 64 位,假如我们想对 1000 位进行一些运算,要如何实现呢?这个类就告诉我们怎么用一个数组,

Java 推荐读物与源代码阅读

1. Java语言基础     谈到Java语言基础学习的书籍,大家肯定会推荐Bruce Eckel的<Thinking in Java>.它是一本写的相当深刻的技术书籍,Java语言基础部分基本没有其它任何一本书可以超越它.该书的作者Bruce Eckel在网络上被称为天才的投机者,作者的<Thinking in C++>在1995年曾获SoftwareDevelopment Jolt Award最佳书籍大奖,<Thinking in Java>被评为1999年Jav

Notepad++源代码阅读——窗口元素组织与布局

1.1 前言 这两天在看notepad++ 1.0版本的源代码.看了许久终于把程序的窗口之间的关系搞清楚了现在把其组织的要点写于此,希望对大家有所帮助. 1.2 窗口元素之间的关系 Notepad++主要有以下窗口元素(见下图). 其中Notepad_plus 是程序的主要窗口,其他:工具栏.状态栏.主次编辑窗口.主次选项卡窗口以及对话框窗口均为主窗口的子窗口.     _mainDocTab 和 _subDocTab 为 类:DocTabView 其成员_pView 分别指向 _mainEdi

Linux-0.11源代码阅读一 加载操作系统

x86系列CPU可以在16位实模式和32位保护模式下运行,实模式的特点是地址总线只有20位,也就是只有1MB的寻址空间,为了兼容老的CPU,Intel x86系列CPU包括最新的CPU在上电时都运行在16位的实模式下,同时在硬件上强行将CS置成0xF000,IP置成0xFFF0,那么CS:IP就指向0xFFFF0这个地址,也就是上电瞬间代码从该处开始执行,而BIOS恰恰就存储在这个地方,可以想象一下,如果连BIOS都没有将会是一个什么结果. BIOS程序被存储在计算机主板上的一块ROM芯片里,首

linux0.11 源代码阅读记录

*/--> pre.src {background-color: Black; color: White;} pre.src {background-color: Black; color: White;} pre.src {background-color: Black; color: White;} pre.src {background-color: Black; color: White;} pre.src {background-color: Black; color: White;}