HashMap:认识——使用——原理

一、HashMap简介

(1)HashMap实现了Map接口,存储的是键值对的数据(key-value)。
(2)HashMap的key和value都允许为null,键唯一,值可重复。
(3)存储的数据是无序的。
(4)由数组,链表和红黑树组成。默认初始容量是16,装载因子为0.75。
(5)非同步的。如果需要同步操作,可使用Map m = Collections.synchronizedMap(new HashMap(...));

二、HashMap的构造方法

HashMap有四个构造方法。

  • HashMap(): 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
Map<String,String> hashMap = new HashMap<String,String>();

HashMap的默认构造函数就是对下面的几个字段进行初始化。

	int threshold;				// 所能容纳key-value对的极限,默认为12(16 * 0.75),超过它,hashmap就会进行扩容,扩容后容量为原来的两倍。
	final float loadFactor;		// 负载因子,默认等于0.75。这里说下length,length代表HashMap中数组的长度,threshold = length * loadFactor。
	int size;					// size代表HashMap中实际存储的key-value对的个数,注意和length的区别。
	int modCount;				// modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化。
  • HashMap(int initialCapacity): 构造一个带指定初始容量 (initialCapacity) 和默认加载因子 (0.75) 的空 HashMap。
Map<String,String> hashMap = new HashMap<String,String>(32);
  • HashMap(int initialCapacity, float loadFactor): 构造一个带指定初始容量和加载因子的空 HashMap。

    注意:虽然可以传入initialCapacity这个指定的初始容量,但HashMap底层还是会自动给我们将initialCapacity这个数,转变成比它大的那个最小的2的幂次方的数。其中涉及方法tableSizeFor,举个例子:如果传入6,那HashMap的初始值为8,如果是14那对应的初始值是16。所以并不是想传入多少他的初始容量就为多少。

  • HashMap(Map<? extends K,? extends V> m): 构造一个映射关系与指定 Map 相同的新 HashMap。

三、HashMap的常用方法

1、添加功能

  • V put(K key,V value)
    如果键是第一次存储,就直接存储元素,返回null
    如果键不是第一次存在,就用值把以前的值替换掉,返回以前的值

2、删除功能

  • void clear() :移除所有的键值对元素
  • V remove(Object key) :根据键删除键值对元素,并把值返回

3、判断功能

  • boolean containsKey(Object key) :判断集合是否包含指定的键
  • boolean containsValue(Object value) :判断集合是否包含指定的值
  • boolean isEmpty() :判断集合是否为空

4、获取功能

  • Set<Map.Entry<K,V>> entrySet() :返回此映射所包含的映射关系的 Set 视图
  • V get(Object key) :根据键获取值
  • Set keySet() :获取集合中所有键的集合
  • Collection values() :获取集合中所有值的集合

5、长度

  • int size() :返回集合中的键值对的对数

四、HashMap的遍历

Java中所有的Map都实现了Map接口,那么一下的遍历方法都使用任何Map实现(HashMap、TreeMap、LinkedHashMap等等)

方法1:通过键找值遍历,其中需要用到方法
Set keySet()
V get(Object key)

import java.util.HashMap;
import java.util.Map;

public class HashMapDemo {

    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(1,"luffy");
        map.put(2,"nami");
        map.put(3,"zoro");

        for (Integer key : map.keySet()) {
            String value = map.get(key);
            System.out.println("Key = " + key + ", Value = " + value);
        }
    }
}
/*
输出结果:
Key = 1, Value = luffy
Key = 2, Value = nami
Key = 3, Value = zoro
*/

方法2:通过entrySet()方法获取map集合的键值对对象,然后通过for-each及该对象的getKey()getValue()进行遍历

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class HashMapDemo {

    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(1,"luffy");
        map.put(2,"nami");
        map.put(3,"zoro");

        Set<Map.Entry<Integer,String>> set = map.entrySet();
        for (Map.Entry<Integer,String> me : set) {
            // 根据键值对对象获取键和值
            Integer key = me.getKey();
            String value = me.getValue();
            System.out.println("Key = " + key + ", Value = " + value);
        }
    }
}

