2021 Java 面试知识全解析
我相信各位小伙伴们或多或少都对 Java 的相关岗位的面试都有一定的了解,知道面试官一定会对某个知识点进行一系列的穷追猛打,直到你默默不语…… 当然这是个基本套路,考校是你对 Java 理解的深度,而这也往往代表着 Java 语言的水平。 力求帮助读者从更高的地方去理解和学习知识。
下面笔者会从浅入深,从 Java 基础模块到项目架构,从整体到部分进行一个浅尝而止的讲诉。希望读者能在阅读文章结束后能够对 Java 的整体知识体系,对面试官提问思路有一个整体性的了解。
概念篇
概念是把所感知的事物的共同本质特点抽象出来,加以概括。我们学习知识时一定少不了对概念,对理论的学习,而这些往往都是一个知识体系中最重要,最基础的部分。
这里同学们可以回忆一下,Java 语言的基础性知识概念,或者说是对 Java 直观的认知有哪些呢?是 Write once, run anywhere?
是面向对象语言? Or other?
是的。提到 Java,这些都是 Java 语言的特点。但需要了解的不仅仅是这些,说及 Java 时,我们脑海中浮现以 Java 为根点,向下无限分出更多。
面试官的提问可能会花样百出,但只要我们心中有数,方能以不变应万变。
经典答案:
Java 是一种面向对象的语言,有两个显著的特性,一是“书写一次,到处运行”(Write once, run anywhere),能够便捷地跨平台工作;二是垃圾收集(GC,Garbage Collection),Java 将内存的分配和回收的工作交给了垃圾收集器(Garbage Collector),大部分情况下,程序员不需要自己去处理。
Java 平台:
JDK(Java Development Kit
)包含 JRE(Java Runtime Environment
)包含 JVM 和 Java 类库, JVM 可以理解 Java 程序运行的容器。
执行过程:
Java 的源代码 --> Javac 编译为字节码(bytecode) --> 加载到 Java 虚拟机 (JVM) --> JVM 内嵌的解释器将字节码转换成为最终的机器码
Oracle JDK 提供的 Hotspot JVM
,都提供了 JIT(Just-In-Time
)编译器,也就是通常所说的动态编译器,JIT 能够在运行时将热点代码编译成机器码,这是 JVM 的一种优化方式,可以提高程序的运行速度。
拓展相关知识:
- Java 面向对象封装,继承,多态等概念
- 泛型、Lambda 等语言特性
- Java 基本类库
- JVM 的基础概念和机制
集合篇
集合框架在 Java 中的重要不用多说。
首先需要明白集合是用来管理和操作数据的,我们在编程时会用到各种各样的数据结构,因此理解并熟悉运用一个语言的集合框架会给我们带来很大的帮助。
Java 集合框架的学习不仅要从浅到深,更要从整体到具体:
- Java 提供的容器(集合和 Map)类型,了解或掌握对应的数据结构、算法,了解源码作者技术选型的巧妙之处。
- 结合性能、并发等领域进行思考。
- 了解集合框架的版本迭代,发展历史等。
数据结构和算法是基本功,有时候面试官往往喜欢提问这方面。以典型排序算法,你至少理解:
- 内部排序,至少掌握基础算法如归并排序、交换排序(冒泡、快排)、选择排序、插入排序等。
- 外部排序,掌握利用内存和外部存储处理超大数据集,至少要理解过程和思路。
考察算法不仅仅是如何简单实现,面试官往往会刨根问底,比如哪些是排序是不稳定的呢(快排、堆排),或者思考稳定意味着什么;对不同数据集,各种排序的最好或最差情况;从某个角度如何进一步优化(比如空间占用,假设业务场景需要最小辅助空间,这个角度堆排序就比归并优异)等,从简单的了解,到进一步的思考,面试官通常还会观察面试者处理问题和沟通时的思路。
实例面试题
面:说说你对 HashMap 的理解吧?(简单而又直接,你自由发挥,我听着)
我:Java 1.8 中,HashMap 是 Key-Value 形式存储的 Map 集合,底层由数组 + 链表/红黑树组成,默认容量 16,每次扩容 2 倍,线程不安全……
面:那你讲讲 HashMap 的 hash 算法,还有为什么扩容会是 2 倍呢?(小伙子继续)
我:
hash = key.hashcode
异或key.hashcode >>> 16
,目的让 key.hashcode 的高 16 位和低 16 位 同时参与计算。减少 Hash 碰撞,将 put 入的数据尽量在数组上均匀分配。- 计算 put 数据在数组中的下标的公式为:
index = (n-1) & hash
(前提 n 是 2 的次幂)等价于hash % n
,但 & 运算效率更高,故 HashMap 选择了容量为 2 的次幂来提高效率。
面:说说扩容是怎么实现的?
我:重新定义一个容器,将旧数据加到新容器中……
面:为什么线程不安全的嘛?怎么做才安全呢?
我:
- 多线程下 size() 方法将不会准确,多线程 put 数据时会导致脏数据,Java 1.7 中
resize()
方法会导致死循环。 - 使用 并发包的
ConcurrentHashMap
,或Synchronized
的 HashMap(不推荐)。
这是一个很常见关于 HashMap 的面试题,相比 List、Set 更有考察点,因此也是面试中十有八九被提及的,有时甚至还会追问你是否有序?如何实现有序等等。
这里不再详细讲解,集合相关资料很多,List、Set、Map 是 Java 开发的必经之路呀。
并发
Java 中的并发一定离不开 Thread,提到 Thread 不得不说下 Java 的线程池实现。
ThreadPoolExecutor 的构建参数可以说是每每面试都会遇到。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize
,所谓的核心线程数,长期驻留的线程数目(除非设置了allowCoreThreadTimeOut
)。这个值可以根据不同的情况有很大区别,例如 newFixedThreadPool 会将其设置为 nThreads,而对于 newCachedThreadPool 则是为 0。maximumPoolSize
,顾名思义,就是线程不够时能够创建的最大线程数。同样进行对比,对于newFixedThreadPool
,当然就是 nThreads,因为其要求是固定大小,而newCachedThreadPool
则是 Integer.MAX_VALUE。keepAliveTime
和TimeUnit
,这两个参数指定了额外的线程能够闲置多久,显然有些线程池不需要它。workQueue
,工作队列,必须是BlockingQueue
。RejectedExecutionHandler
拒绝策略。
通过配置不同的参数,我们就可以创建出行为大相径庭的线程池,这就是线程池高度灵活性的基础。这几个参数是务必一定要去理解、去实践记忆的。
public void execute(Runnable command) {
…
int c = ctl.get();
// 检查工作线程数目,低于corePoolSize则添加Worker 创建核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// isRunning就是检查线程池是否被shutdown
// 工作队列可能是有界的,offer是比较友好的入队方式
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次进行防御性检查
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 尝试添加一个worker,如果失败意味着已经饱和或者被shutdown了
else if (!addWorker(command, false))
reject(command);
}
说完线程池之后。理下 Java 中并发相关:
Synchronized
和ReentrantLock
的区别,不用多说,一定要去理解的。ThreadLocal
的实现变量线程隔离,Volatile 的实现变量在内存中的实时读写,都是经常性的被问到。JUC
包下原子类、CAS
操作、AQS
的实现等等
消息中间件
关于消息中间件,之前因为项目中用到了消息中间件面试被问到,然后一系列下来就栽了,于是只能在事后亡羊补牢了。
RocketMQ
、RabbitMQ
、Kafka
、ActiveMQ
等这些常用的中间件,我想大家可能都有一定了解或使用,研究过其中一些。这章不会介绍这几个 MQ 的区别、优缺点等等。这里我希望能够脱离某个具体的 MQ,以业务的场景去讲解 MQ。
先来简单说下 MQ。
消息总线(Message Queue
)简称 MQ ,是一种跨进程的通信机制,用于上下游传递消息。
消息中间件,顾名思义 MQ 位于架构服务中间部分,将上游服务与下游服务进行解耦,如此发送上游只需要依赖 MQ,逻辑上和物理上都不用依赖其他服务。
MQ 可以将复杂的互联网架构进行解耦,但同时也会带来一系列的问题。
- 系统多了一个 MQ 服务,会增加服务的延时。
- 发送的消息存在丢失的可能,重试则会出现收到重复消息的问题。
- 上游服务无法知道下游的执行结果。
因此在当项目在准备使用 MQ 时一定要合理的分析业务和系统架构。
什么场景适合 MQ,究竟什么时候使用 MQ 呢?
场景:上游服务的运行和下游服务的执行结果无关。
举个例子:某些业务会在用户每天第一次成功登录后,奖励一定的积分,通知某些人等等需求。
对于这类通知类的需求,一般做法:采用 RPC 的调用方式。即登录成功后调用积分服务,调用通知服务等。
缺点:
- 使上下游逻辑 + 物理依赖严重,积分服务挂掉后可能会影响登录服务。
- 新的服务依赖登录服务时需要修改代码。
使用 MQ:
- 登录成功后,向 MQ 发一个消息
- 哪个下游服务关注“登录成功”的消息,主动去 MQ 订阅
优点:
- 上游执行时间短
- 上下游逻辑 + 物理解耦,除了与 MQ 有物理连接,模块之间都不相互依赖
- 新增一个下游消息关注方,上游不需要修改任何代码
- 如何避免消息丢失,即实现消息必达
要想消息必达,有两个核心设计点。
在常用的 MQ 中:
//给发送方服务提供了两个核心API
SendMsg(bytes[] msg)
SendCallback()
// 接收方服务提供了两个核心API
RecvCallback(bytes[] msg)
SendAck()
上游服务发送给 MQ,MQ 发送给下游服务,都可以出现消息丢失,为了降低消息丢失的概率,MQ 需要进行超时和重传。
上游服务发送给 MQ:
如果丢失或者超时,上游服务内的 timer 会重发消息,直到收到 MQ 的确认,如果重传 N 次后还未收到,则 SendCallback 回调发送失败,需要注意的是,这个过程中上游服务可能会收到同一条消息的多次重发。
MQ 发送给下游服务:
如果丢失或者超时,MQ 内的 timer 会重发消息,直到收到下游服务的确认回调,这个过程可能会重发很多次消息,一般采用指数退避的策略,先隔 x 秒重发,2x 秒重发,4x 秒重发,以此类推,需要注意的是,这个过程中 MQ 也可能会收到同一条消息的多次重发。
MQ 如何实现幂等性?
上游服务发送到 MQ 消息丢失情况:
1. 发送端将消息发给服务端 MQ
2. 服务端 MQ 将消息落地
3. 服务端 MQ 回 ACK 给上游服务
如果 1 丢失,上游服务超时后会重发消息,可能导致服务端 MQ 收到重复消息。
此时重发是上游服务发起的,消息的处理是 MQ-server,为了避免步骤 2 落地重复的消息,对每条消息,MQ 系统内部必须生成一个 inner-msg-id,作为去重和幂等的依据,这个内部消息 ID 的特性是:
- 全局唯一
- MQ 生成,具备业务无关性,对消息发送方和消息接收方屏蔽
有了这个 inner-msg-id,就能保证上半场重发,也只有 1 条消息落到 MQ-server 的 DB 中,实现上半场幂等。
MQ 消息发送给下游服务丢失情况:
4. 服务端 MQr 将消息发给下游服务
5. 下游服务回 ACK 给服务端
6. 服务端 MQ 将落地消息删除
下游服务回 ACK 给服务端 MQ,是消息消费业务方的主动调用行为,不能由下游服务自动发起,因为 MQ 系统不知道消费方什么时候真正消费成功。
如果 5 丢失,服务端 MQ 超时后会重发消息,可能导致下游服务收到重复的消息。
此时重发是 MQ 发起的,消息的处理是消息消费业务方,消息重发势必导致业务方重复消费,为了保证业务幂等性,业务消息体中,必须有一个 biz-id,作为去重和幂等的依据,这个业务 ID 的特性是:
- 对于同一个业务场景,全局唯一
- 由业务消息发送方生成,业务相关,对 MQ 透明
- 由业务消息消费方负责判重,以保证幂等
有了这个全局唯一业务 ID,才能够保证消费业务方即使收到重复消息,也只有 1 条消息被消费,保证了幂等。
MQ 除了这 3 个重要概念外,读者还可以去了解下消息延时性,消息削峰填谷等概念。
Spring
提到 Spring 时,估计我们都会立马想到 IoC
和 AOP
两个概念,还有一些我们对其所了解的一些使用、面试相关类的知识。不可否认的是这两个概念几乎要被问烂了,因此在面试的时候如何有深度的去回答面试官是个很重要的技巧。
下面的我会从 Spring 框架设计理念开始讲起,侧重于从 Spring 整体框架的角度去学习 Spring,这样才能在面试官不断的追问下,从容的回答自己的理解。这里力求以最简洁的语言来讲解,阅读中如果有相关迷惑点可自行 baidu。
基础框架
Spring 的三个核心组件,Core
、Context
和 Beans
,构建起了整个 Spring 的底层架构。而在这三个中,Beans 组件又是重中之重,可以说 Bean 是 Spring 中真正的核心角色。
Spring 最关键的一点就是可以把对象之间的依赖关系转而用配置文件来管理,即依赖注入机制,也是我们所常说的 IoC。
我们程序中通过 Spring 注入的对象是被 Bean 包裹着的,而 IoC 容器就是存放 Bean 的。这种设计策略类似于 Java 实现 OOP 的设计理念,是通过构建一个数据结构,然后根据这个数据结构设计他的生存环境,并让它在这个环境中按照一定的规律在不停的运动,在它们的不停运动中设计一系列与环境或者与其他个体完成信息交换。
Bean 组件
Bean 组件在 Spring 的 org.springframework.beans
包下。这个包下的所有类主要功能有:Bean 的定义、Bean 的创建和 Bean 的解析。
我们使用 Spring 时主要是通过 xml 文件配置或注解来声明创建一个 Bean,其他两个则是 Spring 在内部完成了,对我们来说是透明的。
Bean 的定义就是完整的描述了在 Spring 的配置文件中你定义的 <bean/>
节点中所有的信息,包括各种子节点。当 Spring 成功解析你定义的一个 <bean/>
节点后,在 Spring 的内部就被转化成 BeanDefinition 对象。以后所有的操作都是对这个对象完成的。
Bean 的解析过程非常复杂,功能被分的很细,因为这里需要被扩展的地方很多,必须保证有足够的灵活性,以应对可能的变化。Bean 的解析主要就是对 Spring 配置文件的解析。这个解析过程主要通过下图中的类完成:
Context 组件
Context 在 Spring 的 org.springframework.context
包下,前面已经讲解了 Context 组件在 Spring 中的作用,他实际上就是给 Spring 提供一个运行时的环境,用以保存各个对象的状态。
ApplicationContext
的子类主要包含两个方面:
ConfigurableApplicationContext
表示该 Context 是可修改的,也就是在构建 Context 中用户可以动态添加或修改已有的配置信息,它下面又有多个子类,其中最经常使用的是可更新的 Context,即AbstractRefreshableApplicationContext
类。WebApplicationContext
顾名思义,就是为 Web 准备的 Context 他可以直接访问到ServletContext
,通常情况下,这个接口使用的少。
再往下分就是按照构建 Context 的文件类型,接着就是访问 Context 的方式。这样一级一级构成了完整的 Context 等级层次。
总体来说 ApplicationContext
必须要完成以下几件事:
- 标识一个应用环境
- 利用 BeanFactory 创建 Bean 对象
- 保存对象关系表
- 能够捕获各种事件
- Context 作为 Spring 的 IoC 容器,基本上整合了 Spring 的大部分功能,或者说是大部分功能的基础。
Core 组件
Core 组件作为 Spring 的核心组件,包含了很多的关键类,其中一个重要组成部分就是定义了资源的访问方式。这种把所有资源都抽象成一个接口的方式很值得在以后的设计中拿来学习。
Resource
接口封装了各种可能的资源类型,资源都被可以通过InputStream
这个类来获取,屏蔽了所有的资源加载者的差异,默认实现是DefaultResourceLoader
。Context
是把资源的加载、解析和描述工作委托给了ResourcePatternResolver
类来完成,他相当于一个接头人,他把资源的加载、解析和资源的定义整合在一起便于其他组件使用。Core 组件中还有很多类似的方式。
IoC 主要步骤
- 构建
BeanFactory
,以便于产生所需的“演员” - 注册可能感兴趣的事件
- 创建 Bean 实例对象
- 触发被监听的事件
AOP 原理
- 动态代理
- Java 反射机制
拓展几个问题:
动态代理有哪几种?有什么区别?这个网上有很多答案。
笔者遇到过这样一个问题,JDK 动态代理 为什么需要实现接口?感觉挺奇怪的,差点没反应过来……
答案应该是
newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
方法参数需要接口。
数据库
面试问到数据库,不出意外那肯定是 MySQL 了。这里先讲个关于索引相关的面试题。
比如一开始会这样问你:
给你几个字段,你来设计一个索引,以及你回答后会问你为什么要这么设计。
讲讲索引的结构。
这可以说是一个既有深度又有广度的问题,同时这就像一个链表的头节点,是一个开始,你不知道接下来会有多少在等着。面试官会根据你的回答进行一系列的追问。
通常 MySQL(InnoDB 存储引擎)索引结构是 B+ Tree,是一个平衡搜索树。在 B+ 树中,内部节点仅仅起到索引的作用,即在搜索中,不会因为查询和内部节点的关键字一致而停止,会继续向下搜索直到叶子节点。
说说覆盖索引是个怎样的情况?
在 MySQL 中利用索引来查找数据时。如果需要的数据在索引的叶子节点中,直接返回,否则需要进行回表操作。我们称这种情况为覆盖索引的现象。
在理解覆盖索引前需要知道 MySQL 索引的一些概念:
- 聚簇索引:叶子节点包括索引和数据,例:
InnoDB
中主键索引。 - 非聚簇索引:叶子节点包括索引,例:
InnoDB
中除主键索引外建立的索引,也被称为辅助索引。
因此在利用辅助索引查询数据时,如果叶子节点的数据不满足我们的需求时,MySQL 就会进行回表操作,根据查询的结果(包含主键索引的指针)去查询主键索引来获取所需要的数据。
覆盖索引可以说对 SQL 的性能优化很重要。关于索引的应用有很多东西需要去理解。这里希望大家在这方面一定要多下点功夫。
MySQL 中通常怎样查看 SQL 语句性能的?
用 explain 命令来查看 SQL 语句的执行计划。这里一定要记得 explain
执行计划结果里几个常用的字段:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra
- id:查询的序列号,一个 SQL 可能包括多个查询。
- select_type :有
SIMPLE
(简单查询。查询不包含子查询和 union)、primary
、subquery
、derived
几种。 - table:当前 SQL 对应的表。
- possible_keys:可能使用哪些索引来查找。
- key:SQL 实际用到的索引。
- key_len:索引的字节数。
- ref:表查找值所用到的列或常量。
- rows:引擎预估的查询行数。
- Extra:额外一些信息。
这里就不过多详细介绍。读者只有实际通过 SQL 语句一个个验证过,才能真正理解每个字段真正的含义。
索引的优化一方面,但再如何的去优化都有一定的极限,因此分库分表等操作也是解决数据库压力的常用方法。
拓展问题:
- 如何去分库呢?
- 怎样设计主键呢?
- 后面加库呢?
- 怎样实现全局唯一 ID 呢?
- SQL 注入了解嘛?
这些问题都是面试官经常会提到的,这里涉及到,分表算法的 Hash 算法、一致性 Hash、分布式唯一 ID 等等概念。
总结
总的来说,面试可能会万般不同,但核心确是永远不变。对于底层技术,这个问题经常被讨论,阅读源代码当然是个好习惯,理解高质量的代码,对于提高我们自己的分析、设计等能力至关重要。实践和经验很重要。
但一定要有选择性去阅读,不能什么东西都要理解底层,懂当然好,但不能代表一切,毕竟知识和能力是有区别的,当然我们也要尊重面试官的要求。动手实践,手敲代码,和人交流,都是有必要的。我们个人的理解往往会有局限性,在交流中,有些我们平时懵懂的一些概念,在尝试组织语言的时候,突然就想明白了,而且别人的想法也会帮助自己进一步的了解。还有一定要做个记录,以 debug 某个问题的角度,结合实践去验证,让自己能够感到收获,既加深理解,也有实际帮助,激励我们坚持下来。一定要有输出,记录在自己的小本本中,整理体会,交流、验证、提高。