深入学习Java之HashMap - 未完成

深入学习Java之HashMap - 未完成

前言

在前面的几个小节中,我们学习了List接口以及List接口下的几个常用的实现,ArrayListLinkedListVector,接下来的几个小节里,我们将继续学习容器中比较常用的一些实现,包含Map接口、Set接口以及它们对应的实现,本小节主要来学习Map接口及其实现HashMap

HashMap的继承结构

HashMap的继承结构

从上图中可以看到,HashMap实现了Map接口,Cloneable接口以及Serializable接口,并且继承AbastractMap抽象类,其中的Cloneable、Serializable接口只是标记接口,而且我们在前面的小节中已经学习过,所以这里我们就不展开了

同之前学习List一样,我们先从宏观上来学习Map接口以及AbstractMap,然后再深入学习HashMap,剖析HashMap的源码实现

Map接口

所谓的Map,其实就是键值对映射的集合,所谓的键值对,就是指一个由键和值组成的二元组,其中可以通过键来获取值,而且一般来说,如果键相同,则对应的值是相同的,也就是说,如果一个Map中有两个相同的键值对,则他们理论上是同一个键值对。数组可以理解为最简单的键值对集合,也就是最简单的Map,其中的索引就是键,也就是Key,数组中的元素就是值,也就是Value,比如a[1] = a, a[2] = b其中的 1、2是键,而a、b就是它们所对应的值,也可以把Map理解为就是把key映射到value的一个数据结构

接下来我们来看下Java中的Map接口

Map接口

从上图中可以看到,Map接口中提供了非常多的方法,接下来我们来简单了解各个方法的作用

  • contain开头的方法主要用于查看是否map中是否包含该元素,如containKeycontainValue
  • get方法用于根据key获取对应的值
  • put开头的方法用于将键值对放入map中,如put()putAll()
  • remove开头的方法用于将键值对从map中移除
  • keySetvaluesentrySet分为用于获取map中键的集合,值的容器以及键值对的集合

Map中还有一个非常重要的元素,Entry,用于对应存放在Map中的元素的形式,也就是上面所说的键值对,结构如下

Entry接口

从上图中可以看到,Entry接口中定义了操作一个Entry的方法,如获取键、获取值、根据键设置值等,这几个方法相对来说比较见名之意,所以这里我们就不做细致的展开,等到具体学习的时候再进行展开

AbstractMap抽象类

从上面的Map的结构图中可以看到,AbstractMap实现了Map接口,AbstractMap中实现了Map接口中部分通用的方法,如下面具体代码所示

查看Map中是否包含某个值


    public boolean containsValue(Object value) {
        // 获得EntrySet的迭代器
        Iterator<Entry<K,V>> i = entrySet().iterator();
        // 如果输入的值是null,则查找第一个null元素
        if (value==null) {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (e.getValue()==null)
                    return true;
            }
        // 如果不是null,则查找对应的值
        } else {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (value.equals(e.getValue()))
                    return true;
            }
        }
        return false;
    }

查看Map中是否包含某个键


    public boolean containsKey(Object key) {
        // 获取EntrySet的迭代器
        Iterator<Map.Entry<K,V>> i = entrySet().iterator();
        // 判断输入的键是否是null,如果是,则查看键为null的entry
        if (key==null) {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (e.getKey()==null)
                    return true;
            }
        // 如果不是null,则查找对应的键
        } else {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (key.equals(e.getKey()))
                    return true;
            }
        }
        return false;
    }

根据key获取值


    public V get(Object key) {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        if (key==null) {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (e.getKey()==null)
                    return e.getValue();
            }
        } else {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (key.equals(e.getKey()))
                    return e.getValue();
            }
        }
        return null;
    }

删除值


    public V remove(Object key) {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        Entry<K,V> correctEntry = null;
        if (key==null) {
            while (correctEntry==null && i.hasNext()) {
                Entry<K,V> e = i.next();
                if (e.getKey()==null)
                    correctEntry = e;
            }
        } else {
            while (correctEntry==null && i.hasNext()) {
                Entry<K,V> e = i.next();
                if (key.equals(e.getKey()))
                    correctEntry = e;
            }
        }

        V oldValue = null;
        if (correctEntry !=null) {
            oldValue = correctEntry.getValue();
            i.remove();
        }
        return oldValue;
    }

AbstractMap中还有其他一些实现方法,不过由于这些方法在Map的不同实现中会有不同,所以这里我们就不做过多的展开了

深入学习HashMap

在前面的内容中,我们从宏观的角度学习Map的一些常用方法,以及AbstractMap中实现的Map的几个方法,接下来我们将摄入地来学习Map实现类之一的HashMap,并且对HashMap的源码进行剖析

Hash结构的简单介绍

