大家好,我是“卧龙”的第十八代孙诸葛二。
身高约一米八,在一众程序员中,显得格外醒目。虽然常年与代码为伴,但我并非那种格子衫、拘泥于键盘的书生摸样。五官轮廓分明,自带一股英气,尤其是那双目光坚毅的眼睛,可以洞穿所有问题的本质。
常怀家国大义,胸有壮志。
昨日,收到 OPPO 公司的 SP offer,月 base 24k,互联网服务部门,再加上 1.2K 的房补,让我很是心动。
截图来自二哥的 Java 面试指南专栏
心中默念:此番能夺得“OPPO”城中高位,多赖于座上宾“三分恶”撰写的那份举世闻名的八股秘籍——面渣逆袭。
念在 OPPO 如此有诚意的薪资待遇上,我日后定当为其鞠躬尽瘁,驰骋沙场,图霸业大成。
为后世着想,特把上月我与 OPPO 大将“上官老王”的技术一面纪录于《
Java 面试指南
》中。上官老王,精通“Java、Spring Cloud”之术,号称 OPPO 技艺翘楚。此番得其赏识,实乃荣幸之至。
-
-
2、三分恶面渣逆袭在线版:https://javabetter.cn/sidebar/sanfene/nixi.html
OPPO 面经同学1一面
介绍Java的集合框架
Java 集合框架可以分为两条大的支线:
①、Collection,主要由 List、Set、Queue 组成:
-
List 代表有序、可重复的集合,典型代表就是封装了动态数组的 ArrayList 和封装了链表的 LinkedList;
-
Set 代表无序、不可重复的集合,典型代表就是 HashSet 和 TreeSet;
-
Queue 代表队列,典型代表就是双端队列 ArrayDeque,以及优先级队列 PriorityQueue。
②、Map,代表键值对的集合,典型代表就是 HashMap。
二哥的 Java 进阶之路:Java集合主要关系
为什么HashMap不是线程安全的?
HashMap 不是线程安全的,主要有以下几个问题:
①、多线程下扩容会死循环。JDK1.7 中的 HashMap 使用的是头插法插入元素,在多线程的环境下,扩容的时候就有可能导致出现环形链表,造成死循环。
二哥的 Java 进阶之路
不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序。
②、多线程的 put 可能会导致元素的丢失。因为计算出来的位置可能会被其他线程的 put 覆盖。本来哈希冲突是应该用链表的,但多线程时由于没有加锁,相同位置的元素可能就被干掉了。
二哥的 Java 进阶之路
③、put 和 get 并发时,可能导致 get 为 null。线程 1 执行 put 时,因为元素个数超出阈值而导致出现扩容,线程 2 此时执行 get,就有可能出现这个问题。
二哥的 Java 进阶之路:源码截图
因为线程 1 执行完 table = newTab 之后,线程 2 中的 table 此时也发生了变化,此时去 get 的时候当然会 get 到 null 了,因为元素还没有转移。
和ConcurrentHashMap的差异
HashMap 不是线程安全的,因此在早期的 JDK 版本中,是用 Hashtable 来保证线程安全的。Hashtable 是直接在方法上加 synchronized 关键字,比较粗暴。
因此在 Java 的后期版本中,更推荐使用 ConcurrentHashMap和
Collections.synchronizedMap(Map)
包装器。
ConcurrentHashMap是通过锁机制来实现线程安全的吗?
在 JDK 7 时采用的是分段锁机制(Segment Locking),整个 Map 被分为若干段,每个段都可以独立地加锁。因此,不同的线程可以同时操作不同的段,从而实现并发访问。
初念初恋:JDK 7 ConcurrentHashMap
在 JDK 8 及以上版本中,ConcurrentHashMap 的实现进行了优化,不再使用分段锁,而是使用了一种更加精细化的锁——桶锁,以及 CAS 无锁算法。每个桶(Node 数组的每个元素)都可以独立地加锁,从而实现更高级别的并发访问。
初念初恋:JDK 8 ConcurrentHashMap
对于读操作,通常不需要加锁,可以直接读取,ConcurrentHashMap 内部使用了 volatile 变量来保证内存可见性。
对于写操作,ConcurrentHashMap 使用 CAS 操作来实现无锁的更新,这是一种乐观锁的实现,因为它假设没有冲突发生,在实际更新数据时才检查是否有其他线程在尝试修改数据,如果有,采用悲观的锁策略,如 synchronized 代码块来保证数据的一致性。
泛型的作用是什么?
泛型主要用于提高代码的类型安全,它允许在定义类、接口和方法时使用类型参数,这样可以在编译时检查类型一致性,避免不必要的类型转换和类型错误。
没有泛型的时候,像 List 这样的集合类存储的是 Object 类型,导致从集合中读取数据时,必须进行强制类型转换,否则会引发 ClassCastException。
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0); // 必须强制类型转换
Java里线程的生命周期
也就是说,线程的生命周期可以分为五个主要阶段:新建、可运行、运行中、阻塞/等待、和终止。线程在运行过程中会根据状态的变化在这些阶段之间切换。:
三分恶面渣逆袭:Java线程状态变化
说一下JVM内存模型
按照 Java 的虚拟机规范,可以细分为
程序计数器
、
虚拟机栈
、
本地方法栈
、
堆
、
方法区
等。
三分恶面渣逆袭:Java虚拟机运行时数据区
其中
方法区
和
堆
是线程共享的,
虚拟机栈
、
本地方法栈
和
程序计数器
是线程私有的。
什么情况下会发生栈溢出?
栈溢出(StackOverflowError)发生在程序调用栈的深度超过 JVM 允许的最大深度时。栈溢出的本质是因为线程的栈空间不足,导致无法再为新的栈帧分配内存。
二哥的Java进阶之路:栈帧
当一个方法被调用时,JVM 会在栈中分配一个栈帧,用于存储该方法的执行信息。如果方法调用嵌套太深,栈帧不断压入栈中,最终会导致栈空间耗尽,抛出 StackOverflowError。
最常见的栈溢出场景是递归调用,尤其是没有正确的终止条件,导致递归无限进行。
class StackOverflowExample {
public static void recursiveMethod() {
// 没有终止条件的递归调用
recursiveMethod();
}
public static void main(String[] args) {
recursiveMethod(); // 导致栈溢出
}
}
另外,如果方法中定义了特别大的局部变量,栈帧会变得很大,导致栈空间更容易耗尽。
public class LargeLocalVariables {
public static void method() {
int[] largeArray = new int[1000000]; // 大量局部变量
method(); // 递归调用
}
public static void main(String[] args) {
method(); // 导致栈溢出
}
}
垃圾回收的过程是什么?
Java 的垃圾回收过程主要分为标记存活对象、清除无用对象、以及内存压缩/整理三个阶段。不同的垃圾回收器在执行这些步骤时会采用不同的策略和算法。
说一下Spring和Springboot之间有什么差异?
Spring Boot 是 Spring Framework 的一个扩展,提供了一套快速配置和开发的机制,可以帮助我们快速搭建 Spring 项目的骨架,提高生产效率。
特性
|
Spring Framework
|
Spring Boot
|
目的
|
提供企业级的开发工具和库
|
简化 Spring 应用的开发、配置和部署
|
配置方式
|
主要通过 XML 和注解等手动配置
|
提供开箱即用的自动配置
|
启动和运行
|
需要打成 war 包到 Tomcat 等容器下运行
|
已嵌入 Tomcat 等容器,打包成 JAR 文件直接运行
|
依赖管理
|
手动添加和管理依赖
|
使用
spring-boot-starter
简化依赖管理
|
自动配置怎么实现的?
在 Spring 中,自动装配是指容器利用反射技术,根据 Bean 的类型、名称等自动注入所需的依赖。
三分恶面渣逆袭:SpringBoot自动配置原理
在 Spring Boot 中,开启自动装配的注解是
@EnableAutoConfiguration
。
二哥的 Java 进阶之路:@EnableAutoConfiguration 源码
Spring Boot 为了进一步简化,直接通过
@SpringBootApplication
注解一步搞定,这个注解包含了
@EnableAutoConfiguration
注解。
二哥的 Java 进阶之路:@SpringBootApplication源码
对MySQL事务的理解
事务是一个或多个 SQL 语句组成的一个执行单元,这些 SQL 语句要么全部执行成功,要么全部不执行,不会出现部分执行的情况。事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
事务的主要作用是保证数据库操作的一致性,即事务内的操作,要么全部成功,要么全部失败回滚,不会出现中间状态。这对于维护数据库的完整性和一致性非常重要。
事务具有四个基本特性,也就是通常所说的 ACID 特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
三分恶面渣逆袭:事务四大特性
事务里一致性怎么理解?
一致性确保事务从一个一致的状态转换到另一个一致的状态。
比如在银行转账事务中,无论发生什么,转账前后两个账户的总金额应保持不变。假如 A 账户(100 块)给 B 账户(10 块)转了 10 块钱,不管成功与否,A 和 B 的总金额都是 110 块。
对MySQL索引的理解
数据库文件是存储在磁盘上的,磁盘 I/O 是数据库操作中最耗时的部分之一。没有索引时,数据库会进行全表扫描(Sequential Scan),这意味着它必须读取表中的每一行数据来查找匹配的行(时间效率为 O(n))。当表的数据量非常大时,就会导致大量的磁盘 I/O 操作。
有了索引,就可以直接跳到索引指示的数据位置,而不必扫描整张表,从而大大减少了磁盘 I/O 操作的次数。
MySQL 的 InnoDB 存储引擎默认使用 B+ 树来作为索引的数据结构,而 B+ 树的查询效率非常高,时间复杂度为 O(logN)。
索引文件相较于数据库文件,体积小得多,查到索引之后再映射到数据库记录,查询效率就会高很多。
索引就好像书的目录,通过目录去查找对应的章节内容会比一页一页的翻书快很多。
三分恶面渣逆袭:索引加快查询远离
什么情况下索引失效?
-
在索引列上使用函数或表达式
:如果在查询中对索引列使用了函数或表达式,那么索引可能无法使用,因为数据库无法预先计算出函数或表达式的结果。例如:
SELECT * FROM table WHERE YEAR(date_column) = 2021
。
-
使用不等于(
<>
)或者 NOT 操作符:这些操作符通常会使索引失效,因为它们会扫描全表。
-
使用 LIKE 操作符,但是通配符在最前面
:如果 LIKE 的模式串是以“%”或者“_”开头的,那么索引也无法使用。例如:
SELECT * FROM table WHERE column LIKE '%abc'
。
-
OR 操作符
:如果查询条件中使用了 OR,并且 OR 两边的条件分别涉及不同的索引,那么这些索引可能都无法使用。
-
Redis常见数据结构
Redis 有五种基本数据类型,这五种数据类型分别是:string(字符串)、hash(哈希)、list(列表)、set(集合)、sorted set(有序集合,也叫 zset)。
三分恶面渣逆袭:Redis基本数据类型