2021-Java后端工程师面试指南-(Java基础篇)(下)

简介: 前言文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…种一棵树最好的时间是十年前,其次是现在


来聊聊 ArrayList和LinkedList区别

  • ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
  • 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
  • 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。


说说Vector和ArrayList 区别:

  • Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。
  • 当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样,ArrayList就有利于节约内存空间。
  • 我们也可以用Collections.synchronizedList 来生成一个线程安全的List


说说HashSet和TreeSet的区别:

  • TreeSet 是二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值。
  • HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。
  • HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的 String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。


说说HashMap和HashTable的区别:

  • HashMap是非线程安全的,Hashtable是线程安全的,所以Hashtable重量级一些,因为使用了synchronized关键字来保证线程安全。
  • HashMap允许key和value都为null,而Hashtable都不能为null。
  • HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常


上面的是我们集合的基础,下面来看看一些进阶面试题


能不能深度的总结下ArrayList

  • 默认的无参构造的数组大小为10
  • ArrayList用for循环遍历比iterator迭代器遍历快
  • 一起来总结下它的扩容过程,第一步判断是否需要扩容(就是通过计算当前我数组的长度加上我要添加到数组的长度的和minCapacity 去和当前容量去比较,如果需要的话,那就进行第一次扩容,第一次扩容是的容量大小是原来的1.5倍,扩容之后再把扩容之后的值和前面的那个minCapacity和比较 如果还小的话,就进行第二次扩容,把容量扩成minCapacity,如果minCapacity大于最大容量,则就扩容为最大容量21亿左右


为什么ArrayList的elementData是用transient修饰的

  • ArrayList实现了Serializable接口,这意味着ArrayList是可以被序列化的,用transient修饰elementData意味着我不希望elementData数组被序列化
  • 序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法。


你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?

  • 如果两个对象相等,则 hashcode 一定也是相同的
  • 两个对象相等,对两个对象分别调用 equals 方法都返回 true
  • 两个对象有相同的 hashcode 值,它们也不一定是相等的
  • 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
  • hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)


聊聊HashSet 如何检查重复

当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。


说说HashMap 的底层实现

JDK1.8 之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

JDK1.8之后

相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。


那么你可以聊聊红黑树吗

首先我们知道红黑数是一个二叉查找树, 它有以下的特点

  • 左子树上所有结点的值均小于或等于它的根结点的值。
  • 右子树上所有结点的值均大于或等于它的根结点的值。
  • 左、右子树也分别为二叉排序树。

但是一般的二叉树在极端情况下,可能变成线性查找了,那么就失去它的查找特性意义了,而红黑树是一个自平衡树,它最重要的是增加了下面3个规则来确保它的自平衡

  • 每个叶子节点都是黑色的空节点(NIL节点)。
  • 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

这里小六六说下,红黑树还是很复杂的,所以一般往下不会问了,如果变态点,就还会问问自平衡的过程,这边我就不一一解释了,大家自行去找。它的变色,它的左旋,右旋,哈哈确实掉头发。。


