Java的HashMap源码解析(中高级Java工程师面试必备)

哈希表介绍

首先简单介绍一下哈希表的一些基本的概念。

哈希表概念

  • 哈希表(Hash table),又称为散列表,通过关键字(key)根据哈希函数(Hash function,散列函数)确定地址而进行直接访问。
  • 不同关键字可能散列到同一个地址上,称为冲突(collision)。
  • 装载因子(Load factor)为哈希表中的元素个数对该表大小的比。

哈希表的实现

不使用链表实现(例如数组)

关键字输入到哈希函数中确定其在哈希表中的位置,放入表中。如果遇到冲突,解决方法常用的有线性探测法平方探测法双散列等。因为Java HashMap使用分离链接法实现,所以这里不详细叙述不使用链表实现

分离链接法(重点)

将散列到同一个值到所有元素保留在一个表中。新插入的元素在链表前端,不仅仅节省时间,而且最新插入的元素最有可能不久又被访问。
如图所示:

Java的HashMap源码解析(中高级Java工程师面试必备)_第1张图片
分离链接法--也是Java HashMap的数据结构

再哈希(rehashing)

当哈希表装载因子到达一定的值,将会尝试扩大哈希表以减少元素冲突,这个过程就是再哈希(又称为再散列)。过程分为两步:

  1. 扩大表的大小
  2. 使用新的哈希函数把旧哈希表中的元素重新插入到新哈希表中

HashMap源码解析

Java8之后HashMap的实现分为两种:

  1. 分离链接法,每个桶(bin)中使用链表结构保存相同hash值的元素;
  2. 红黑树,每个桶中的所有元素(即相同hash值的元素)使用红黑树结构保存。

分离链接法实现HashMap

在Java8之前HashMap的唯一实现方式。面试可能会问到:

问:在Java8中HashMap有哪些改变?
答:当元素个数增多时,改变为红黑树存储元素。在哈希表容量小于64时使用分离链接法,而超过64并且桶中元素大于8,则对这个桶中的元素使用红黑树进行树化。

源码解析
构造函数
  • 函数tableSizeFor(int cap)生成大于等于整型cap的最小的2的幂次结果。函数tableSizeFor详情
  • 哈希表使用惰性初始化,构造函数只是设定哈希表参数,而表的空间只有在放入元素时才分配。
static final int MAXIMUM_CAPACITY = 1 << 30;//表的最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认装载因子
/*生成一个指定元素容量和装载因子的空哈希表*/
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);
    /*设置自定的装载因子,和产生一个大于等于n的最小的2的幂次长度的哈希表*/
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);//例如,输入15,则表长度为16
  }
/*生成一个默认装载因子,指定容量的空哈希表*/
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/*生成一个容量为默认16的空哈希表*/
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/*使用一个map m中的元素初始化新map,装载因子为默认值*/
public HashMap(Map m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
变量的意义

上文提到哈希表使用惰性初始化空间只有在放入元素时才分配,构造函数只是设定哈希表参数,那么哈希表的参数都包括那些呢?

  • table数组每个元素位置相当于一个桶;
  • 哈希表的大小必须是2的幂次。Java HashMap表的大小为什么必须是2的幂次
transient Node[] table;//表使用node数组表示,数组每个元素相当于一个桶,相同哈希值的元素放入同一个桶中
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//默认表的容量,具体就是table数组的默认大小
static final int TREEIFY_THRESHOLD = 8;//树化时桶中最小的元素容量
static final int UNTREEIFY_THRESHOLD = 6;//如果桶中元素容量低于这个值,退化到链表
static final int MIN_TREEIFY_CAPACITY = 64;//表的容量只有达到64,各个桶才有机会进行树化,否则扩容
transient int size;//哈希表中元素个数
int threshold;//哈希表中元素个数超过threshold则扩容
final float loadFactor;//装载因子
transient int modCount;//修改次数
HashMap的Node结点

注意:

  1. 结点的key是final修饰的,具有不可变性,为减少对key计算哈希值所消耗的时间,使用变量hash记录key的哈希值;
  2. 结点重载自己的hashCode和equals函数
static class Node implements Map.Entry {
    /*结点包含四个变量,分别是key、key的哈希值、value以及链表中指向后继的指针*/
    final int hash;
    final K key;
    V value;
    Node next;
    Node(int hash, K key, V value, Node 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; }
    /*考虑到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;
    }
    /*key和value均相等则是相等的结点*/
    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;
    }
}
增删改查基本操作
  1. 增加操作