哈希结构,也就是Hash,是一种常用的数据结构,主要就是通过将键进行哈希计算,将大范围的数据映射到一个小的范围中,从而减少对其所占用的空间,比如说,有数据范围在1-100w的数据,而这些数据可能只有1000个,如果采用数组来存放,则需要的空间时非常大的,而且,造成的浪费也是非常明显的,这个时候,如果采用一个hash函数,将1-100w的数据范围映射到一个比较小的空间,比如最简单的MOD 1w(也就是哈希映射,MOD 1W,也就是所采用的哈希函数)则将数据的空间有效地减少了,不过由于将大范围的数据映射到小范围,则必然会造成一些数据映射到同一个空间,这就是哈希冲突,而解决哈希冲突除了需要一个良好的哈希函数外,还需要有处理哈希冲突的方法

常用的哈希函数

  • 直接寻址法,取key或者key的某个线性函数
  • 数字分析法,根据key自身的特性,选取某几位
  • 平方取中法,key平方后取中间几位
  • 折叠法,将key切割成位数一样的几个部分,然后进行折叠
  • 随机数法,采用随机函数
  • 除留余数法,key MOD一个数,上面举例所采用的方法

解决哈希冲突的方法

  • 开放地址法
  • 再哈希法
  • 链地址法(比较常用),将key相同的元素组成一个链

如果对于上面的概念不是很熟悉的话,则需要额外查看资料进行补充,可以参考常见hash算法的原理哈希冲突的处理方法

HashMap源码剖析

HashMap,是一个非常常用的数据结构,其本质就是对key做了hash的Map的集合,由于采用了Hash方法,HashMap的取元素的效率非常高,接近于O(1),而存放,删除元素的效率也是比较高的,接下来我们来剖析JDK中HashMap的实现,从前辈们的代码中学习具体的HashMap的具体实现

需要注意的小细节

  • HashMap是允许null值以及null键的,也就是说,在HashMap中,key=null是允许的,value=null也是允许的
  • HashMap是非线程安全的
  • HashMap中的元素的顺序是无法保证的
  • HashMap中有两个比较重要的属性,装填因子(loadfactor,默认为0.75)和容量(bcapacity)

HashMap的成员

    // 默认容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30
    // 默认装填因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f

    // HashMap的核心,本质就是键值对节点数组,数组中的每个元素都是一个链表
    // 从这里也可以看出,HashMap中采用的哈希冲突解决方法为链地址法
    transient Node<K,V>[] table;

    // HashMap中所有的键值对集合
    transient Set<Map.Entry<K,V>> entrySet;

    // 键值对数量
    transient int size;

    // 装填因子
    final float loadFactor;

    // 阈值,当容量达到该值时,进行扩容
    int threshold;

    // 节点,也就是前面所提到的键值对
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> 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; }

        // 计算hashcode
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }  

        // 设置值并且返回旧值
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        // 判断两个Node是否相等,只有键以及值都相等才算相等
        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;
        }
    }

构造方法


    // 提供初始容量和装填因子来构造
    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);
    }

    // 通过给定的数值计算大小
    // 这里计算的目的的使得n的左边的第一个1的右边全部为1
    // 最大值为2^32,然后执行n+1,也就是产生进位,使得
    // 所有n成为原本的值的2倍中最近接2的幂的数
    // 好厉害啊,原来还可以这么做,学习了:)
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

    // 仅提供初始容量,采用默认的装填因子,也就是0.75f
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    // 使用另一个Map来构造,此时采用默认的装填因子
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    // 将一个Map中的元素放入HashMap
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            // 如果此时的HashMap中没有元素,也就是采用该Map的元素来初始化HashMap
            if (table == null) { 
                // 使用该map的大小/装填因子,计算出此时所需要的table的大小
                // 装填因子 = 实际使用容量/table大小
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            // 如果此时的hashMap不为空,则判断所需要的大小是否已经超过需要进行扩容的阈值,如果超过,则进行扩容
            else if (s > threshold)
                resize();
            // 遍历该map,并且将所有的键值对放入HashMap中
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

    // 调整大小,也就是扩容
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果原来的hashMap中已经有元素了
        if (oldCap > 0) {
            // 如果就容量已经超过最大值,则将阈值调整至Integer.MAX_VALUE
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 将新容量调整为原来的两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 将新阈值调整为原来的两倍
        }
        else if (oldThr > 0) // 如果阈值大于0,则将新容量设置为阈值
            newCap = oldThr;
        else {               // 将容量设置为默认容量,也就是16,并且计算初始时的阈值
            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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = 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;
                    // 如果是树节点,则按树的操作方式,这里的底层是红黑树,不过
                    // 目前还没有学习到,无法对其进行解析
                    // HashMap果然复杂,还要好好加油才是 :(
                    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;
    }

    // 将指定的键值对放入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;
        // 这里写得非常精简,首先tab指向table然后判断table是否为空
        // 不为空则n=tab的长度,如果n=0,则进行扩容并且获取扩容后的长度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 根据hash值计算元素所要放入的位置,如果此时该位置没有元素,则直接放入即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 如果有元素,则说明产生了hash冲突
        else {
            Node<K,V> e; K k;
            // 判断所要插入的元素是否是第一个元素,判断的标准为hash相等,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) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    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;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

本来想继续研究下去的,不过,感觉有一些内容还不了解,所以目前只研究到这里,等过两天研究懂了再进行补充

猜你喜欢

转载自blog.csdn.net/xuhuanfeng232/article/details/77121842
今日推荐