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

前言

文本已收录至我的GitHub仓库,欢迎Star:https://github.com/bin392328206/six-finger
种一棵树最好的时间是十年前,其次是现在

Tips

面试指南系列,很多情况下不会去深挖细节,是小六六以被面试者的角色去回顾知识的一种方式,所以我默认大部分的东西,作为面试官的你,肯定是懂的。

https://www.processon.com/view/link/600ed9e9637689349038b0e4

上面的是脑图地址

叨絮

可能大家觉得有点老生常谈了,确实也是。面试题,面试宝典,随便一搜,根本看不完,也看不过来,那我写这个的意义又何在呢?其实嘛我写这个的有以下的目的

  • 第一就是通过一个体系的复习,让自己前面的写的文章再重新的过一遍,总结升华嘛
  • 第二就是通过写文章帮助大家建立一个复习体系,我会将大部分会问的的知识点以点带面的形式给大家做一个导论

然后下面是前面的文章汇总

  • 2021-Java后端工程师面试指南-(引言)

最后就是以面试题的形式来回顾所有的知识点,会整理一些比较常见的面试题和自己实际开发碰到的问题等题目。

Java基本功

Java字符型常量和字符串常量的区别?

我们在开发的过程中,用的比较多的应该是字符串,所以要熟悉下字符常量,我们可以回答

  • 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的 0 个或若干个字符
  • 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
  • 占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节

String和StringBuilder、StringBuffer的区别?

  • Java 平台提供了两种类型的字符串:String 和 StringBuffer/StringBuilder,它们可以储存和操作字符串。
  • 其中 String 是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的。
  • 而 StringBuffer/StringBuilder 类表示的字符串对象可以直接进行修改。StringBuilder 是 Java 5 中引入的,它和 StringBuffer 的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被 synchronized 修饰,因此它的效率也比 StringBuffer 要高。

小六六多嘴下,其实也是在告诫自己,其实我相信这个题目的答案大部分的人都是会的,也背的滚瓜烂熟,但是我们真正在开发的过程中确是没有遵守的,比如有时候我们处理一些逻辑的时候,需要拼接些字段的时候我们会习惯的用+,不知道有没有同款开发。哈哈,小六六和大家一起尽量养成好的开发习惯哈

说说反射的用途及实现

Java 反射机制是一个非常强大的功能,在很多的项目比如 Spring,MyBatis 都都可以看到反射的身影。通过反射机制,我们可以在运行期间获取对象的类型信息。利用这一点我们可以实现工厂模式和代理模式等设计模式,同时也可以解决 Java 泛型擦除等令人苦恼的问题。

获取一个对象对应的反射类,在 Java 中有下列方法可以获取一个对象的反射类

  • new 一个对象,然后对象.getClass()方法
  • 通过 Class.forName() 方法
  • 使用 类.class

说说有没有碰到BigDecimal的坑,或者说需要注意的点

  • 我们在使用BigDecimal时,为了防止精度丢失,推荐使用它的 BigDecimal(String) 构造方法来创建对象。
    其实就是想知道我们的实际开发是否有注意到这些点,

说说你平时是怎么把一个逗号隔开的字符串变成一个集合的,类似于("1,2,3")这种

  • List myList = Arrays.stream(myArray).collect(Collectors.toList()),建议使用这种方式,而不是List myList = Arrays.asList(1, 2, 3);
  • 至于原因就是Arrays.asList()将数组转换为集合后,底层其实还是数组

说说String s="abc"和String s=new String("abc")区别;

  • 相信大家对这道题并不陌生,答案也是众所周知的,1个或2个。
  • 首先在堆中(不是常量池)创建一个指定的对象"abc",并让str引用指向该对象
  • 在字符串常量池中查看,是否存在内容为"abc"字符串对象
  • 若存在,则将new出来的字符串对象与字符串常量池中的对象联系起来
  • 若不存在,则在字符串常量池中创建一个内容为"abc"的字符串对象,并将堆中的对象与之联系起来

聊聊Java中的SPI

系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。

