zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    ASP.NET 表单验证 Part.1(理解表单验证)
    Silverlight 简介 Part.3(设计 Siverlight 页面)
    ASP.NET 成员资格 Part.3(LoginStatus、LoginView、PasswordRecovery)
    ASP.NET 网站部署 Part.1(安装IIS、复制文件部署网站)
    ASP.NET Dynamic Data Part.1(创建动态数据应用程序)
    ASP.NET 安全模型 Part.2(SSL)
    ASP.NET MVC Part.2(扩展基本的 MVC 应用程序)
    ASP.NET 网站部署 Part.2(使用 Web 部署)
    开发高级 Web 部件
    创建 Web 部件(WebPart 类、简单的 Web 部件)
  • 原文地址:https://www.cnblogs.com/zhoug2020/p/13386026.html
Copyright © 2011-2022 走看看