关于HashMap的一些知识点整理

只愿长相守 提交于 2020-03-17 01:11:17

本文来自网上各个知识点的整理

        HashMap采用Entry数组(Java8叫Node)来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。

但链表的查询复杂度大,一但链表过长,查询的效率下降。

在Jdk1.8中HashMap的实现方式做了一些改变,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。但每次插入新的数据,都得维护红黑树的结构,复杂度为O(logn)。

链表:插入复杂度O(1),查找复杂度O(n)
红黑树:插入复杂度O(logn),查找复杂度O(logn)
在这里插入图片描述
但既然红黑树这么棒,那为什么HashMap为什么不直接就用红黑树呢?
        因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树。
那为什么链表长度为8时才会选择使用红黑树呢?
        为了配合使用分布良好的HashCode,树节点很少使用。并且在理想状态下,受随机分布的HashCode影响,链表中的节点遵循泊松分布,而且根据统计,链表中节点数是8的概率已经接近千分之一,而且此时链表的性能已经很差了。所以在这种比较罕见和极端的情况下,才会把链表转变为红黑树。因为链表转换为红黑树也是需要消耗性能的,特殊情况特殊处理,为了挽回性能,权衡之下,才使用红黑树,提高性能。通常情况下,链表长度很难达到8,但是特殊情况下链表长度为8,哈希表容量又很大,造成链表性能很差的时候,只能采用红黑树提高性能,这是一种应对策略。
HashMap一些常用方法:

		HashMap<String,Integer> hash=new HashMap<String,Integer>();
		HashMap<String,Integer> hash1=new HashMap<String,Integer>();
		//判断hash是否为空
		System.out.println(hash.isEmpty());//true
		//向hash中添加值(替换这个key以前的值并返回以前的值,如果没有,则返回null)
		System.out.println(hash.put("demo", 1));//null
		System.out.println(hash.put("demo", 2));//1
		//得到hash中key对应的value值
		System.out.println(hash.get("1"));//null
		System.out.println(hash.get("demo"));//2
		System.out.println(hash.isEmpty());//false
		//判断hash中是否存在这个key
		System.out.println(hash.containsKey("demo"));//true
		System.out.println(hash.containsKey("demo1"));//false
		//判断hash中对否存在这个value
		System.out.println(hash.containsValue(1));//false
		System.out.println(hash.containsValue(2));//true
		//删除这个key下的value,返回这个value
		System.out.println(hash.remove("1"));//null
		hash.put("demo1", 3);
        System.out.println(hash.remove("demo"));//2
        hash.put("demo2", 3);
        //显示所有的value
        System.out.println(hash.values());//[3,3]
        //显示hash里的值的个数
        System.out.println(hash.size());//2
        //显示hash中所有的key
        System.out.println(hash.keySet());//[demo1,demo2]
        //显示所有的key和value
        System.out.println(hash.entrySet());//[demo1=3, demo2=3]
        hash1.put("demo3", 6);
        //将同一类型的hash1的值添加到hash中
        hash.putAll(hash1);
        hash.put("demo3", 4);
        //删除这个key和value
        System.out.println(hash.remove("demo3", 1));//false
        System.out.println(hash.remove("demo3", 4));//true
        //替换这个key和value,返回之前的value
        System.out.println(hash.replace("demo2", 1));//3
        //克隆这个hash
        System.out.println(hash.clone());//{demo1=3, demo2=1}
        Object clone = hash.clone();
        System.out.println(clone);//{demo1=3, demo2=1}
        //判断当前hash是否存在key,若存在,返回value;若不存在,则执行put操作
        System.out.println(hash.putIfAbsent("demo1", 3));//3
        System.out.println(hash.putIfAbsent("demo3", 8));//null       
        //判断当前hash是否存在key,若存在,返回value;若不存在,则返回null,不进行put操作
        System.out.println(hash.computeIfPresent("demo1", (k,v) -> v+1));//4
        System.out.println(hash.computeIfPresent("demo5", (k,v) -> v+1));//null
        System.out.println(hash);//{demo3=8, demo1=3, demo2=1}
        //如果当前 hash的value为xx时则值为xx否则为xx
        hash.compute("demo2", (k,v)->v==1?-1:0);
        System.out.println(hash);//{demo3=8, demo1=3, demo2=-1}
        //判断当前hash是否存在key,若存在,则进行lambda表达式;若不存在,则进行put操作,返回value值
        System.out.println(hash.merge("demo5", 2, (oldVal, newVal) -> oldVal + newVal));//2
        System.out.println(hash.merge("demo1", 2, (oldVal, newVal) -> oldVal + newVal));//6
        System.out.println(hash);//{demo3=8, demo5=2, demo1=6, demo2=-1}
        //当hash中有这个key时,就使用这个key值,并返回这个key对应的value;如果没有就返回默认值defaultValue 。
        System.out.println(hash.getOrDefault("demo1", 1));//6
        System.out.println(hash.getOrDefault("demo", -1));//6
    	System.out.println(hash);//{demo3=8, demo5=2, demo1=6, demo2=-1}

