Java HashMap 源码解析

继上一篇文章Java集合框架综述后,今天正式开始分析具体集合类的代码,首先以既熟悉又陌生的HashMap开始。html

本文源码分析基于Oracle JDK 1.7.0_71,请知悉。java

$ java -version
java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)

签名(signature)

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

能够看到HashMap继承了算法

  • 标记接口Cloneable,用于代表HashMap对象会重写java.lang.Object#clone()方法,HashMap实现的是浅拷贝(shallow copy)。
  • 标记接口Serializable,用于代表HashMap对象能够被序列化

比较有意思的是,HashMap同时继承了抽象类AbstractMap与接口Map,由于抽象类AbstractMap的签名为api

public abstract class AbstractMap<K,V> implements Map<K,V>

Stack Overfloooow上解释到:数组

在语法层面继承接口Map是多余的,这么作仅仅是为了让阅读代码的人明确知道HashMap是属于Map体系的,起到了文档的做用安全

AbstractMap至关于个辅助类,Map的一些操做这里面已经提供了默认实现,后面具体的子类若是没有特殊行为,可直接使用AbstractMap提供的实现。数据结构

Cloneable接口

It's evil, don't use it.

Cloneable这个接口设计的很是很差,最致命的一点是它里面居然没有clone方法,也就是说咱们本身写的类彻底能够实现这个接口的同时不重写clone方法。多线程

关于Cloneable的不足,你们能够去看看《Effective Java》一书的做者给出的理由,在所给连接的文章里,Josh Bloch也会讲如何实现深拷贝比较好,我这里就不在赘述了。oracle

Map接口

在eclipse中的outline面板能够看到Map接口里面包含如下成员方法与内部类:app


Map_field_method

Map_field_method

能够看到,这里的成员方法不外乎是“增删改查”,这也反映了咱们编写程序时,必定是以“数据”为导向的。

上篇文章讲了Map虽然并非Collection,可是它提供了三种“集合视角”(collection views),与下面三个方法一一对应:

  • Set<K> keySet(),提供key的集合视角
  • Collection<V> values(),提供value的集合视角
  • Set<Map.Entry<K, V>> entrySet(),提供key-value序对的集合视角,这里用内部类Map.Entry表示序对

AbstractMap抽象类

AbstractMapMap中的方法提供了一个基本实现,减小了实现Map接口的工做量。
举例来讲:

若是要实现个不可变(unmodifiable)的map,那么只需继承AbstractMap,而后实现其entrySet方法,这个方法返回的set不支持add与remove,同时这个set的迭代器(iterator)不支持remove操做便可。

相反,若是要实现个可变(modifiable)的map,首先继承AbstractMap,而后重写(override)AbstractMap的put方法,同时实现entrySet所返回set的迭代器的remove方法便可。

设计理念(design concept)

哈希表(hash table)

HashMap是一种基于哈希表(hash table)实现的map,哈希表(也叫关联数组)一种通用的数据结构,大多数的现代语言都原生支持,其概念也比较简单:key通过hash函数做用后获得一个槽(buckets或slots)的索引(index),槽中保存着咱们想要获取的值,以下图所示


hash table demo

hash table demo

很容易想到,一些不一样的key通过同一hash函数后可能产生相同的索引,也就是产生了冲突,这是在所不免的。
因此利用哈希表这种数据结构实现具体类时,须要:

  • 设计个好的hash函数,使冲突尽量的减小
  • 其次是须要解决发生冲突后如何处理。

后面会重点介绍HashMap是如何解决这两个问题的。

HashMap的一些特色

  • 线程非安全,而且容许key与value都为null值,HashTable与之相反,为线程安全,key与value都不容许null值。
  • 不保证其内部元素的顺序,并且随着时间的推移,同一元素的位置也可能改变(resize的状况)
  • put、get操做的时间复杂度为O(1)。
  • 遍历其集合视角的时间复杂度与其容量(capacity,槽的个数)和现有元素的大小(entry的个数)成正比,因此若是遍历的性能要求很高,不要把capactiy设置的太高或把平衡因子(load factor,当entry数大于capacity*loadFactor时,会进行resize,reside会致使key进行rehash)设置的太低。
  • 因为HashMap是线程非安全的,这也就是意味着若是多个线程同时对一hashmap的集合试图作迭代时有结构的上改变(添加、删除entry,只改变entry的value的值不算结构改变),那么会报ConcurrentModificationException,专业术语叫fail-fast,尽早报错对于多线程程序来讲是颇有必要的。
  • Map m = Collections.synchronizedMap(new HashMap(...)); 经过这种方式能够获得一个线程安全的map。

源码剖析

