深入学习Java之HashMap

来源:互联网 发布:百度云域名注册 编辑:程序博客网 时间:2024/06/10 04:05

深入学习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;    }

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

原创粉丝点击