HashMap的工作原理
        HashMap是以键值对(key-value)的形式存储元素,通过put(key,value)和get(key)方法存储和获取对象。HashMap需要一个hash函数,它使用hashCode()和equals()方法来向集合/从集合添加和检索元素。当调用put()方法的时候,HashMap会调用传入的键对象的hashCode()方法来返回一个hashCode值,通过hashCode值找到bucket位置来存Entry对象。HashMap是在bucket中储存键对象和值对象,作为Map.Entry。

当两个不同的键对象的hashCode相同时会发生什么?
        两个对象的hashCode一样,所以他们的bucket的位置相同,碰撞发生,HashMap使用LinkedList存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在LinkedList中。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。put和get都首先会调用hashcode方法,去查找相关的key,当有冲突时,再调用equals。

如果两个键的hashcode相同,将如何获取值对象?
        调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
        默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

重新调整HashMap大小存在什么问题
        当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

HashMap 为什么是线程不安全的
        在put的时候,因为该方法不是同步的,假如有两个线程A,B它们的put的key的hash值相同,不论是从头插入还是从尾插入,假如A获取了插入位置为x,但是还未插入,此时B也计算出待插入位置为x,则不论AB插入的先后顺序肯定有一个会丢失

        在扩容的时候,jdk1.8之前是采用头插法,当两个线程同时检测到HashMap需要扩容,在进行同时扩容的时候有可能会造成链表的循环,主要原因就是,采用头插法,新链表与旧链表的顺序是反的,在1.8后采用尾插法就不会出现这种问题,同时1.8的链表长度如果大于8就会转变成红黑树。

HashMap和HashTable有什么区别
        HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary(已被废弃,详情看源代码)。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
         Hashtable比HashMap多提供了elments() 和contains() 两个方法。HashMap将contains()方法拆分为containsKey()和containsValue()。

        HashMap是非synchronized的,意味着不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。而HashTable是synchronized的,说明HashTable是线程安全的,多个线程可以共享一个HashTable。

        HashMap可以接受有一个key为null,多个value为null,所以当get()返回的是NULL时,不能说明为空,要通过containsKey()来判断。而Hashtable不支持NULL。

        HashMap的Iterator是fail-fast迭代器。当有其它线程改变了HashMap的结构(增加,删除,修改元素),将会抛出ConcurrentModificationException。不过,通过Iterator的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。在JDK8之前的版本中,Hashtable是没有fast-fail机制的。在JDK8及以后的版本中 ,Hashtable也是使用fast-fail的。

        HashMap的初始长度为16,之后每次扩充变为原来的两倍。在创建时,如果给定了大小,则将其扩充为2的幂次方大小。Hashtable的初始长度是11,之后每次扩充容量变为之前的2n+1(n为上一次的长度)。在创建时,如果给定了大小,则直接使用给定大小。

        在根据元素的 key计算hash值时,Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数发来获得最终的位置。而HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。

可以使用CocurrentHashMap来代替HashTable吗?
        HashTable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。Hashtable作为jdk1.2遗留下来的类,到jdk1.8没有大改,所以对数据的一致性要求较低的话可以使用ConcurrentHashMap来替代Hashtable。
        CocurrentHashMap底层采用分段的数组+链表实现,线程安全
        CocurrentHashMap通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
        HashTable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占, ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
        CocurrentHashMap有些方法需要跨段,比如size()和containsValue(),它们可能需 要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放 所有段的锁
        扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个 Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

为什么String, Interger这样的wrapper类适合作为键?
        String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

可以使用自定义的对象作为键吗?
        这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

HashMap和HashSet的区别

HashMap HashSet
HashMap实现了Map接口 HashSet实现了Set接口
HashMap储存键值对 HashSet仅仅存储对象
是引用put()方法将元素放入map中 使用add()的方法将元素放入set中
HashMap实现了Map接口 HashSet实现了Set接口
HashMap中使用键值对象计算hashCode值 HashSet采用成员对象来计算hashCode值,
对于两个对象来说hashCode可能相同,
所以equals()方法用来判断对象的相等性,
如果两个对象不同的话,那么返回false
HashMap比价块因为采用唯一的键来获取对象 HashSet较比HashMap来说慢

HashMap扩容机制
        当HashMap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了,就是原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

那么HashMap什么时候进行扩容呢?
        当HashMap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置.

那么为什么数组长度为2的n次方幂是最好
        当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!