首先从构造函数开始讲,HashMap遵循集合框架的约束,提供了一个参数为空的构造函数与有一个参数且参数类型为Map的构造函数。除此以外,还提供了两个构造函数,用于设置HashMap的容量(capacity)与平衡因子(loadFactor)。

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;
    threshold = initialCapacity;
    init();
}
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

从代码上能够看到,容量与平衡因子都有个默认值,而且容量有个最大值

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

能够看到,默认的平衡因子为0.75,这是权衡了时间复杂度与空间复杂度以后的最好取值(JDK说是最好的😂),太高的因子会下降存储空间可是查找(lookup,包括HashMap中的put与get方法)的时间就会增长。

这里比较奇怪的是问题:容量必须为2的指数倍(默认为16),这是为何呢?解答这个问题,须要了解HashMap中哈希函数的设计原理。

哈希函数的设计原理

/**
  * 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 = hashSeed;
     if (0 != h && k instanceof String) {
         return sun.misc.Hashing.stringHash32((String) k);
     }
     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);
 }
 /**
  * Returns index for hash code h.
  */
 static int indexFor(int h, int length) {
     // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
     return h & (length-1);
 }

看到这么多位操做,是否是以为晕头转向了呢,仍是搞清楚原理就好了,毕竟位操做速度是很快的,不能由于很差理解就不用了😊。
网上说这个问题的也比较多,我这里根据本身的理解,尽可能作到通俗易懂。

在哈希表容量(也就是buckets或slots大小)为length的状况下,为了使每一个key都能在冲突最小的状况下映射到[0,length)(注意是左闭右开区间)的索引(index)内,通常有两种作法:

  1. 让length为素数,而后用hashCode(key) mod length的方法获得索引
  2. 让length为2的指数倍,而后用hashCode(key) & (length-1)的方法获得索引

HashTable用的是方法1,HashMap用的是方法2。

由于本篇主题讲的是HashMap,因此关于方法1为何要用素数,我这里不想过多介绍,你们能够看这里

重点说说方法2的状况,方法2其实也比较好理解:

由于length为2的指数倍,因此length-1所对应的二进制位都为1,而后在与hashCode(key)作与运算,便可获得[0,length)内的索引

可是这里有个问题,若是hashCode(key)的大于length的值,并且hashCode(key)的二进制位的低位变化不大,那么冲突就会不少,举个例子:

Java中对象的哈希值都32位整数,而HashMap默认大小为16,那么有两个对象那么的哈希值分别为:0xABAB00000xBABA0000,它们的后几位都是同样,那么与16异或后获得结果应该也是同样的,也就是产生了冲突。

形成冲突的缘由关键在于16限制了只能用低位来计算,高位直接舍弃了,因此咱们须要额外的哈希函数而不仅是简单的对象的hashCode方法了。
具体来讲,就是HashMap中hash函数干的事了

首先有个随机的hashSeed,来下降冲突发生的概率

而后若是是字符串,用了sun.misc.Hashing.stringHash32((String) k);来获取索引值

最后,经过一系列无符号右移操做,来把高位与低位进行异或操做,来下降冲突发生的概率

右移的偏移量20,12,7,4是怎么来的呢?由于Java中对象的哈希值都是32位的,因此这几个数应该就是把高位与低位作异或运算,至于这几个数是如何选取的,就不清楚了,网上搜了半天也没统一且让人信服的说法,你们能够参考下面几个连接:

HashMap.Entry

HashMap中存放的是HashMap.Entry对象,它继承自Map.Entry,其比较重要的是构造函数

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    // setter, getter, equals, toString 方法省略
    public final int hashCode() {
        //用key的hash值与上value的hash值做为Entry的hash值
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    /**
     * This method is invoked whenever the value in an entry is
     * overwritten by an invocation of put(k,v) for a key k that's already
     * in the HashMap.
     */
    void recordAccess(HashMap<K,V> m) {
    }
    /**
     * This method is invoked whenever the entry is
     * removed from the table.
     */
    void recordRemoval(HashMap<K,V> m) {
    }
}

能够看到,Entry实现了单向链表的功能,用next成员变量来级连起来。

介绍完Entry对象,下面要说一个比较重要的成员变量

/**
 * The table, resized as necessary. Length MUST Always be a power of two.
 */
//HashMap内部维护了一个为数组类型的Entry变量table,用来保存添加进来的Entry对象
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

你也许会疑问,Entry不是单向链表嘛,怎么这里又须要个数组类型的table呢?
我翻了下以前的算法书,其实这是解决冲突的一个方式:链地址法(开散列法),效果以下:


链地址法处理冲突获得的散列表

