Java常见面试题 非常实用【个人经验】

62 阅读8分钟

一、 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摊销时间)。
  • 内存占用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关键字的作用是什么?它能保证原子性吗?
回答要点

  • 作用

    1. 保证可见性:当一个线程修改了volatile变量,新值会立即被刷新到主内存,并使其他线程工作内存中的该变量副本失效,从而保证其他线程能读到最新值。
    2. 禁止指令重排序:通过插入内存屏障(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就会一直存在,造成内存泄漏。
  • 解决方法每次使用完 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的生命周期?
回答要点(简化版):

  1. 实例化:通过构造器或工厂方法创建Bean实例。

  2. 属性填充:为Bean的属性设置值和对其他Bean的引用(依赖注入)。

  3. BeanPostProcessor前置处理:调用 postProcessBeforeInitialization 方法。

  4. 初始化

    • 如果Bean实现了 InitializingBean 接口,执行 afterPropertiesSet() 方法。
    • 调用配置中指定的自定义初始化方法 (init-method)。
  5. BeanPostProcessor后置处理:调用 postProcessAfterInitialization 方法(AOP代理对象就在此环节生成)。

  6. Bean就绪:存在于Spring容器中,可以被使用。

  7. 销毁

    • 如果Bean实现了 DisposableBean 接口,执行 destroy() 方法。
    • 调用配置中指定的自定义销毁方法 (destroy-method)。

2. Spring 事务失效的常见场景

问题: 你在项目中遇到过Spring事务失效的情况吗?怎么解决的?
回答要点

  1. 方法非public@Transactional 只能用于public方法。

  2. 自调用问题:同一个类中,一个没有事务的方法A调用了有事务的方法B,B的事务会失效。因为事务是基于AOP代理的,自调用不走代理。

    • 解决:注入自己的Bean (@Autowired private MyService self;),然后用 self.methodB() 调用。
  3. 异常被捕获:默认只在抛出 RuntimeException 和 Error 时回滚。如果抛出的异常被自己 catch 住了,或者抛出了受检异常(IOException等)且未配置 rollbackFor,事务不会回滚。

  4. 数据库引擎不支持:如MySQL的MyISAM引擎不支持事务。


这些题目覆盖了Java面试80%以上的核心考点,务必深入理解而不是死记硬背。祝你面试顺利!