• Java HashMap用法与实现 ZZ


    为了做题用Java语法替代C++map的常用语法,记录一下,剖析原理以后再补上。

    1.import java.util.HashMap;//导入;

    2.HashMap<K, V> map=new HashMap<K, V>();//定义map,K和V是类,不允许基本类型;

    3.void clear();//清空

    4.put(K,V);//设置K键的值为V

    5.V get(K);//获取K键的值

    6.boolean isEmpty();//判空

    7.int size();//获取map的大小

    8.V remove(K);//删除K键的值,返回的是V,可以不接收

    9.boolean containsKey(K);//判断是否有K键的值

    10.boolean containsValue(V);//判断是否有值是V

    11.Object clone();//浅克隆,类型需要强转;如HashMap<String , Integer> map2=(HashMap<String, Integer>) map.clone();


    1.继承与实现

    继承AbstractMap<K,V>,实现Map<K,V>, Cloneable, Serializable

    2.基本属性

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始化大小 16 
    static final float DEFAULT_LOAD_FACTOR = 0.75f;     //负载因子0.75
    static final Entry<?,?>[] EMPTY_TABLE = {};         //初始化的默认数组
    transient int size;     //HashMap中元素的数量
    int threshold;          //判断是否需要调整HashMap的容量

    3.实现方式

    jdk1.7是数组+链表,jdk1.8是数组+链表+红黑树。

    二叉查找树、平衡二叉树、红黑树的概念

    二叉查找树,值唯一,在建树的时候判断插入的节点,如果比根节点小就插到左边,比根节点大就插入到右边,查找的时候通过判断选择正确的方向找下去,而不用遍历一整棵树,效率高。但如果插入值的时候是按顺序插入的,一直加在左边或者右边形成一条链,查找和插入的效率就很慢,所以有了平衡二叉树。

    平衡二叉树,左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。通过左旋右旋各种旋实现的,具体就不清楚了。这种旋转就避免了二叉查找树退化成链表导致查找效率过低的情况。但是严格控制高度的绝对值之差又导致在插入的时候频繁地旋转,浪费时间,所以有了红黑树。

    红黑树,在每个节点加一个存储为表示节点的颜色,非红即黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,在子树高度差上没有那么严格,旋转的次数比较少。因此,红黑树是一种弱的平衡二叉树。

    4.了解一下hashCode

    (一直以为hashCode是唯一的,错得离谱啊)

    Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。对象在jvm上的内存位置是唯一的,但是不同对象的hashcode可能相同,它还要包括其他内容,再根据一定的算法去算出一个值,算出来的可能一样,这就是哈希冲突。

    5.哈希冲突

    HashMap存的是对象,那就有一个哈希值,如果哈希值一样,用链表解决哈希冲突,先定位到数组下标,再去链表里查找。

    1.7是链表,头插,我猜测头插的理由是:新加入的值应该比旧的值更有可能用到,定位到数组节点时,在头部能更快找到。不论头插还是尾插,都需要把整条链表遍历一遍,确定key在不在链表里。1.7版本中,产生哈希冲突时,遍历一条链表查找对象,时间复杂度时O(n),随着链表越来越长,查找的时间越来越大。

    为了提高这个冲突的查找效率,1.8在链表长度超过8时,把链表转变成红黑树,大大减少查找时间。为了防止链表或红黑树巨大,需要了解扩容这个概念。

    6.扩容机制与负载因子

    初始容器容量是16,负载因子默认0.75,最大容量230。意思就是当前容量到达12(16*0.75=12)的时候,会触发扩容机制。数据结构就是为了省时间省空间,扩容机制和负载因子的设定肯定也是为了效率。

    (1)为什么负载因子是0.75?

    如果负载因子太大,例如1时,只有当数组全部填充才会扩容,意味着会有大量的哈希冲突,红黑树变大变复杂,不利于添加查找。如果负载因子太小,例如0.5或者更低时,容量到达一半或者还不到一半的时候就开始扩容,看起来就有点浪费空间。负载因子的设定肯定是权衡了哈希冲突和容量大小。(个人推测,产生大量的对象放进容器,记录哈希值和冲突情况,测试不同负载因子耗费的时间和空间,再用数据分析的方法多方面考虑,选一个最佳的负载因子作为默认值)如果想要空间换时间,减小负载因子,减少哈希冲突。

    (2)容器容量为什么是2的幂次方?

    先了解一下put方法的流程:

    • 先检查大小,如果需要扩容就先扩容;
    • 重新计算key的哈希值hash,定位到数组中的下标;
    • 如果位置上没有元素就直接插入,结束;
    • 如果有元素就用equal检查key是否相同,如果相同就把新value替换旧value
    • key不同就往链表里继续找,没找到key就插入,找得到就替换旧value。

    定位到数组中的下标,最简单的方法就是对容量求模index=hash%n,然而源码的计算方法是index=(n-1)&hash。

    n是2的幂次方,n-1的二进制全是1,按位与和求模结果差不多,但是位运算是直接对内存数据进行操作,不需要转成十进制,快。

    那么每次扩容也要是2的幂次方才能保证n-1的二进制全是1,如果不全是1计算出来的index不均匀。扩容总不会扩4倍8倍,所以是2倍。

    扩容时原本位置也是有规律去变化的,不会丢失原来的索引。

    例如一个对象的hash二进制是10111(23),在容量为16时,对15按位与计算得到的索引为

    10111

    &1111

    =0111(7)

    当容量扩大到32时,对31按位与计算得到的索引为

     10111

    &11111

    =10111(23)

    23-7=16,16正好是扩容的大小。

    7.线程不安全

    在接近临界点时,若此时两个或者多个线程进行put操作,都会进行resize(扩容)和reHash(为key重新计算所在位置),而reHash在并发的情况下可能会形成链表环。总结来说就是在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。为什么在并发执行put操作会引起死循环?是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。jdk1.7的情况下,并发扩容时容易形成链表环,此情况在1.8时就好太多太多了。

    因为在1.8中当链表长度达到阈值(默认长度为8)时,链表会被改成树形(红黑树)结构。如果删剩节点变成7个并不会退回链表,而是保持不变,删剩6个时就会变回链表,7不变是缓冲,防止频繁变换。

    在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
    在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。

    8.哈希碰撞拒绝服务攻击

    用哈希碰撞发起拒绝服务攻击(DOS,Denial-Of-Service attack),常见的场景是攻击者可以事先构造大量相同哈希值的数据,然后以JSON数据的形式发送给服务器,服务器端在将其构建成为Java对象过程中,通常以Hashtable或HashMap等形式存储,哈希碰撞将导致哈希表发生严重退化,算法复杂度可能上升一个数据级,进而耗费大量CPU资源。

    9.和兄弟HashTable的异同

    (1)继承和实现

    HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都同时实现了Map、Cloneable(可复制)、Serializable(可序列化)这三个接口。存储的内容是基于key-value的键值对映射,不能有重复的key,而且一个key只能映射一个value。HashSet底层就是基于HashMap实现的。

    (2)key-value

    HashMap支持key-value、null-value、key-null、null-null这4种方式,但HashTable只支持key-value。

    HashMap不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断,因为使用get()的时候,当返回null时,你无法判断到底是不存在这个key,还是这个key就是null,还是key存在但value是null。

    (3)扩容

    HashMap:默认初始容量是16,严格要求是2的幂次方,每次扩容到原来的2倍

    HashTable:默认初始容量是11,不要求是2的幂次方,每次扩容到原来的2倍+1

    (4)求索引index

    HashMap求索引时用&运算,index=(n-1)&hash

    HashTable求索引用模运算,index = (hash & 0x7FFFFFFF) % n

    (5)线程安全方面

    HashMap线程不安全,在并法包Java. util. concurrent的作用下它有一个对应的线程安全类ConcurrentHashMap

    HashTable是线程安全的,它的一些方法加了synchronized。

    10.了解一下LinkedHashMap

    从Linked这个名字可以知道肯定和链表有关,它的数据结构附加了双向链表,弥补HashMap无序的缺点。

    HashMap在存入的时候通过&计算索引,这个索引不是有序的,所以在遍历HashMap的时候,无法获得插入时的顺序。而LinkedHashMap把插入的节点用链表连接起来,通过链表来遍历,可以获得插入时的顺序。(在不知道这个东西的情况下,要我获取HashMap的插入顺序的话,我会开两个ArrayList或者LinkedList来记录顺序,并且一一对应key和value)。线程不安全。

    11.了解一下HashSet

    Map是映射,那就是key-value。Set是集合,无序不重复,存的只是key,不是两个对象组成的键值对key-value。底层数据结构是HashMap,它存的对象放在key里。线程不安全。

    12.了解一下HashTree

    底层数据结构是裸的红黑树,保证元素有序,没有比较器Comparator的情况按照key的自然排序,可自定义比较器。线程不安全。

    参考:https://yuanrengu.com/2020/ba184259.html

  • 相关阅读:
    Java JVM启动参数
    使用Navicat连接MySQL8.0版本报1251错误
    安装MySQL和出现的问题解决
    跨域问题:解决跨域的三种方案
    Java8 新特性lambda表达式(一)初始
    搭建docker私有仓库
    crontab定时任务
    CentOS610 php环境安装
    Docker常用命令
    PHP调用python脚本执行时报错
  • 原文地址:https://www.cnblogs.com/zhoug2020/p/13386026.html
Copyright © 2020-2023  润新知