zoukankan      html  css  js  c++  java
  • 云时代架构阅读笔记四——深入的、详细的介绍Map以及HashMap

    原文链接:https://mp.weixin.qq.com/s/GhZ-RfjscNsOraqD2z_eng

    今天,笔者要介绍的是Java集合框架中的Map集合,在日常工作中Map的运用也十分广泛。

    与List集合、Set集合隶属于Collection不同,Map是一个独立的接口,与Collection相同级别的接口。

    重要的是,Map集合提供了一个不一样的元素存储方法,利用“key—value”的形式进行存储。其中,每个键映射一个值。而在Set集合中,元素的存储就是利用Map的这一特性来实现。

    简单的介绍了下Map集合,接下来,就让笔者对其主要实现类HashMap、TreeMap、HashTable进行详细的说明

     

     

    1 Map常用方法

    在具体介绍之前,我们先了解下Map接口本身,以便了解所有实现的共同点。

    public interface Map<K,V> {

     

        //返回Map中的key--value的数目

        int size();

     

        //如果Map不包含任何key--value,则返回 true

        boolean isEmpty();

     

        //如果Map中包含指定key的映射,则返回true

        boolean containsKey(Object key);

     

        //如果此Map将一个或多个键映射到指定值,则返回 true

        boolean containsValue(Object value);

     

        //返回与指定键关联的值

        V get(Object key);

     

        //将指定值与指定键相关联

        V put(K key, V value);

     

        //从Map中删除键和关联的值

        V remove(Object key);

     

        //将指定Map中的所有映射复制到此map

        void putAll(java.util.Map<? extends K, ? extends V> m);

     

        //从Map中删除所有映射

        void clear();

     

        //返回Map中所包含键的Set集合

        Set<K> keySet();

     

        //返回 map 中所包含值的 Collection集合。

        Collection<V> values();

     

        //返回Map中所包含映射的Set视图。Set中的每个元素都是一个 Map.Entry 对象

        Set<java.util.Map.Entry<K, V>> entrySet();

     

        //比较指定对象与此 Map 的等价性

        boolean equals(Object o);

     

        //返回此 Map 的哈希码

        int hashCode();

     

        //Map集合中存储key--value的对象Entry,在Map集合内形成数组结构

        interface Entry<K,V> {

     

            V getValue();

     

            V setValue(V value);

     

            boolean equals(Object o);

     

            int hashCode();

        }

    }

    2 HashMap

    HashMap,我们工作中经常使用的集合之一,也是面试中最常被问到的集合。想必,你一定听到过:“来,说说HashMap的实现”等之类的问题!

    下面,来做具体介绍:

    HashMap基于哈希表,底层结构由数组来实现,添加到集合中的元素以“key—value”形式保存到数组中,在数组中key—value被包装成一个实体来处理—-也就是上面Map接口中的Entry。

    在HashMap中,Entry[]保存了集合中所有的键值对,当我们需要快速存储、获取、删除集合中的元素时,HashMap会根据hash算法来获得“键值对”在数组中存在的位置,以来实现对应的操作方法。

    此时,细心的朋友可能会问,既然是基于哈希表的实现,那么当新增的元素出现了hash值重复了怎么办,怎么插入呢?

    专业上来说,hash值重复的情况,我们称之为哈希碰撞(又或者哈希冲突)。在HashMap中,是通过链表的形式来解决的,在hash值重复的数组角标下,通过链表将新插入的元素依次排列,当然如果插入的key相同,那么我们会将新插入的value覆盖掉原有的value;

     

    像上图所示,当产生了hash冲突后,会在产生冲突的角标下,生成链表,依次排列。

    HashMap继承于AbstractMap,实现了Map, Cloneable, Serializable接口。
    (1)HashMap继承AbstractMap,得到了Map接口中定义方法的实现,减少实现Map接口所需的工作;
    (2)HashMap实现Map,得到了Map接口定义的所有方法,其中一部分AbstractMap已实现;
    (3)HashMap实现Cloneable,得到了clone()方法,可以实现克隆功能;
    (4)HashMap实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议。

    它具有如下特点:

    • 允许存入null键,null值(null值只有一个,并存于数组第一个位置)
    • 无序集合,而且顺序会随着元素的添加而随时改变(添加顺序,迭代顺序不一致)
    • 随着元素的增加而动态扩容(与ArrayList原理一致)
    • 不存在重复元素(得益于hashCode算法和equals方法)
    • 线程不安全

    3 HashMap基本操作

    下面,我们来看下HashMap的常用方法:

     public static void main(String[] agrs){

        //创建HashMap集合:

        Map<String,String> map = new HashMap<String,String>();

        System.out.println("HashMap元素大小:"+map.size());

     

        //元素添加:

        map.put("hi","hello");

        map.put("my","hello");

        map.put("name","hello");

        map.put("is","hello");

        map.put("jiaboyan","hello");

     

        //遍历1:获取key的Set集合

        for(String key:map.keySet()){

            System.out.println("map的key是:"+key);

            System.out.println("map的value是:"+map.get(key));

        }

     

        //遍历2:得到Set集合迭代器

        Set<Map.Entry<String,String>> mapSet1 = map.entrySet();

        Iterator<Map.Entry<String,String>> iterator = mapSet1.iterator();

        while(iterator.hasNext()){

            Map.Entry<String,String> mapEntry = iterator.next();

            System.out.println("map的key是:" + mapEntry.getKey());

            System.out.println("map的value是:" + mapEntry.getValue());

        }

     

        //遍历3:转换成Set集合,增强for循环

        Set<Map.Entry<String,String>> mapSet2 = map.entrySet();

        for(Map.Entry<String,String> mapEntry : mapSet2){

            System.out.println("map的key是:" + mapEntry.getKey());

            System.out.println("map的value是:" + mapEntry.getValue());

        }

     

        //元素获取:通过key获取value

        String keyValue = map.get("jiaboyan");

        System.out.println("HashMap的key对应的value:" + keyValue);

     

        //元素替换:map没有提供直接set方法,而是使用新增来完成更新操作

        map.put("jiaboyan","helloworld");

        System.out.println("HashMap的key对应的value:" + map.get("jiaboyan"));

     

        //元素删除:

        String value = map.remove("jiaboyan");

        System.out.println("HashMap集合中被删除元素的value" + value);

        //清空所有元素:

        map.clear();

     

        //hashMap是否包含某个key:

        boolean isContain = map.containsKey("hello");

        //hashMap是否为空:

        boolean isEmpty = map.isEmpty();

    }

    4 HashMap源码讲解(基于JDK1.7.0_45

    接下来,我们对HashMap源码进行分析。

    同理,我们还是带着问题去理解HashMap的实现!

    1.HashMap底层数据结构如何?

    2.HashMap如何保存数据,增删改咋实现?

    3.HashMap如何扩容?

    4.为什么扩容的大小一定要是2的整数次幂,也就是2的N次方.

    • HashMap成员变量

    我们先了解下HashMap中有哪些成员变量:table、DEFAULT_INITIAL_CAPACITY、DEFAULT_LOAD_FACTOR等;

    其中,table就是HashMap底层保存元素的数组,默认情况下先赋值为空数组EMPTY_TABLE;

    DEFAULT_INITIAL_CAPACITY是HashMap中数组的默认初始化大小。在JDK1.7.0_45版本中,当首次put(新增)元素时,会新建一个容量为16的Entry[]数组赋值给table属性;

    DEFAULT_LOAD_FACTOR是HashMap扩容的关键参数,当HashMap中存储的元素个数达到一定的数量时,Entry[]会进行扩容。这个一定数量的值就是根据DEFAULT_LOAD_FACTOR计算得来,主要是数组大小*DEFAULT_LOAD_FACTOR;

    public class HashMap<K,V> extends AbstractMap<K,V>

            implements Map<K,V>, Cloneable, Serializable {

     

        //hashMap中的数组初始化大小:1 << 4=2^4=16

        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

     

        //1<<30 表示1左移30位,每左移一位乘以2,所以就是1*2^30=1073741824。

        static final int MAXIMUM_CAPACITY = 1 << 30;

     

        //默认装载因子:

        static final float DEFAULT_LOAD_FACTOR = 0.75f;

     

        //HashMap默认初始化的空数组:

        static final java.util.HashMap.Entry<?,?>[] EMPTY_TABLE = {};

     

        //HashMap中底层保存数据的数组:HashMap其实就是一个Entry数组

        transient java.util.HashMap.Entry<K,V>[] table = (java.util.HashMap.Entry<K,V>[]) EMPTY_TABLE;

     

        //Hashmap中元素的个数:

        transient int size;

     

        //threshold:等于capacity * loadFactory,决定了HashMap能够放进去的数据量

        int threshold;

     

        //loadFactor:装载因子,默认值为0.75,它决定了bucket填充程度;

        final float loadFactor;

     

        //HashMap被操作的次数:

        transient int modCount;

     

    }

    • HashMap的最终实现

    在HashMap中,存储元素的是Entry[]数组,而其中的元素也就是Entry对象:

    static class Entry<K,V> implements Map.Entry<K,V> {

        //Entry属性-也就是HashMap的key

        final K key;

     

        //Entry属性-也就是HashMap的value

        V value;

     

        //指向下一个节点的引用:实现单向链表结构

        java.util.HashMap.Entry<K,V> next;

     

        //此Entry的hash值:也就是key的hash值

        int hash;

     

        // 构造函数:

        Entry(int h, K k, V v, java.util.HashMap.Entry<K,V> n) {

            value = v;

            next = n;

            key = k;

            hash = h;

        }

        public final K getKey() { return key;}

        public final V getValue() { return value; }

        public final V setValue(V newValue) {

            V oldValue = value;

            value = newValue;

            return oldValue;

        }

        public final boolean equals(Object o) {

     

            if (!(o instanceof Map.Entry))

                return false;

            Map.Entry e = (Map.Entry)o;

            Object k1 = getKey();

            Object k2 = e.getKey();

            if (k1 == k2 || (k1 != null && k1.equals(k2))) {

                Object v1 = getValue();

                Object v2 = e.getValue();

                if (v1 == v2 || (v1 != null && v1.equals(v2)))

                    return true;

            }

            return false;

        }

        public final int hashCode() {

            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());

        }

        public final String toString() {

            return getKey() + "=" + getValue();

        }

        void recordAccess(java.util.HashMap<K,V> m) {

        }

        void recordRemoval(java.util.HashMap<K,V> m) {

        }

    }

    • HashMap构造函数

    我们来看下,HashMap具体的构造是如何实现?

    最终都指定到了public HashMap(int initialCapacity, float loadFactor)方法中,对一些成员变量进行赋值。传入的初始容量,并没有改变Entry[]容量大小;

    public class HashMap<K,V> extends AbstractMap<K,V>

                implements Map<K,V>, Cloneable, Serializable {

     

        //构造方法:初始化容量  指定装载因子

        public HashMap(int initialCapacity, float loadFactor) {

            if (initialCapacity < 0)

                throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);

            //指定初始化容量大于 HashMap规定最大容量的话,就将其设置为最大容量;

            if (initialCapacity > MAXIMUM_CAPACITY)

                initialCapacity = MAXIMUM_CAPACITY;

            //不能小于0,判断参数float的值是否是数字

            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);

        }

     

        //无参构造:默认初始化容量16、默认装载因子0.75

        public HashMap() {

            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);

        }

     

        public HashMap(Map<? extends K, ? extends V> m) {

            this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,

                    DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);

            inflateTable(threshold);

     

            putAllForCreate(m);

        }

    }

    • HashMap新增元素

    对于HashMap来说,新增操作可谓是一个重要的方法,其中包括了最核心的扩容实现;

    首先,直接来看put(K key, V value)方法:

    public V put(K key, V value) {

        //如果调用put方法时(第一次调用put方法),还是空数组,则进行初始化操作

        if (table == EMPTY_TABLE) {

            //进行初始化HashMap的table属性,进行容量大小设置

            inflateTable(threshold);

        }

        //如果新增的key为null:

        if (key == null)

            //调用key为null的新增方法:

            return putForNullKey(value);

     

        //计算新增元素的hash值:

        int hash = hash(key);

     

        //根据hash值和数组长度,计算出新增的元素应该位于数组的哪个角标下:

        int i = indexFor(hash, table.length);

     

        //判断计算出的角标下,是否有相同的key,可以理解遍历该角标下的链表

        for (java.util.HashMap.Entry<K,V> e = table[i]; e != null; e = e.next) {

            Object k;

            //根据计算出的hash值,以及 equals方法 / == 来判断key是否相同:

            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

                //key相同,则替换原有key下的value:

                V oldValue = e.value;

                e.value = value;

                e.recordAccess(this);

                //返回被替换的值:

                return oldValue;

            }

        }

        modCount++;

        //向HashMap中增加元素:

        addEntry(hash, key, value, i);

        return null;

    }

  • 相关阅读:
    [题解]luogu-P1494 小Z的袜子 普通莫队
    [板子] 线性基
    [板子]字符串-KMP与AC自动机
    [板子]线段树求逆序对
    任务表
    [学习笔记]数列分块入门九题[LOJ6277-6285]
    Python常用高级函数
    Python的闭包和装饰器
    Python的迭代器和生成器
    Python的命名空间
  • 原文地址:https://www.cnblogs.com/DaisyYuanyq/p/11038638.html
Copyright © 2011-2022 走看看