链地址法处理冲突获得的散列表


就是相同索引值的Entry,会以单向链表的形式存在

链地址法的可视化

网上找到个很好的网站,用来可视化各类常见的算法,很棒。瞬间以为国外大学比国内的强不知多少倍。
下面的连接能够模仿哈希表采用链地址法解决冲突,你们能够本身去玩玩😊

get操做

get操做相比put操做简单,因此先介绍get操做

public V get(Object key) {
    //单独处理key为null的状况
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
    if (size == 0) {
        return null;
    }
    //key为null的Entry用于放在table[0]中,可是在table[0]冲突链中的Entry的key不必定为null
    //因此须要遍历冲突链,查找key是否存在
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}
final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    //首先定位到索引在table中的位置
    //而后遍历冲突链,查找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操做(含update操做)

由于put操做有可能须要对HashMap进行resize,因此实现略复杂些

private void inflateTable(int toSize) {
    //辅助函数,用于填充HashMap到指定的capacity
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);
    //threshold为resize的阈值,超事后HashMap会进行resize,内容的entry会进行rehash
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}
/**
 * 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.
 */
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    //这里的循环是关键
    //当新增的key所对应的索引i,对应table[i]中已经有值时,进入循环体
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //判断是否存在本次插入的key,若是存在用本次的value替换以前oldValue,至关于update操做
        //并返回以前的oldValue
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    //若是本次新增key以前不存在于HashMap中,modCount加1,说明结构改变了
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
    //若是增长一个元素会后,HashMap的大小超过阈值,须要resize
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //增长的幅度是以前的1倍
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    //首先获得该索引处的冲突链Entries,第一次插入bucketIndex位置时冲突链为null,也就是e为null
    Entry<K,V> e = table[bucketIndex];
    //而后把新的Entry添加到冲突链的开头,也就是说,后插入的反而在前面(第一次还真没看明白)
    //table[bucketIndex]为新加入的Entry,是bucketIndex位置的冲突链的第一个元素
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
//下面看看HashMap是如何进行resize,庐山真面目就要揭晓了😊
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //若是已经达到最大容量,那么就直接返回
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    //initHashSeedAsNeeded(newCapacity)的返回值决定了是否须要从新计算Entry的hash值
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
 * Transfers all entries from current table to newTable.
 */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //遍历当前的table,将里面的元素添加到新的newTable中
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            //最后这两句用了与put放过相同的技巧
            //将后插入的反而在前面
            newTable[i] = e;
            e = next;
        }
    }
}
/**
 * Initialize the hashing mask value. We defer initialization until we
 * really need it.
 */
final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    //这里说明了,在hashSeed不为0或知足useAltHash时,会重算Entry的hash值
    //至于useAltHashing的做用能够参考下面的连接
    // http://stackoverflow.com/questions/29918624/what-is-the-use-of-holder-class-in-hashmap
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}

remove操做

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    //能够看到删除的key若是存在,就返回其所对应的value
    return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
    if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    int i = indexFor(hash, table.length);
    //这里用了两个Entry对象,至关于两个指针,为的是防治冲突链发生断裂的状况
    //这里的思路就是通常的单向链表的删除思路
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;
    //当table[i]中存在冲突链时,开始遍历里面的元素
    while (e != null) {
        Entry<K,V> next = e.next;
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            modCount++;
            size--;
            if (prev == e) //当冲突链只有一个Entry时
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }
    return e;
}

到如今为止,HashMap的增删改查都介绍完了。
通常而言,认为HashMap的这四种操做时间复杂度为O(1),由于它hash函数性质较好,保证了冲突发生的概率较小。

fast-fail的HashIterator

集合类用Iterator类来遍历其包含的元素,接口Enumeration已经不推荐使用。相比Enumeration,Iterator有下面两个优点:

  1. Iterator容许调用者在遍历集合类时删除集合类中包含的元素(相比Enumeration增长了remove方法)
  2. 比Enumeration的命名更简短

HashMap中提供的三种集合视角,底层都是用HashIterator实现的。

private abstract class HashIterator<E> implements Iterator<E> {
    Entry<K,V> next;        // next entry to return
    //在初始化Iterator实例时,纪录下当前的修改次数
    int expectedModCount;   // For fast-fail
    int index;              // current slot
    Entry<K,V> current;     // current entry
    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
            Entry[] t = table;
            //遍历HashMap的table,依次查找元素
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    }
    public final boolean hasNext() {
        return next != null;
    }
    final Entry<K,V> nextEntry() {
        //在访问下一个Entry时,判断是否有其余线程有对集合的修改
        //说明HashMap是线程非安全的
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();
        if ((next = e.next) == null) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
        current = e;
        return e;
    }
    public void remove() {
        if (current == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Object k = current.key;
        current = null;
        HashMap.this.removeEntryForKey(k);
        expectedModCount = modCount;
    }
}
private final class ValueIterator extends HashIterator<V> {
    public V next() {
        return nextEntry().value;
    }
}
private final class KeyIterator extends HashIterator<K> {
    public K next() {
        return nextEntry().getKey();
    }
}
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() {
        return nextEntry();
    }
}