聊聊Java 8有哪些特性

  • 函数式接口
  • 接口可以有实现方法,而且不需要实现类去实现其方法。
  • lambda表达式
  • stream流
  • 日期时间 API LocalDateTime年月日十分秒;LocalDate日期;LocalTime时间
  • Optional 类
    虽然说目前最新的版本已经是15了,但是大部分企业还是用的8,所以就聊聊这个拉。

说说成员变量与局部变量的区别有哪些?

  • 成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  • 从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存
  • 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  • 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

说说构造方法有哪些特性?

  • 名字与类名相同。
  • 没有返回值,但不能用 void 声明构造函数。
  • 生成类的对象时自动执行,无需调用。

说说==和equals

  • == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
  • equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
    • 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
    • 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

聊聊Java的四种引用,强弱软虚

小六六这边说下,其实我们作为一个crud仔,平时真正接触到的也许就是强引用了,但是也不是说其他的引用方式没有用,存在即合理,我们来看看他们具体的作用吧!感觉应该放到JVM模块的,算了,就这个吧

  • 强引用:最普遍的一种引用方式,如String s = "abc",变量s就是字符串“abc”的强引用,只要强引用存在,则垃圾回收器就不会回收这个对象。
  • 软引用:用于描述还有用但非必须的对象,如果内存足够,不回收,如果内存不足,则回收。一般用于实现内存敏感的高速缓存(自定义内存cached的时候用),软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。
  • 弱引用:弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。(这个就比较垃圾了)
  • 虚引用:虚引用不会改变对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样。(其实小六六自己对于虚引用的理解还是因为ThreadLocal,防止我们忘记remove)

Java容器集合

聊聊数组的特点呗

  • 一致性:数组只能保存相同数据类型元素,元素的数据类型可以是任何相同的数据类型。
  • 有序性:数组中的元素是有序的,通过下标访问。
  • 不可变性:数组一旦初始化,则长度(数组中元素的个数)不可变。 数组是一种连续存储线性结构,元素类型相同,大小相等

说说Java常见的集合有哪些

嗯,我觉得这题会经常问,算是一个对集合考查的引入吧

  • Map接口和Collection接口是所有集合框架的父接口
  • Collection接口的子接口包括:Set接口和List接口
  • Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  • Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  • List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

简单聊聊 Set、List、Map的区别

List

  • 可以允许重复的对象。
  • 可以插入多个null元素。
  • 是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。

Set

  • 不允许重复对象
  • 无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
  • 只允许一个 null 元素

Map

  • Map不是collection的子接口或者实现类。Map是一个接口。
  • Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的。
  • Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。

他们的使用场景

  • 如果你经常会使用索引来对容器中的元素进行访问,那么 List 是你的正确的选择。如果你已经知道索引了的话,那么 List 的实现类比如 ArrayList 可以提供更快速的访问,如果经常添加删除元素的,那么肯定要选择LinkedList。
  • 如果你想容器中的元素能够按照它们插入的次序进行有序存储,那么还是 List,因为 List 是一个有序容器,它按照插入顺序进行存储。
  • 如果你想保证插入元素的唯一性,也就是你不想有重复值的出现,那么可以选择一个 Set 的实现类,比如 HashSet、LinkedHashSet 或者 TreeSet。所有 Set 的实现类都遵循了统一约束比如唯一性,而且还提供了额外的特性比如 TreeSet 还是一个 SortedSet,所有存储于 TreeSet 中的元素可以使用 Java 里的 Comparator 或者 Comparable 进行排序。LinkedHashSet 也按照元素的插入顺序对它们进行存储。
  • 如果你以键和值的形式进行数据存储那么 Map 是你正确的选择。你可以根据你的后续需要从 Hashtable、HashMap、TreeMap 中进行选择。 `

来聊聊 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基础这块也算是蛮扎实了。

日常求赞

好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是真粉

创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见

微信 搜 "六脉神剑的程序人生" 回复888 有我找的许多的资料送给大家

你可能感兴趣的:(2021-Java后端工程师面试指南-(Java基础篇))