java集合之Map的常见实现类(1)---HashMap

本篇文章我们主要来看看Map的相关内容。
需要学习Collection详解的可以查看我的另一篇文章:java集合之Collection

Map

1.1 Map 简介

什么是Map?

Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键对象和一个值对象。其中,键对象不允许重复,而值对象可以重复,并且值对象还可以是 Map 类型的,就像数组中的元素还可以是数组一样。

特点:该集合存储键(key)值(value)对,一对一对往里存,而且要保证键(key)的唯一性。
什么时候使用Map集合:当数据之间存在映射关系时,优先考虑Map集合。

1.2 Map 框架示意图

map

1.3 Map常用方法

  • V put(K key, V value):将指定的值与此映射中的指定键关联,添加键值对。
  • void putAll(Map<? extends K,? extends V> m):从指定映射中将所有映射关系复制到此映射中,批量添加键值对。
  • void clear():从此映射中移除所有映射关系,清空所有键值对。
  • V remove(Object key):如果存在一个键的映射关系,则将其从此映射中移除,删除单个键值对。
  • boolean containsKey(Object key):如果此映射包含指定键的映射关系(是否包含该键),则返回 true。
  • boolean containsValue(Object value):如果此映射将一个或多个键映射到指定值(是否包含该值),则返回 true。
  • boolean isEmpty():如果此映射未包含键-值映射关系,该map集合为空,则返回 true。
  • V get(Object key):返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
  • int size():返回此映射中的键-值映射关系(键值对)数
  • Collection values():返回此映射中包含的值的 Collection 视图(集合)。

代码示例:

import java.util.*;
public class MapDemo {
 
	public static void main(String[] args) {
		Map<Integer, Integer> map1 = new HashMap<Integer, Integer>();
		Map<Integer, Integer> map2 = new HashMap<Integer, Integer>();
		map1.put(1, 100);
		map1.put(2, 200);
		map1.put(3, 300);
		map2.put(11, 1100);
		map2.put(12, 1200);
		map2.put(13, 1300);
		// 查看包含的元素个数
		System.out.println(map1.size());
		System.out.println(map1);
		// 存入新的key为1的数据,覆盖原(1,100)
		map1.put(1,99);
		System.out.println(map1);
		// 根据Key获取Value
		System.out.println("map1 get key 2 is: "+map1.get(2));
		// 判断是否包含某个Key或则Value
		System.out.println("map1中有key为4的元素? "+map1.containsKey(4));
		System.out.println("map1中有Value为300的元素? "+map1.containsValue(300));
		// 将map2中的元素复制到map1中
		map1.putAll(map2);
		System.out.println("after copy, map1 now is: "+map1);
		System.out.println("after copy, map2 now is: "+map2);
		// 从map2中移出一组键值对(通过key)
		map2.remove(12);
		System.out.println("After remove 12, map2 is: "+map2);
		// 清空map2
		map2.clear();
		System.out.println("After clear, map2 is: "+map2+" and size is: "+map2.size());
	}
}

运行结果:

3
{1=100, 2=200, 3=300}
{1=99, 2=200, 3=300}
map1 get key 2 is: 200
map1中有key为4的元素? false
map1中有Value为300的元素? true
after copy, map1 now is: {1=99, 2=200, 3=300, 11=1100, 12=1200, 13=1300}
after copy, map2 now is: {11=1100, 12=1200, 13=1300}
After remove 12, map2 is: {11=1100, 13=1300}
After clear, map2 is: {} and size is: 0

1.4 Map的两种取出方式:

Map集合的取出原理:将Map集合转成Set集合,再通过迭代器取出。

  • Set keySet():返回此映射中包含的键的 Set 视图(集合)。

将map中所有的键存入到Set集合,因为set具备迭代器,所有迭代方式取出所有的键再根据get()方法 ,获取每一个键对应的值
在这里插入图片描述

代码示例:

public class MapDemo2 {

    public static void main(String[] args) {
        Map<String,String>  map = new HashMap<String,String>();
        map.put("01", "zhangsan1");
        map.put("02","zhangsan2");
        map.put("03", "zhangsan3");
        map.put("04", "zhangsan4");
        
        //先获取map集合的所有键的set集合,keySet();
        Set<String> k = map.keySet(); //Set<String>相当于返回值类型
        
        //有了Set集合,就可以获取其迭代器.(注意Set集合的类型要和迭代器保持一致)
        Iterator<String> it = k.iterator();
        
        while(it.hasNext()){
            String key = it.next();
            //有了键,就可以通过map集合的get方法获取对应的值
            String value =map.get(key);
            System.out.println("key:"+key+"---value:"+value);
        }
    }
}

运行结果:

      key:01---value:zhangsan1
      key:02---value:zhangsan2
      key:03---value:zhangsan3
      key:04---value:zhangsan4

