一、 Java 基础 (必考)
1. HashMap 的原理
问题: 说一下HashMap的实现原理,扩容机制,1.7和1.8的区别?
回答要点:
-
结构:数组 + 链表(1.7) / 数组 + 链表 + 红黑树(1.8)。通过key的hashcode经过扰动函数(高16位异或低16位)得到hash值,然后
(n-1) & hash确定数组下标。 -
put流程:计算hash -> 找数组下标 -> 如果该位置为空,直接插入 -> 如果不为空,遍历链表/树(比较hash和key)-> 如果key存在,覆盖value;如果不存在,插入到链表尾部(1.7头插法,1.8尾插法)。
-
扩容:默认初始容量16,负载因子0.75。当
size > capacity * loadFactor时,容量扩为原来的2倍。扩容后需要rehash,1.8优化了rehash过程,元素的新位置要么是原位置,要么是原位置 + oldCap。 -
区别:
- 1.7:头插法(多线程下可能造成死循环)、数组+链表
- 1.8:尾插法、数组+链表+红黑树(链表长度>8且数组长度>=64时转树)、优化hash算法和扩容机制。
2. ArrayList 和 LinkedList 的区别
问题: ArrayList和LinkedList有什么区别?分别在什么场景下使用?
回答要点:
-
底层结构:
ArrayList:基于动态数组。LinkedList:基于双向链表。
-
操作效率:
- 随机访问 (get/set) :
ArrayList是O(1),LinkedList是O(n)。 - 头部插入/删除:
ArrayList是O(n)(需要移动元素),LinkedList是O(1)。 - 尾部插入/删除:两者都是O(1)(ArrayList摊销时间)。
- 随机访问 (get/set) :
-
内存占用:
ArrayList更节省空间(仅存储数据),LinkedList每个节点需要额外空间存储前后节点的引用。 -
使用场景:
ArrayList:查询多,增删少的场景(90%以上的场景)。LinkedList:频繁在头部或中间进行增删,而查询较少的场景。
3. 深拷贝 vs 浅拷贝
问题: 讲一讲深拷贝和浅拷贝的区别?
回答要点:
- 浅拷贝:只复制对象本身和其基本数据类型字段,对于引用类型字段,只复制引用地址,因此原对象和拷贝对象会共享同一个内部对象。
Object.clone()默认是浅拷贝。 - 深拷贝:不仅复制对象本身,还会递归复制所有引用类型字段指向的对象。生成的对象和原对象完全独立,互不影响。
- 实现方式:实现
Cloneable接口并重写clone方法。浅拷贝直接调用super.clone();深拷贝需要在clone方法中手动调用引用字段的clone方法。 - 工具:
BeanUtils.copyProperties是浅拷贝。生产环境推荐使用 MapStruct 进行映射(性能高,类型安全)。
二、 Java 并发 (必考)
1. synchronized 和 ReentrantLock 的区别
问题: 说说synchronized和ReentrantLock的区别?
回答要点:
-
本质:
synchronized是JVM层面的关键字;ReentrantLock是API层面的类(JDK提供)。 -
使用:
synchronized不需要手动释放锁;ReentrantLock必须lock()和unlock()配合try/finally使用。 -
功能:
ReentrantLock更灵活、功能更丰富。- 公平锁:
ReentrantLock可以设置公平性(先等待的线程先获得锁),synchronized是非公平锁。 - 等待可中断:
lock.lockInterruptibly()可以响应中断。 - 尝试获取锁:
tryLock()可以尝试获取锁,获取失败直接返回。 - 绑定多个条件:一个
ReentrantLock可以绑定多个Condition对象,用于精确唤醒线程。
- 公平锁:
-
性能:在JDK1.6之后,两者性能相差不大。优先使用
synchronized(简洁可靠),需要高级功能时再用ReentrantLock。
2. volatile 关键字
问题: volatile关键字的作用是什么?它能保证原子性吗?
回答要点:
-
作用:
- 保证可见性:当一个线程修改了volatile变量,新值会立即被刷新到主内存,并使其他线程工作内存中的该变量副本失效,从而保证其他线程能读到最新值。
- 禁止指令重排序:通过插入内存屏障(Memory Barrier)防止JVM和处理器进行重排序优化。
-
不能保证原子性:
volatile适用于一写多读的场景(如状态标志位flag)。对于复合操作(如i++),它无法保证线程安全,仍需使用synchronized或Atomic类。
3. ThreadLocal 的原理和内存泄漏问题
问题: 讲一下ThreadLocal的原理,以及为什么会内存泄漏?
回答要点:
-
原理:在每个线程
Thread内部,都有一个ThreadLocalMap的成员变量, key 是ThreadLocal对象本身(弱引用),value 是存储的值。ThreadLocal提供了线程隔离的局部变量。 -
内存泄漏:
- Key的泄漏:
ThreadLocalMap的 Key 是弱引用指向ThreadLocal对象。如果外部强引用消失,Key 会在下一次GC时被回收,但 Value 是强引用,会导致 Value 无法被访问到,也无法被回收。 - 根本原因:线程的生命周期很长(如线程池中的线程),
ThreadLocalMap的生命周期就和线程一样长。如果ThreadLocal用完没有手动remove(),这个Entry就会一直存在,造成内存泄漏。
- Key的泄漏:
-
解决方法:每次使用完
ThreadLocal,都必须调用其remove()方法来清理当前线程的Map中的Entry。
三、 JVM (必考)
1. Java内存区域(运行时数据区)
问题: 说一下JVM的内存结构,哪些区域是线程共享的,哪些是线程私有的?
回答要点:
-
线程私有:
- 程序计数器:当前线程所执行的字节码的行号指示器。
- 虚拟机栈:存储栈帧,每个方法调用对应一个栈帧(局部变量表、操作数栈、动态链接、方法出口等)。
- 本地方法栈:为Native方法服务。
-
线程共享:
- 堆:存放对象实例和数组。是GC管理的主要区域。
- 方法区:存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等。JDK8后称为“元空间”(Metaspace),使用本地内存。
2. 垃圾回收算法 & GC Roots
问题: 有哪些垃圾回收算法?哪些对象可以作为GC Roots?
回答要点:
-
算法:
- 标记-清除:效率不高,产生内存碎片。
- 复制:效率高,无碎片,但浪费一半内存。常用于新生代(Eden, S0, S1)。
- 标记-整理:无碎片,但效率偏低。常用于老年代。
- 分代收集:现代GC器的通用思想,将堆分为新生代和老年代,根据不同代的特点采用不同的算法。
-
GC Roots(判断对象是否存活的根节点):
- 虚拟机栈中引用的对象。
- 本地方法栈中JNI引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 被同步锁
synchronized持有的对象。 - JVM内部的引用(如基本数据类型对应的Class对象,系统类加载器等)。
四、 数据库/MySQL (必考)
1. 事务的隔离级别
问题: 说说MySQL的事务隔离级别和它们分别解决了什么问题?
回答要点:
- 读未提交:什么问题都没解决。会有脏读、不可重复读、幻读。
- 读已提交:解决了脏读。但仍有不可重复读和幻读。
- 可重复读:解决了脏读、不可重复读。MySQL的默认级别,通过MVCC在一定程度上避免了幻读,但并非完全解决。
- 串行化:最高级别,通过加锁解决所有问题,但性能最差。
2. 索引:B+树 vs. 哈希索引
问题: 为什么MySQL的InnoDB索引使用B+树而不是哈希表或二叉树?
回答要点:
-
哈希索引:适合等值查询,O(1)速度。但不支持范围查询,无法排序,哈希冲突影响性能。
-
二叉树/红黑树:树的高度不均匀,深度可能很大,导致磁盘IO次数多(因为每次读取一个节点可能是一次磁盘IO)。
-
B+树:
- 矮胖:多路平衡查找树,层数少,减少磁盘IO次数。
- 适合范围查询:所有数据都存储在叶子节点,且叶子节点之间有指针链接,方便范围查询和排序。
- 查询稳定:任何查询都需要从根节点到叶子节点,路径长度稳定。
五、 框架 (Spring/SpringBoot)
1. Spring Bean的生命周期
问题: 说一下Spring中一个Bean的生命周期?
回答要点(简化版):
-
实例化:通过构造器或工厂方法创建Bean实例。
-
属性填充:为Bean的属性设置值和对其他Bean的引用(依赖注入)。
-
BeanPostProcessor前置处理:调用
postProcessBeforeInitialization方法。 -
初始化:
- 如果Bean实现了
InitializingBean接口,执行afterPropertiesSet()方法。 - 调用配置中指定的自定义初始化方法 (
init-method)。
- 如果Bean实现了
-
BeanPostProcessor后置处理:调用
postProcessAfterInitialization方法(AOP代理对象就在此环节生成)。 -
Bean就绪:存在于Spring容器中,可以被使用。
-
销毁:
- 如果Bean实现了
DisposableBean接口,执行destroy()方法。 - 调用配置中指定的自定义销毁方法 (
destroy-method)。
- 如果Bean实现了
2. Spring 事务失效的常见场景
问题: 你在项目中遇到过Spring事务失效的情况吗?怎么解决的?
回答要点:
-
方法非public:
@Transactional只能用于public方法。 -
自调用问题:同一个类中,一个没有事务的方法A调用了有事务的方法B,B的事务会失效。因为事务是基于AOP代理的,自调用不走代理。
- 解决:注入自己的Bean (
@Autowired private MyService self;),然后用self.methodB()调用。
- 解决:注入自己的Bean (
-
异常被捕获:默认只在抛出
RuntimeException和Error时回滚。如果抛出的异常被自己catch住了,或者抛出了受检异常(IOException等)且未配置rollbackFor,事务不会回滚。 -
数据库引擎不支持:如MySQL的MyISAM引擎不支持事务。
这些题目覆盖了Java面试80%以上的核心考点,务必深入理解而不是死记硬背。祝你面试顺利!