xiguayaaaaa
文章28
标签4
分类7
集合框架Map笔记

集合框架Map笔记

集合框架Map笔记

Map概述

Map用于保存具有映射(一对一)关系的数据,Map集合保存两个值,一个是key(键),一个是值(value),key和value都可以是任何引用类型的数据。key不允许重复,value可以重复。

通过key,总能找到唯一的,确定的value。

Map的实现类有HashMap,Hashtable,Properties,SortedMap等。

HashMap

常用API

方法 描述
put(K key,V value) 添加键值对
get(Object key) 根据键获取值
keySet() 获取keySet
entrySet() 获取entrySet
clear() 清空
containsKey(Object key) 判断是否存在key
remove(Object key) 根据key删除键值对
remover(Object key,Object value) 根据key和value删除键值对
size() 获取元素个数
isEmpty() 判断map是否为空

HashMap存放元素的流程

HashMap的元素存放是一个复杂的过程,过程中涉及到哈希表的扩容,红黑树和链表的相互转换。

HashMap中几个关键的成员变量:

//HashMap默认容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//HashMap最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//HashMap负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;;
//链表转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树转换为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//链表转换为红黑树时Hash表的容量阈值
static final int MIN_TREEIFY_CAPACITY = 64;
//保存HashMap的Hash表
transient Node<K,V>[] table;
//HashMap中元素个数
transient int size;

HashMap添加元素回调用put方法,而put方法内部又调用了putVal方法:

public V put(K key, V value) {
    //调用putVal方法
    return putVal(hash(key), key, value, false, true);
}

putVal()是HashMap添加元素的核心:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断哈希表是否为空
    if ((tab = table) == null || (n = tab.length) == 0)
        //扩容
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)//判断tab[i]是否有元素
        //如果没有元素则直接存放
        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))))
             //如果key相等则替换
             e = p;
         //如果key不相等,判断哈希表已有的节点是不是红黑树
         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);
                     //如果链表长度大于等于7,则进制红黑树转换相关工作
                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                         treeifyBin(tab, hash);
                         break;
                     }
                      //如果key相等则在链表中替换
                     if (e.hash == hash &&
                         ((k = e.key) == key || (key != null && key.equals(k))))
                         break;
                     
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果元素个数大于临界值则扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

方法过程可以描述为:

  1. 判断底层hash表是否为空,如果为空则先扩容。
  2. 如果不为空,根据元素的key计算在hash表中的位置。
  3. 判断计算出的位置有无元素,如果无元素则直接放入hash表,并直接跳到第9步。
  4. 如果计算出的位置有元素,判断key是否相等,如果相等就直接赋值。
  5. 如果计算出的位置有元素,但key不相等,判断此处是否为红黑树,如果是红黑树则以树的方式插入。
  6. 如果计算出的位置有元素,但key不相等,不是红黑树,说明此处为链表,遍历链表准备插入。
  7. 如果遍历链表如果新增的元素和链表中的元素key相等则替换,如果新增元素与链表中已有元素key都不相等,则在已有元素末尾链表插入。
  8. 如果链表长度大于8,判断hash表容量是否大于64,如果大于64则转换成红黑树,并以红黑树的方式插入元素。
  9. 添加元素后判断元素个数是否大于扩容阈值,如果大于则扩容。

HashMap实际是一个数组,当两个元素的hash值相等时(hash冲突),如果key也相等,则修改原先的值(也称值覆盖),如果key不相等,就会产生链表。

链表的第一位(索引为0)是第一个出现hash冲突的元素,当这个链表的长度大于等于7的时候,进入 treeifyBin(tab, hash)制红黑树转换相关工作。

执行treeifyBin(tab, hash),若数组的长度小于64,则执行resize()扩容,16->32。当下一次put元素时,再次执行resize()扩容,32->64。

满足链表长度大于8,数组长度大于等于64,将链表转化为红黑树。

HashMap扩容流程

final Node<K,V>[] resize() {
    //获取原有hash表
    Node<K,V>[] 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倍小于最大容量,并且原容量小于默认容量10,就将扩容阈值修改为原阈值的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
        }
    //如果当前hash表没有数据,则使用初始化的值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    //如果是第一次添加元素则使用默认容量16,扩容阈值为16*0.75=12
    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"})
    //创建新的hash表
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        //遍历hash表开始扩容
        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 { // preserve order
                    //链表复制操作
                    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;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
  1. 调用无参构造器时,实例化的HashMap底层数组是null,当第一次调用put()方法时进行扩容,初始容量为16。
  2. 调用有参构造指定了容量和负载因子时,会根据指定的正整数找到不小于指定容量的2的次幂,将这个数赋值给扩容阈值(threshold),当第一次put()方法时,会将阈值赋值给容量,并计算新的阈值=容量x负载因子。
  3. 如果不是第一次扩容,则容量变为原容量的2倍,阈值也变为原来的2倍。
JDK1.7和JDK1.8中HashMap的区别:

JDK1.7的HashMap是用数组+链表实现的,采用头插法,可能产生死循环。

JDK1.8的HashMap是用数组+链表+红黑树实现的,采用尾插法,当链表长度大于8并且数组长度大于64时,会将链表转化为红黑树,如果链表长度已经为8,但是数组长度小于64时,会先扩容;当红黑树的节点数小于6时会退化成链表。

HashMap的遍历方式

  1. 获取keySet()后遍历keySet()获取到key然后通过key获取value
  2. 获取entrySet()后遍历entrySet(),相比于第一种写法稍显复杂,但是能更好的体现Map中的数据结构
  3. 使用Lambda表达式遍历,相比前两种是最简洁的方式,但是代码可读性略差
public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        
        map.put("张三", "计算机科学与技术");
        map.put("李四", "软件工程");
        map.put("王五", "网络工程");
        
        //第一种:获取keySet()并遍历
        Set<String> set = map.keySet();
        //获取迭代器
        Iterator<String> it = set.iterator();
        while(it.hasNext()) {
            String key = it.next();
            String value = map.get(key);
            System.out.println(key+"==================="+value);
        }
        for (; it.hasNext();) {
            String key = it.next();
            String value = map.get(key);
            System.out.println(key+"==================="+value);
        }
        for (String key : set) {
            String value = map.get(key);
            System.out.println(key+"==================="+value);
        }
        //第二种:获取Entry
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        //使用迭代器
        Iterator<Map.Entry<String, String>> entryIt = entrySet.iterator();
        
        while (entryIt.hasNext()) {
            Map.Entry<String, String> entry = entryIt.next();
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+"==================="+value);
        }
        
        for (; entryIt.hasNext();) {
            Map.Entry<String, String> entry = entryIt.next();
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+"==================="+value);
        }
        
        for (Entry<String, String> entry : entrySet) {
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+"==================="+value);
        }
        //第三种:Lambda表达式
        map.forEach((key,value)->System.out.println(key+"==================="+value));
    }

HashMap和Hashtable的区别:

都是Map接口的实现。

Hashtable是线程安全的,HashMap是非线程安全的,HashMap比Hashtable效率更高,多个线程访问同一个Map对象时建议使用Hashtable。

Hashtable不能使用null作为key和value,如果使用,回抛出NullPointerException空指针异常;HashMap则可以使用null作为key和value。

HashMap初始长度为16,扩容为原长度的2倍,Hashtable初始长度为11,扩容为原长度的2n+1倍。

本文作者:西瓜
本文链接:https://xiguayaaaaa.github.io/2022/08/03/%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6Map%E7%AC%94%E8%AE%B0/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可