zoukankan      html  css  js  c++  java
  • Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例

    转载 http://www.cnblogs.com/skywang12345/p/3310835.html

    https://tech.meituan.com/java-hashmap.html

    目录

    第1部分 HashMap介绍
    第2部分 HashMap数据结构
    第3部分 HashMap源码解析(基于JDK1.8)
        第3.1部分 HashMap的属性
        第3.2部分 HashMap的构造函数
        第3.3部分 HashMap的主要函数
        第3.4部分 HashMap实现的Cloneable接口
        第3.5部分 HashMap实现的Serializable接口
    第4部分 HashMap遍历方式
    第5部分 HashMap十问

    第1部分 HashMap介绍

    HashMap简介

    HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
    HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
    HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。

    HashMap 的扩容开销很大(需要创建新数组、重新哈希、分配等等)所以有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行扩容(resize)操作(即重建内部数据结构),即将哈希表的桶数(buckets)调整为当前的2倍。

    通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点),过低的话频繁进行扩容(resize),导致性能降低。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少进行扩容(resize)操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生扩容操作。

    HashMap的构造函数

    HashMap共有4个构造函数,如下:

    // 默认构造函数。
    HashMap()
    
    // 指定“容量大小”的构造函数
    HashMap(int initialCapacity)
    
    // 指定“容量大小”和“加载因子”的构造函数
    HashMap(int initialCapacity, float loadFactor)
    
    // 包含“子Map”的构造函数
    HashMap(Map<? extends K, ? extends V> m)

    第2部分 HashMap数据结构

    HashMap的继承关系

    public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable { }

    HashMap与Map关系如下图

    从图中可以看出: 
    (01) HashMap继承于AbstractMap类,实现了Map接口。Map是"key-value键值对"接口,AbstractMap实现了"键值对"的通用函数接口。 
    (02) HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
      table是一个Note[]数组类型,而Note实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Note数组中的。 
      size是HashMap的大小,它是HashMap保存的键值对的数量。 
      threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值="容量*加载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
      loadFactor就是加载因子。 
      modCount是用来实现fail-fast机制的。 

    第3部分 HashMap源码解析(基于JDK1.8)

    为了更了解HashMap的原理,下面对HashMap源码代码作出分析。

    在详细介绍HashMap的代码之前,我们需要了解:HashMap就是一个散列表,它是通过“拉链法”解决哈希冲突的
    还需要再补充说明的一点是影响HashMap性能的有两个参数:初始容量(initialCapacity) 和加载因子(loadFactor)。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行扩容操作(即重建内部数据结构),即将哈希表的桶数(buckets)调整为当前的2倍。

    第3.1部分 HashMap的属性

    3.1.1 HashMap中静态属性和实例属性

    public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{
    
        // 默认的初始容量是16,必须是2的幂。
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  
    
        // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
        static final int MAXIMUM_CAPACITY = 1 << 30;
    
        // 默认加载因子
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
        // 一个桶中链表的树化阈值,当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点
        static final int TREEIFY_THRESHOLD = 8;
    
        // 一个桶中红黑树链表化的阈值 当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原为链表结构
        static final int UNTREEIFY_THRESHOLD = 6;
    
        // 哈希表的最小树形化的桶的容量,当哈希表中的容量大于这个值时,表中的桶才能进行树形化,否则只会扩容,而不进行树化
        static final int MIN_TREEIFY_CAPACITY = 64;
    
        // 存储数据的Node数组,长度是2的幂。
         transient Node[] table;
    
        // HashMap的大小,它是HashMap保存的键值对的数量
        transient int size;
    
        // HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
        int threshold;
    
        // 加载因子实际大小
        final float loadFactor;
    
        // HashMap被改变的次数
        transient volatile int modCount;
    
    }

    HashMap中的key-value都是存储在Node数组中的。

    3.1.2 数据节点Node的数据结构

    说明: 在jdk1.8中“链地址”是由数组+链表+红黑树组成,所以HashMap桶中有两种结构,链表结构和红黑树结构

    链表结构

    static class Node<K,V> implements Map.Entry<K,V> {
            final int hash; //key的hash值(高16和低16异或)
            final K key;
            V value;
            Node<K,V> next;  // 指向下一个节点
    }

      其中Note类型,我们可以看出 Note实际上就是一个单向链表,Note实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数。这些都是基本的读取/修改key、value值的函数。

    红黑树结构

     static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
            TreeNode<K,V> parent;  // 树化后的节点的父节点
            TreeNode<K,V> left;      // 树的左子节点
            TreeNode<K,V> right;    // 树的右子节点
            TreeNode<K,V> prev;    // 在由链表树化时节点的前驱节点
            boolean red;   //节点颜色
            TreeNode(int hash, K key, V val, Node<K,V> next) {
                super(hash, key, val, next);
            }
    }

    由于它继承自 LinkedHashMap.Entry ,而 LinkedHashMap.Entry 继承自 HashMap.Node ,所以它也拥有Node类中属性(hash、key、value、next)以及LinkedHashMap.Entry 的(before、after)属性:

    public class LinkedHashMap<K,V>  extends HashMap<K,V>  implements Map<K,V>{
       
       //内部类Entry
    static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } }

    第3.2部分 HashMap的构造函数

    HashMap共包括4个构造函数

    // 默认构造函数, 创建一个空的哈希表,初始容量为 16,加载因子为 0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    
    // 创建一个空的哈希表,指定“容量大小" 
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    // 创建一个空的哈希表,指定“容量大小”和“加载因子” 
    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);
    }
    
    // 创建一个内容为参数 m 的内容的哈希表
    public HashMap(Map<? extends K, ? extends V> m) {
        tthis.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    说明:如果不是创建一个内容为参数 m 的内容的哈希表,则创建的hashMap对象中Node[] table 为空null,只有在第一次存放元素时table数组会初始化。

    tableSizeFor(int)

    第三种构造方法调用了tableSizeFor(int),该方法用来返回大于等于cap的最小2^次幂值;假如你传入的是 7,返回的初始容量为 8 。

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

    说明 请注意,在调用此方法的地方将方法返回值赋值给了threshold。是因为在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。

    putMapEntries()

    第四种构造方法调用了putMapEntries(),这个方法用于向哈希表中添加整个集合 

     final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
            int s = m.size();
            if (s > 0) {
                if (table == null) {  //数组还是空,初始化参数
                    float ft = ((float)s / loadFactor) + 1.0F;
                    int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                             (int)ft : MAXIMUM_CAPACITY);
                    if (t > threshold)
                        threshold = tableSizeFor(t);
                }
                else if (s > threshold) //数组不为空,超过阈值就扩容
                    resize();
                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);
                }
            }
        }

    说明 在HashMap初始化时,符合s大于0 并且 table等于null的情况。

    第3.3部分 HashMap的主要函数

    确定哈希桶数组索引位置

    不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些(避免hash碰撞),尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。

     static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    以及put、get、remove方法中的计算桶的位置的代码

    tab[i = (n - 1) & hash]

    这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算

    对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值总是相同的。在HashMap中 通过h & (table.length -1)来得到该对象的保存位置,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

    在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

    下面举例说明下,n为table的长度。

    hashMap哈希算法例图

    3.3.1 put()

    put() 的作用是对外提供接口,让HashMap对象可以通过put()将“key-value”添加到HashMap中。其原理图:

    其源码:

    //添加指定的键值对到 Map 中,如果已经存在,就替换
    public V put(K key, V value) {
        //先调用 hash() 方法计算key的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 {
            // 步骤③:节点key存在,直接覆盖value
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            //p 指向要插入的桶第一个元素的位置,如果 p 的哈希值、键和要添加的一样,就停止找,e 指向 p
                e = p;
            else if (p instanceof TreeNode) // 步骤④:判断该链为红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {// 步骤⑤:该链为链表
                 for (int binCount = 0; ; ++binCount) {
                    //如果要put的元素在链表中不存在,则将元素插到链表后面
                    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;
                }
            }
            //如果要put的元素在链表中存在
            if (e != null) {
                V oldValue = e.value;
                //替换,返回
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
      // 步骤⑥:超过最大容量 就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    说明 put方法的过程描述

    ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

    ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

    ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

    ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

    ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

    ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

    3.3.2 . 扩容机制

    当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,而Java里的数组是无法自动扩容的,则使用一个新的数组代替已有的容量小的数组 然后再把旧bucket中元素放到新的bucket中 。

    final Node<K,V>[] resize() {
         Node<K,V>[] oldTab = table;
         int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) { 
            if (oldCap >= MAXIMUM_CAPACITY) { //如果oldCap超过最大值,则只调整threshold,返回原来的hash表
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //新的容量为旧的两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //如果旧容量小于等于 16,新的阈值就是旧阈值的两倍
                newThr = oldThr << 1; // double threshold
        }
        //如果旧容量为 0 ,并且旧阈值>0,说明之前创建了哈希表但没有添加元素,初始化容量等于阈值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //旧容量、旧阈值都是0,说明还没创建哈希表,容量为默认容量,阈值为 容量*加载因子
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果新的阈值为 0 ,就得用 新容量*加载因子 重计算一次
        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;
        //遍历:把每个bucket都移动到新的buckets中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    //旧的桶置为空,便于GC回收
                    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 { //保留旧哈希表桶中链表的顺序
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        //do-while 循环赋值给新哈希表
                        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);
                        // 原索引放到bucket里
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                       // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }          

    说明 因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

    hashMap 1.8 哈希算法例图1

    元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

    hashMap 1.8 哈希算法例图2

     因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,

    这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。

    3.3.3 get()

    get() 的作用是获取key对应的value,它的实现代码如下:

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 直接命中
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 未命中
            if ((e = first.next) != null) {
                // 在树中get
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 在链表中get
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

    说明

    1. bucket里的第一个节点,直接命中;
    2. 如果有冲突,则通过key.equals(k)去查找对应的entry,若为树,则在树中通过key.equals(k)查找,O(logn);若为链表,则在链表中通过key.equals(k)查找,O(n)。

    3.3.4 remove()

    remove() 的作用是删除“键为key”元素

    public V remove(Object key) {
            Node<K,V> e;
            return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
    }
    
    final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
            Node<K,V>[] tab; Node<K,V> p; int n, index;
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
                Node<K,V> node = null, e; K k; V v;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    node = p;
                else if ((e = p.next) != null) {
                    if (p instanceof TreeNode)
                        node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                    else {
                        do {
                            if (e.hash == hash &&
                                ((k = e.key) == key ||
                                 (key != null && key.equals(k)))) {
                                node = e;
                                break;
                            }
                            p = e;
                        } while ((e = e.next) != null);
                    }
                }
                if (node != null && (!matchValue || (v = node.value) == value ||
                                     (value != null && value.equals(v)))) {
                    if (node instanceof TreeNode)
                        ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                    else if (node == p)
                        tab[index] = node.next;
                    else
                        p.next = node.next;
                    ++modCount;
                    --size;
                    afterNodeRemoval(node);
                    return node;
                }
            }
            return null;
    }

    第3.4部分 HashMap实现的Cloneable接口

    HashMap实现了Cloneable接口,即实现了clone()方法。
    clone()方法的作用很简单,就是克隆一个HashMap对象并返回。

    public Object clone() {
            HashMap<K,V> result;
            try {
                result = (HashMap<K,V>)super.clone();
            } catch (CloneNotSupportedException e) {
                // this shouldn't happen, since we are Cloneable
                throw new InternalError(e);
            }
            result.reinitialize();
            result.putMapEntries(this, false);
            return result;
     }

    第3.5部分 HashMap实现的Serializable接口

    HashMap实现java.io.Serializable,分别实现了串行读取、写入功能。
    串行写入函数是writeObject(),它的作用是将HashMap的“总的容量,实际容量,所有的Entry”都写入到输出流中。
    而串行读取函数是readObject(),它的作用是将HashMap的“总的容量,实际容量,所有的Entry”依次读出

    private void writeObject(java.io.ObjectOutputStream s)
            throws IOException {
            int buckets = capacity();
            // Write out the threshold, loadfactor, and any hidden stuff
            s.defaultWriteObject();
            s.writeInt(buckets);
            s.writeInt(size);
            internalWriteEntries(s);
        }
    
        /**
         * Reconstitute the {@code HashMap} instance from a stream (i.e.,
         * deserialize it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws IOException, ClassNotFoundException {
            // Read in the threshold (ignored), loadfactor, and any hidden stuff
            s.defaultReadObject();
            reinitialize();
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new InvalidObjectException("Illegal load factor: " +
                                                 loadFactor);
            s.readInt();                // Read and ignore number of buckets
            int mappings = s.readInt(); // Read number of mappings (size)
            if (mappings < 0)
                throw new InvalidObjectException("Illegal mappings count: " +
                                                 mappings);
            else if (mappings > 0) { // (if zero, use defaults)
                // Size the table using given load factor only if within
                // range of 0.25...4.0
                float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
                float fc = (float)mappings / lf + 1.0f;
                int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                           DEFAULT_INITIAL_CAPACITY :
                           (fc >= MAXIMUM_CAPACITY) ?
                           MAXIMUM_CAPACITY :
                           tableSizeFor((int)fc));
                float ft = (float)cap * lf;
                threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                             (int)ft : Integer.MAX_VALUE);
                @SuppressWarnings({"rawtypes","unchecked"})
                    Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
                table = tab;
    
                // Read the keys and values, and put the mappings in the HashMap
                for (int i = 0; i < mappings; i++) {
                    @SuppressWarnings("unchecked")
                        K key = (K) s.readObject();
                    @SuppressWarnings("unchecked")
                        V value = (V) s.readObject();
                    putVal(hash(key), key, value, false, false);
                }
            }
        }

    第4部分 HashMap遍历方式

    4.1 遍历HashMap的键值对

    第一步:根据entrySet()获取HashMap的“键值对”的Set集合。
    第二步:通过Iterator迭代器遍历“第一步”得到的集合。

    // 假设map是HashMap对象
    // map中的key是String类型,value是Integer类型
    Integer integ = null;
    Iterator iter = map.entrySet().iterator();
    while(iter.hasNext()) {
        Map.Entry entry = (Map.Entry)iter.next();
        // 获取key
        key = (String)entry.getKey();
            // 获取value
        integ = (Integer)entry.getValue();
    }

    4.2 遍历HashMap的键

    第一步:根据keySet()获取HashMap的“键”的Set集合。
    第二步:通过Iterator迭代器遍历“第一步”得到的集合。

    // 假设map是HashMap对象
    // map中的key是String类型,value是Integer类型
    String key = null;
    Integer integ = null;
    Iterator iter = map.keySet().iterator();
    while (iter.hasNext()) {
            // 获取key
        key = (String)iter.next();
            // 根据key,获取value
        integ = (Integer)map.get(key);
    }

    4.3 遍历HashMap的值

    第一步:根据value()获取HashMap的“值”的集合。
    第二步:通过Iterator迭代器遍历“第一步”得到的集合。

    // 假设map是HashMap对象
    // map中的key是String类型,value是Integer类型
    Integer value = null;
    Collection c = map.values();
    Iterator iter= c.iterator();
    while (iter.hasNext()) {
        value = (Integer)iter.next();
    }

    5.HashMap十问

    1. 什么时候会使用HashMap?他有什么特点?
    HashMap是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Node(hash, key, value, next)对象。

    2. 你知道HashMap的工作原理吗?
    HashMap通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

    3. 你知道get和put的原理吗?equals()和hashCode()的都有什么作用?
    通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点

    4. 你知道hash的实现吗?为什么要这样实现?
    在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = key.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。

    5. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
    如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。

    6.为什么哈希表的容量一定要是 2的整数次幂?

    首先,capacity 为 2的整数次幂的话,计算桶的位置 h&(length-1) 就相当于对 length 取模,提升了计算效率;
    其次,capacity 为 2 的整数次幂的话,为偶数,这样 capacity-1 为奇数,奇数的最后一位是 1,这样便保证了 h&(capacity-1) 的最后一位可能为 0,也可能为 1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性;
    而如果 capacity 为奇数的话,很明显 capacity-1 为偶数,它的最后一位是 0,这样 h&(capacity-1) 的最后一位肯定为 0,即只能为偶数,这样任何 hash 值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间。

    因此,哈希表容量取 2 的整数次幂,有以下 2 点好处:
    1. 使用减法替代取模,提升计算效率;
    2. 为了使不同 hash 值发生碰撞的概率更小,尽可能促使元素在哈希表中均匀地散列。

    7.HashMap 中 equals() 和 hashCode() 有什么作用?

    HashMap 的添加、获取时需要通过 key 的 hashCode() 进行 hash(),然后计算下标 ( n-1 & hash),从而获得要找的同的位置。当发生冲突(碰撞)时,利用 key.equals() 方法去链表或树中去查找对应的节点。

    8.  通过实现readObject/writeObject两个方法自定义了序列化的内容

     在HashMap中table 被变量用transient 所修饰,所以该变量不会被默认的序列化机制序列化,而自定义序列化方法的原因:

    1. table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间
    2. 同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。
    以上两个问题中,第一个问题比较好理解,第二个问题解释一下。HashMap 的get/put/remove等方法第一步就是根据 hash 找到键所在的桶位置,但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。

  • 相关阅读:
    MATLAB批量读入图片
    MATLAB小技巧
    Ubuntu下OpenCV不能被某个python版本识别
    切换Ubuntu系统python默认版本的方法
    LoadRunner内部结构(2)
    LoadRunner例子:检查点为参数的一个例子
    LoadRunner中字符串的操作
    LoadRunner脚本实例来验证参数化的取值
    LoadRunner编程之文件的操作
    LoadRunner关联函数的脚本实例--如何操作关联参数
  • 原文地址:https://www.cnblogs.com/lizhouwei/p/9162278.html
Copyright © 2011-2022 走看看