代码示例:

public class MapDemo3 {
    public static void main(String[] args) {
    	Hashtable<String , String> hashTable;
		TreeMap<String , String> treeMap;
		HashMap<String , String> hashMap;
		LinkedHashMap<String, String> linkedHashMap;
	for(int i = 0; i < 10; i++) {
			hashTable.put(String.valueOf(i), i + "" + i);
			treeMap.put(String.valueOf(i), i + "" + i);
			hashMap.put(String.valueOf(i), i + "" + i);
			linkedHashMap.put(String.valueOf(i), i + "" + i);
		}
	System.out.println("Hashtable: " + hashTable.keySet());
	System.out.println("TreeMap: " + treeMap.keySet());
	System.out.println("HashMap: " + hashMap.keySet());
	System.out.println("LinkedHashMap: " + linkedHashMap.keySet());
}

运行结果:

Hashtable: [9876543210]
TreeMap: [0123456789]
HashMap: [0123456789]
LinkedHashMap: [0123456789]
  • Set<Map.Entry<K,V>> entrySet():返回此映射中包含的映射关系的 Set 视图(集合)。

将Map集合中的映射关系存入到了Set集合中,而这个映射关系的数据类型就Map.Entry。
Map.Entry:其实Entry也是一个接口,它是Map接口中的一个内部接口,getKey()和getValue是接口Map.Entry<K,V>中的方法,返回对应的键和对应的值
在这里插入图片描述
代码示例:

public class MapDemo4 {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<String,String>();
        map.put("02", "zhangsan2");
        map.put("03", "zhangsan3");
        map.put("01", "zhangsan1");
        map.put("04", "zhangsan4");
        
        //将Map集合中的映射关系取出,出入到Set集合中
           Set<Map.Entry<String,String>> es = map.entrySet();
           Iterator<Map.Entry<String,String>> it = es.iterator();
           
           while(it.hasNext()){
               Map.Entry<String, String> mey = it.next();
               
               //getKey()和getValue是接口Map.Entry<K,V>中的方法,返回对应的键和对应的值
               String key = mey.getKey();
               String value = mey.getValue();
               
               System.out.println(key+":"+value);
               
           }
    }    
}

运行结果:

  04:zhangsan4
  01:zhangsan1
  02:zhangsan2
  03:zhangsan3

常见问题:
Entry是接口 ,但是为什么不定义定义在外面呢?
原因:
Entry代表的是映射关系,先有Map集合,才有的映射关系,所以它是Map集合内部的事物,因此将Entry定义为在Map的内部集合,可以直接访问Map集合中的元素

在这里插入图片描述
看到这里的时候大伙儿估计都明白了为什么HashMap为什么要选择Entry数组来存放key-value,因为Entry实现的Map.Entry接口里面定义了getKey(),getValue(),setKey(),setValue()等方法,对键值对进行了一个封装便于后面的操作。

HashMap

2.1 HashMap简介

HashMap是Map的常见实现类,也是Java中使用最频繁的储存键值对的结构,即散列表,它存储的是键值对(key-value)映射。HashMap 继承于AbstractMap。

2.2 HashMap的工作原理

Java中的数组在添加或者删除的时候,都会复制一个新数组,比较耗内存,但是数组遍历比较高效(寻址容易,插入和删除困难);然而,链表则是遍历比较慢,而添加和删除元素代价低(寻址困难,插入和删除容易)。HashMap则是巧妙的结合这两点——哈希表

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些键值对(Entry)分散存储在一个数组中,这个数组就是HashMap的主干。简单的来说HashMap是由数组+链表组成的。下面我们可以通过HashMap的结构图来看到它的基本结构。
在这里插入图片描述
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。
上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next我们存储的是key和value。其中hash通过key计算的hashcode值,next是指向下一个节点。由此可以理解存储原理了(详细看源码分析)。

2.3 HashMap的源码分析

2.3.1 重要属性
  • static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 : 默认的初始容量 (16)

  • static final int MAXIMUM_CAPACITY = 1 << 30 : 最大容量

  • static final float DEFAULT_LOAD_FACTOR = 0.75f : 负载因子:该属性是表示在扩容之前容量占有率的一个标尺。它控制数组存放Node是的疏密程度。loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,查找效率降低。loadFactor越小,也就是趋近于0,那么数组中存放的数据也就越稀。默认的0.75f是一个权衡后的值。

  • Entry[] table : Entry数组类型,HashMap的键值对都是存储在Entry数组中

  • int threshold; : 阈值。用于判断是否需要调整HashMap的容量。threshold = 容量 * 加载因子,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。

  • static final int TREEIFY_THRESHOLD = 8:如果链表超过了这个值,则将单链表转变为红黑树。