HashMap 的长度为什么是 2 的幂次方

  • 首先考虑奇数行不行,在计算hash的时候,确定落在数组的位置的时候,计算方法是(n - 1) & hash ,奇数n-1为偶数,偶数2进制的结尾都是0,经过&运算末尾都是0,会增加hash冲突,并且有一半的空间不会有数据
  • 为啥要是2的幂,不能是2的倍数么,比如6,10?
  • hashmap 结构是数组,每个数组里面的结构是node(链表或红黑树),正常情况下,如果你想放数据到不同的位置,肯定会想到取余数确定放在那个数据里,  计算公式: hash % n,这个是十进制计算。在计算机中,  (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效。
  • 只有是2的幂数的数字经过n-1之后,二进制肯定是  ...11111111  这样的格式,这种格式计算的位置的时候(&),完全是由产生的hash值类决定,而不受n-1(组数长度的二进制) 影响。你可能会想, 受影响不是更好么,又计算了一下,类似于扰动函数,hash冲突可能更低了,这里要考虑到扩容了,2的幂次方*2,在二进制中比如4和8,代表2的2次方和3次方,他们的2进制结构相 似,比如 4和8   00000100    0000 1000   只是高位向前移了一位,这样扩容的时候,只需要判断高位hash,移动到之前位置的倍数就可以了,免去了重新计算位置的运算。


HashMap 的扩容伐值为什么是0.75

  • 当负载因子是1.0的时候,也就意味着,只有当数组的8个值(这个图表示了8个)全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。
  • 负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。
  • 基本上为什么是0.75的答案也就出来了,这是时间和空间的权衡。


HashMap树化的条件

这个小六六说下,如果没看过源码,肯定印象没有那么深刻

  • 第一个肯定是发生了hash碰撞
  • 并且链表的长度大于8就会树化,小于6之后会退化
  • 还有一个条件就是整个hashmap的容量要大于64


JDK1.7版本的hashmap死循环问题知道吗

小六六也大致说下,就是1.7在多线程的情况下,扩容的时候,假设2个线程同时扩容导致的我们链表的相互引用,导致的死循环,也就是我们所说的链表尾插,其实这也不算bug,因为官方明确说了 hashmap不应该在多线程的环境下使用,具体大家自行百度


说说hashmap的put操作

  • 第一步当然是先计算key的hash值(有过处理的 (h = key.hashCode()) ^ (h >>> 16))
  • 第二步调用putval方法,然后判断是否容器中全部为空,如果是的话,就把容器的容量扩容。
  • 第三步,把最大容量和hash值求&值(i = (n - 1) & hash),判断这个数组下标是否有数据,如果没有就把它放进去。还要判断key的equals方法,看是否需要覆盖。
  • 第四步,如果有,说明发生了碰撞,那么继续遍历判断链表的长度是否大于8,如果大于8,就继续把当前链表变成红黑树结构。
  • 第五步,如果没有到8,那么就直接把数据存在链表的尾部
  • 第六步,最后将容器的容量+1。


说说hashmap的resize的操作

  • 如果到达最大容量,那么返回当前的桶,并不再进行扩容操作,否则的话扩容为原来的两倍,返回扩容后的桶;
  • 根据扩容后的桶,修改其他的成员变量的属性值;
  • 根据新的容量创建新的扩建后的桶,并更新桶的引用;
  • 如果原来的桶里面有元素就需要进行元素的转移;
  • 在进行元素转移的时候需要考虑到元素碰撞和转红黑树操作;
  • 在扩容的过程中,按次从原来的桶中取出链表头节点,并对该链表上的所有元素重新计算hash值进行分配;
  • 在发生碰撞的时候,将新加入的元素添加到末尾;
  • 在元素复制的时候需要同时对低位和高位进行操作。


聊聊ConcurrentHashMap 1.7和1.8的比较

ConcurrentHashMap是conccurrent家族中的一个类,由于它可以高效地支持并发操作,以及被广泛使用,经典的开源框架Spring的底层数据结构就是使用ConcurrentHashMap实现的。与同是线程安全的老大哥HashTable相比,它已经更胜一筹,因此它的锁更加细化,而不是像HashTable一样为几乎每个方法都添加了synchronized锁,这样的锁无疑会影响到性能。

1.7和1.8实现线程安全的思想也已经完全变了其中抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想。

  • 不采用segment而采用node,锁住node来实现减小锁粒度。
  • 设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。
  • 使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
  • sizeCtl的不同值来代表不同含义,起到了控制的作用。


聊聊ConcurrentHashMap的sizeCtl

  • 负数代表正在进行初始化或扩容操作
  • -1代表正在初始化
  • -N 表示有N-1个线程正在进行扩容操作
  • 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。还后面可以看到,它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。


说说ConcurrentHashMap的put方法

  • 第一步,一进去肯定判断key value是否为null 如果为null 抛出异常
  • 第二步,当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,如果没有就初始化数组。
  • 第三步, 通过计算hash值来确定放在数组的哪个位置如果这个位置为空则直接添加(CAS的加锁方式),如果不为空的话,则取出这个节点来
  • 第四步,如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
  • 第五步,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作
  • 第六步,如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
  • 第七步,如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
  • 第八步,最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数


结尾

这个就是一些基础的面试题,当然还是很多不一定那么全,但是如果把这些都能回答出来,其实Java基础这块也算是蛮扎实了。

相关文章
|
26天前
|
Java 程序员
Java社招面试中的高频考点:Callable、Future与FutureTask详解
大家好,我是小米。本文主要讲解Java多线程编程中的三个重要概念:Callable、Future和FutureTask。它们在实际开发中帮助我们更灵活、高效地处理多线程任务,尤其适合社招面试场景。通过 Callable 可以定义有返回值且可能抛出异常的任务;Future 用于获取任务结果并提供取消和检查状态的功能;FutureTask 则结合了两者的优势,既可执行任务又可获取结果。掌握这些知识不仅能提升你的编程能力,还能让你在面试中脱颖而出。文中结合实例详细介绍了这三个概念的使用方法及其区别与联系。希望对大家有所帮助!
163 60
|
2天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
33 14
|
5天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
34 13
|
25天前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
65 16
|
22天前
|
Java 程序员 调度
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
55 9
|
27天前
|
安全 Java 程序员
Java面试必问!run() 和 start() 方法到底有啥区别?
在多线程编程中,run和 start方法常常让开发者感到困惑。为什么调用 start 才能启动线程,而直接调用 run只是普通方法调用?这篇文章将通过一个简单的例子,详细解析这两者的区别,帮助你在面试中脱颖而出,理解多线程背后的机制和原理。
59 12
|
1月前
|
SQL Java 数据库连接
Java MyBatis 面试题
Java MyBatis相关基础面试题
|
前端开发 Java 数据安全/隐私保护
🚀Java后端人眼中的前端,和前端人眼中的有不一样吗?5️⃣🚀
列表最大的特点就是 整齐 、整洁、 有序,跟表格类似,但是他可组合自由度会更高。
199 0
🚀Java后端人眼中的前端,和前端人眼中的有不一样吗?5️⃣🚀
|
存储 前端开发 Java
🚀Java后端人眼中的前端,和前端人眼中的有不一样吗?4️⃣🚀
表格的现在还是较为常用的一种标签,但不是用来布局,**常见显示、展示表格式数据。**因为它可以让数据显示的非常的规整,可读性非常好。
149 0
🚀Java后端人眼中的前端,和前端人眼中的有不一样吗?4️⃣🚀
|
前端开发 Java
🚀Java后端人眼中的前端,和前端人眼中的有不一样吗?3️⃣🚀
段落标签可以把 HTML 文档分割为若干段落,在网页中要把文字有条理地显示出来,离不开段落标签,就如同我们平常写文章一样,整个网页也可以分为若干个段落。
158 0
🚀Java后端人眼中的前端,和前端人眼中的有不一样吗?3️⃣🚀

热门文章

最新文章