1.hashmap Դ?码面? ????
2.我说HashMap初始容量是16,面试官让我回去等通知
3.Java面试问题:HashMap的码面底层原理
4.hashmap面试题,Android大厂高频面试题解析,码面建议收藏
5.原创|如果懂了HashMap这两点,码面面试就没问题了
6.面试官问:HashMap中变量modCount真实作用是码面什么?
hashmap Դ?? ????
在Java中,HashMap的码面密码提取指标源码底层数据结构随着版本变迁有所变化。在JDK1.7中,码面HashMap采用"数组加链表"的码面结构,数组作为主体,码面链表则用于处理哈希冲突。码面而在JDK1.8中,码面引入了"数组加链表加红黑树"的码面结构,当链表过长时,码面会转为红黑树,码面以提高搜索效率,码面红黑树的搜索时间复杂度为O(logn)。
HashMap的主要特点包括:通过哈希函数快速查找,键值对无序存储,但插入和删除操作可能会涉及到链表或红黑树的调整。解决哈希冲突的方法中,HashMap采用的是链地址法,即拉链法。当元素过多导致桶内链表过长时,会触发链表转为红黑树的条件。
数组长度大于后,才会考虑使用红黑树,因为小数组中红黑树可能导致性能下降。设置负载因子为0.和初始化临界值为,是为了平衡空间和时间效率,避免频繁扩容。当负载因子较小,空间浪费会多,而负载因子较大,查找效率低。
计算哈希值时,通常使用hashCode方法,通过位运算生成索引。若多个对象的hashCode相等,会引发哈希碰撞,JDK8之前是通过链表解决,之后引入链表和红黑树。插入操作中,HashMap会根据负载因子调整容量并处理碰撞,如扩容后通过位运算快速确定新位置。
通常情况下,HashMap的key使用不可变类如Integer和String,以确保其哈希值稳定。桶中节点超过8才转为红黑树,是基于统计学分析得出的合理阈值,以减少空间浪费和提高效率。
尽管HashMap是非线程安全的,但通过合适的同步策略可以在多线程环境中使用。计算hash值时异或处理低位和高位,是为了提高随机性,减少哈希冲突,确保高效查找。
我说HashMap初始容量是,面试官让我回去等通知
HashMap是大顶源码工作和面试中常见的数据类型,但很多人只停留在会用的层面,对它的底层实现原理并不深入理解。让我们一起深入浅出地解析HashMap的底层实现。
考虑以下面试问题,你能完整回答几个呢?
1. HashMap的底层数据结构是什么?
JDK1.7使用数组+链表,通过下标快速查询,解决哈希冲突。JDK1.8进行了优化,引入了红黑树,查询效率提升到O(logn)。在JDK1.8中,数组+链表+红黑树结构,当链表长度达到8,并且数组长度大于时,链表会转换为红黑树。
2. HashMap的初始容量是多少?
在JDK1.7中,初始容量为,但在JDK1.8中,初始化时并未指定容量,而是在首次执行put操作时才初始化容量。初始化时仅指定了负载因子大小。
3. HashMap的put方法流程是怎样的?
源码揭示了put方法的流程,包括哈希计算、桶定位、插入或替换操作等。
4. HashMap为何要设置容量为2的倍数?
为了更高效地计算key对应的数组下标位置,当数组长度为2的倍数时,可以通过逻辑与运算快速计算下标位置,比取模运算更快。
5. HashMap为何线程不安全?
因为HashMap的所有修改方法均未加锁,导致在多线程环境下无法保证数据的一致性和安全性。例如,一个线程删除key后,其他线程可能还无法察觉,导致数据不一致;在扩容时,另一个线程可能添加元素,但由于没有加锁,元素可能丢失,影响数据安全性。
6. 解决哈希冲突的方法有哪些?
常见的方法包括链地址法、线性探测法、再哈希法等。
7. JDK1.8扩容流程有何优化?
JDK1.7在扩容时会遍历原数组,重新哈希,计算新数组下标,效率较低。而JDK1.8则优化了流程,只遍历原数组,通过新旧数组下标映射减少操作,提高了效率。
推荐阅读:《我爱背八股系列》
面试官问关于订单ID、分库分表、分布式锁、消息队列、MySQL索引、锁原理、查询性能优化等八股文问题时,注释源码.幸亏有总结的全套八股文。
以上内容是关于HashMap的深入解析和面试常见问题的解答,希望能够帮助到大家。
Java面试问题:HashMap的底层原理
JDK1.8中HashMap的put()和get()操作的过程
put操作:
①首先判断数组是否为空,如果数组为空则进行第一次扩容(resize)
②根据key计算hash值并与上数组的长度-1(int index = key.hashCode()&(length-1))得到键值对在数组中的索引。
③如果该位置为null,则直接插入
④如果该位置不为null,则判断key是否一样(hashCode和equals),如果一样则直接覆盖value
⑤如果key不一样,则判断该元素是否为 红黑树的节点,如果是,则直接在 红黑树中插入键值对
⑥如果不是 红黑树的节点,则就是 链表,遍历这个 链表执行插入操作,如果遍历过程中若发现key已存在,直接覆盖value即可。
如果 链表的长度大于等于8且数组中元素数量大于等于阈值,则将 链表转化为 红黑树,(先在 链表中插入再进行判断)
如果 链表的长度大于等于8且数组中元素数量小于阈值,则先对数组进行扩容,不转化为 红黑树。
⑦插入成功后,判断数组中元素的个数是否大于阈值(threshold),超过了就对数组进行扩容操作。
get操作:
①计算key的hashCode的值,找到key在数组中的位置
②如果该位置为null,就直接返回null
③否则,根据equals()判断key与当前位置的值是否相等,如果相等就直接返回。
④如果不等,再判断当前元素是否为树节点,如果是树节点就按 红黑树进行查找。
⑤否则,按照 链表的方式进行查找。
3.HashMap的扩容机制
4.HashMap的初始容量为什么是?
1.减少hash碰撞 (2n ,=2^4)
2.需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。
3.防止分配过小频繁扩容
4.防止分配过大浪费资源
5.HashMap为什么每次扩容都以2的整数次幂进行扩容?
因为Hashmap计算存储位置时,使用了(n - 1) & hash。只有当容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突,所以扩容必须2倍就是为了维持容量始终为2的幂次方。
6.HashMap扩容后会重新计算Hash值吗?
①JDK1.7
JDK1.7中,HashMap扩容后,所有的key需要重新计算hash值,然后再放入到新数组中相应的位置。
②JDK1.8
在JDK1.8中,HashMap在扩容时,需要先创建一个新数组,然后再将旧数组中的数据转移到新数组上来。
此时,旧数组中的数据就会根据(e.hash & oldCap),数据的hash值与扩容前数组的长度进行与操作,根据结果是否等于0,分为2类。
1.等于0时,glew源码该节点放在新数组时的位置等于其在旧数组中的位置。
2.不等于0时,该节点在新数组中的位置等于其在旧数组中的位置+旧数组的长度。
7.HashMap中当 链表长度大于等于8时,会将 链表转化为 红黑树,为什么是8?
如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么 红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现 链表很长的情况。在理想情况下, 链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从 链表向 红黑树的转换。
8.HashMap为什么线程不安全?
1.在JDK1.7中,当并发执行扩容操作时会造成死循环和数据丢失的情况。
在JDK1.7中,在多线程情况下同时对数组进行扩容,需要将原来数据转移到新数组中,在转移元素的过程中使用的是头插法,会造成死循环。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会通过判断,将执行插入操作。
假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
9.为什么HashMapJDK1.7中扩容时要采用头插法,JDK1.8又改为尾插法?
JDK1.7的HashMap在实现resize()时,新table[ ]的列表队头插入。
这样做的目的是:避免尾部遍历。
避免尾部遍历是为了避免在新列表插入数据时,遍历到队尾的位置。因为,直接插入的效率更高。
对resize()的设计来说,本来就是要创建一个新的table,列表的顺序不是很重要。但如果要确保插入队尾,还得遍历出 链表的队尾位置,然后插入,是dwt源码一种多余的损耗。
直接采用队头插入,会使得 链表数据倒序。
JDK1.8采用尾插法是避免在多线程环境下扩容时采用头插法出现死循环的问题。
.HashMap是如何解决哈希冲突的?
拉链法(链地址法)
为了解决碰撞,数组中的元素是单向 链表类型。当 链表长度大于等于8时,会将 链表转换成 红黑树提高性能。
而当 链表长度小于等于6时,又会将 红黑树转换回单向 链表提高性能。
.HashMap为什么使用 红黑树而不是B树或 平衡二叉树AVL或二叉查找树?
1.不使用二叉查找树
二叉 排序树在极端情况下会出现线性结构。例如:二叉 排序树左子树所有节点的值均小于根节点,如果我们添加的元素都比根节点小,会导致左子树线性增长,这样就失去了用树型结构替换 链表的初衷,导致查询时间增长。所以这是不用二叉查找树的原因。
2.不使用 平衡二叉树
平衡二叉树是严格的平衡树, 红黑树是不严格平衡的树, 平衡二叉树在插入或删除后维持平衡的开销要大于 红黑树。
红黑树的虽然查询性能略低于 平衡二叉树,但在插入和删除上性能要优于 平衡二叉树。
选择 红黑树是从功能、性能和开销上综合选择的结果。
3.不使用B树/B+树
HashMap本来是数组+ 链表的形式, 链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。
如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面,这个时候遍历效率就退化成了 链表。
.HashMap和Hashtable的异同?
①HashMap是⾮线程安全的,Hashtable是线程安全的。
Hashtable 内部的⽅法基本都经过 synchronized 修饰。
②因为线程安全的问题,HashMap要⽐Hashtable效率⾼⼀点。
③HashMap允许键和值是null,而Hashtable不允许键或值是null。
HashMap中,null 可以作为键,这样的键只有 ⼀个,可以有 ⼀个或多个键所对应的值为 null。
HashTable 中 put 进的键值只要有 ⼀个 null,直接抛出 NullPointerException。
④ Hashtable默认的初始 大小为,之后每次扩充,容量变为原来的2n+1。
HashMap默认的初始 大⼩为,之后每次扩充,容量变为原来的2倍。
⑤创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的 ⼤⼩, ⽽ HashMap 会将其扩充为2的幂次⽅ ⼤⼩。
⑥JDK1.8 以后的 HashMap 在解决哈希冲突时当 链表⻓度 大于等于8时,将 链表转化为红⿊树,以减少搜索时间。Hashtable没有这样的机制。
Hashtable的底层,是以数组+ 链表的形式来存储。
⑦HashMap的父类是AbstractMap,Hashtable的父类是Dictionary
相同点:都实现了Map接口,都存储k-v键值对。
.HashMap和HashSet的区别?
HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码⾮常⾮常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet ⾃⼰不得不实现之外,其他⽅法都是直接调用 HashMap 中的⽅法)
1.HashMap实现了Map接口,HashSet实现了Set接口
2.HashMap存储键值对,HashSet存储对象
3.HashMap调用put()向map中添加元素,HashSet调用add()方法向Set中添加元素。
4.HashMap使用键key计算hashCode的值,HashSet使用对象来计算hashCode的值,在hashCode相等的情况下,使用equals()方法来判断对象的相等性。
5.HashSet中的元素由HashMap的key来保存,而HashMap的value则保存了一个静态的Object对象。
.HashSet和TreeSet的区别?
相同点:HashSet和TreeSet的元素都是不能重复的,并且它们都是线程不安全的。
不同点:
①HashSet中的元素可以为null,但TreeSet中的元素不能为null
②HashSet不能保证元素的排列顺序,TreeSet支持自然 排序、定制 排序两种 排序方式
③HashSet底层是采用 哈希表实现的,TreeSet底层是采用 红黑树实现的。
④HashSet的add,remove,contains方法的时间复杂度是 O(1),TreeSet的add,remove,contains方法的时间复杂度是 O(logn)
.HashMap的遍历方式?
①通过map.keySet()获取key,根据key获取到value
②通过map.keySet()遍历key,通过map.values()遍历value
③通过Map.Entry(String,String) 获取,然后使用entry.getKey()获取到键,通过entry.getValue()获取到值
④通过Iterator
hashmap面试题,Android大厂高频面试题解析,建议收藏
在技术的海洋中,不断涌起的新潮与迭代,是否让你感到迷失?本篇文章将为你提供指南,揭示学习哪些技能,如何跟上时代步伐,避免被快速淘汰。
首先,让我们了解大型互联网公司如百度、腾讯、阿里等在技术岗位面试中的流程。面试通常包含Java基础、JVM相关、集合、并发、线程、线程池、IO、Android基础、Activity、Service、BroadcastReceiver、Fragment、序列化、IPC、View事件机制、View绘制、View动画、Handler、AsyncTask、Bitmap压缩、ListView与RecyclerView、数据存储、Android开源框架、OkHttp、Retrofit、RxJava、Glide、ButterKnife、EventBus、Android性能优化、Android屏幕适配、Android打包、Android架构等知识点。
要成为Android高级架构师,你不仅需要具备抽象思维和分析能力,这能帮助你全面理解和掌控系统。通过经验积累与持续学习,你能够更准确地理解需求,并运用软件工程思想将其转化为可实现的代码。经验的积累需要时间,但有意识地学习前人的经验,可以加速这一过程。
此外,架构师还应具备软件工程领域的经验,以便准确理解需求并将其转化为实现方案。通过整理架构师进阶系列的学习笔记,包括《设计思想解读开源框架》和《°全方面性能优化》,你将获得宝贵的知识和技能,助你成为真正的Android架构师。
原创|如果懂了HashMap这两点,面试就没问题了
HashMap在后端面试中经常被问及,比如默认初始容量、加载因子和线程安全性等问题。通常,这些问题能对答如流,表明对HashMap有较好的理解。然而,近期团队的技术分享中,我从两个角度获得了一些新见解,现在分享给大家。
首先,让我们探讨如何找到比初始容量值大的最小的2的幂次方整数。通常,使用默认构造器时,HashMap的初始容量为,加载因子为0.。这样做可能导致在数据量大时频繁进行扩容,影响性能。因此,通常会预估容量并使用带容量的构造器创建。通过分析源码,我们可以得知HashMap数组部分长度范围为[0,2^]。要找到比初始容量大的最小的2的幂次方整数,我们需重点关注tableSizeFor方法。此方法巧妙地设计,当输入的容量本身为2的整数次幂时,返回该容量;否则,返回比输入容量大的最小2的整数次幂。此设计旨在确保容量始终为2的整数次幂,从而优化哈希操作,避免哈希冲突。在获取key对应的数组下标时,通过key的哈希值与数组长度-1进行与运算,这种方法依赖于容量为2的整数次幂的特性,以确保哈希值的分散性。
容量为2的整数次幂的关键在于,它允许通过与运算高效地定位key对应的数组下标。容量不是2的整数次幂时,与运算后的哈希值可能会导致位数为0的冲突,影响数据定位的准确性。tableSizeFor方法在计算过程中,首先对输入的容量进行-1操作,以避免容量本身就是2的整数次幂时,计算结果为容量的2倍。接着,通过连续的移位与或操作,找到比输入容量大的最小的2的整数次幂。这种方法确保了内存的有效利用,避免了不必要的扩容。
下面,让我们通过一个示例来详细解释算法中的移位与或操作。假设初始容量n为一个位的整数,例如:n = xxx xxxxxxxx xxxxxxxx xxxxxxxx(x表示该位上是0还是1,具体值不关心)。首先,执行n |= n >> 1操作,用n本身与右移一位后的n进行或操作,可以将n的最高位的1及其紧邻的右边一位置为1。接下来,重复此操作,进行n |= n >> 2、n |= n >> 4、n |= n >> 8和n |= n >> 。最后,将n与最大容量进行比较,如果大于等于2^,则返回最大容量;否则,返回n + 1,找到比n大的最小的2的整数次幂。
在实践中,这确保了在给定容量范围内高效地找到合适的容量值。例如,输入时,输出为,即比大的最小的2的整数次幂。
接下来,我们探讨HashMap在处理key时进行哈希处理的特殊操作。在执行put操作时,首先对key进行哈希处理。在源码中,可以看到执行了(h = key.hashCode()) ^ (h >> )的操作。这个操作将key的hashCode值与右移位后的值进行异或操作,将哈希值的高位和低位混合计算,以生成更离散的哈希值。通过演示,我们可以发现,当三个不同的key生成的hashCode值的低位完全相同、高位不同时,它们在数组中的下标会相同,导致哈希冲突。通过异或操作,我们解决了这个问题,使得经过哈希处理后的key能被更均匀地分布在数组中,提高了数据的分散性,减少了哈希冲突。
总结来说,这两个点揭示了HashMap在容量和哈希处理上的一些巧妙设计,这些设计提高了数据结构的效率和性能。理解这些原理不仅有助于解决面试问题,还能在实际工作中借鉴这些思想,优化数据存储和访问效率。希望我的讲解能帮助大家掌握这两个知识点,如有任何疑问,欢迎留言或私聊。通过深入研究和实践,我们可以更好地理解和利用HashMap这一强大的数据结构。
面试官问:HashMap中变量modCount真实作用是什么?
在网上查找关于HashMap中变量modCount的作用时,常见的解释是与fail-fast机制相关。fail-fast机制是Java集合框架中的一种策略,旨在提供快速失败,避免迭代过程中有其他线程修改集合时,抛出ConcurrentModificationException异常。这一机制在源代码中的实现是通过modCount值,每次对HashMap内容的修改都会增加此值,进而迭代器在初始化时将其设置为expectedModCount。在迭代过程中,迭代器会检查modCount与expectedModCount是否相等,不等时则表明有其他线程修改了Map,进而抛出异常。然而,在JDK7和JDK8版本中,modCount变量并未被声明为volatile,这与早期版本有所不同。
实际上,JDK7和JDK8中对于modCount的处理方式并未改变fail-fast机制的初衷。关键在于,modCount的存在是为了帮助实现ConcurrentModificationException的抛出,以防止在迭代过程中有其他线程修改集合。虽然modCount在这些版本中未显式声明为volatile,但这并不意味着在多线程环境下,modCount的修改不再具有可见性。在多线程环境下,modCount的修改仍能被其他线程看到,因此仍然能够达到fail-fast机制的目的。
进一步分析,modCount的存在主要为了配合ConcurrentModificationException的使用。在JDK源码中,ConcurrentModificationException的注释表明此异常并不总是表示对象被其他线程同时修改。它可能由一系列违反对象约定的方法调用引发。因此,modCount的存在是为了在某些特定情况下,如使用带有fail-fast机制的迭代器时,检测到集合内容的修改并抛出异常,以保护程序的正确性和稳定性。
综上所述,虽然JDK7和JDK8版本中modCount的声明方式与早期版本有所不同,但这并未改变其在实现fail-fast机制中的核心作用。modCount的存在仍然对于检测和防止迭代过程中集合内容被其他线程修改至关重要,确保了程序的健壮性和可靠性。
面试长知识了!Java 关键字 transient 尽然还能这么用
在深入研究HashMap源码时,我发现链表table数组中的transient关键字具有意想不到的用法。transient关键字通常与序列化相关,当我们需要在网络上传输对象数据时,它能控制哪些字段不被序列化。
序列化和反序列化是Java对象处理的重要概念。序列化是将对象转化为字节序列,以便存储或网络传输,而反序列化则是将这些字节序列恢复为Java对象。在序列化过程中,有时我们希望某些敏感字段不被包含,这时transient关键字就派上用场了。
transient关键字用于标记那些在序列化时不包含的成员变量。它仅适用于变量,不能修饰方法或类。举个例子,如果产品对象Product有价格、数量和总价,总价通常是通过其他两个字段计算得出,这时我们可以使用transient关键字阻止总价字段被序列化。
然而,值得注意的是,transient修饰的字段在实现Serializable接口的序列化中默认不会被包含。但通过实现Externalizable接口,我们可以手动指定哪些字段,包括transient修饰的,需要进行序列化。这验证了transient关键字对于序列化的控制并非绝对,取决于我们如何选择序列化方式。
总之,transient关键字是Java中控制序列化细节的重要工具,理解它的使用场景和特性能帮助我们在实际编程中更好地处理数据传输问题。希望这些信息对你的编程之路有所助益,记得关注我的公众号“猿芯”,获取更多编程知识。