• HashMap的key可以是可变的对象吗???


    HashMap的key可以是可变的对象吗???

      大家都知道,HashMap的是key-value(键值对)组成的,这个key既可以是基本数据类型对象,如Integer,Float,同时也可以是自己编写的对象,那么问题来了,这个作为key的对象是否能够改变呢?或者说key能否是一个可变的对象?如果可以该HashMap会怎么样?

    可变对象

      可变对象是指创建后自身状态能改变的对象。换句话说,可变对象是该对象在创建后它的哈希值(由类的hashCode()方法可以得出哈希值)可能被改变

      为了能直观的看出哈希值的改变,下面编写了一个类,同时重写了该类的hashCode()方法和它的equals()方法【至于为什么要重写equals方法可以看博客:http://www.cnblogs.com/0201zcr/p/4769108.html】,在查找和添加(put方法)的时候都会用到equals方法。

      在下面的代码中,对象MutableKey的键在创建时变量 i=10 j=20,哈希值是1291。

      然后我们改变实例的变量值,该对象的键 i 和 j 从10和20分别改变成30和40。现在Key的哈希值已经变成1931。

      显然,这个对象的键在创建后发生了改变。所以类MutableKey是可变的。

      让我们看看下面的示例代码:

    复制代码
    public class MutableKey {
        private int i;
        private int j;
     
        public MutableKey(int i, int j) {
            this.i = i;
            this.j = j;
        }
     
        public final int getI() {
            return i;
        }
     
        public final void setI(int i) {
            this.i = i;
        }
     
        public final int getJ() {
            return j;
        }
     
        public final void setJ(int j) {
            this.j = j;
        }
     
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + i;
            result = prime * result + j;
            return result;
        }
     
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (!(obj instanceof MutableKey)) {
                return false;
            }
            MutableKey other = (MutableKey) obj;
            if (i != other.i) {
                return false;
            }
            if (j != other.j) {
                return false;
            }
            return true;
        }
    }
    复制代码

    测试:

    复制代码
    public class MutableDemo {
     
        public static void main(String[] args) {
     
            // Object created
            MutableKey key = new MutableKey(10, 20);
            System.out.println("Hash code: " + key.hashCode());
     
            // Object State is changed after object creation.
            key.setI(30);
            key.setJ(40);
            System.out.println("Hash code: " + key.hashCode());
        }
    }
    复制代码

    结果:

    Hash code: 1291
    Hash code: 1931

       只要MutableKey 对象的成员变量i或者j改变了,那么该对象的哈希值改变了,所以该对象是一个可变的对象。

    HashMap如何存储键值对

      HashMap底层是使用Entry对象数组存储的,而Entry是一个单项的链表。当调用一个put()方法将一个键值对添加进来是,先使用hash()函数获取该对象的hash值,然后调用indexFor方法查找到该对象在数组中应该存储的下标,假如该位置为空,就将value值插入,如果该下标出不为空,则要遍历该下标上面的对象,使用equals方法进行判断,如果遇到equals()方法返回真的则进行替换,否则将其插入,源码详解可看:http://www.cnblogs.com/0201zcr/p/4769108.html

      查找时只需要查询通过key值获取获取hash值,然后找到其下标,遍历该下标下面的Entry对象即可查找到value。【具体看下面源码及其解释】

    在HashMap中使用可变对象作为Key带来的问题

      如果HashMap Key的哈希值在存储键值对后发生改变,Map可能再也查找不到这个Entry了

    复制代码
    public V get(Object key)   
    {   
     // 如果 key 是 null,调用 getForNullKey 取出对应的 value   
     if (key == null)   
         return getForNullKey();   
     // 根据该 key 的 hashCode 值计算它的 hash 码  
     int hash = hash(key.hashCode());   
     // 直接取出 table 数组中指定索引处的值,  
     for (Entry<K,V> e = table[indexFor(hash, table.length)];   
         e != null;   
         // 搜索该 Entry 链的下一个 Entr   
         e = e.next)         // ①  
     {   
         Object k;   
         // 如果该 Entry 的 key 与被搜索 key 相同  
         if (e.hash == hash && ((k = e.key) == key   
             || key.equals(k)))   
             return e.value;   
     }   
     return null;   
    }   
    复制代码

      上面是HashMap的get()方法源码,通过上面我们可以知道,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。 

      同时我们也看到,判断是否找到该对象,我们还需要判断他的哈希值是否相同,假如哈希值不相同,根本就找不到我们要找的值。

      如果Key对象是可变的,那么Key的哈希值就可能改变。在HashMap中可变对象作为Key会造成数据丢失。

      下面的例子将会向你展示HashMap中有可变对象作为Key带来的问题。

    复制代码
    import java.util.HashMap;
    import java.util.Map;
     
    public class MutableDemo1 {
     
        public static void main(String[] args) {
     
            // HashMap
            Map<MutableKey, String> map = new HashMap<>();
     
            // Object created
            MutableKey key = new MutableKey(10, 20);
     
            // Insert entry.
            map.put(key, "Robin");
     
            // This line will print 'Robin'
            System.out.println(map.get(key));
     
            // Object State is changed after object creation.
            // i.e. Object hash code will be changed.
            key.setI(30);
     
            // This line will print null as Map would be unable to retrieve the
            // entry.
            System.out.println(map.get(key));
        }
    }
    复制代码

    输出:

    Robin
    null

    如何解决

      在HashMap中使用不可变对象。在HashMap中,使用String、Integer等不可变类型用作Key是非常明智的。 

      我们也能定义属于自己的不可变类

      如果可变对象在HashMap中被用作键,那就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可。

      在下面的Employee示例类中,哈希值是用实例变量id来计算的。一旦Employee的对象被创建,id的值就不能再改变。只有name可以改变,但name不能用来计算哈希值。所以,一旦Employee对象被创建,它的哈希值不会改变。所以Employee在HashMap中用作Key是安全的。

    复制代码
    import java.util.HashMap;
    import java.util.Map;
     
    public class MutableSafeKeyDemo {
     
        public static void main(String[] args) {
            Employee emp = new Employee(2);
            emp.setName("Robin");
     
            // Put object in HashMap.
            Map<Employee, String> map = new HashMap<>();
            map.put(emp, "Showbasky");
     
            System.out.println(map.get(emp));
     
            // Change Employee name. Change in 'name' has no effect
            // on hash code.
            emp.setName("Lily");
            System.out.println(map.get(emp));
        }
    }
     
    class Employee {
        // It is specified while object creation.
        // Cannot be changed once object is created. No setter for this field.
        private int id;
        private String name;
     
        public Employee(final int id) {
            this.id = id;
        }
     
        public final String getName() {
            return name;
        }
     
        public final void setName(final String name) {
            this.name = name;
        }
     
        public int getId() {
            return id;
        }
     
        // Hash code depends only on 'id' which cannot be
        // changed once object is created. So hash code will not change
        // on object's state change
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + id;
            return result;
        }
     
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Employee other = (Employee) obj;
            if (id != other.id)
                return false;
            return true;
        }
    }
    复制代码

    输出

    Showbasky
    Showbasky

       致谢:感谢您的耐心阅读!

    本文翻译自 Coding Geek, 原文地址。英文水平有限,有些地方翻译得不太精确

    绝大多数Java开发者都在使用Map类,尤其是HashMap。HashMap是一种简单易用且强大的存取数据的方法。但是,有多少人知道HashMap内部是如何工作的?几天前,为了对这个基本的数据结构有深入的了解,我阅读大量的HashMap源码(开始是Java7,然后是Java8)。在这篇文章里,我会解释HashMap的实现,介绍Java8的新实现,聊一聊性能,内存,还有使用HashMap时已知的一些问题。

    内部存储

    HashMap 类实现了Map<k,v>接口,这个接口的基本主要方法有:

    • V put(K key, V value)
    • V get(Object key)
    • V remove(Object key)
    • Boolean containsKey(Object key)

    HashMap使用了内部类Entry<k,v>来存储数据,这个类是一个带有两个额外数据的简单 键-值对 结构:

    • 一个是另一个Entry<k,v>的引用,这样HashMap可以像单独的链表一样存储数据
    • 一个hash值,代表了key的哈希值,避免了HashMap每次需要的时候再来计算

    下面是Java7里Entry的部分实现:

    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            int hash;
    …
    }
    

    HashMap存储数据到多个单独的entry链表里,所有的链表都登记到一个Entry数组里(Entry<K,V>[] array),并且这个内部数组默认容量是16。

    下面的图片展示了一个HashMap实例的内部存储,一个可为null的Entry数组,每一个Entry都可以链接到另一个Entry来形成一个链表:

    HashMap数据存储示意图

    所有具有相同哈希值的key都会放到同一个链表里,具有不同哈希值的key最终也有可能在同一个链表里。

    当调用 put(K key, V value)或者get(Object key)这些方法时,会先计算这个Entry应该存放的链表在内部数组中的索引(index),然后方法会迭代整个链表来寻找具有相同key的Entry(使用key的 equals()方法)

    get()方法,会返回这个Entry关联的value值(如果Entry存在)
    put(K key, V value)方法,如果Entry存在则重置value值,如果不存在,则以key,value参数构造一个Entry并插入到链表的头部。

    获取链表在数组内的索引通过三个步骤确定:

    • 首先获取Key的哈希值
    • 对哈希值再次进行哈希运算,避免出现一个很差的哈希算法,把所有的数据放到内部数组的同一个链表里
    • 对再次哈希的哈希值进行数组长度(最小为1)的位掩码运算,这个运算保证生成的索引不会比数组的长度大,你可以把它当成一个优化过的取模运算

    下面是Java7 和 Java8处理索引的源代码:

    // the "rehash" function in JAVA 7 that takes the hashcode of the key
    static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    // the "rehash" function in JAVA 8 that directly takes the key
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    // the function that returns the index from the rehashed hash
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
    

    为了更高效的运作,内部数组的大小必须是2的指数大小,让我们来看看这是为什么。

    想象一下数组大小是17,掩码值就是16(size-1),16的二进制表示是 0…010000,那么对于任何哈希值H通过位运算H AND 16得到的索引就只会是16或者0,这意味着17大小的链表数组只会使用到两个:索引为0的和索引为16的,非常浪费。

    但是,如果你取2的指数大小例如16,位运算是 H AND 15,15的二进制表示是 0…001111, 那么取索引的运算就会输出0~15之间的值,大小16的数据就能完全使用到。举例:

    • 如果H = 952 二进制表示为 0..0111011 1000, 相关的索引就是 0…01000 = 8
    • 如果H = 1576 二进制表示为 0..01100010 1000, 相关的索引就是 0…01000 = 8
    • 如果H = 12356146 二进制表示为 010111100100010100011 0010, 相关的索引就是 0…00010 = 2
    • 如果H = 59843 二进制表示为 0111010011100 0011, 相关的索引就是 0…00011 = 3

    这就是为什么数组的大小必须是2的指数大小,这个机制对开发人员是透明的,如果选择了一个37大小的HashMap,那么Map会自动选择37之后的一个2的指数大小(64)来做为内部数组的容量。

    自动调整大小

    我们获取到索引之后,函数(put,get或者remove) 访问/迭代 关联的链表,检查是否有指定key对应的Entry。 不做改动的话,这个机制会带来性能问题,因为这个函数会遍历整个链表来检查Entry是否存在。

    想象一下如果内部数组大小是初始值16,我们有两百万条数据需要存储,最好的情况下, 每个链表里平均有 125 000个数据(2000000/16).因此,每个get(),remove(),put()会导致125 000个迭代或者操作。为了避免出现这种情况,HashMap会自动调整它的内部数组大小来保持每个链表尽可能的短。

    当你创建一个HashMap时,你可以指定一个初始化大小和一个载入因数:

    public HashMap(int initialCapacity, float loadFactor)
    

    如果不指定参数,缺省的initialCapacity是16,loadFactor是0.75,initialCapacity即代表了Map内部数组的大小。

    每次当你调用put()方法加入一个新的Entry时,这个方法会检测是否需要增加内部数组大小,因此map存储了两个数据:

    • map的大小,代表了HashMap里 Entry的数量,每次新增或者移除Entry时都会更新这个值
    • 一个阈值: 内部数组大小 * 载入因数 ,每次自动调整大小后都会刷新

    添加一个新Entry时,put函数会检查 map的大小 是否大于阈值 ,如果大于,则会创建一个双倍大小的数组,当新数组的大小改变,索引计算函数(返回 哈希值 & (数组大小-1) 的位运算)也会跟着改变。因此,数组的重新调整新建了两倍数量的链表,并且 重新分发现有的Entry到这些数组内(注:原文括号有下面一句补充,暂时不明白是什么意思。看HashMap的源代码,是所有的数据分发到新的数组内,旧的直接弃用)

    (the old ones and the newly created).

    自动调整的目的是减少链表的长度从而减小 put(),remove(),get()等函数的时间开销,所有具有相同哈希值的Entry在重新调整大小后还会在同一个链表内,原来在同一个链表内具有不同哈希值的Entry则有可能不在同一个链表内了。

    上面这个图展示了一个HashMap自动调整前后的情况,在调整前,为了拿到Entry E,必须要迭代5次,调整后,只需要两次。速度快了两倍!

    注意:HashMap只会增加内部数组的大小,没有提供方法变小。

    线程安全

    如果你已经了解过HashMap,你知道它不是线程安全的,但是有没有想过为什么?

    想象一下这种场景:你有一个写线程只往Map里写新数据,还有一个读线程只往里读数据,为什么不能很好的运作?

    因为在重新调整内部数组大小的时候,如果线程正在写或者取对象,Map可能会使用调整前的索引,这样就找不到调整后的Entry所在的位置了。

    最坏的情况是:两个线程同时往里面放数据,同时调用了调整内部数组大小的方法。当两个线程都在修改链表时,Map其中的某个链表可能会陷入一个内部循环,如果你试图在这个链表里取数据时,可能会永远取不到值。

    HashTable 为了避免这种情况,做了线程安全的实现。但是,所有的CRUD方法都是 同步阻塞的,所以会很慢。例如,线程1调用get(key1),线程2调用get(key2),线程3调用get(key3),同一时间只会有一个线程能拿到值,即使他们本来可以同时获取这三个值。

    其实从Java5开始就有一个更高效的线程安全的HashMap的实现了:ConcurrentHashMap。只有链表是同步阻塞的,因此多线程可以同时get,put,或者remove数据,只要没有访问同一个链表或者重新调整内部数组大小就行。在多线程应用里,使用这种实现显然会更好。

    key的不变性

    为什么字符串和整数是HashMap的Key的一种很好的实现呢? 大多是因为他们的不变性。如果你选择自己新建一个Key类并且不保证它的不变性的话,在HashMap里面可能就会丢失数据,让我们来看下面一种使用情况:

    • 你有一个key,内部值是1
    • 你用这个key往HashMap里存了一个数据
    • HashMap从这个key的哈希码里生成了一个哈希值(就是从1的哈希码获取)
    • Map在最近创建的Entry里存储了这个哈希值
    • 你把key的内部值改成2
    • key的哈希码改变了但是HashMap不知道(因为已经存了旧的哈希值)
    • 你想要用改变后的key获取数据
    • Map会计算你的key的新哈希码,来定位到数据位于哪个链表:
      • 情况1:你已经改了你的key,map试图从错误的链表里寻找数据,当然找不到
      • 情况2:你很幸运!改变后的key生成的索引和改变前一样,map遍历整个链表寻找具有相同key的Entry。但是为了匹配key,map先会匹配key的哈希值然后调用equals()方法来对照。因为你改变后的key哈希值也已经变了,map最终也找不到相应的Entry (注:应该也有可能找到错误的数据出来)

    这里有一个具体的例子,我存了两个键值对到Map里,我修改了第一个key并且试图拿出这两个值,只有第二个值有返回,第一个值已经丢失在Map里:

    public class MutableKeyTest {
    
        public static void main(String[] args) {
    
            class MyKey {
                Integer i;
    
                public void setI(Integer i) {
                    this.i = i;
                }
    
                public MyKey(Integer i) {
                    this.i = i;
                }
    
                @Override
                public int hashCode() {
                    return i;
                }
    
                @Override
                public boolean equals(Object obj) {
                    if (obj instanceof MyKey) {
                        return i.equals(((MyKey) obj).i);
                    } else
                        return false;
                }
    
            }
    
            Map<MyKey, String> myMap = new HashMap<>();
            MyKey key1 = new MyKey(1);
            MyKey key2 = new MyKey(2);
    
            myMap.put(key1, "test " + 1);
            myMap.put(key2, "test " + 2);
    
            // modifying key1
            key1.setI(3);
    
            String test1 = myMap.get(key1);
            String test2 = myMap.get(key2);
    
            System.out.println("test1= " + test1 + " test2=" + test2);
    
        }
    
    }
    
    

    输出结果是test1= null test2=test 2,和预期的一样,Map用改变后的key1找不回第一个字符串。

    JAVA8的改进

    Java8里,HashMap的内部表示已经改变了很多了。的确,Java7里HashMap的实现有1K行代码,而Java8里有2K。我前面所说的大部分都是真的,除了Entry链表。在Java8里,仍然存在一个内部数组不过里面存储的都是节点(Node),但是节点包含的信息和Entry完全一样,因为也可以看做链表,下面是Java8里节点实现的部分代码:

       static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
    

    那么对比Java7最大的变化是什么呢?节点(Nodes)可以被树节点(TreeNodes)继承。树节点是一种红黑树的数据结构,存储了更多信息,可以让你以O(log(n))的算法复杂度新增,删除或者是获取一个元素。

    下面是一个树节点内存储的数据的详细列表供参考:

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        final int hash; // inherited from Node<K,V>
        final K key; // inherited from Node<K,V>
        V value; // inherited from Node<K,V>
        Node<K,V> next; // inherited from Node<K,V>
        Entry<K,V> before, after;// inherited from LinkedHashMap.Entry<K,V>
        TreeNode<K,V> parent;
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;
        boolean red;
    

    红黑树是一种自平衡的二分搜索树。它的内部机制确定了不管是新增还是移除节点,长度永远在log(n)内。使用这种树的一个主要优点是,当一个内部表有许多相同的数据在同一个容器内时,在树中搜索会花费O(log(n))的时间复杂度,而链表会花费log(n)

    如你所见,树比链表占用了更多的空间(我们稍后会谈到这个)。

    通过继承,内部表可以包含 节点(链表) 和 树节点(红黑树)两种节点。Oracle通过下面的规则,决定同时使用这两种数据结构:

    • 如果一个内部表的索引超过8个节点,链表会转化为红黑树
    • 如果内部表的索引少于6个节点,树会变回链表

    上图展示了一个Java8 HashMap的内部数组的结构,具有树(桶0),和链表(桶1,2,3) ,桶0因为有超过8个节点所以结构是树。

    内存开销

    JAVA7

    使用HashMap会带来一定的内存开销,在Java7里,一个HashMap用Entry包含了 许多键值对,一个Entry里会有:

    • 下一个entry的引用
    • 一个预计算好的哈希值(整型)
    • 一个key的引用
    • 一个value的引用

    此外,Java7里 HashMap使用一个 Entry的内部数组。假设 一个HashMap包含了N个元素,内部数组容量是 C, 额外内存开销约为:
    sizeOf(integer) * N + sizeOf(reference) * (3 * N +C)

    • 一个整数是 4 字节
    • 一个引用的大小取决于 JVM/OS/Precessor 不过通常也是4字节

    小贴士:从JAVA7起,HashMap类初始化的方法是懒惰的,这意味着即使你分配了一个HashMap,内部Entry数组在内存里也不会分配到空间( 4 * 数组大小 个字节),直到你调用第一个put()方法

    JAVA8

    java8的实现里,获取内存用量变得稍微复杂了一点。因为 Entry 和 树节点包含的数据是一样的,但是树节点会多6个引用和1个布尔值。

    如果全部都是 普通链表节点,那么内存用量和java7一样。
    如果全部都是 树节点,内存用量变成:
    N * sizeOf(integer) + N * sizeOf(boolean) + sizeOf(reference)* (9*N+CAPACITY )
    在大多数标准的 JVM里,这个式子等于 44 * N + 4 * CAPACITY字节

    性能问题

    倾斜HashMap和平衡HashMap

    最好的情况下,get/put方法只有 O(1)的时间复杂度。但是,如果你不关心key的哈希函数,调用put/get/方法可能会非常慢。

    put/get的良好性能取决于如何分配数据到内部数组不同的索引。如果key的哈希函数设计不良,你会得到一个倾斜的HashMap(和内部数组大小无关)。所有在最长链表上的put/get会非常慢,因为会遍历整个链表。最坏的情况下(所有数据都在同一个索引下), 时间复杂度是O(n).

    下面是一个例子,第一个图片展示了一个倾斜HashMap,第二个图则是一个平衡的HashMap:

    这个倾斜HashMap在索引0上的get/put非常耗时,获取Entry K会进行6次迭代

    在这个平衡HashMap内,获取Entry K只要进行3次迭代。这两个HashMap存储的数据量相同,内部数组大小也一样。唯一的区别,就是分发数据的key的哈希函数。

    下面是一个极端的例子,我创建了一个哈希函数,把两百万的数据都放到同一个数组索引下:

    public class Test {
    
        public static void main(String[] args) {
    
            class MyKey {
                Integer i;
                public MyKey(Integer i){
                    this.i =i;
                }
    
                @Override
                public int hashCode() {
                    return 1;
                }
    
                @Override
                public boolean equals(Object obj) {
                …
                }
    
            }
            Date begin = new Date();
            Map <MyKey,String> myMap= new HashMap<>(2_500_000,1);
            for (int i=0;i<2_000_000;i++){
                myMap.put( new MyKey(i), "test "+i);
            }
    
            Date end = new Date();
            System.out.println("Duration (ms) "+ (end.getTime()-begin.getTime()));
        }
    }
    

    在我的机器上(core i5-2500k @ 3.6Ghz),这个程序跑了超过45分钟(java 8u40),45分钟后我中断了这个程序。

    现在,我运行相同的代码,只是使用下面的哈希函数:

            @Override
        public int hashCode() {
            int key = 2097152-1;
            return key+2097152*i;
            }
    

    结果只花了 46秒 !! 这个哈希函数比先前那一个有一个更好的数据分发所以put函数运行快得多。

    如果我还是运行这段代码,但是换成下面这个更好的哈希函数:

     @Override
     public int hashCode() {
     return i;
     }
    

    现在,程序只需要2秒。

    我希望你意识到哈希函数有多么重要。如果上面的测试在java7上运行,第一个和第二个测试的性能甚至还会更差(java7的复杂度是 O(n),java8是 O(log(n)))

    当你使用HashMap时,你需要找到一个哈希函数,可以 把key分发到尽量多的索引上,为了做到这一点,你需要避免哈希碰撞。字符串是不错的一种key,因为它有 很不错的哈希函数。整数做key也不错,因为它的哈希函数就是本身的值。

    重设大小的开销

    如果你需要存储大量数据,你应该在创建HashMap时设置一个接近你预期值的初始化大小。如果你不这么做,map会用默认的 16数组大小和0.75的 载入因数。 前面11个put会很快但是第12个(16*0.75)会创建一个容量为32的新数组,第13~23个put也会很快但是第24个会再次创建一个双倍大小的数组。这个内部重设大小的操作会出现在第48次,96次,192次……。在数据量较小时,这个操作很快,但是当数据量增大时,这个操作会费时数秒到数分钟不等。通过指定预期初始化大小,你可以避免这些操作开销。

    但是这也有一个弊端,如果你设置了一个很大的数组大小像 2^28而你只用了2^26,你会浪费掉大量的内存(这个例子里大约是 2^30 字节)

    总结

    对于简单的使用,你不需要知道HashMap是如何工作的,因为你感觉不出 O(1)、O(n)、O(log(n))的区别。但是了解这种最常用的数据结果的底层机制总是有好处的,何况,对于java开发者来说,这是一个很典型的面试问题。在大数据量时,知道它是如果工作的,知道哈希函数的重要性 就变得非常重要了。

    希望这篇文章能帮助你加深对HashMap实现细节的了解。

  • 相关阅读:
    Sql与Asp.Net数据类型对应
    EditPlus 使用技巧集萃
    VB.NET and C# Comparison
    测试后行之CodeSmith模板
    ASP.NET需要改进的地方
    LeetCode: Minimum Path Sum
    LeetCode: Merge k Sorted Lists
    LeetCode: Merge Intervals
    LeetCode: Maximum Subarray
    LeetCode: Median of Two Sorted Arrays
  • 原文地址:https://www.cnblogs.com/diegodu/p/6824139.html
Copyright © 2020-2023  润新知