- wio
-
集合可以说是学习 Java 中最重要的一块知识点了,无论做任何业务系统,集合总是最为基础的那块 API。我第一次接触集合,是在我大三的时候,那时候去面试,面试官问我:你了解过集合吗?可惜那时候没什么项目经验,所以基本没有了解过,因此也错失了机会。
到了现在,我已经工作了5年了,也做过了大大小小十几个项目。这些项目中有简单的 SSH 项目,也有分布式高并发的复杂项目。无论在哪个项目中,关于集合的时候是必不可少的。但我现在慢慢回顾过去做的项目,我发现自己使用到的集合还是比较少,基本上只有:ArrayList、HashSet、HashMap 这几个。
但当我开始深入去了解 JDK 集合的整个体系时,我发现之前的我了解得确实非常浅显。例如关于 List 的实现有 ArrayList、LinkedList、Vector、Stack 这四种实现,但我们很多时候只是直接使用 ArrayList,而不是根据场景去选择。
1.学习集合源码,能够让我们使用得更加准确。
当我们深入学习了源码之后,我们就能够了解其特性,从而能够根据我们的使用场景去做出更好的选择,从而让我们的代码运行效率更高。
我们举一个最简单的例子 —— ArrayList 和 LinkedList。它们两者底层采用了完全不同的实现方式,ArrayList 使用数组实现,而 LinkedList 则使用链表实现。这使得 ArrayList 的读取效率高,而 LinkedList 的读取效率低。但因为 LinkedList 采用链表实现,所以其增加和删除比较方便,而 ArrayList 则比较麻烦。所以 ArrayList 比较适合用于读场合较多的情况,而 LinkedList 比较适合用于增加、删除较多的场景。
我们来看另外一个例子 —— HashMap 和 TreeMap。乍看之下,他们都是 Map 集合的实现,但是它们内部有着截然不同的实现。HashMap 是 Map 接口的哈希实现,其内部使用了链表和红黑树实现。而 TreeMap 是 Map 接口的有序实现,其内部使用了红黑树实现。所以 HashMap 一般用来存储 key、value 的实现,而 TreeMap 常用存储需要排序的元素。
除了我们举的这两个例子之外,还有许多这样的例子,比如:HashMap 与 LinkedHashMap 的区别,HashMap 与 WeakHashMap 的区别,LinkedList 与 ArrayDeque 的区别。
2.学习集合源码,让我们学习经典的设计方式。
在集合的整个架构设计中,其类继承体系非常简单,但是却很经典。例如:Collection 接口设计了集合通用的操作,每个集合类型都有对应的接口(List、Set、Map),每个集合类型都有对应的抽象实现(AbstractList、AbstractSet、AbstractMap)等。
当我们阅读这些源码的时候,这种设计方式都会潜移默化地影响我们。当我们之后自己设计一个框架的时候,我们就会不知不觉地用上去。所有的创新都是从模仿开始的,所以阅读优秀的集合源码很重要。
3.帮助通过面试,获得更高的薪酬。
现在关于集合的原理是 Java 工程师面试的家常菜,几乎每一个企业的面试都会问到。如果你连这块东西都没搞清楚,那么你就不需要聊其他了,直接被干掉。而如果你能将整个 Java 集合体系清晰地说出去,并且举一反三地对比,那么你就比其他人优秀了。
4.学习经典的数据结构。
还记得大学在学习数据结构的时候,我们都是从理论上去记忆。但是当我看完集合源码之后,我忽然发现——JDK集合源码简直就是数据结构的最佳实践呀!
数据结构中最为基础的几个结构为:顺序表、单链表、双向链表、队列、栈、二叉堆、红黑树、哈希表。这些所有的实现都能在 JDK 集合的实现中找到。例如:ArrayList 就是顺序表的实现,LinkedList 就是双向链表的实现,Stack 就是栈的实现,HashMap 就是哈希表的实现,TreeMap 就是红黑树的实现,PriorityQueue 就是二叉堆的实现。
5.所有技术的基础
集合源码可以说是 JDK 所有源码中最为简单的一块了,而且也是其他所有源码的基础。例如线程池的源码中也大量使用了阻塞队列,如果你连集合源码都搞不懂,那么线程池的源码你也肯定看不懂的。而如果线程池源码看不懂,那么你 netty 的源码也看不懂的。netty 源码看不懂,那么 dubbo 的源码也是看不懂的。
看明白了么?这些技术都是一换扣着一换的。如果你想要后续学习更加快速,那么你就必须把最基础的东西学明白了。如果连最基础的东西都没学明白,就直接去学其他更复杂的东西,最后只会越来越难,最终逃脱不了放弃的命运。
读到了这里,我相信你也对集合的重要性有了不一样的认识。那么接下来一段时间,就让我和你一起来深入学学集合源码吧。如果觉得读了有用,那么请给我一个赞吧。你们的赞是我继续写下去的动力!
hashmap底层实现原理
hashmap底层实现原理是SortedMap接口能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable从结构实现来讲,HashMap是:数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。扩展资料从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组。Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对),除了K,V,还包含hash和next。HashMap就是使用哈希表来存储的。哈希表为解决冲突,采用链地址法来解决问题,链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。2023-08-14 03:15:041
HashMap底层实现原理解析
我们常见的有集合数据有三种结构:1、数组结构 2、链表结构 3、哈希表结构 下面我们来看看各自的数据结构的特点: 而我们常见的HashMap就是这样的一种数据结构 (1)、首先将k,v封装到Node对象当中(节点)。 (2)、然后它的底层会调用K的hashCode()方法得出hash值。 (3)、通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。 (1)、先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。 (2)、通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。 原因 : 增删是在链表上完成的,而查询只需扫描部分,则效率高。 HashMap集合的key,会先后调用两个方法,hashCode and equals方法,这这两个方法都需要重写。 因为equals方法默认比较的是两个对象的内存地址 https://blog.csdn.net/qq_37840993/article/details/1080485972023-08-14 03:15:171
一图了解ConcurrentHashMap底层原理
1、ConcurrentHashMap底层数据结构是一个数组table 2、table数组上挂着单向链表或红黑树 3、new ConcurrentHashMap();如果没有指定长度的话,默认是16,并且数组长度必须是2的n次幂,若自定义初始化的长度不是2的n次幂,那么在初始化数组时,会吧数组长度设置为大于自定义长度的最近的2的n次幂。(如:自定义长度为7,那么实际初始化数组后的长度为8) 4、底层是使用synchronized作为同步锁,并且锁的粒度是数组的具体索引位(有些人称之为分段锁)。 5、Node节点,hash>0,当hash冲突时,会形成一个单向链表挂在数组上。 6、ForwardindNode,继承至Node,其hash=-1,表示当前索引位的数据已经被迁移到新数组上了 7、ReservationNode,继承至Node,其hash=-3,表示当前索引位被占用了(compute方法) 8、TreenBin,继承至Node,其hash=-2,代表当前索引下挂着一颗红黑树 9、lockState为ConcurrentHashMap底层自己实现的基于cas的读写锁,锁粒度是具体的某颗树。当向红黑树进行增,删操作时,首先会先上sync同步锁,然后自平衡的时候会上lockState写锁,当读红黑树的时候,会上lockState读锁,然后判断此时是否有线程正持有写锁,或是否有线程正在等待获取写锁,若有,则读线程会直接去读双向链表,而不是去读红黑树。 10、TreeNode,hash>0,为红黑树具体节点。TreeBin的root代表红黑树的根节点,first代表双向链表的第一个节点 11、双向链表:构建红黑树时还维护了一个双向链表,其目的为:(1)当并发写读同一颗红黑树的时候,读线程判断到有线程正持有写锁,那么会跑去读取双向链表,避免因为并发写读导致读线程等待或阻塞。(2)当扩容的时候,会去遍历双向链表,重新计算节点hash,根据新的hash判断放在新数组的高位还是地位 1、触发扩容的规则: 1)添加完元素后,若判断到当前容器元素个数达到了扩容的阈值(数组长度*0.75),则会触发扩容 2)添加完元素后,所在的单向链表长度>=8,则会转为红黑树,不过在转红黑树之前会判断数组长度是否小于64,若小于64则会触发扩容 2、table为扩容前的数组,nextTable为扩容中创建的新数组,迁移数据完毕后需要将nextTable赋值给table 3、扩容后的数组是元素组长度的2倍 4、ln,hn分别表示高位和低位的链表(高链,低链)。即,扩容时,会用n&h==0来判断元素应该放在新数组的i位置还是i+n位置。n:元素组长度;h:元素hash值;i:元素所在的原数组索引位;。这样就会出现有些元素会被挂在低位,有些元素会被挂在高位,从而达到打散元素的目的。 5、红黑树扩容时,会遍历双向链表,并且计算n&h==0来判断元素放在低位(lo)还是高位(ho),确定完元素的去处之后,会判断分别判断两个双向链表(lo,ho)的长度是否大于6,若大于6则将还是以一颗红黑树的结构挂在数组上,若<=6的话,则转为单向链表,挂在数组上2023-08-14 03:15:241
Java中的HashMap的工作原理是什么?
Java中的HashMap是以键值对(key-value)的形式存储元素的。HashMap需要一个hash函数,它使用hashCode()和equals()方法来向集合/从集合添加和检索元素。当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。如果key已经存在了,value会被更新成新值。HashMap的一些重要的特性是它的容量(capacity),负载因子(loadfactor)和扩容极限(thresholdresizing)。2023-08-14 03:15:352
HashMap原理 — 扩容机制及存取原理
回顾一下基本概念: 一. put方法 HashMap使用哈希算法得到数组中保存的位置,然后调用put方法将key-value对保存到table变量中。我们通过图来演示一下存储的过程。 简单解释一下: 我们关注一下这里面最重要的三个方法,hash(),putVal(),resize(). 1. hash方法 我们通过hash方法计算索引,得到数组中保存的位置,看一下源码 我们可以看到HashMap中的hash算法是通过key的hashcode值与其hashcode右移16位后得到的值进行异或运算得到的,那么为什么不直接使用key.hashCode(),而要进行异或操作?我们知道hash的目的是为了得到进行索引,而hash是有可能冲突的,也就是不同的key得到了同样的hash值,这样就很容易产业碰撞,如何减少这种情况的发生呢,就通过上述的hash(Object key)算法将hashcode 与 hashcode的低16位做异或运算,混合了高位和低位得出的最终hash值,冲突的概率就小多了。举个例子: 我们的hash(Object key)算法一个道理,最终的hash值混合了高位和低位的信息,掺杂的元素多了,那么最终hash值的随机性越大,而HashMap的table下标依赖于最终hash值与table.length()-1的&运算,这里的&运算类似于挑包子的过程,自然冲突就小得多了。计算过程如下: 2. putVal方法 通过putVal方法将传递的key-value对添加到数组table中。 3. resize方法 HashMap通过resize()方法进行扩容,容量规则为2的幂次 二. get方法 我们先简单说一下get(Object key)流程,通过传入的key通过hash()算法得到hash值,在通过(n - 1) & hash找到数组下标,如果数组下标所对应的node值正好key一样就返回,否则找到node.next找到下一个节点,看是否是treenNode,如果是,遍历红黑树找到对应node,如果不是遍历链表找到node。我们看一下源码 这几个方法是核心,虽然HashMap还有很多常用方法,不过大体和这几个方法有关,或者实现逻辑相似,这里就不再多说了。 三. 总结 本文在上一章基本概念和底层结构的基础上,从源码的角度讲解了扩容机制以及存取原理,主要分析了put方法和get方法,put方法的核心为hash(),putVal(),resize(),get方法的核心为getNode()。2023-08-14 03:15:421
HashMap实现原理
HashMap在实际开发中用到的频率非常高,面试中也是热点。所以决定写一篇文章进行分析,希望对想看源码的人起到一些帮助,看之前需要对链表比较熟悉。 以下都是我自己的理解,欢迎讨论,写的不好轻喷。 HashMap中的数据结构为散列表,又名哈希表。在这里我会对散列表进行一个简单的介绍,在此之前我们需要先回顾一下 数组 、 链表 的优缺点。 数组和链表的优缺点取决于他们各自在内存中存储的模式,也就是直接使用 顺序存储 或 链式存储 导致的。无论是数组还是链表,都有明显的缺点。而在实际业务中,我们想要的往往是寻址、删除、插入性能都很好的数据结构,散列表就是这样一种结构,它巧妙的结合了数组与链表的优点,并将其缺点弱化(并不是完全消除) 散列表的做法是将key映射到数组的某个下标,存取的时候通过key获取到下标(index)然后通过下标直接存取。速度极快,而将key映射到下标需要使用 散列函数 ,又名 哈希函数 。说到哈希函数可能有人已经想到了,如何将key映射到数组的下标。 图中计算下标使用到了以下两个函数: 值得注意的是,下标并不是通过hash函数直接得到的,计算下标还要对hash值做index()处理。 Ps:在散列表中,数组的格子叫做 桶 ,下标叫做 桶号 ,桶可以包含一个key-value对,为了方便理解,后文不会使用这两个名词。 以下是哈希碰撞相关的说明: 以下是下标冲突相关的说明: 很多人认为哈希值的碰撞和下标冲突是同一个东西,其实不是的,它们的正确关系是这样的, hashCode发生碰撞,则下标一定冲突;而下标冲突,hashCode并不一定碰撞 上文提到,在jdk1.8以前HashMap的实现是 散列表 = 数组 + 链表 ,但是到目前为止我们还没有看到链表起到的作用。事实上,HashMap引入链表的用意就是解决下标冲突。 下图是引入链表后的散列表: 如上图所示,左边的竖条,是一个大小为16的数组,其中存储的是链表的头结点,我们知道,拥有链表的头结点即可访问整个链表,所以认为这个数组中的每个下标都存储着一个链表。其具体做法是,如果发现下标冲突,则 后插入的节点以链表的形式追加到前一个节点的后面 。 这种使用链表解决冲突的方法叫做: 拉链法 (又叫链地址法)。HashMap使用的就是拉链法,拉链法是冲突发生以后的解决方案。 Q:有了拉链法,就不用担心发生冲突吗? A:并不是!由于冲突的节点会不停的在链表上追加,大量的冲突会导致单个链表过长,使查询性能降低。所以一个好的散列表的实现应该从源头上减少冲突发生的可能性,冲突发生的概率和哈希函数返回值的均匀程度有直接关系,得到的哈希值越均匀,冲突发生的可能性越小。为了使哈希值更均匀,HashMap内部单独实现了hash()方法。 以上是散列表的存储结构,但是在被运用到HashMap中时还有其他需要注意的地方,这里会详细说明。 现在我们清楚了散列表的存储结构,细心的人应该已经发现了一个问题:Java中数组的长度是固定的, 无论哈希函数是否均匀,随着插入到散列表中数据的增多,在数组长度不变的情况下,链表的长度会不断增加 。这会导致链表查询性能不佳的缺点出现在散列表上,从而使散列表失去原本的意义。为了解决这个问题,HashMap引入了扩容与负载因子。 以下是和扩容相关的一些概念和解释: Ps: 扩容要重新计算下标 , 扩容要重新计算下标 , 扩容要重新计算下标 ,因为下标的计算和数组长度有关,长度改变,下标也应当重新计算。 在1.8及其以上的jdk版本中,HashMap又引入了红黑树。 红黑树的引入被用于替换链表,上文说到,如果冲突过多,会导致链表过长,降低查询性能,均匀的hash函数能有效的缓解冲突过多,但是并不能完全避免。所以HashMap加入了另一种解决方案,在往链表后追加节点时,如果发现链表长度达到8,就会将链表转为红黑树,以此提升查询的性能。2023-08-14 03:15:491
JAVA中的HASHSET和HASHMap的底层实现是怎样的?大致讲一下。
上面的仁兄讲的挺清楚的,回楼主的追问,hash值是不会一样的2023-08-14 03:15:573
hashmap底层中的链表是干什么用的
Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合(Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理)2023-08-14 03:16:361
【老实李】JDK1.8中HashMap的红黑树
上一篇文章 HashMap的底层原理探索 我们分析了JDK1.7中Hashmap的源码实现,但是在JDK1.8的时候HashMap的实现做了很大的变动和优化。1.7和1.7之前HashMap都是“数组+链表”实现的,1.8之后就是“数组+(链表或红黑树)”来实现的了。这里我特地将“链表或红黑树”用一对括号括在一起,因为HashMap底层依旧是一个数组,然后数组中的元素是链表或者红黑树来实现的。 HashMap的实现就先介绍到这,下面就先讲一下红黑树,然后才能理解为什么要用红黑树来优化HashMap,用红黑树有什么好处。 在介绍红黑树之前我们要先理解二叉查找树的数据结构。下面简单介绍一下。 上面这张图就是一个二叉查找树。二叉查找树有如下几条特性 (1).左子树上所有结点的值均小于或等于它的根结点的值。 (2).右子树上所有结点的值均大于或等于它的根结点的值。 (3).左、右子树也分别为二叉排序树 那既然他名字中是有“查找”的,那么他是怎么查找的呢?比如我们要查找10这个元素,查找过程为:首先找到根节点,然后根据二叉查找树的第一二条特性,我们知道要查找的10>9所以是在根节点的右边去查找,找到13,10<13,所以接着在13的左边找,找到11,10<11,继续在11的左边查找,这样就找到了10.这其实就是二分查找的思想。最后我们要查出结果所需的最大次数就是二叉树的高度!(二分查找的思想:找到数组的中间位置的元素v,将数组分成>v和<v两部分,然后将v和要查找的数据进行一个比较,如果大于v那么就在>v的部分再次进行二分查找,否则就在<v的部分进行二分查找,直到找到对应的元素。) 那既然要查出结果所需的最大次数就是二叉树的高度,那这个高度会不会有时候很长呢? 比如我们依次插入 根节点为9如下五个节点:7,6,5,4,3。依照二叉查找树的特性,结果会变成什么样呢?7,6,5,4,3一个比一个小,那么就会成一条直线,也就是成为了线性的查询,时间复杂度变成了O(N)级别。为了解决这种情况,该红黑树出场了。 红黑树其实就是一种 自平衡 的二叉查找树。他这个自平衡的特性就是对HashMap中链表可能会很长做出的优化。 红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求: 性质1. 节点是红色或黑色。 性质2. 根节点是黑色。 性质3 每个叶节点(NIL节点,空节点)是黑色的。 性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 下面这棵树就是一个典型的红黑树 红黑树那么多规则,那么在插入和删除元素会不会破坏红黑树的规则呢?什么情况下会破坏?什么情况下不会? 比如我们向原红黑树插入为14的新节点就不会破坏规则 向原红黑树插入值为21的新节点就破坏了规则 那么红黑树是怎么维护这个二叉查找树的自平衡性的呢? 红黑树通过 “变色”和“旋转” 来维护红黑树的规则,变色就是让黑的变成红的,红的变成黑的,旋转又分为“左旋转”和“右旋转”。这个比较复杂,容易晕,我们就只要知道红黑树就是通过这种方式来实现它的自平衡性就行了。 JDK1.7的时候是使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置,如果hashcode相同,或者hashcode取模后的结果相同(hash collision),那么这些key会被定位到Entry数组的同一个格子里,这些key会形成一个链表。在hashcode特别差的情况下,比方说所有key的hashcode都相同,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表。也就是说时间复杂度在最差情况下会退化到O(n) 红黑树我们大致了解了,他的好处就是他的自平衡性,让他这棵树的最大高度为2log(n+1),n是树的节点数。那么红黑树的复杂度就只有 O(log n)。 下面我们通过追踪JDK1.8 HashMap的put方法的源码来理解 put方法调用了putVal方法 通过putVal方法可以看到这里的数组和1.7不同,是使用了一个Node数组来存储数据。那这个Node和1.7里面的Entry的区别是什么呢? HashMap中的红黑树节点 采用的是TreeNode 类 TreeNode和Entry都是Node的子类,也就说Node可能是链表结构,也可能是红黑树结构。 如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个格子里。如果同一个格子里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。 先分析到这。。。。。2023-08-14 03:16:441
为什么面试要问hashmap 的原理
这个是基础题呀,主要看你对常用的东西是否真的了解如果只是会用,而不了解原理,他们就认为你没有专研的精神也就是说潜力不大2023-08-14 03:16:564
java的LinkedHashSet是怎样实现存取有序的, 底层原理是什么
看源码去吧2023-08-14 03:17:063
LRU算法的原理与实现
LRU是Least Recently Used的缩写,即最近最少使用算法,应用面非常的广泛,比如redis当中的内存淘汰策略。因为计算机的内存都是有限的,当用户访问的数据如果存在内存当中直接返回的给用户的话,效率会非常快,但是如果内存不存在,在去磁盘里面查找的,效率会大打折扣。所以我们希望在有限的内存空间当中,多存放点热点数据,用户不经常访问的数据,尽量淘汰掉,避免占用内存空间。 使用双向链表来实现LRU 这篇文章已经用双向链表来实现过LRU算法了,但是基于双向链表的特性,使得该算法的时间复杂度为O(n),显然不是最优的算法,那么有没有算法,可以达到O(1),当然是有的,早早的计算机科学家已经想到,并且已经实现了。 在笔者介绍接下来的内容时,还是希望先了解一下两篇博文: 一、 图解HashMap原理 二、 图解LinkedHashMap 之前使用双向链表去实现LRU算法时,时间复杂度没有达到O(1),主要原因在于遍历结点时,带来的时间开销,那么换句话说,要实现遍历结点时,时间复杂度为O(1),第一时间想到的应该是hash数组,但是hash算法可能会存在不同的key值,产生相同的hash值,那么可以将不同key,但是相同hash值的结点,以单链表的形式存放。这样仅仅是实现了存取时间复杂度为O(1),如何实现数据能够按访问顺序存放呢?并且增删的时间复杂度为O(1),这个可以使用双向链表来实现,所以综合来讲,就是实现散列数组+双向链表来使用LRU,可以达到时间复杂度为O(1)。 逻辑视图如下: 咋一看这个图乱的很,稍微解释一下,如果感觉理解上有点困难,可以先去了解一下之前推荐的两篇博文,那里会介绍的更加详细一点。 1.最左侧其实就是一个普通的数组,数组的大小必须是2的倍数,这个原因是什么呢?因为这个数组的存放方式是散列的,意思就是需要key.hashcode & (length -1)才能得出存放位置的方式,hash的好处是可以根据key值,在时间复杂度为O(1)的前提找到对应的存放位置,这也是我们的初衷,说到这里其实还没有解释为什么一定要是2的倍数,因为2的倍数-1,这个数的二进制,一定全是1,比如16-1=15,二进制表示就是1111,&运算符其实就是将值全部化成二进制逐位与,比如10111011 & 1111 = 1011 = 11,但是如果不是2的倍数,比如7-1=6,化成二进制就是0110,由于末位是0,不管什么二进制值与0110做&运算,一定是偶数,这样会导致散列分布不均匀。 2.不同key值,相同hash值,如何存放呢?相同的hash值做与运算一定能够得到相同的数组脚标,这些结点,以单链表的形式存在,就是图中数组右侧的单链表。 3.如何实现按访问顺序?上图除去数组和挂在数组右侧的单链表,还有绿色和黄色的单向箭头,在右上角还有一个双向链表的头指针。其实这些箭头就是维护不同结点的访问顺序,即双向链表。 总上所述,这种数据结构定义的结构体如下: class Node{ Object key; //存放key值,用于计算存放数组脚标位置 Object value;//存放元素值 int hash;//存放key.hashcode Node next;//维护单链表顺序 Node before;//维护双向链表顺序 Node after; } 笔者用java实现如下,感兴趣可以用自己喜欢的语言去实现一遍,加深理解: 其实以上实现底层原理就是LinkedHashMap的实现原理,只不过笔者做了一些简化,去掉了繁琐的继承,扩容等,突出了算法核心,如果读者感兴趣也可以去研究一下LinkedHashMap的源码。 使用LinkedHashMap来实现LRU算法: 看起来是不是简单了很多,因为LinkedHashMap底层已经封装好了,我们直接调用就好,但是作为一个想要变优秀的码农,一定要知其然知其所以然。 使用散列+双向链表的方式是如何实现O(1)复杂度的?在实现LRU算法过程中,无非两种操作,查找和修改,使用散列数组实现查找时间复杂度为O(1),使用双向链表实现修改复杂度为O(1),并且双向链表还可以维护访问顺序,所以使用这种方式,可以达到O(1)。2023-08-14 03:17:141
BitMap原理与实现
比较经典的问题是: 在只能够使用2G的内存中,如何完成以下操作: ①:对10亿个不重复的整数进行排序。 ②:找出10亿个数字中重复的数字。 无论是排序还是找重复的数字都需要将这10亿个数字加入到内存中在去进行操作,很明显,题目给出的2G内存限制说明了在这样的场景下是不能够将所有数都加入到内存中的 1000000000* 4/(1024* 1024* 1024) = 3.725G 那么这时候就需要用到 BitMap结构了 bitMap使用一个bit为0/1作为map的value来标记一个数字是否存在,而map的key值正是这个数字本身。 相比于一般的数据结构需要用4个byte去存储数值本身,相当于是节省了 4*8:1 = 32倍的内存空间 bitMap不一定要用bit数组,可以使用 int,long等等的基本数据类型实现,因为其实质都是在bit位上存数据,用哪种类型只是决定了最终实现出来的BitMap的内置数组中单个元素存放数据的多少 u2003u2003u2003u2003例如:java中的BitSet使用Long数组 BitMap的实现当然少不了位运算,先来明确几个常见位运算,这是实现BitMap的基础: set(bitIndex): 添加操作 u2003u2003u2003u20031 .确定该数处于数组中的哪个元素的位上 u2003u2003u2003u2003 int wordIndex = bitIndex >> 5; 因为我用的是int[]实现,所以这里右移 5 位(2^5 = 32) u2003u2003u2003u20032 .确定相对于该元素中的位置偏移 u2003u2003u2003u2003 int bitPosition = bitIndex & ((1 << 5) - 1); 这里相当于是 bitIndex % (1<<5)的取模运算,因为当取模运算的除数是2的次幂,所以可以使用以下的位运算来计算,提升效率(对比HashMap的容量为什么总是2的幂次方的问题,HashMap求下标时也是使用 hash&(n-1)) tips: 位运算的优先级是低于+,-等等的,所以要加上括号,防止发生不可描述的错误 u2003u2003u2003u20033 .将该位置1 u2003u2003u2003u2003 bits[wordIndex] |= 1 << bitPosition; 相当于是将指定位置处的bit值置1,其他位置保持不变,也就是将以这个bitIndex为key的位置为1 tips: 这里是参考了网上的各位大佬的文章,取余 + 按位或,又对比了下BitSet的源码: u2003u2003u2003u2003 words[wordIndex] |= (1L << bitIndex); 没有取余操作,直接|,这两个一样吗?答案当然是一样的 举个栗子: u2003u2003u2003u2003 1 << 148 == 1<< 20 u2003u2003u2003u2003 u2003u2003u2003u2003 1L << 125 ==1L<< 61 即对于int和long型数据,直接左移其位数相当于是附带了对其的取模操作 总结 :使用Bit-map的思想,我们可以将存储空间进行压缩,而且可以对数字进行快速排序、去重和查询的操作。 Bloom Fliter是Bit-map思想的一种扩展,它可以在允许低错误率的场景下,大大地进行空间压缩,是一种拿错误率换取空间的数据结构 当一个元素加入布隆过滤器中的时候,会进行哪些操作: 当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作: 然后,一定会出现这样一种情况: 不同的字符串可能哈希出来的位置相同 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率),因此: 布隆过滤器可能会存在误判的情况 总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在 。 Bloom Filter的应用: 常用于解决缓存穿透等场景。2023-08-14 03:17:351
工作3年的Java程序员应该掌握哪些技能
如果我面试3年的工程师。至少看他使用的框架是否了解。 你用的是ssh,ssi还是hadoop。只要你用了,就必须对其比较熟悉。另外java的基本内容,容器类,工具类的使用。数据库的使用。没人会指望你会所有的框架,但是你使用的熟不熟悉,代表你是否好学,是否能力足够。2023-08-14 03:17:533
小米二面是不是基本就定了
所谓一面,就是初试。一般来说,人力资源部门面试主要看一些基本条件,比如学历是否符合,经验是否匹配,性格是否OK。 一方面会有一定比例的淘汰,HR通知求职者参与一方面的依据是简历。如果第一印象达不到简历上的描述,或者专业技能达不到标准,或者不符合企业文化(比如不诚信),就会直接被淘汰。只有符合公司要求的求职者才会被推荐到两边。 二面是指复试,一般是决定结果的面试(个别需要三面的情况除外)常由用人部门领导面试,主要看求职者能否在团队中发挥什么作用,能否融入团队合作。 以上只是常规流程,实际操作中会出现一些例外情况: 开发工程师等一些技术岗位,一方面可能是技术面,另一方面可能是经理面或总监面,HR在面试中可能不起主导作用。 也有一些公司招聘流程较长,一面是HR面,二面是部门经理面,三面是VP面(确保符合企业文化)。 无论面试分几轮,求职者的考核要点主要包括:是否匹配岗位,是否符合企业文化,能否融入团队等等。 在这一轮面试中,一旦涉及到薪酬福利待遇的问题,双方的谈论没有到一个点,往往就会失败,所以二面并不是一定成功的,可变的因素还有很多。2023-08-14 03:18:033
ArrayList的添加和删除操作实现原理图解
上一篇 <<< Java集合类图总览 下一篇 >>> ArrayList的动态扩容、ModCount及fail-fast原理 Arraylist数据结构: 集合底层使用动态数组实现,随机查询效率非常快,插入和删除需要移动整个数组、效率低。 相关文章链接: <<< Java集合类图总览 <<< ArrayList的动态扩容、ModCount及fail-fast原理 <<< LinkedList增删改查操作底层实现原理 <<< 数组拷贝的几种方式及和链表结构的对比 <<< Jdk1.7HashMap源码分析 <<< Jdk1.7HashMap如何扩容及解决死循环问题 <<< JDK1.8HashMap源码分析 <<< ConcurrentHashMap在JDK1.8版本比1.7改进了什么 <<< JDK8的HashMap中红黑树左旋右旋原理图解 <<< 基于LinkedHashMap手写LRU淘汰策略 <<< HashSet集合底层实现原理 <<< HashTable底层实现原理及和ConcurrentHashMap区别 <<< java集合常见面试题2023-08-14 03:18:171
荣耀能二次面试么
西安:通用软件开发工程师(已面试通过)。 9月4日笔试,9月15日综合测评,9月25日荣耀一面技术面,9月26日荣耀二面(综合面) 笔试:三道算法题,和华为一样。 综合测评:前后一致即可。 技术一面:时间31分钟。主要是项目,有一些八股。1、项目了解,大概15分钟,项目挖的比较深,提了3个问题,幸好都回答出来了。2、分别讲一下hashmap在1.7和1.8的底层原理。 3、线程不安全的情况。我用死锁举例来说明。4、事务的隔离级别。5、说一下可重复度。6、mybatis用得少就没怎么问。综合二面:时间40分钟,主要是项目,有一些八股。二面面试官评价给了优秀。1、项目了解,大概20分钟。对项目中遇到的技术性难题进行描述。 2、Java面向对象与面向过程。3、创建线程池的方法。4、实现多线程的几种方式。5、spring的IOC和AOP。 再就是一些开放性问题。1、有没有女朋友?2、我看你投的是西安岗位,愿不愿意来深圳。3、你对荣耀了解多少?4、手里有没有其他offer,你对荣耀面试整体流程及面试难度感觉怎么样,这里使劲夸就行。5、为什么想来荣耀,我明确表达了想去的想法,那为什么不去华为,华为和荣耀你怎么选?我说荣耀科技园离我家近,华为离得远。 最后是反问环节:我问了入职培训的问题,技术栈以及面试表现和建议。面完后说我这里给你通过,面试表现为优秀,期待你早日加入荣耀。 感谢!!!期待早日拿到意向书。2023-08-14 03:18:261
Okhttp解析(五)缓存的处理
大家好,之前我们讲解了Okhttp网络数据请求相关的内容,这一节我们讲讲数据缓存的处理。本节按以下内容讲解Okhttp缓存相关的内容。 缓存的使用场景很多,通过它可以将数据通过一定的规则存储起来,再次请求数据的时候就可以快速从缓存中读取了,缓存有以下优势。 HTTP本身提供了一套缓存相关的机制。这套机制定义了相关的字段和规则,用来客户端和服务端进行缓存相关的协商,如响应的数据是否需要缓存,缓存有效期,缓存是否有效,服务器端给出指示,而客户端则根据服务端的指示做具体的缓存更新和读取缓存工作。http缓存可以分为两类: 强制缓存,在缓存数据未失效的情况下,可以直接使用缓存数据,有两个字段Expires和Cache-Control用于标明失效规则。 表示过期时间,由服务端返回。那么下次请求数据时,判断这个Expires过期时间是否已经过了,如果还没有到过期时间,则使用缓存,如果过了过期时间,则重新请求服务器的数据。Expires格式如下: 不过因为服务器和客户端的时间并不是同步的,用一个绝对时间作为过期的标记并不是很明智,所以HTTP1.1之后更多的是Cache-Control,它的控制更加灵活。 表示缓存的控制,有服务端返回。它有以下几个取值: 默认情况下是private,也就是不能共享的。Cache-Control格式如下: 对比缓存,表示需要和服务端进行相关信息的对比,由服务器决定是使用缓存还是最新内容,如果服务器判定使用缓存,返回响应吗304,判定使用最新内容,则返回响应码200和最新数据。对比缓存的判定字段有两组: ETag表示资源的一种标识信息,用于标识某个资源,由服务端返回,优先级更高。格式如下: 然后客户端再次请求时,加入字段If-None-Match,格式如下: 服务端收到请求的该字段时(之前的Etag值),和资源的唯一标识进行对比,如果相同,说明没有改动,则返回状态码304,如果不同,说明资源被改过了,则返回状态码200和整个内容数据。 Last-Modified表示资源的最近修改时间,由服务端返回,优先级更低。格式如下: Last-Modified 由服务器返回,表示响应的数据最近修改的时间。 If-Modified-Since 由客户端请求,表示询问服务器这个时间是不是上次修改的时间。如果服务端该资源的修改时间小于等于If-Modified-Since指定的时间,说明资源没有改动,返回响应状态码304,可以使用缓存。如果服务端该资源的修改时间大于If-Modified-Since指定的时间,说明资源又有改动了,则返回响应状态码200和最新数据给客户端,客户端使用响应返回的最新数据。 Last-Modified字段的值(服务端返回的资源上次修改时间),常常被用于客户端下次请求时的If-Modified-Since字段中。 HTTP的缓存规则是优先考虑强制缓存,然后考虑对比缓存。 Okhttp缓存相关的类有如下: 要开启使用Okhttp的缓存其实很简单,只需要给OkHttpClient对象设置一个Cache对象即可,创建一个Cache时指定缓存保存的目录和缓存最大的大小即可。 那么下面我们来看看Okhttp缓存执行的大概流程 Okhttp的缓存流程分为读取缓存和存储缓存两个过程,我们分别分析。 读取使用缓存的流程从HttpEngine的sendRequest发送请求开始。 接下来我们分析 从Cache的get方法开始。它按以下步骤进行。 如果存在缓存的话,在指定的缓存目录中,会有两个文件“****.0”和“****.1”,分别存储某个请求缓存的响应头和响应体信息。(“****”是url的md5加密值)对应的ENTRY_METADATA响应头和ENTRY_BODY响应体。缓存的读取其实是由DiskLruCache来读取的,DiskLruCache是支持Lru(最近最少访问)规则的用于磁盘存储的类,对应LruCache内存存储。它在存储的内容超过指定值之后,就会根据最近最少访问的规则,把最近最少访问的数据移除,以达到总大小不超过限制的目的。 接下来我们分析CacheStrategy缓存策略是怎么判定的。 直接看CacheStrategy的get方法。缓存策略是由请求和缓存响应共同决定的。 接来下我们看看CacheControl类里有些什么。 可以发现,它就是用于描述响应的缓存控制信息。 然后我们再看看Okhttp存储缓存是怎么进行的。 存储缓存的流程从HttpEngine的readResponse发送请求开始的。 可以看到这里先通过maybeCache写入了响应头信息,再通过cacheWritingResponse写入了响应体信息。我们再进去看Cache的put方法实现。 我们继续看Cache的writeTo方法,可以看到是写入一些响应头信息。 到这里Okhttp缓存的读取和存储流程我们就清楚了。可以说,缓存的使用策略基本都是按照HTTP的缓存定义来实现的,所以对HTTP缓存相关字段的理解是很重要的。然后关于DiskLruCache是如何管理缓存文件的,这个其实也很好理解,首先的原则就是按照LRU这种最近最少使用删除的原则,当总的大小超过限定大小后,删除最近最少使用的缓存文件,它的LRU算法是使用LinkedHashMap进行维护的,这样来保证,保留的缓存文件都是更常使用的。具体实现大家可以分析DiskLruCache和LinkedHashMap的实现原理。2023-08-14 03:18:331
一个中级java工程师应该掌握哪些知识?有什么途径去获取这些知识?
一个中级java工程师应该掌握编码能力和业务能力,可以通过网上课程获取这些知识,中级java工程师需要综合能力,丰富的经验。2023-08-14 03:18:533
原则和原理,有什么区别!?
原则就是各种事情的底线不能越过,原理就是各种事物是怎样构成的。2023-08-14 03:19:334
c++ 为什么hashmap占用内存多
用hashmap>作为数据结构,然后,添加删除什么的就在hashmap里面的hashset加一个A比如说2023-08-14 03:19:432
hashmap的最大容量是多少,在多少的时候会导致查询响应过慢
很大吧,现在的NOSQL不就是这种原理吗,专门用来处理大数据的。2023-08-14 03:19:512
Java中的HashMap的工作原理是什么?
在Windows下运行应用程序时出现非法操作的提示此类故障引起原因较多,在如下几钟可能:(1) 系统文件被更改或损坏,倘若由此引发则打开一些系统自带的程序时就会出现非法操作,(例如,打开控制面板)(2) 驱动程序未正确安装,此类故障一般表现在显卡驱动程序之止,倘若由此引发,则打开一些游戏程序时就会产生非法操作,有时打开一此网页也会出现这种程况。(3) 内存质量不好,降低内存速度也可能会解决这个问题。(4) 软件不兼容,如,IE 5。5在Windows 98 SE 上,当打开多个网页也会产生非法操作。2023-08-14 03:20:451
Java中的HashMap的工作原理是什么?
【答案】:Java中的HashMap是以键值对(key-value)的形式存储元素的。HashMap需要一个hash函数,它使用hashCode()和equals()方法来向集合/从集合添加和检索元素。当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。如果key已经存在了,value会被更新成新值。HashMap的一些重要的特性是它的容量(capacity),负载因子(load factor)和扩容极限(threshold resizing)。2023-08-14 03:20:531
hashmap实现原理
从结构实现来讲,HashMap是:数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。2023-08-14 03:21:012
HashMap与HashTable的区别
HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable。HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步(如果是ArrayList:List lst = Collections.synchronizedList(new ArrayList());如果是HashMap:Map map = Collections.synchronizedMap(new HashMap());)。Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。2023-08-14 03:21:167
Java中HashMap和TreeMap的区别深入理解
首先介绍一下什么是Map.在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value.这就是我们平时说的键值对。 HashMap通过hashcode对其内容进行快速查找,而 TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。 HashMap 非线程安全 TreeMap 非线程安全 线程安全 在Java里,线程安全一般体现在两个方面: 1、多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized.如ArrayList和Vector,HashMap和Hashtable (后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。 2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。 1.AbstractMap抽象类和SortedMap接口 AbstractMap抽象类:(HashMap继承AbstractMap)覆盖了equals()和hashCode()方法以确保两个相等映射返回相同的哈希码。如果两个映射大小相等、包含同样的键且每个键在这两个映射中对应的值都相同,则这两个映射相等。映射的哈希码是映射元素哈希码的总和,其中每个元素是Map.Entry接口的一个实现。因此,不论映射内部顺序如何,两个相等映射会报告相同的哈希码。 SortedMap接口:(TreeMap继承自SortedMap)它用来保持键的有序顺序。SortedMap接口为映像的视图(子集),包括两个端点提供了访问方法。除了排序是作用于映射的键以外,处理SortedMap和处理SortedSet一样。添加到SortedMap实现类的元素必须实现Comparable接口,否则您必须给它的构造函数提供一个Comparator接口的实现。TreeMap类是它的唯一一份实现。 2.两种常规Map实现 HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。 (1)HashMap(): 构建一个空的哈希映像 (2)HashMap(Map m): 构建一个哈希映像,并且添加映像m的所有映射 (3)HashMap(int initialCapacity): 构建一个拥有特定容量的空的哈希映像 (4)HashMap(int initialCapacity, float loadFactor): 构建一个拥有特定容量和加载因子的空的哈希映像 TreeMap:基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。 (1)TreeMap():构建一个空的映像树 (2)TreeMap(Map m): 构建一个映像树,并且添加映像m中所有元素 (3)TreeMap(Comparator c): 构建一个映像树,并且使用特定的比较器对关键字进行排序 (4)TreeMap(SortedMap s): 构建一个映像树,添加映像树s中所有映射,并且使用与有序映像s相同的比较器排序 3.两种常规Map性能 HashMap:适用于在Map中插入、删除和定位元素。 Treemap:适用于按自然顺序或自定义顺序遍历键(key)。 4.总结 HashMap通常比TreeMap快一点(树和哈希表的数据结构使然),建议多使用HashMap,在需要排序的Map时候才用TreeMap. import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; public class HashMaps { public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(); map.put("a", "aaa"); map.put("b", "bbb"); map.put("c", "ccc"); map.put("d", "ddd"); Iterator<String> iterator = map.keySet()。iterator(); while (iterator.hasNext()) { Object key = iterator.next(); System.out.println("map.get(key) is :" + map.get(key)); } // 定义HashTable,用来测试 Hashtable<String, String> tab = new Hashtable<String, String>(); tab.put("a", "aaa"); tab.put("b", "bbb"); tab.put("c", "ccc"); tab.put("d", "ddd"); Iterator<String> iterator_1 = tab.keySet()。iterator(); while (iterator_1.hasNext()) { Object key = iterator_1.next(); System.out.println("tab.get(key) is :" + tab.get(key)); } TreeMap<String, String> tmp = new TreeMap<String, String>(); tmp.put("a", "aaa"); tmp.put("b", "bbb"); tmp.put("c", "ccc"); tmp.put("d", "cdc"); Iterator<String> iterator_2 = tmp.keySet()。iterator(); while (iterator_2.hasNext()) { Object key = iterator_2.next(); System.out.println("tmp.get(key) is :" + tmp.get(key)); } } } 运行结果如下: map.get(key) is :ddd map.get(key) is :bbb map.get(key) is :ccc map.get(key) is :aaa tab.get(key) is :bbb tab.get(key) is :aaa tab.get(key) is :ddd tab.get(key) is :ccc tmp.get(key) is :aaa tmp.get(key) is :bbb tmp.get(key) is :ccc tmp.get(key) is :cdc HashMap的结果是没有排序的,而TreeMap输出的结果是排好序的。 下面就要进入本文的主题了。先举个例子说明一下怎样使用HashMap: import java.util.*; public class Exp1 { public static void main(String[] args){ HashMap h1=new HashMap(); Random r1=new Random(); for (int i=0;i<1000;i++){ Integer t=new Integer(r1.nextInt(20)); if (h1.containsKey(t)) ((Ctime)h1.get(t))。count++; else h1.put(t, new Ctime()); } System.out.println(h1); } } class Ctime{ int count=1; public String toString(){ return Integer.toString(count); } } 在HashMap中通过get()来获取value,通过put()来插入value,ContainsKey()则用来检验对象是否已经存在。可以看出,和ArrayList的操作相比,HashMap除了通过key索引其内容之外,别的方面差异并不大。 前面介绍了,HashMap是基于HashCode的,在所有对象的超类Object中有一个HashCode()方法,但是它和equals方法一样,并不能适用于所有的情况,这样我们就需要重写自己的HashCode()方法。下面就举这样一个例子: import java.util.*; public class Exp2 { public static void main(String[] args){ HashMap h2=new HashMap(); for (int i=0;i<10;i++) h2.put(new Element(i), new Figureout()); System.out.println("h2:"); System.out.println("Get the result for Element:"); Element test=new Element(5); if (h2.containsKey(test)) System.out.println((Figureout)h2.get(test)); else System.out.println("Not found"); } } class Element{ int number; public Element(int n){ number=n; } } class Figureout{ Random r=new Random(); boolean possible=r.nextDouble()>0.5; public String toString(){ if (possible) return "OK!"; else return "Impossible!"; } } 在这个例子中,Element用来索引对象Figureout,也即Element为key,Figureout为value.在Figureout中随机生成一个浮点数,如果它比0.5大,打印"OK!",否则打印"Impossible!".之后查看Element(3)对应的Figureout结果如何。 结果却发现,无论你运行多少次,得到的结果都是"Not found".也就是说索引Element(3)并不在HashMap中。这怎么可能呢? 原因得慢慢来说:Element的HashCode方法继承自Object,而Object中的HashCode方法返回的HashCode对应于当前的地址,也就是说对于不同的对象,即使它们的内容完全相同,用HashCode()返回的值也会不同。这样实际上违背了我们的意图。因为我们在使用 HashMap时,希望利用相同内容的对象索引得到相同的目标对象,这就需要HashCode()在此时能够返回相同的值。在上面的例子中,我们期望 new Element(i) (i=5)与 Elementtest=newElement(5)是相同的,而实际上这是两个不同的对象,尽管它们的内容相同,但它们在内存中的地址不同。因此很自然的,上面的程序得不到我们设想的结果。下面对Element类更改如下: class Element{ int number; public Element(int n){ number=n; } public int hashCode(){ return number; } public boolean equals(Object o){ return (o instanceof Element) && (number==((Element)o)。number); } } 在这里Element覆盖了Object中的hashCode()和equals()方法。覆盖hashCode()使其以number的值作为 hashcode返回,这样对于相同内容的对象来说它们的hashcode也就相同了。而覆盖equals()是为了在HashMap判断两个key是否相等时使结果有意义(有关重写equals()的内容可以参考我的另一篇文章《重新编写Object类中的方法》)。修改后的程序运行结果如下: h2: Get the result for Element: Impossible! 请记住:如果你想有效的使用HashMap,你就必须重写在其的HashCode()。 还有两条重写HashCode()的原则: [list=1] 不必对每个不同的对象都产生一个唯一的hashcode,只要你的HashCode方法使get()能够得到put()放进去的内容就可以了。即"不为一原则". 生成hashcode的算法尽量使hashcode的值分散一些,不要很多hashcode都集中在一个范围内,这样有利于提高HashMap的性能。即"分散原则".至于第二条原则的具体原因,有兴趣者可以参考Bruce Eckel的《Thinking in Java》,在那里有对HashMap内部实现原理的介绍,这里就不赘述了。 掌握了这两条原则,你就能够用好HashMap编写自己的程序了。不知道大家注意没有,java.lang.Object中提供的三个方法:clone(),equals()和hashCode()虽然很典型,但在很多情况下都不能够适用,它们只是简单的由对象的地址得出结果。这就需要我们在自己的程序中重写它们,其实java类库中也重写了千千万万个这样的方法。利用面向对象的多态性--覆盖,Java的设计者很优雅的构建了Java的结构,也更加体现了Java是一门纯OOP语言的特性。2023-08-14 03:21:341
java面试需要哪些基础
面向对象软件开发的优点有哪些?答:开发模块化,更易维护和修改;代码之间可以复用;增强代码的可靠性、灵活性和可理解性。多态的定义?答:多态是编程语言给不同的底层数据类型做相同的接口展示的一种能力。一个多态类型上的操作可以应用到其他类型的值上面。继承的定义?答:继承给对象提供了从基类获取字段和方法的能力。继承提供了代码的重用行,也可以在不修改类的情况下给现存的类添加新特性抽象的定义?抽象和封装的不同点?答:抽象是把想法从具体的实例中分离出来的步骤,因此,要根据他们的功能而不是实现细节来创建类。Java支持创建只暴漏接口而不包含方法实现的抽象的类。这种抽象技术的主要目的是把类的行为和实现细节分离开。抽象和封装是互补的概念。一方面,抽象关注对象的行为。另一方面,封装关注对象行为的细节。一般是通过隐藏对象内部状态信息做到封装,因此,封装可以看成是用来提供抽象的一种策略。接口和抽象类的区别是什么?答:Java提供和支持创建抽象类和接口。它们的实现有共同点,不同点在于:接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。类可以实现很多个接口,但是只能继承一个抽象类类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。抽象类可以在不提供接口方法实现的情况下实现接口。Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量。Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含main方法的话是可以被调用的。什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?答:上次电话面试被问到了这个问题,一脸蒙B。Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。 这里分享一个讲Java虚拟机的趣文,小说的形式。很有趣,读完你就明白虚拟机的工作原理:http://blog.jobbole.com/101114/”static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?答:“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。Java中的方法覆盖(Overriding)和方法重载(Overloading)是什么意思?答:这道题如果面试考概念,80%会被问到。太容易被混淆了,上次面试时候,一紧张就说反了。Java中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同的情况。与此相对,方法覆盖是说子类重新定义了父类的方法。方法覆盖必须有相同的方法名,参数列表和返回类型。覆盖者可能不会限制它所覆盖的方法的访问。Java支持的数据类型有哪些?什么是自动拆装箱?答:byte,short,int,long,float,double,boolean,char自动装箱是Java编译器在基本数据类型和对应的对象包装类型之间做的一个转化。比如:把int转化成Integer,double转化成Double,等等。反之就是自动拆箱。Java中,什么是构造函数?什么是构造函数重载?什么是复制构造函数?答:当新对象被创建的时候,构造函数会被调用。每一个类都有构造函数。在程序员没有给类提供构造函数的情况下,Java编译器会为这个类创建一个默认的构造函数。Java中构造函数重载和方法重载很相似。可以为一个类创建多个构造函数。每一个构造函数必须有它自己唯一的参数列表。Java不支持像C++中那样的复制构造函数,这个不同点是因为如果你不自己写构造函数的情况下,Java不会创建默认的复制构造函数。Java支持多继承么?答:Java中类不支持多继承,只支持单继承(即一个类只有一个父类)。 但是java中的接口支持多继承,或者说一个类可以实现多个接口。(接口的作用是用来扩展对象的功能,一个子接口继承多个父接口,说明子接口扩展了多个功能,当类实现接口时,类就扩展了相应的功能)。什么是值传递和引用传递?答:这个问题也比较经典。 值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量. 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。 所以对引用对象进行操作会同时改变原对象。一般认为,java内的传递都是值传递。好像还是稀里糊涂这边有个例子:http://blog.csdn.net/yangjingyuan/article/details/5862944 大家看完应该就理解了创建线程有几种不同的方式?答:有三种方式可以用来创建线程: 继承Thread类 实现Runnable接口应用程序可以使用Executor框架来创建线程池实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。概括的解释下线程的几种可用状态。答:一句话说不明白这个问题,可以参考我的这篇文章:http://blog.csdn.net/zjwcdd/article/details/51517096同步方法和同步代码块的区别是什么?答:同步方法默认用this或者当前类class对象作为锁;同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;这篇文章的例子不错:http://blog.sina.com.cn/s/blog_4ae8f77f0101iifx.html在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?答:监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。Java集合类框架的基本接口有哪些?答:集合类接口指定了一组叫做元素的对象。集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序。有的集合类允许重复的键,有些不允许。Java集合类提供了一套设计良好的支持对一组对象进行操作的接口和类。Java集合类里面最基本的接口有: Collection:代表一组对象,每一个对象都是它的子元素。 Set:不包含重复元素的Collection。 List:有顺序的collection,并且可以包含重复元素。 Map:可以把键(key)映射到值(value)的对象,键不能重复。什么是迭代器(Iterator)?答:Iterator接口提供了很多对集合元素进行迭代的方法。每一个集合类都包含了可以返回迭代器实例的迭代方法。迭代器可以在迭代的过程中删除底层集合的元素,但是不可以直接调用集合的remove(Object Obj)删除,可以通过迭代器的remove()方法删除。Iterator和ListIterator的区别是什么?答:Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。Java中的HashMap的工作原理是什么?答:Java中的HashMap是以键值对(key-value)的形式存储元素的。HashMap需要一个hash函数,它使用hashCode()和equals()方法来向集合/从集合添加和检索元素。当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。如果key已经存在了,value会被更新成新值。HashMap的一些重要的特性是它的容量(capacity),负载因子(load factor)和扩容极限(threshold resizing)。hashCode()和equals()方法的重要性体现在什么地方?答:Java中的HashMap使用hashCode()和equals()方法来确定键值对的索引,当根据键获取值的时候也会用到这两个方法。如果没有正确的实现这两个方法,两个不同的键可能会有相同的hash值,因此,可能会被集合认为是相等的。而且,这两个方法也用来发现重复元素。所以这两个方法的实现对HashMap的精确性和正确性是至关重要的。HashMap和Hashtable有什么区别?答:HashMap和Hashtable都实现了Map接口,因此很多特性非常相似。但是,他们有以下不同点: HashMap允许键和值是null,而Hashtable不允许键或者值是null。 Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。HashMap提供了可供应用迭代的键的集合,因此,HashMap是快速失败的。另一方面,Hashtable提供了对键的列举(Enumeration)。一般认为Hashtable是一个遗留的类。数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用Array而不是ArrayList?答:Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。Array大小是固定的,ArrayList的大小是动态变化的。ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。ArrayList和LinkedList有什么区别?答:ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。 LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。Comparable和Comparator接口是干什么的?列出它们的区别。答:Java提供了只包含一个compareTo()方法的Comparable接口。这个方法可以个给两个对象排序。具体来说,它返回负数,0,正数来表明输入对象小于,等于,大于已经存在的对象。Java提供了包含compare()和equals()两个方法的Comparator接口。compare()方法用来给两个输入参数排序,返回负数,0,正数表明第一个参数是小于,等于,大于第二个参数。equals()方法需要一个对象作为参数,它用来决定输入参数是否和comparator相等。只有当输入参数也是一个comparator并且输入参数和当前comparator的排序结果是相同的时候,这个方法才返回true。什么是Java优先级队列(Priority Queue)?答:PriorityQueue是一个基于优先级堆的无界队列,它的元素是按照自然顺序(natural order)排序的。在创建的时候,我们可以给它提供一个负责给元素排序的比较器。PriorityQueue不允许null值,因为他们没有自然顺序,或者说他们没有任何的相关联的比较器。最后,PriorityQueue不是线程安全的,入队和出队的时间复杂度是O(log(n))。如何权衡是使用无序的数组还是有序的数组?答:有序数组最大的好处在于查找的时间复杂度是O(log n),而无序数组是O(n)。有序数组的缺点是插入操作的时间复杂度是O(n),因为值大的元素需要往后移动来给新元素腾位置。相反,无序数组的插入时间复杂度是常量O(1)。Java集合类框架的最佳实践有哪些?答:根据应用的需要正确选择要使用的集合的类型对性能非常重要,比如:假如元素的大小是固定的,而且能事先知道,我们就应该用Array而不是ArrayList。有些集合类允许指定初始容量。因此,如果我们能估计出存储的元素的数目,我们可以设置初始容量来避免重新计算hash值或者是扩容。为了类型安全,可读性和健壮性的原因总是要使用泛型。同时,使用泛型还可以避免运行时的ClassCastException。使用JDK提供的不变类(immutable class)作为Map的键可以避免为我们自己的类实现hashCode()和equals()方法。编程的时候接口优于实现。 底层的集合实际上是空的情况下,返回长度是0的集合或者是数组,不要返回null。Enumeration接口和Iterator接口的区别有哪些?答:Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。HashSet和TreeSet有什么区别?答:HashSet是由一个hash表来实现的,因此,它的元素是无序的。add(),remove(),contains()方法的时间复杂度是O(1)。另一方面,TreeSet是由一个树形的结构来实现的,它里面的元素是有序的。因此,add(),remove(),contains()方法的时间复杂度是O(logn)。Java中垃圾回收有什么目的?什么时候进行垃圾回收?答:垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?答:垃圾回收器(garbage collector)决定回收某对象时,就会运行该对象的finalize()方法 但是在Java中很不幸,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说filalize()可能永远不被执行,显然指望它做收尾工作是靠不住的。 那么finalize()究竟是做什么的呢?它最主要的用途是回收特殊渠道申请的内存。Java程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种JNI(Java Native Interface)调用non-Java程序(C或C++),finalize()的工作就是回收这部分的内存。如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?答: 不会,在下一个垃圾回收周期中,这个对象将是可被回收的。Java堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)?答:JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。在Java中,对象什么时候可以被垃圾回收?答:当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。Java中的两种异常类型是什么?他们有什么区别?答:Java中有两种异常:受检查的(checked)异常和不受检查的(unchecked)异常。不受检查的异常不需要在方法或者是构造函数上声明,就算方法或者是构造函数的执行可能会抛出这样的异常,并且不受检查的异常可以传播到方法或者是构造函数的外面。相反,受检查的异常必须要用throws语句在方法或者是构造函数上声明。Java中Exception和Error有什么区别?答:Exception和Error都是Throwable的子类。Exception用于用户程序可以捕获的异常情况。Error定义了不期望被用户程序捕获的异常。具体解释和图说明见:http://blog.csdn.net/zjwcdd/article/details/51508475throw和throws有什么区别?答:throw关键字用来在程序中明确的抛出异常,相反,throws语句用来表明方法不能处理的异常。每一个方法都必须要指定哪些异常不能处理,所以方法的调用者才能够确保处理可能发生的异常,多个异常是用逗号分隔的。异常处理完成以后,Exception对象会发生什么变化?答:Exception对象会在下一个垃圾回收过程中被回收掉。finally代码块和finalize()方法有什么区别?答:无论是否抛出异常,finally代码块都会执行,它主要是用来释放应用占用的资源。finalize()方法是Object类的一个protected方法,它是在对象被垃圾回收之前由Java虚拟机来调用的。2023-08-14 03:21:451
循环依赖的底层原理
从 Spring IOC 容器中获取 bean 实例的流程:从context.getBean()方法开始 三级缓存为什么会移除掉? 在三级缓存中找 找到了就会执行AOp 产出代理对象 然后将代理对象放入到二级缓存。三级缓存一定会能到的对象 但不一定 会执行 aop 二级缓存找不到 才会触发aop(通过lambda表达式,执行函数式接口,执行aop) 产生代理对象放入二级缓存,放入之后需要移除掉对应的三级缓存(保证只执行一次aop)如果三级缓存中对象不需要执行aop操作 ,那么产生的对象仍然要放入二级缓存 ,这是放入的对象是原始对象 为什么单例池:concurentHashmap是,二级缓存是hashmap,三级缓存是hashmap? 三级缓存的AOP过程需要加锁以保证操作的原子性 因为三级缓存的函数接口 内部已经加了锁,保证了操作的原子性 所以没必要使用concurenthashmap 问题:源码中加synchronized锁的意义? 背景: 二级缓存中的aService对象是三级缓存中的lambda表达式生成出来的, 他们是成对的,二级缓存中存在代理对象,则三级缓存中不应该存在lambda表达式; 或者说,三级缓存中存在lambda表达式,则二级缓存中不应当有该代理对象 解答: 其中: 是将原始对象信息存入三级缓存的操作,存入的是lambda表达式: getEarlyBeanReference(beanName, mbd, bean)执行的结果,getEarlyBeanReference会对是否需要提前AOP进行判断,如果需要进行AOP,则生成代理对象放入二级缓存。 其中: 1、A doCreateBean()初始化,由于还未创建,从一级缓存查不到,此时只是一个半成品(提前暴露的对象),放入三级缓存singletonFactories; 2、A发现自己需要B对象,但是三级缓存中未发现B,创建B的原始对象,将带有B对象信息(beanName,bd,原始对象)的Lambda表达式放入singletonFactories; 3、B发现自己需要A对象,从一级缓存singletonObjects没找到,并知道了A对象正在创建,就确认发生了循环依赖,这时候再去二级缓存earlySingletonObjects中寻找A对象,没找到就继续在三级缓存singletonFactories中寻找A对象(一定能找到),于是执行三级缓存中的lambda表达式得到一个代理对象或者是原始对象A(A中属性未赋值),将A放入二级缓存earlySingletonObjects,同时从三级缓存删除对应beanName的表达式; 同理向三级缓存加入对象时,也会从二级缓存中将相同BeanName的记录删除掉,所以二级缓存与三级缓存的之间的来两步操作具有原子性。 4、将A注入到对象B中; 5、B完成属性填充,执行初始化方法,将自己放入第一级缓存中(此时B是一个完整的对象); 6、A得到对象B,将B注入到A中; 7、A完成属性填充,初始化,并放入到一级缓存中 注意:在对象创建开始的时候,会对对象创建状态利用Set: singletonsCurrentlyInCreation进行记录:是否是正在创建,可用于判断是否发生了循环依赖。 @Lazy注解的作用: initializeBean Spring解决循环依赖问题--B站视频讲解 Spring 循环依赖的“常见”面试问题2023-08-14 03:22:041
java 为什么使用hashmap
http://blog.csdn.net/rickiyeat/article/details/527087942023-08-14 03:22:164
如何实现线程安全的HashMap
HashTable不就可以吗。2023-08-14 03:22:298
Hashpmap的原理,HashMap怎样保证key的唯一性
并不能保证,但是可以通过判断,得到现在有无数据,若有,进入下一层。2023-08-14 03:22:461
hashtable和hashmap的区别是什么?
一、继承父类不同Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类;但二者都实现了Map接口。二、线程的安全性1、HashTable是同步(方法中使用了Synchronize)的;而HashMap是未同步(方法中缺省Synchronize)的。2、Hashtable线程安全,因为它每个方法中都加入了Synchronize,在多线程并发的环境下,可以直接使用Hashtable,不需自己在加同步;HashMap线程不安全,因为HashMap底层是一个Entry数组,当发生hashmap冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。三、是否有contains方法1、HashTable有一个contains(Object value)方法,功能和containsValue方法(Object value)功能一样。2、HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey。四、可否允许有null值key、value都是对象,但是不能拥有重复key值,value值可以重复出现。1、Hashtable中,key和value都不允许出现null值。2、HashMap允许null值(key和value都可以),因为在HashMap中null可以作为健,而它对应的值可以有多个null。五、遍历方式内部实现不同1.HashTable使用Enumeration,HashMap使用Iterator。2023-08-14 03:22:541
java中hashset和hashmap有什么特点?
什么是HashSetHashSet实现了Set接口,它不允许集合中有重复的值,当我们提到HashSet时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。public boolean add(Object o)方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。什么是HashMapHashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。HashMap允许键和值为null。HashMap是非synchronized的,但collection框架提供方法能保证HashMap synchronized,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。public Object put(Object Key,Object value)方法用来将元素添加到map中。你可以阅读这篇文章看看HashMap的工作原理,以及这篇文章看看HashMap和HashTable的区别。HashSet和HashMap的区别*HashMap* *HashSet* HashMap实现了Map接口 HashSet实现了Set接口 HashMap储存键值对 HashSet仅仅存储对象 使用put()方法将元素放入map中 使用add()方法将元素放入set中 HashMap中使用键对象来计算hashcode值 HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false HashMap比较快,因为是使用唯一的键来获取对象 HashSet较HashMap来说比较慢2023-08-14 03:23:233
java7和java8对hashmap做了哪些优化
HashMap的原理介绍此乃老生常谈,不作仔细解说。一句话概括之:HashMap是一个散列表,它存储的内容是键值对(key-value)映射。Java 7 中HashMap的源码分析首先是HashMap的构造函数代码块1中,根据初始化的Capacity与loadFactor(加载因子)初始化HashMap.//代码块1 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; threshold = initialCapacity; init(); }Java7中对于<key1,value1>的put方法实现相对比较简单,首先根据 key1 的key值计算hash值,再根据该hash值与table的length确定该key所在的index,如果当前位置的Entry不为null,则在该Entry链中遍历,如果找到hash值和key值都相同,则将值value覆盖,返回oldValue;如果当前位置的Entry为null,则直接addEntry。代码块2public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }//addEntry方法中会检查当前table是否需要resize void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); //当前map中的size 如果大于threshole的阈值,则将resize将table的length扩大2倍。 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }Java7 中resize()方法的实现比较简单,将OldTable的长度扩展,并且将oldTable中的Entry根据rehash的标记重新计算hash值和index移动到newTable中去。代码如代码块3中所示,//代码块3 --JDK7中HashMap.resize()方法void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } /** * 将当前table的Entry转移到新的table中 */ void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }HashMap性能的有两个参数:初始容量(initialCapacity) 和加载因子(loadFactor)。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。根据源码分析可以看出:在Java7 中 HashMap的entry是按照index索引存储的,遇到hash冲突的时候采用拉链法解决冲突,将冲突的key和value插入到链表list中。然而这种解决方法会有一个缺点,假如key值都冲突,HashMap会退化成一个链表,get的复杂度会变成O(n)。在Java8中为了优化该最坏情况下的性能,采用了平衡树来存放这些hash冲突的键值对,性能由此可以提升至O(logn)。代码块4 -- JDK8中HashMap中常量定义 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; static final int TREEIFY_THRESHOLD = 8; // 是否将list转换成tree的阈值 static final int UNTREEIFY_THRESHOLD = 6; // 在resize操作中,决定是否untreeify的阈值 static final int MIN_TREEIFY_CAPACITY = 64; // 决定是否转换成tree的最小容量 static final float DEFAULT_LOAD_FACTOR = 0.75f; // default的加载因子在Java 8 HashMap的put方法实现如代码块5所示,代码块5 --JDK8 HashMap.put方法 public V put(K key, V value) { 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; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //table为空的时候,n为table的长度 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // (n - 1) & hash 与Java7中indexFor方法的实现相同,若i位置上的值为空,则新建一个Node,table[i]指向该Node。 else { // 若i位置上的值不为空,判断当前位置上的Node p 是否与要插入的key的hash和key相同 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;//相同则覆盖之 else if (p instanceof TreeNode) // 不同,且当前位置上的的node p已经是TreeNode的实例,则再该树上插入新的node。 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 在i位置上的链表中找到p.next为null的位置,binCount计算出当前链表的长度,如果继续将冲突的节点插入到该链表中,会使链表的长度大于tree化的阈值,则将链表转换成tree。 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }再看下resize方法,由于需要考虑hash冲突解决时采用的可能是list 也可能是balance tree的方式,因此resize方法相比JDK7中复杂了一些,代码块6 -- JDK8的resize方法 inal 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) { threshold = Integer.MAX_VALUE;//如果超过最大容量,无法再扩充table return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // threshold门槛扩大至2倍 } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } 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];// 创建容量为newCap的newTab,并将oldTab中的Node迁移过来,这里需要考虑链表和tree两种情况。2023-08-14 03:23:301
Hashpmap的原理,HashMap怎样保证key的唯一性
去看看这个帖子吧,或许对你有所帮助http://www.cnblogs.com/yxysuanfa/p/6932010.html2023-08-14 03:23:381
Java中的强引用,软引用,弱引用,虚引用有什么用
这两天在看 Reference 相关的东西,虽然接触到的场景不多,但感觉还是比较有用的。在非常关心内存的情况下,有可能能派上用场。在涉及到某一个对象被 GC,需要得到通知,然后再做某些相关处理的时候,有可能派得上用场。对于第一点,目前接触到的就是 WeakHahsMap。像我们平常写缓存的时候,最简单的就是在一个类中public static Map<String,String> user_map = new HashMap<String, String>();然后再在全局使用它。这里就可以参考下 Tomcat 实现的这个 LRU 缓存:tomcat/ConcurrentCache.java at trunk · apache/tomcat · GitHub,它就用到了 WeakHashMap,WeakHashMap也用到了 WeakReference,可以参考下。WeakHahsMap 的实现原理简单来说就是里面的 Entry 使用继承了 WeakReference,那么当 Entry 的 key 不再被使用的时候,也就是被 GC 的时候,那么该 Entry 就会进入到 ReferenceQueue 中, WeakHashMap 在你调用相关方法的时候,那么就会把这个 Entry 从 ReferenceQueue 中删除,并且做相关处理(也就是把这个 key 删除)。这时候你就会发现,你的一个键值对没有了。对于第二点,目前接触到的就是一个 FileCleaningTracker,它的作用是,把一个文件和一个对象关联起来,当这个对象被 GC 的时候,文件也帮我们删除掉。FileCleaningTracker 的实现简单来说就是用了一个虚引用。它里面有一个 Tracker 的内部类,他继承自 PhantomReference,你可以理解成 WeakHashMap中的 Entry(其实也很像了)。这个 Tracker 就和我们的关联对象 marker 关联起来(你可以理解成 WeakHashMap 中的 Entry 的 key),那么当 marker 被回收的时候,Tracker 就会被GC 弄到 ReferenceQueue 中,那么 FileCleaningTracker 就会把 Tracker 从 ReferenceQueue 移除出来,并且执行相关操作(delete file)。最后:自己理解的引用(强,软,弱虚)是和 GC 比较相关的,能够在一个 对象被 GC 的时候,应用程序得到通知,然后执行相关操作。像 WeakHashMap 就是这样,底层发现 key 没了,上层就把 entry 删了。ps:第6条:消除过期对象的引用(WeakHashMap的缓存应用):愿无岁月可回首第6条:WeakHashMap扩展知识1(原理与Reference相关):愿无岁月可回首第6条:Reference应用-FileCleaningTracker:愿无岁月可回首最后的最后补充。。。突然想起,之前自己在工作中涉及到这样一种情况,我需要对用户投资的钱进行一个每日的加息,有一天客户忘了把加息的数据给我,然后给了我一份 csv 文件,让我加上。我当时的做法是,让运维把文件放在固定的地方,程序读取,执行逻辑,然后运维再手动删除。 这个时候,其实也可以通过这种 FileCleaningTracker,当读取完之后,自动的删除这个文件,就不用运维来做了。(这可能是一种应用场景)2023-08-14 03:23:451
hashmap为什么不是线程安全的
首页所有文章资讯Web架构基础技术书籍教程Java小组工具资源谈谈HashMap线程不安全的体现2016/10/18|分类:基础技术|6条评论|标签:HASHMAP,并发分享到:26原文出处:HoseeHashMap的原理以及如何实现,之前在JDK7与JDK8中HashMap的实现中已经说明了。那么,为什么说HashMap是线程不安全的呢?它在多线程环境下,会发生什么情况呢?1.resize死循环我们都知道HashMap初始容量大小为16,一般来说,当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash,这个成本相当的大。voidresize(intnewCapacity){Entry[]oldTable=table;intoldCapacity=oldTable.length;if(oldCapacity==MAXIMUM_CAPACITY){threshold=Integer.MAX_VALUE;return;}Entry[]newTable=newEntry[newCapacity];transfer(newTable,initHashSeedAsNeeded(newCapacity));table=newTable;threshold=(int)Math.min(newCapacity*loadFactor,MAXIMUM_CAPACITY+1);}voidtransfer(Entry[]newTable,booleanrehash){intnewCapacity=newTable.length;for(Entrye:table){while(null!=e){Entrynext=e.next;if(rehash){e.hash=null==e.key?0:hash(e.key);}inti=indexFor(e.hash,newCapacity);e.next=newTable[i];newTable[i]=e;e=next;}}}大概看下transfer:对索引数组中的元素遍历对链表上的每一个节点遍历:用next取得要转移那个元素的下一个,将e转移到新Hash表的头部,使用头插法插入节点。循环2,直到链表节点全部转移循环1,直到所有索引数组全部转移经过这几步,我们会发现转移的时候是逆序的。假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1。这时候就有点头绪了,死锁问题不就是因为1->2的同时2->1造成的吗?所以,HashMap的死锁问题就出在这个transfer()函数上。2023-08-14 03:23:531
java7和java8对hashmap做了哪些优化
HashMap的原理介绍x0dx0ax0dx0a此乃老生常谈,不作仔细解说。x0dx0a一句话概括之:HashMap是一个散列表,它存储的内容是键值对(key-value)映射。x0dx0ax0dx0aJava 7 中HashMap的源码分析x0dx0ax0dx0a首先是HashMap的构造函数代码块1中,根据初始化的Capacity与loadFactor(加载因子)初始化HashMap.x0dx0a//代码块1x0dx0a public HashMap(int initialCapacity, float loadFactor) {x0dx0a if (initialCapacity < 0)x0dx0a throw new IllegalArgumentException("Illegal initial capacity: " +x0dx0a initialCapacity);x0dx0a if (initialCapacity > MAXIMUM_CAPACITY)x0dx0a initialCapacity = MAXIMUM_CAPACITY;x0dx0a if (loadFactor <= 0 || Float.isNaN(loadFactor))x0dx0a throw new IllegalArgumentException("Illegal load factor: " +loadFactor);x0dx0ax0dx0a this.loadFactor = loadFactor;x0dx0a threshold = initialCapacity;x0dx0a init();x0dx0a }x0dx0ax0dx0aJava7中对于的put方法实现相对比较简单,首先根据 key1 的key值计算hash值,再根据该hash值与table的length确定该key所在的index,如果当前位置的Entry不为null,则在该Entry链中遍历,如果找到hash值和key值都相同,则将值value覆盖,返回oldValue;如果当前位置的Entry为null,则直接addEntry。x0dx0a代码块2x0dx0apublic V put(K key, V value) {x0dx0a if (table == EMPTY_TABLE) {x0dx0a inflateTable(threshold);x0dx0a }x0dx0a if (key == null)x0dx0a return putForNullKey(value);x0dx0a int hash = hash(key);x0dx0a int i = indexFor(hash, table.length);x0dx0a for (Entry e = table[i]; e != null; e = e.next) {x0dx0a Object k;x0dx0a if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {x0dx0a V oldValue = e.value;x0dx0a e.value = value;x0dx0a e.recordAccess(this);x0dx0a return oldValue;x0dx0a }x0dx0a }x0dx0ax0dx0a modCount++;x0dx0a addEntry(hash, key, value, i);x0dx0a return null;x0dx0a }x0dx0ax0dx0a//addEntry方法中会检查当前table是否需要resizex0dx0a void addEntry(int hash, K key, V value, int bucketIndex) {x0dx0a if ((size >= threshold) && (null != table[bucketIndex])) {x0dx0a resize(2 * table.length); //当前map中的size 如果大于threshole的阈值,则将resize将table的length扩大2倍。x0dx0a hash = (null != key) ? hash(key) : 0;x0dx0a bucketIndex = indexFor(hash, table.length);x0dx0a }x0dx0ax0dx0a createEntry(hash, key, value, bucketIndex);x0dx0a }x0dx0ax0dx0aJava7 中resize()方法的实现比较简单,将OldTable的长度扩展,并且将oldTable中的Entry根据rehash的标记重新计算hash值和index移动到newTable中去。代码如代码块3中所示,x0dx0a//代码块3 --JDK7中HashMap.resize()方法x0dx0avoid resize(int newCapacity) {x0dx0a Entry[] oldTable = table;x0dx0a int oldCapacity = oldTable.length;x0dx0a if (oldCapacity == MAXIMUM_CAPACITY) {x0dx0a threshold = Integer.MAX_VALUE;x0dx0a return;x0dx0a }x0dx0ax0dx0a Entry[] newTable = new Entry[newCapacity];x0dx0a transfer(newTable, initHashSeedAsNeeded(newCapacity));x0dx0a table = newTable;x0dx0a threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);x0dx0a }x0dx0ax0dx0a /**x0dx0a * 将当前table的Entry转移到新的table中x0dx0a */x0dx0a void transfer(Entry[] newTable, boolean rehash) {x0dx0a int newCapacity = newTable.length;x0dx0a for (Entry e : table) {x0dx0a while(null != e) {x0dx0a Entry next = e.next;x0dx0a if (rehash) {x0dx0a e.hash = null == e.key ? 0 : hash(e.key);x0dx0a }x0dx0a int i = indexFor(e.hash, newCapacity);x0dx0a e.next = newTable[i];x0dx0a newTable[i] = e;x0dx0a e = next;x0dx0a }x0dx0a }x0dx0a }x0dx0ax0dx0aHashMap性能的有两个参数:初始容量(initialCapacity) 和加载因子(loadFactor)。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。x0dx0a根据源码分析可以看出:在Java7 中 HashMap的entry是按照index索引存储的,遇到hash冲突的时候采用拉链法解决冲突,将冲突的key和value插入到链表list中。x0dx0a然而这种解决方法会有一个缺点,假如key值都冲突,HashMap会退化成一个链表,get的复杂度会变成O(n)。x0dx0a在Java8中为了优化该最坏情况下的性能,采用了平衡树来存放这些hash冲突的键值对,性能由此可以提升至O(logn)。x0dx0a代码块4 -- JDK8中HashMap中常量定义x0dx0a static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; x0dx0a static final int TREEIFY_THRESHOLD = 8; // 是否将list转换成tree的阈值x0dx0a static final int UNTREEIFY_THRESHOLD = 6; // 在resize操作中,决定是否untreeify的阈值x0dx0a static final int MIN_TREEIFY_CAPACITY = 64; // 决定是否转换成tree的最小容量x0dx0a static final float DEFAULT_LOAD_FACTOR = 0.75f; // default的加载因子x0dx0ax0dx0a在Java 8 HashMap的put方法实现如代码块5所示,x0dx0a代码块5 --JDK8 HashMap.put方法x0dx0a public V put(K key, V value) {x0dx0a return putVal(hash(key), key, value, false, true);x0dx0a }x0dx0ax0dx0a final V putVal(int hash, K key, V value, boolean onlyIfAbsent,x0dx0a boolean evict) {x0dx0a Node[] tab; Node p; int n, i;x0dx0a if ((tab = table) == null || (n = tab.length) == 0)x0dx0a n = (tab = resize()).length; //table为空的时候,n为table的长度x0dx0a if ((p = tab[i = (n - 1) & hash]) == null)x0dx0a tab[i] = newNode(hash, key, value, null); // (n - 1) & hash 与Java7中indexFor方法的实现相同,若i位置上的值为空,则新建一个Node,table[i]指向该Node。x0dx0a else {x0dx0a // 若i位置上的值不为空,判断当前位置上的Node p 是否与要插入的key的hash和key相同x0dx0a Node e; K k;x0dx0a if (p.hash == hash &&x0dx0a ((k = p.key) == key || (key != null && key.equals(k))))x0dx0a e = p;//相同则覆盖之x0dx0a else if (p instanceof TreeNode)x0dx0a // 不同,且当前位置上的的node p已经是TreeNode的实例,则再该树上插入新的node。x0dx0a e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);x0dx0a else {x0dx0a // 在i位置上的链表中找到p.next为null的位置,binCount计算出当前链表的长度,如果继续将冲突的节点插入到该链表中,会使链表的长度大于tree化的阈值,则将链表转换成tree。x0dx0a for (int binCount = 0; ; ++binCount) {x0dx0a if ((e = p.next) == null) {x0dx0a p.next = newNode(hash, key, value, null);x0dx0a if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1stx0dx0a treeifyBin(tab, hash);x0dx0a break;x0dx0a }x0dx0a if (e.hash == hash &&x0dx0a ((k = e.key) == key || (key != null && key.equals(k))))x0dx0a break;x0dx0a p = e;x0dx0a }x0dx0a }x0dx0a if (e != null) { // existing mapping for keyx0dx0a V oldValue = e.value;x0dx0a if (!onlyIfAbsent || oldValue == null)x0dx0a e.value = value;x0dx0a afterNodeAccess(e);x0dx0a return oldValue;x0dx0a }x0dx0a }x0dx0a ++modCount;x0dx0a if (++size > threshold)x0dx0a resize();x0dx0a afterNodeInsertion(evict);x0dx0a return null;x0dx0a }x0dx0ax0dx0a再看下resize方法,由于需要考虑hash冲突解决时采用的可能是list 也可能是balance tree的方式,因此resize方法相比JDK7中复杂了一些,x0dx0a代码块6 -- JDK8的resize方法x0dx0a inal Node[] resize() {x0dx0a Node[] oldTab = table;x0dx0a int oldCap = (oldTab == null) ? 0 : oldTab.length;x0dx0a int oldThr = threshold;x0dx0a int newCap, newThr = 0;x0dx0a if (oldCap > 0) {x0dx0a if (oldCap >= MAXIMUM_CAPACITY) {x0dx0a threshold = Integer.MAX_VALUE;//如果超过最大容量,无法再扩充tablex0dx0a return oldTab;x0dx0a }x0dx0a else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&x0dx0a oldCap >= DEFAULT_INITIAL_CAPACITY)x0dx0a newThr = oldThr << 1; // threshold门槛扩大至2倍x0dx0a }x0dx0a else if (oldThr > 0) // initial capacity was placed in thresholdx0dx0a newCap = oldThr;x0dx0a else { // zero initial threshold signifies using defaultsx0dx0a newCap = DEFAULT_INITIAL_CAPACITY;x0dx0a newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);x0dx0a }x0dx0a if (newThr == 0) {x0dx0a float ft = (float)newCap * loadFactor;x0dx0a newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?x0dx0a (int)ft : Integer.MAX_VALUE);x0dx0a }x0dx0a threshold = newThr;x0dx0a @SuppressWarnings({"rawtypes","unchecked"})x0dx0a Node[] newTab = (Node[])new Node[newCap];// 创建容量为newCap的newTab,并将oldTab中的Node迁移过来,这里需要考虑链表和tree两种情况。2023-08-14 03:24:001
ConcurrentHashMap 弱一致的迭代器 是什么原理
ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。2023-08-14 03:24:071
Java中HashMap和TreeMap的区别深入理解
两者都是非线程安全,前者无排序,后者会自动排序2023-08-14 03:24:333
java 中的hashmap如何解决冲突?就是如何保证两个不同的object放到hashmap中,计算key值时没有问题?
大概是不同的object计算出的hascode不同2023-08-14 03:24:522
hashtable和hashmap有什么区别,共同之处?、
该题可以在baidu ,有很多解答.我这需要说明下 这个是面试常 问的但hashtable 基本被淘汰了 ,可用concurrentMap 替代2023-08-14 03:25:003
ConcurrentHashMap 弱一致的迭代器 是什么原理
concurrenthashmap是弱一致的,iterator都是弱一致性的,两者的迭代器的一致性不同的。当删除了第7个元素的时候,B会感知到已删除,所以B的后续迭代都是真实的数据。2023-08-14 03:25:071
hashmap 为什么线程不安全
一直以来只是知道HashMap是线程不安全的,但是到底HashMap为什么线程不安全,多线程并发的时候在什么情况下可能出现问题?HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示: Map m = Collections.synchronizedMap(new HashMap(...));1、[java] view plain copyvoid addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); } 在hashmap做put操作的时候会调用到以上的方法。现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失2、[java] view plain copyfinal Entry<K,V> removeEntryForKey(Object key) { int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; } 删除键值对的代码如上:当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改3、addEntry中当加入新的键值对后键值对总数量超过门限值的时候会调用一个resize操作,代码如下:[java] view plain copyvoid resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } 这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。2023-08-14 03:25:171
SharedPreferences原理分析
前言 想要实现简单数据的持久化,我们首先会想到的方法肯定是SharedPreferences,有没有思考过这个我们使用了很久的类有什么缺点。 getSharedPreferences的实现是在ContextImpl里面。 源码分析: getSharedPreferences(packageName, MODE_PRIVATE) => getSharedPreferencesPath(name) =>getSharedPreferences(file, mode) 其实getSharedPreferences的源码很简单,通过getSharedPreferencesPath返回一个File,由源码可知,SharedPreferences内部存储使用的是xml文件。 我们根据文件首先去缓存(ArrayMap)里面找,如果没有找到,那么就创建SharedPreferencesImpl对象。 在SharedPreferencesImpl的构造方法中,会调用startLoadFromDisk()方法。然后通过BufferedInputStream去操作文件。 接下来我们分析SharedPreferences的存储数据的源码。 我们以getString方法为例,其实就是从map中取出对应的值。这个map的值就是在loadFromDisk()方法中通过读取file得到的。当然如果是空的话就会创建一个HashMap。 下面我们分析put的流程。 edit()方法会创建一个EditorImpl对象,所以我们不应该在循环中调用edit()方法。 EditorImpl是SharedPreferencesImpl中的一个内部类。putInt()方法实际上就是将值存储到EditorImpl中的HashMap中。 最后调用commit()方法进行提交。这里我们注意commitToMemory()和enqueueDiskWrite(mcr, null )这两个方法。源码我就不贴了,有兴趣的可以自己去看看。 commitToMemory()方法作用是将需要写入文件的数据存储到一个map中即mapToWriteToDisk。 enqueueDiskWrite()方法会新建一个线程调用writeToFile()方法将上一步中的map数据通过FileOutputStream写入到xml文件中。 apply()方法我们稍后分析。 答案是肯定的,我们都知道apply()方法采用的是异步,使用线程进行提交,那么为什么会造成ANR。 在apply()方法中我们会新建一个任务进行数据的保存,然后调用 QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)加入队列。 我们都知道创建Activity时会在ActivityThread中调用handlePauseActivity方法。 在handlePauseActivity()方法中有一个QueuedWork.waitToFinish()等待队列完成。所以,当任务所需时间过长时,这时候我们跳转Activity的时候,依旧会造成ANR。2023-08-14 03:25:241
ConcurrentHashMap 弱一致的迭代器 是什么原理
concurrenthashmap是弱一致的,iterator 都是弱一致性的,两者的迭代器的一致性不同的。当删除了第7个元素的时候,B会感知到已删除,所以B的后续迭代都是真实的数据。2023-08-14 03:25:311
HashMap和HashSet的区别
1 HashMap和HashSet的区别是Java面试中最常被问到的问题。如果没有涉及到Collection框架以及多线程的面试,可以说是不完整。而Collection框架的问题不涉及到HashSet和HashMap,也可以说是不完整。HashMap和HashSet都是collection框架的一部分,它们让我们能够使用对象的集合。collection框架有自己的接口和实现,主要分为Set接口,List接口和Queue接口。它们有各自的特点,Set的集合里不允许对象有重复的值,List允许有重复,它对集合中的对象进行索引,Queue的工作原理是FCFS算法(First Come, First Serve)。23 首先让我们来看看什么是HashMap和HashSet,然后再来比较它们之间的分别。4 什么是HashSet56 HashSet实现了Set接口,它不允许集合中有重复的值,当我们提到HashSet时,第一件事情就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。78 public boolean add(Object o)方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。9 什么是HashMap10 11 HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。HashMap允许键和值为null。HashMap是非synchronized的,但collection框架提供方法能保证HashMap synchronized,这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。12 13 public Object put(Object Key,Object value)方法用来将元素添加到map中。14 15 你可以阅读这篇文章看看HashMap的工作原理,以及这篇文章看看HashMap和HashTable的区别。16 HashSet和HashMap的区别17 *HashMap* *HashSet*18 HashMap实现了Map接口 HashSet实现了Set接口19 HashMap储存键值对 HashSet仅仅存储对象20 使用put()方法将元素放入map中 使用add()方法将元素放入set中21 HashMap中使用键对象来计算hashcode值 HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false22 HashMap比较快,因为是使用唯一的键来获取对象 HashSet较HashMap来说比较慢2023-08-14 03:25:381
HashMap为什么不安全?
1、HashMap线程不安全原因: 原因: JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer (),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。2023-08-14 03:25:488
ArrayList和Vector的区别是什么?HashMap和Hashtable的区别呢?
ArrayList和Vector都实现了List接口,我们可以分析其源代码,很容易找出它们的区别:Vector中的add方法如下:publicsynchronizedbooleanadd(Ee){}ArrayList中的add方法如下:publicbooleanadd(Ee)很显然区别就在于一个有synchronized即线程同步,而另一外没有,参考源代码你会发现绝大部分方法都是这样的。也就是说当存在多线程访问时,Vector比ArrayList要安全,但这种安全的代价就是要付出更多的系统性能一般情况下我们是使用ArrayList,因为存在多线程去访问同一个list对象的可能性并不是太多。对于HashMap和Hashtable原理是一样的,Hashtable实现了线程同步,能确保多线程访问时的安全性,性能要比hashMap低2023-08-14 03:27:571