/*使用已存在的map赋值*/
final void putMapEntries(Map m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // 惰性初始化
            float ft = ((float)s / loadFactor) + 1.0F;//根据数据量和负载因子计算哈希表的大小
            int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);//拓展为大于等于的t最小的2的幂次方,暂时设定threshold为这个值
        }
        else if (s > threshold)//如果元素值超过threshold扩容
            resize();
        for (Map.Entry e : m.entrySet()) {//循环遍历
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);//执行每个结点的插入操作
        }
    }
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)//如果没有初始化table,使用resize函数初始化
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)//如果i位置没有数据,直接放置在i位置
        tab[i] = newNode(hash, key, value, null);
    else {
        Node e; K k;
        //如果i位置与插入的相等,这个结点可能是链表的第一个结点,也可能是红黑树的根结点
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)//如果是红黑树,执行树的插入
            e = ((TreeNode)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);//插入链表的结尾
                    // 如果链表的结点数超出TREEIFY_THRESH进行树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                //如果结点存在
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { //原来存在key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)//存在的情况下也插入,更新value
                e.value = value;
            afterNodeAccess(e);;//函数在HashMap中无执行逻辑,是为兼容LinkedHashMap的方法
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)//元素个数超过阈值扩容
        resize();
    afterNodeInsertion(evict);//函数在HashMap中无执行逻辑,是为兼容LinkedHashMap的方法
    return null;
}
public void putAll(Map m) {//对外接口,把m中元素插入当前表中
    putMapEntries(m, true);
}
@Override
public V putIfAbsent(K key, V value) {//对外接口,如果不存在插入
    return putVal(hash(key), key, value, true, true);
}
public V put(K key, V value) {//对外接口,如果不存在插入新的结点,如果存在更新value值
    return putVal(hash(key), key, value, false, true);
}
  1. 扩容操作
  • 切记MAXIMUM_CAPACITY是哈希表数组table的最大长度,数组table的长度一定是2的幂次方。当等于MAXIMUM_CAPACITY时,threshold为整型最大值;否则均扩张为原来的2倍。
  • 在HashMap实现中,仅仅保留装载因子loadFactor和最大元素个数的阈值threshold。所以如果数组table尚未分配空间,此时的capacity记录在threshold中。
  • 因为新表要么不扩容,要么扩容为原来的2倍大小。旧表的元素插入新表时,同一桶中的结点可能位置是放入与原来相同的索引位置j或者位置j+oldCap,其实只需要区分哈希值中oldCap的二进制表示1所在的那位是0还是1,如果为0放入低位,因为哈希值与newCap-1做与操作后,这一位仍旧为0;1放入高位,哈希值与newCap-1做与操作这一位变为1。