2.3.2 构造函数
    // 构造函数一
   public HashMap() {
         this.loadFactor = DEFAULT_LOAD_FACTOR;
         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
         table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
     }
 
    // 构造函数二
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
 
    // 构造函数三
    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;
        this.threshold = tableSizeFor(initialCapacity);
        }
    // 构造函数四
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

我们用的最多的就是第一个无参构造方法,默认的初始容量(16)和加载因子(0.75)。
构造函数二调用的构造函数三,手动设置初始容量大小。
构造函数三是手动设置初始容量和负载因子。
构造函数是将别的map映射到自己的map中。

2.3.3 存储数据put
public V put(K key, V value) {
     // 若“key为null”,则将该键值对添加到table[0]中。
         if (key == null) 
            return putForNullKey(value);
     // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
         int hash = hash(key.hashCode());
     //搜索指定hash值在对应table中的索引
         int i = indexFor(hash, table.length);
     // 循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
         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))) { //如果key相同则覆盖并返回旧值
                  V oldValue = e.value;
                 e.value = value;
                 e.recordAccess(this);
                 return oldValue;
              }
         }
     //修改次数+1
         modCount++;
     //将key-value添加到table[i]处
     addEntry(hash, key, value, i);
     return null;
}

我们看看putForNullKey(value)方法:

private V putForNullKey(V value) {
          for (Entry<K,V> e = table[0]; e != null; e = e.next) {
              if (e.key == null) {   //如果有key为null的对象存在,则覆盖掉
                  V oldValue = e.value;
                  e.value = value;
                  e.recordAccess(this);
                  return oldValue;
             }
         }
         modCount++;
         addEntry(0, null, value, 0); //如果键为null的话,则hash值为0
         return null;
 }

如果key为null的话,hash值为0,对象存储在数组中索引为0的位置。即table[0];

我们再看来一下如何通过key的hashCode值计算hash码:

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

得到hash码之后就会通过hash码去计算出应该存储在数组中的索引,计算索引的函数如下:

static int indexFor(int h, int length) { //根据hash值和数组长度算出索引值
         return h & (length-1);  //这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出,&运算时当同时为1,结果为1,否则为0
     }

我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。

根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:

  • 如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。
  • 如果这两个 Entry 的 key通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。
  • 如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部

addEntry() 方法:

  void addEntry(int hash, K key, V value, int bucketIndex) {
          Entry<K,V> e = table[bucketIndex]; //取得数组中索引为bucketIndex的Entry对象
          table[bucketIndex] = new Entry<>(hash, key, value, e);//用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象的next构成链表。
          if (size++ >= threshold) //判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容
              resize(2 * table.length); //HashMap扩容是扩为原来的两倍。
  }
2.3.4 数据读取 get
public V get(Object key) {   
    if (key == null)   
        return getForNullKey();   
    int hash = hash(key.hashCode());   
    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.equals(k)))   
            return e.value;   
    }   
    return null;   
}  

从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

2.3.5 调整大小
 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];
         transfer(newTable);//用来将原先table的元素全部移到newTable里面
         table = newTable;  //再将newTable赋值给table
         threshold = (int)(newCapacity * loadFactor);//重新计算临界值
     }

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

当HashMap中的元素个数超过 数组大小 * loadFactor 时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过 16 * 0.75 =12的时候,就把数组的大小扩展为 2 * 16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,扩容是需要进行数组复制的,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

Java8 中的HashMap

3.1 Java8 中HashMap的工作原理

JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。
在这里插入图片描述
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。

我们根据数组元素中,第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树的。

3.2 Java8 中HashMap的源码分析

3.2.1 put() 方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
 
// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第四个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
    // 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
 
    else {// 数组该位置有数据
        Node<K,V> e; K k;
        // 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
        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) {
                // 插入到链表的最后面(Java7 是插入到链表的最前面)
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个
                    // 会触发下面的 treeifyBin,也就是将链表转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果在该链表中找到了"相等"的 key(== 或 equals)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
                    break;
                p = e;
            }
        }
        // e!=null 说明存在旧值的key与要插入的key"相等"
        // 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
3.2.2 数组扩容

resize() 方法用于初始化数组或数组扩容,每次扩容后,容量为原来的 2 倍,并进行数据迁移。

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) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
        newCap = oldThr;
    else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候
        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;
 
    // 用新的数组大小初始化新的数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 如果是初始化数组,到这里就结束了,返回 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)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果是红黑树,具体我们就不展开了
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { 
                    // 这块是处理链表的情况,
                    // 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序
                    // loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的
                    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;
                        // 第二条链表的新的位置是 j + oldCap,这个很好理解
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
3.2.3 get() 方法

计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)
判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步
判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步遍历链表,直到找到相等(==或equals)的 key.

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 判断第一个节点是不是就是需要的
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 判断是否是红黑树
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)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;
}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值