方法3:通过Iterator的hasNext()next()方法进行遍历

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class HashMapDemo {

    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(1,"luffy");
        map.put(2,"nami");
        map.put(3,"zoro");

        Iterator<Map.Entry<Integer, String>> entries = map.entrySet().iterator();
        while (entries.hasNext()) {
            Map.Entry<Integer, String> entry = entries.next();
            // 这里同样需要借助getKey()和getValue()方法
            System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
        }
    }
}

方法4:通过Map的keySet()和values()直接对它的键或值遍历

import java.util.HashMap;
import java.util.Map;

public class HashMapDemo {

    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(1,"luffy");
        map.put(2,"nami");
        map.put(3,"zoro");

        //遍历map中的键
        for (Integer key : map.keySet()) {
            System.out.println("Key = " + key);
        }

        //遍历map中的值
        for (String value : map.values()) {
            System.out.println("Value = " + value);
        }
    }
}
/*
输出结果:
Key = 1
Key = 2
Key = 3
Value = luffy
Value = nami
Value = zoro
*/

五、HashMap获取哈希桶数组索引

不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。而HashMap的底层是由数组+链表+红黑树(1.8以后新加了红黑树)构成,对于数组的索引,也就是哈希桶数组的索引,HashMap里的源码是这样实现计算的:

方法一:
static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}

也就是根据key,采用hash算法对key进行哈希计算,从而获得一个int型的哈希桶索引,一共有三步:取key的hashCode值对hashCode值的高低16位进行异或hashCode值与length-1取模计算下标

六、HashMap的put方法

JDK1.8HashMap的put方法源码如下:

  public V put(K key, V value) {
      // 对key的hashCode()做hash
      return putVal(hash(key), key, value, false, true);
  }
  
  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                 boolean evict) {
      Node<K,V>[] tab; Node<K,V> p; int n, i;
      // 步骤①:tab为空则创建
     if ((tab = table) == null || (n = tab.length) == 0)
         n = (tab = resize()).length;
     // 步骤②:计算index,并对null做处理 
     if ((p = tab[i = (n - 1) & hash]) == null) 
         tab[i] = newNode(hash, key, value, null);
     else {
         Node<K,V> e; K k;
         // 步骤③:节点key存在,直接覆盖value
         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) {
                 if ((e = p.next) == null) {
                     p.next = newNode(hash, key,value,null);
                      //链表长度大于8转换为红黑树进行处理
                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
                         treeifyBin(tab, hash);
                     break;
                 }
                  // key已经存在直接覆盖value
                 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;
         }
     }

48     ++modCount;
49     // 步骤⑥:超过最大容量 就扩容
50     if (++size > threshold)
51         resize();
52     afterNodeInsertion(evict);
53     return null;
54 }

注意几点:
① 1.7之前,HashMap在执行put方法时,如果发生了哈希冲突,则在哈希桶对应的链表采用头插法进行添加元素;而1.8之后采用的是尾插法,目的是为了保证hashmap扩容之后,相同链表上的元素还在一起。
② 1.8之后,当链表的长度大于TREEIFY_THRESHOLD (8)时,是会自动将链表转成红黑树的,这是一个优化,在红黑树上对数据进行查询等操作会比在链表上更高效。

七、HashMap的扩容机制

HashMap的扩容涉及到几个值

  • size:HashMap中实际存储的键值对对数
  • length:HashMap的capacity,也就是其目前规定的总容量,默认为16
  • threshold:扩容的临界值,它的计算为 => threshold = length * loadFactor,所以默认为12(16*0.75)
  • loadFactor: 负载因子,默认为0.75

HashMap的扩容机制是这样实现的,当size大于threshold时,就会进行扩容,1.8之后通过数组拷贝、重新计算哈希桶索引的方法将HashMap扩充为原来的2倍。默认情况下,当size大于12时进行扩容。

参考:
Java 8系列之重新认识HashMap

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值