final Node[] resize() {
    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //旧表容量大于0
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {//旧表容量超过最大容量,不可以扩容
            threshold = Integer.MAX_VALUE;//新表的元素个数阈值为最大整型值
            return oldTab;
        }
        /*新表扩容为旧表容量的2倍*/
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    /*旧表为null,那么初始化的容量是存储在变量threshold。在HashMap实现中*/
    /*在HashMap实现中,仅仅保留装载因子loadFactor和最大元素个数的阈值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[] newTab = (Node[])new Node[newCap];
    table = newTab;//初始化新表
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)//红黑树执行分裂操作
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                else { // 保持原有顺序,把原来链表分裂成两个链表
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node 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) {//低位链表放置在原来的索引j位置
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {//高位链表放置在索引j+oldCap的位置
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
  1. 查询操作
final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    /*hash值定位所在位置*/
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))//先判断第一个结点
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)//如果是红黑树,执行树的查找
                return ((TreeNode)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 V get(Object key) {//查询key的结点,如果存在返回其value,否则返回null
    Node e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
@Override
//Java8新增特性
public V getOrDefault(Object key, V defaultValue) {//查询key的结点,如果存在返回其value,否则返回默认值
    Node e;
    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
public boolean containsKey(Object key) {//判断是否有key的结点
    return getNode(hash(key), key) != null;
}
/*查找value值是否存在*/
public boolean containsValue(Object value) {
    Node[] tab; V v;
    if ((tab = table) != null && size > 0) {
        //遍历表中所有结点
        //因此查询value操作相当费时,时间复杂度为O(n),n为表中元素个数
        for (int i = 0; i < tab.length; ++i) {
            for (Node e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value || (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}
  1. 修改操作
    Java8引入的新特性和新功能
@Override
/*为key-oldValue对的结点进行赋值,新值为newValue,key不变*/
public boolean replace(K key, V oldValue, V newValue) {
    Node e; V v;
    if ((e = getNode(hash(key), key)) != null && 
          ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
        e.value = newValue;
        afterNodeAccess(e);
        return true;
     }
    return false;
}
@Override
/*键为key的结点,赋值value*/
public V replace(K key, V value) {
    Node e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}
@Override
/*对表中的每个结点执行function功能*/
/*红黑树表示的结点同样具有next指针*/
/*红黑树结点不仅仅具有常规的left和right指针,同时具有prev指针、next指针以及parent指针*/
public void replaceAll(BiFunction function) {
    Node[] tab;
    if (function == null)
        throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
        int mc = modCount;
        for (int i = 0; i < tab.length; ++i) {
            for (Node e = tab[i]; e != null; e = e.next) {
                e.value = function.apply(e.key, e.value);
            }
        }
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}
  1. 删除操作
public V remove(Object key) {
    Node e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
@Override
/*Java8引入的新操作,当key-value均匹配是才删除结点*/
public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
}
final Node removeNode(int hash, Object key, Object value, 
                  boolean matchValue, boolean movable) {
    Node[] tab; Node p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
        Node node = null, e; K k; V v;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//第一个结点
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)//树结点,执行树搜索操作
                node = ((TreeNode)p).getTreeNode(hash, key);
            else {//遍历链表,e为查询的当前结点,p为e的前驱结点
                do {
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                 } while ((e = e.next) != null);
            }
        }
        /*node记录查询到的结点*/
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)//红黑树执行删除操作
                ((TreeNode)node).removeTreeNode(this, tab, movable);
            else if (node == p)//如果是第一个结点匹配成功
                tab[index] = node.next;//桶指向第一个结点的后继结点
            else
                p.next = node.next;//删除node结点
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
拷贝函数

执行一个浅拷贝。返回一个表的拷贝,但是表中结点的key和value本身没有被拷贝。

@SuppressWarnings("unchecked")
@Override
public Object clone() {
    HashMap result;
    try {
        result = (HashMap)super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e);
    }
    result.reinitialize();
    result.putMapEntries(this, false);
    return result;
}
Java8新增函数
@Override
/*如果不存在key对应的value,则执行mappingFunction产生value值放入表中*/
public V computeIfAbsent(K key, Function mappingFunction) {
    if (mappingFunction == null)
        throw new NullPointerException();
    int hash = hash(key);
    Node[] tab; Node first; int n, i;
    int binCount = 0;
    TreeNode t = null;
    Node old = null;
    if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /*判断key是否存在于表中*/
    if ((first = tab[i = (n - 1) & hash]) != null) {
        if (first instanceof TreeNode)
            old = (t = (TreeNode)first).getTreeNode(hash, key);
        else {
            Node e = first; K k;
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                    old = e;
                    break;
                }
                ++binCount;
            } while ((e = e.next) != null);
        }
        V oldValue;
        /*如果存在value,不做任何操作*/
        if (old != null && (oldValue = old.value) != null) {
            afterNodeAccess(old);
            return oldValue;
        }
    }
    /*产生新的value值*/
    V v = mappingFunction.apply(key);
    if (v == null) {
        return null;
    } else if (old != null) {
        /*key之前不存在对应的value*/
        old.value = v;
        afterNodeAccess(old);
        return v;
    } else if (t != null)
        /*红黑树结构*/
        t.putTreeVal(this, tab, hash, key, v);
    else {
        /*插入链表第一个结点位置*/
        tab[i] = newNode(hash, key, v, first);
        if (binCount >= TREEIFY_THRESHOLD - 1)
            treeifyBin(tab, hash);
    }
    ++modCount;
    ++size;
    afterNodeInsertion(true);
    return v;
}

/*存在key结点执行remappingFunction操作*/
public V computeIfPresent(K key, BiFunction remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node e; V oldValue;
    int hash = hash(key);
    //找到键为key的结点
    if ((e = getNode(hash, key)) != null && (oldValue = e.value) != null) {
        //key-oldValue对进行remappingFunction操作,产生新的value
        V v = remappingFunction.apply(key, oldValue);
        //新value不为null,覆盖原有的oldValue
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        //新value为null,删除key的结点
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}

@Override
/*computeIfPresent和computeIfAbsent功能的结合*/
public V compute(K key, BiFunction remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    int hash = hash(key);
    Node[] tab; Node first; int n, i;
    int binCount = 0;
    TreeNode t = null;
    Node old = null;
    if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /*查看key的结点*/
    if ((first = tab[i = (n - 1) & hash]) != null) {
        if (first instanceof TreeNode)
            old = (t = (TreeNode)first).getTreeNode(hash, key);
        else {
            Node e = first; K k;
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                    old = e;
                    break;
                }
                ++binCount;
            } while ((e = e.next) != null);
        }
    }
    /*找到产生新的value*/
    V oldValue = (old == null) ? null : old.value;
    V v = remappingFunction.apply(key, oldValue);
    /*存在*/
    if (old != null) {
        if (v != null) {
            old.value = v;
            afterNodeAccess(old);
        }
        else
            removeNode(hash, key, null, false, true);
    }
    /*不存在*/
    else if (v != null) {
        /*红黑树*/
        if (t != null)
            t.putTreeVal(this, tab, hash, key, v);
        else {
            tab[i] = newNode(hash, key, v, first);
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        ++modCount;
        ++size;
        afterNodeInsertion(true);
    }
    return v;
}

@Override
public V merge(K key, V value, BiFunction remappingFunction) {
    if (value == null)
        throw new NullPointerException();
    if (remappingFunction == null)
        throw new NullPointerException();
    int hash = hash(key);
    Node[] tab; Node first; int n, i;
    int binCount = 0;
    TreeNode t = null;
    Node old = null;
    if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /*查找key对应的结点*/
    if ((first = tab[i = (n - 1) & hash]) != null) {
        if (first instanceof TreeNode)
            old = (t = (TreeNode)first).getTreeNode(hash, key);
        else {
            Node e = first; K k;
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                    old = e;
                    break;
                }
                ++binCount;
            } while ((e = e.next) != null);
         }
    }
    /*存在的情况*/
    if (old != null) {
        V v;
        /*新旧value值执行remappingFunction操作返回结果,如果旧value为null,结果是新value*/
        if (old.value != null)
            v = remappingFunction.apply(old.value, value);
        else
            v = value;
        if (v != null) {
            old.value = v;
            afterNodeAccess(old);
        }
        else
            removeNode(hash, key, null, false, true);
        return v;
    }
    /*不存在的情况,插入到表中*/
    if (value != null) {
        if (t != null)
            t.putTreeVal(this, tab, hash, key, value);
        else {
            tab[i] = newNode(hash, key, value, first);
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        ++modCount;
        ++size;
        afterNodeInsertion(true);
    }
    return value;
}

@Override
/*表中每个结点的key和value执行action操作*/
public void forEach(BiConsumer action) {
    Node[] tab;
    if (action == null)
        throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
        int mc = modCount;
        for (int i = 0; i < tab.length; ++i) {
            for (Node e = tab[i]; e != null; e = e.next)
                action.accept(e.key, e.value);
        }
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

红黑树实现HashMap

Java8 HashMap的红黑树实现

HashMap的迭代器和EntrySet实现

Java8 HashMap的迭代器和转化Set的实现

相关算法例题

数组中找到两数之和为给定目标值
无重复字符的最长子串

你可能感兴趣的:(Java的HashMap源码解析(中高级Java工程师面试必备))