序列化

介绍到这里,基本上算是把HashMap中一些核心的点讲完了,但还有个比较严重的问题:保存Entry的table数组为transient的,也就是说在进行序列化时,并不会包含该成员,这是为何呢?

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

为了解答这个问题,咱们须要明确下面事实:

  • Object.hashCode方法对于一个类的两个实例返回的是不一样的哈希值

咱们能够试想下面的场景:

咱们在机器A上算出对象A的哈希值与索引,而后把它插入到HashMap中,而后把该HashMap序列化后,在机器B上从新算对象的哈希值与索引,这与机器A上算出的是不同的,因此咱们在机器B上get对象A时,会获得错误的结果。

因此说,当序列化一个HashMap对象时,保存Entry的table是不须要序列化进来的,由于它在另外一台机器上是错误的。

由于这个缘由,HashMap重写了writeObjectreadObject 方法

private void writeObject(java.io.ObjectOutputStream s)
    throws IOException
{
    // Write out the threshold, loadfactor, and any hidden stuff
    s.defaultWriteObject();
    // Write out number of buckets
    if (table==EMPTY_TABLE) {
        s.writeInt(roundUpToPowerOf2(threshold));
    } else {
       s.writeInt(table.length);
    }
    // Write out size (number of Mappings)
    s.writeInt(size);
    // Write out keys and values (alternating)
    if (size > 0) {
        for(Map.Entry<K,V> e : entrySet0()) {
            s.writeObject(e.getKey());
            s.writeObject(e.getValue());
        }
    }
}
private static final long serialVersionUID = 362498820763181265L;
private void readObject(java.io.ObjectInputStream s)
     throws IOException, ClassNotFoundException
{
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new InvalidObjectException("Illegal load factor: " +
                                           loadFactor);
    }
    // set other fields that need values
    table = (Entry<K,V>[]) EMPTY_TABLE;
    // Read in number of buckets
    s.readInt(); // ignored.
    // Read number of mappings
    int mappings = s.readInt();
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                           mappings);
    // capacity chosen by number of mappings and desired load (if >= 0.25)
    int capacity = (int) Math.min(
                mappings * Math.min(1 / loadFactor, 4.0f),
                // we have limits...
                HashMap.MAXIMUM_CAPACITY);
    // allocate the bucket array;
    if (mappings > 0) {
        inflateTable(capacity);
    } else {
        threshold = capacity;
    }
    init();  // Give subclass a chance to do its thing.
    // Read the keys and values, and put the mappings in the HashMap
    for (int i = 0; i < mappings; i++) {
        K key = (K) s.readObject();
        V value = (V) s.readObject();
        putForCreate(key, value);
    }
}
private void putForCreate(K key, V value) {
    int hash = null == key ? 0 : hash(key);
    int i = indexFor(hash, table.length);
    /**
     * Look for preexisting entry for key.  This will never happen for
     * clone or deserialize.  It will only happen for construction if the
     * input Map is a sorted map whose ordering is inconsistent w/ equals.
     */
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }
    createEntry(hash, key, value, i);
}

简单来讲,在序列化时,针对Entry的key与value分别单独序列化,当反序列化时,再单独处理便可。

总结

在总结完HashMap后,发现这里面一些核心的东西,像哈希表的冲突解决,都是算法课上学到,不过因为“年代久远”,已经忘得差很少了,我以为忘

  • 一方面是因为时间久不用
  • 另外一方面是因为自己没理解好

平时多去思考,这样在遇到一些性能问题时也好排查。

还有一点就是咱们在分析某些具体类或方法时,不要花太多时间一些细枝末节的边界条件上,这样很得不偿失,倒不是说这么边界条件不重要,程序的bug每每就是边界条件没考虑周全致使的。
只是说咱们能够在理解了这个类或方法的整体思路后,再来分析这些边界条件。
若是一开始就分析,那真是丈二和尚——摸不着头脑了,随着对它工做原理的加深,才有可能理解这些边界条件的场景。

今天到此为止,下次打算分析TreeMap。Stay Tuned!🍺。我已经写完了,两篇文章对比看,效果更好。

相关文章
相关标签/搜索