一、Java 语言核心特性(面向对象编程)
核心知识点梳理:
面向对象三大特性:
- 封装:隐藏对象内部实现,通过 public 方法暴露接口(例:类的 private 字段 + get/set 方法)。
- 继承:子类继承父类非 private 属性和方法,减少代码冗余(注意:Java 单继承,通过接口实现多继承效果)。
- 多态:同一行为的不同表现形式(编译时多态:方法重载;运行时多态:子类重写父类方法,父类引用指向子类对象)。
接口与抽象类的区别:
- 接口:全抽象(JDK8 后可有 default 方法),字段默认 public static final,类可实现多个接口。
- 抽象类:可含抽象方法和具体方法,字段可非静态,类只能继承一个抽象类。
- 应用场景:接口用于定义规范(如 DAO 层接口),抽象类用于抽取共性实现(如 BaseService)。
其他高频点:
- 重载(Overload)vs 重写(Override):重载是同一类中方法名相同、参数不同;重写是子类覆盖父类方法,参数和返回值需一致(协变返回类型除外)。
- 关键字:
static
(静态变量 / 方法属于类,非实例)、final
(修饰类不可继承、方法不可重写、变量不可修改)、this
(当前实例)、super
(父类引用)。
面试常见问题及回答思路:
Q:多态的实现原理是什么?
A:核心是 “动态绑定”:编译时编译器根据父类引用类型检查方法是否存在,运行时 JVM 根据实际对象类型调用对应子类的重写方法(依赖方法表:每个类的 Class 对象中维护方法表,记录方法的实际入口地址)。
举例:List list = new ArrayList(); list.add(1);
编译时用 List 接口检查 add 方法,运行时实际调用 ArrayList 的 add 实现。Q:为什么 Java 不支持多继承?
A:避免 “菱形继承” 问题(多个父类有相同方法时,子类无法确定继承哪一个),通过接口多实现替代,接口仅定义规范,无具体实现冲突。
二、集合框架
核心知识点梳理:
整体结构:
- 接口:
Collection
(List/Set)、Map
; - 核心实现类:
- List:
ArrayList
(数组,查快改慢)、LinkedList
(双向链表,增删快查慢); - Set:
HashSet
(底层 HashMap,无序去重)、TreeSet
(红黑树,有序去重,需实现 Comparable); - Map:
HashMap
(数组 + 链表 / 红黑树,JDK1.8 后链表长度 > 8 转红黑树)、ConcurrentHashMap
(线程安全,JDK1.7 分段锁,JDK1.8 CAS+synchronized)。
- List:
- 接口:
高频细节:
HashMap
:初始容量 16,负载因子 0.75,扩容为 2 倍;key 为 null 存放在索引 0;线程不安全(多线程 put 可能导致死循环,JDK1.8 后修复但仍有数据覆盖问题)。HashSet
:本质是HashMap
的 key(value 为常量),所以去重依赖 key 的hashCode()
和equals()
(先比 hashCode,再比 equals)。- 线程安全集合:
Vector
(所有方法 synchronized,效率低)、Collections.synchronizedList()
(包装类,加锁粒度同 Vector)、CopyOnWriteArrayList
(写时复制,读不加锁,适合读多写少)。
面试常见问题及回答思路:
Q:ArrayList 和 LinkedList 的区别?如何选型?
A:从底层结构、操作效率、内存占用三方面说:- 底层:ArrayList 是动态数组,LinkedList 是双向链表;
- 效率:ArrayList 随机访问(get (index))快(O (1)),增删(尤其是中间位置)慢(需移动元素,O (n));LinkedList 增删快(O (1),找到位置后修改指针),随机访问慢(O (n));
- 选型:查多改少用 ArrayList,增删多(尤其是中间)用 LinkedList;小数据量时差异不大。
Q:HashMap 的 put 流程?JDK1.7 和 1.8 有什么区别?
A:put 流程:- 计算 key 的 hash 值(hashCode () 高 16 位异或低 16 位,减少哈希冲突);
- 计算索引(hash & (容量 - 1));
- 若桶位为空,直接放新节点;若不为空,判断 key 是否存在(hash+equals),存在则替换 value;不存在则插入链表 / 红黑树;
- 插入后若链表长度 > 8 且容量 >=64,转红黑树;
- 若元素数 > 容量 * 负载因子,触发扩容(2 倍)。
区别:1.7 是头插法(扩容时可能死循环),1.8 是尾插法;1.8 新增红黑树优化长链表查询;1.7 的 hash 计算更简单(1.8 增加高 16 位异或)。
三、多线程编程基础
核心知识点梳理:
线程创建方式:
- 继承
Thread
类(重写 run ()); - 实现
Runnable
接口(重写 run (),可多线程共享资源); - 实现
Callable
接口(重写 call (),有返回值,结合Future
获取结果)。
- 继承
线程状态:
新建(New)→ 就绪(Runnable)→ 运行(Running)→ 阻塞(Blocked/Waiting/Timed Waiting)→ 终止(Terminated)。- 阻塞原因:synchronized 未拿到锁(Blocked)、wait ()(Waiting)、sleep (long)/join (long)(Timed Waiting)。
同步机制:
synchronized
:可修饰方法(锁当前对象 / 类)、代码块(锁括号内对象);JDK1.6 后优化为偏向锁→轻量级锁→重量级锁(锁升级),减少性能消耗。Lock
接口:ReentrantLock
(可重入、可中断、可超时、公平锁 / 非公平锁),需手动lock()
和unlock()
(建议放 finally)。- 区别:synchronized 是 JVM 层面隐式锁,自动释放;Lock 是 API 层面显式锁,灵活但需手动释放。
线程池:
- 核心参数:
corePoolSize
(核心线程数)、maximumPoolSize
(最大线程数)、keepAliveTime
(非核心线程空闲存活时间)、workQueue
(任务队列)。 - 常用线程池:
Executors.newFixedThreadPool()
(固定线程数)、newCachedThreadPool()
(缓存线程池,可扩容)、newSingleThreadExecutor()
(单线程);实际开发建议用ThreadPoolExecutor
手动创建,避免资源耗尽(如 FixedThreadPool 的队列无界可能 OOM)。
- 核心参数:
面试常见问题及回答思路:
Q:synchronized 和 volatile 的区别?
A:两者都用于线程安全,但作用不同:- volatile:修饰变量,保证可见性(一个线程修改后,其他线程立即看到)和禁止指令重排序(如单例模式双重检查锁中修饰 instance),但不保证原子性(例:i++ 仍会有线程安全问题);
- synchronized:保证原子性(临界区代码互斥执行)、可见性(解锁前刷新内存)、有序性(临界区代码单线程执行),但开销比 volatile 大。
总结:volatile 适合修饰状态标志(如 boolean isRunning),synchronized 适合复杂逻辑的同步。
Q:什么是死锁?如何避免?
A:死锁是两个或多个线程互相持有对方需要的锁,导致无限等待。
例:线程 1 持有锁 A,等待锁 B;线程 2 持有锁 B,等待锁 A。
避免:1. 按固定顺序获取锁(如都先获取 A 再获取 B);2. 定时释放锁(tryLock (time));3. 减少锁的持有时间。
四、JVM 内存结构和垃圾回收机制
核心知识点梳理:
内存结构(JDK8+):
- 线程私有:
- 程序计数器:记录当前线程执行的字节码行号,唯一不会 OOM 的区域;
- 虚拟机栈:存储栈帧(局部变量表、操作数栈、返回地址等),栈深过深会 StackOverflowError(如递归无出口),内存不足会 OOM;
- 本地方法栈:类似虚拟机栈,为 Native 方法服务。
- 线程共享:
- 堆:存储对象实例,GC 主要区域,分新生代(Eden+From Survivor+To Survivor)和老年代;内存不足会 OOM;
- 方法区(元空间,MetaSpace):存储类信息、常量、静态变量等,JDK8 前是永久代(PermGen),元空间使用本地内存,默认无上限(可通过 - XX:MaxMetaspaceSize 限制),内存不足会 OOM。
- 线程私有:
垃圾回收(GC):
- 回收区域:堆和方法区(常量、无用类)。
- 对象存活判断:可达性分析(以 GC Roots 为起点,不可达的对象标记为可回收);GC Roots 包括:虚拟机栈中引用的对象、本地方法栈中引用的对象、静态变量、常量等。
- 引用类型:强引用(new 的对象,不会被回收)、软引用(OOM 前回收,适合缓存)、弱引用(GC 时必回收,如 WeakHashMap)、虚引用(回收时通知,跟踪 GC)。
- 垃圾收集算法:
- 标记 - 清除:标记可回收对象,直接清除,会产生碎片;
- 标记 - 复制:将存活对象复制到另一块区域,无碎片,适合新生代(Eden:From:To=8:1:1);
- 标记 - 整理:标记后将存活对象移到一端,清除另一端,适合老年代。
- 垃圾收集器:
- 新生代:SerialGC(单线程,STW 长)、Parallel Scavenge(多线程,注重吞吐量)、ParNew(多线程,配合 CMS);
- 老年代:Serial Old(单线程,配合 SerialGC)、Parallel Old(多线程,配合 Parallel Scavenge)、CMS(并发标记清除,低延迟,三步 STW:初始标记、重新标记、并发清除,有碎片)、G1(Region 分区,兼顾吞吐量和延迟,Mixed GC 回收部分老年代)。
面试常见问题及回答思路:
Q:JVM 堆内存分代?为什么这么划分?
A:堆分新生代(Young)和老年代(Old),新生代又分 Eden、From Survivor、To Survivor。
原因:基于 “大部分对象朝生夕死” 的经验法则(新生代对象存活时间短,回收频繁;老年代对象存活时间长,回收少),分代后可采用不同 GC 算法优化效率:新生代用标记 - 复制(效率高,适合短生命周期对象),老年代用标记 - 整理(避免碎片,适合长生命周期对象)。Q:G1 收集器的工作原理?
A:G1 将堆划分为多个大小相等的 Region(默认 1-32MB),每个 Region 可动态标记为 Eden、Survivor、Old。
流程:1. 初始标记(STW,标记 GC Roots 直接关联的对象);2. 并发标记(遍历对象引用链,无 STW);3. 最终标记(STW,处理并发标记遗漏的对象);4. 筛选回收(STW,根据 Region 的回收价值排序,优先回收价值高的,混合回收部分老年代 Region)。
优势:可设置最大 STW 时间,兼顾吞吐量和延迟,适合大堆(如 8GB 以上)。
五、复习建议
- 画思维导图:把每个模块的知识点串联起来(如集合框架→线程安全集合→多线程同步),形成知识网。
- 看源码:重点看
ArrayList
的add()
、HashMap
的put()
、Thread
的start()
等核心方法,理解底层实现。 - 刷面试题:针对每个知识点,找 5-10 道高频题练习(如牛客网、力扣 Java 面试专题),确保能清晰表达。
- 模拟面试:找同学互相提问,训练临场反应,避免紧张忘词。
核心是 “理解原理 + 能举例子”,面试官更看重你是否真的懂,而不是背概念。比如讲 HashMap 时,能说出自己项目中遇到的哈希冲突问题及解决方法,会比单纯背流程更加分。
List list = new ArrayList(); 这个是小类型转换成大类型是自动转换是吗,如果反过来呢
在 Java 中,List list = new ArrayList();
涉及的是多态中的向上转型,而非基本数据类型的自动转换(小转大),但逻辑上有相似之处:
ArrayList
是List
接口的实现类,List
是父类型(接口可视为 “抽象父类”),ArrayList
是子类型。- 这种 “父类引用指向子类对象” 的写法是自动向上转型,无需显式声明,编译器允许这种转换(因为子类一定符合父类的接口规范)。
如果反过来写 ArrayList arrayList = new List();
,则完全不允许,原因是:
List
是接口,不能直接实例化(new List()
本身语法错误);- 即使是类之间的转换(如
ArrayList
与LinkedList
,同属List
子类型),父类型也不能直接转换为子类型,因为父类型引用的对象可能根本不是目标子类型的实例。
若要将父类型引用转回子类型,必须满足两个条件:
- 父类型引用的实际对象是目标子类型的实例;
- 必须通过显式强制转型声明。
例如:
List list = new ArrayList(); // 向上转型(自动)
ArrayList arrayList = (ArrayList) list; // 向下转型(显式强制,合法,因为list实际指向ArrayList)
但如果实际对象类型不匹配,强制转型会抛出 ClassCastException
:
List list = new LinkedList();
ArrayList arrayList = (ArrayList) list; // 运行时异常:LinkedList不能转为ArrayList