Java 高频考点
语言基础
基本数据类型
| 数据类型 | 字节大小 | 范围 | 备注 |
|---|---|---|---|
| byte | 1 字节(8 位) | -128 到 127 | |
| short | 2 字节(16 位) | -32,768 到 32,767 | |
| int | 4 字节(32 位) | -2,147,483,648 到 2,147,483,647 | |
| long | 8 字节(64 位) | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 结尾需加 L(如 100L) |
| float | 4 字节(32 位) | 约 ±3.40282347E+38 (有效位数:6-7 位) | 结尾需加 f(如 3.14f) |
| double | 8 字节(64 位) | 约 ±1.79769313486231570E+308 (有效位数:15 位) | 默认小数类型(如 3.14) |
| char | 2 字节(16 位) | 0 到 65,535(对应 Unicode 字符,如 'A'、'中') | 用单引号表示(如 'a') |
| boolean | 不固定(通常 1 位/字节) | true 或 false | JVM 可能优化为 1 位或占用 1 字节空间 |
自动装箱拆箱
自动拆箱:将包装类自动转换为基础类型
自动装箱:将基础类型自动转换为包装类型
java
Integer a = 10; // 自动装箱
int b = a; // 自动拆箱原理:
java
Integer a = Integer.valueOf(100); // 自动装箱
int b = a.intValue(); // 自动拆箱自动装箱拆箱场景:
- 基本数据类型放入集合类,会将基础类型自动装箱为包装类型
- 基本类型和包装类型比大小会自动拆箱为基本类型比较
注意,对于 Integer 是存在缓存的:
Integer a = 100;
Integer b = 100;
System.out.println(a == b);结果为 true,因为 Java5 引入了缓存机制导致 -128 ~ 127 范围内直接采用缓存中的对象比较,因此在这个范围内的 Integer 对象的判断是相等的,在 Java6 中可以通过设置虚拟机参数:-XX AutoBoxCacheMax=size 修改
== 跟 equals 区别
== 运算符:
- 用于比较两个变量的引用,即判断它们是否指向同一个对象。
- 对于基本数据类型(如
int、char等),==比较的是它们的值。
示例:
java
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2); // 输出 false,因为它们是不同的对象equals() 方法:
- 用于比较两个对象的内容(即它们的状态)。
- 默认情况下,
Object类中的equals()方法与==相同,但通常会被重写,以实现基于内容的比较。
示例:
java
System.out.println(str1.equals(str2)); // 输出 true,因为内容相同综上:
- 使用
==比较引用(内存地址)。 - 使用
equals()比较内容(逻辑相等性)。
在比较字符串或自定义对象时,通常应该使用 equals() 方法。
接口和抽象类的区别
| 对比项 | 接口(Interface) | 抽象类(Abstract Class) |
|---|---|---|
| 定义 | 定义方法的规范,不包含具体实现(仅方法声明)。 | 不能被直接实例化的类,可包含抽象方法和具体方法。 |
| 示例 | PayService 接口,声明 pay 方法。 | AbstractPayService 抽象类,实现 PayService 接口并提供 pay 方法的具体逻辑。 |
| 方法实现 | - Java 8 前:所有方法默认抽象,无实现代码。 - Java 8 后:支持 default 默认方法。 | 可包含抽象方法(需子类实现)和具体方法(可直接继承使用)。 |
| 成员变量 | 默认是 public static final,必须显式初始化。 | 可以定义任意访问修饰符(private/protected/public)的成员变量,无需强制初始化。 |
| 构造器 | 无构造器。 | 可以有构造器(用于初始化成员变量),但类本身不能被实例化。 |
| 继承与实现 | 类通过 implements 实现接口,支持多实现。 | 类通过 extends 继承抽象类,仅支持单继承。 |
| 设计目的 | 强调行为规范(“能做什么”),如定义跨类别的通用能力。 | 提供部分共性实现(“是什么”),用于复用代码和约束子类行为。 |
| 适用场景 | 需要统一方法规范但实现逻辑多样的场景(如支付、日志等)。 | 需要封装部分公共逻辑,同时强制子类实现特定方法的场景(如模板模式)。 |
注意事项:
- 在实际开发中,通常会先定义接口,然后实现接口。
- 如果多个实现类中有共同的可复用代码,可以在接口和实现类之间添加一个抽象类,将公共代码提取到抽象类中。
集合相关
ArrayList 和 LinkedList
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 存储结构 | 基于动态数组,直接存储数据内容。 | 基于双向链表,存储节点(Node),每个节点包含数据及前后指针。 |
| 查询效率 | 支持下标随机访问,时间复杂度为 O(1)。 | 不支持下标访问,需从头/尾遍历链表,平均时间复杂度为 O(n)。 |
| 修改效率 | 1.尾部操作:无扩容时 O(1)(扩容时需复制数组)。 2.非尾部操作:需移动元素,时间复杂度 O(n)。 | 插入/删除只需修改相邻节点的指针,时间复杂度 O(1)(但定位操作仍需 O(n))。 |
| 线程安全 | 非线程安全,需通过 Collections.synchronizedList 包装或使用局部变量。 | 同 ArrayList,需外部同步或使用线程安全容器。 |
| 适用场景 | 频繁查询、尾部增删。 | 频繁在任意位置插入/删除。 |
ArrayList 扩容规则:
- 调用
ensureCapacity方法,将数据长度乘以1.5倍。 - 底层源码实现:当前数量 + 当前数量
>>1位 相当于除以2。
ArrayList的Fail-Fast机制
- 通过记录
modCount参数来实现,增减元素会改变modCount的值。 - 在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
HashMap
| 对比项 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 数据结构 | 数组 + 链表(链表散列) | 数组 + 链表 + 红黑树 |
| 结构转换 | 无红黑树结构 | 链表长度 ≥ 8,且散列数组长度 ≥ 64 时转红黑树 |
| 数组长度 | 允许非 2 的幂次方长度 | 强制为 2 的幂次方长度(可以位运算优化哈希计算) |
| 哈希计算 | 通过取模计算槽位 | 用 hash & (n-1) 替代取模 |
| 扩容时机 | 插入数据前扩容 | 插入数据后扩容(避免无效扩容,减少内存浪费) |
| 扩容逻辑 | 所有元素重新计算哈希并分配槽位 | 通过高位判断元素是否迁移到新槽位(性能优化) |
| 插入逻辑 | 链表使用头插法(可能导致死循环) | 链表使用尾插法 |
为什么要在1.8之后改成尾插法:
- 1.7是头插法 1.8尾插法,链表头部插入元素会导致元素顺序和插入顺序颠倒,尾插法在尾部插入保持原来的顺序
- 多线程扩容时容易产生并发死循环:
- 线程 A 和线程 B 同时对同一个桶进行操作,线程 A 正在进行扩容操作,而线程 B 正在向该桶插入新元素
- 由于头插法的特性,线程 B 插入的新元素会成为链表的新头节点
- 由于扩容操作会将原链表的节点逆序插入到新链表中,线程 A 可能会在逆序过程中读取到线程 B 插入的新头节点
负载因子为什么选择0.75:
- 假设一个bucket空和非空的概率为0.5,通过二项式定理计算,当容量趋于无穷大时,合理值大概在0.7左右。
- 由于临界值(threshold)= 负载因子(loadFactor)* 容量(capacity),而容量永远是2的幂。
- 为了保证负载因子与容量的乘积是一个整数,0.75 是一个比较合理的选择,因为这个数与任何2的幂相乘的结果都是整数。
ConcurrentHashMap
| 对比项 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 数据结构 | 分段数组(Segment)+ 链表(每个 Segment 独立锁) | 数组 + 链表 + 红黑树(类似 HashMap,锁粒度细化到链表头或树根节点) |
| 锁机制 | 分段锁(ReentrantLock),每个 Segment 独立加锁 | CAS + synchronized 锁单个桶(链表头或树根节点),锁粒度更小 |
| 并发限制 | 由 Segment 数量决定(默认 16),并发性能受限于分段数 | 无固定分段,锁粒度细化到桶级别,理论上并发度更高 |
| 哈希计算 | 两次哈希(先定位 Segment,再定位桶) | 单次哈希(直接定位桶),优化哈希算法减少冲突 |
| 扩容机制 | 每个 Segment 独立扩容,扩容期间阻塞该分段的写操作 | 多线程协同扩容(迁移数据时,其他线程可协助完成桶迁移) |
| 迭代器特性 | 弱一致性(迭代过程中可能反映其他线程的修改) | 弱一致性,但实现更高效(基于快照或分段遍历) |
为什么在1.8废弃了分段锁:
- 锁粒度优化:通过对单个节点加锁,降低了锁的竞争。
- 锁性能优化:很多更新操作使用无锁的CAS操作,提高了效率,尤其在读多写少的场景下。
- 内存优化:通过减少锁的数量和使用更简洁的数据结构,提高了内存效率。
多线程相关
创建线程的方式
四种创建方式:
- 继承 Thread 类,重写 run 方法
- 实现 Runnable 接口,实现 run 方法
- 实现 Callable 接口,创建 FutureTask 类对象通过构造方法传入 Callable 对象,最后再通过创建 Thread 对象传入 FutureTask
- 通过线程池创建
注意:
- 一般用实现 runnable 接口,不用继承 thread 类,java 不支持多继承,但允许调用多个接口。
- start 方法和 run 方法的区别:直接调用 run 是在当前线程运行的,start 则是启动新的线程运行方法。
- Runnable 和 Callable 的区别:Runnable 从 JDK1.0 开始就有了,Callable 是在 JDK1.5 增加的。它们的主要区别是 Callable 的 call() 方法可以返回值和抛出异常,而 Runnable 的 run() 方法没有这些功能。
线程池核心参数
corePoolSize(核心线程数)
- 初始线程数量,默认长期存活(即使空闲)
- 特殊设置:
allowCoreThreadTimeOut(true)可使核心线程超时回收
maximumPoolSize(最大线程数)
- 线程池扩容上限,触发条件:
工作队列满 + 当前线程数 < max - 扩容创建的是"临时线程",受keepAliveTime控制
keepAliveTime(线程保活时间)
- 控制临时线程的空闲存活时间
- 计算公式:
线程空闲时间 > keepAliveTime → 回收线程
workQueue(工作队列)
| 队列类型 | 特性 | 适用场景 |
|---|---|---|
| ArrayBlockingQueue | 有界队列,数组实现 | 流量突发控制 |
| LinkedBlockingQueue | 可选有界/无界,链表实现 | 默认无界(需警惕OOM) |
| SynchronousQueue | 零容量队列,直接传递任务 | 高吞吐量场景 |
| PriorityBlockingQueue | 优先级队列 | 任务分级处理 |
threadFactory(线程工厂):
- 用于设置创建线程
- 通过
newThread()方法提供创建线程,该方法创建的线程都是“非守护线程”而且“线程优先级都是默认优先级”
handler(拒绝策略):
| 策略类型 | 特性 | 适用场景 |
|---|---|---|
| AbortPolicy | 直接抛出RejectedExecutionException异常,阻止系统继续运行 | 需要快速失败并通知开发人员的场景 |
| CallerRunsPolicy | 使用调用者线程直接执行被拒绝的任务 | 适合需要保证任务一定被执行,且能接受调用线程阻塞的场景 |
| DiscardOldestPolicy | 丢弃队列中最旧的任务(队首任务),然后尝试重新提交当前任务 | 适合允许丢弃部分历史任务的场景 |
| DiscardPolicy | 静默丢弃被拒绝的任务,不做任何处理 | 适合允许任务丢失的非关键业务场景 |
可以通过实现 RejectedExecutionHandler 接口创建定制化策略。
线程间通信方式
| 同步机制 | 特点 | 主要用途 |
|---|---|---|
| synchronized | 关键字修饰代码块/方法,自动获取/释放锁,不可中断,非公平锁,简单易用。 | 单线程访问共享资源,避免竞态条件。 |
| ReentrantLock | 显式锁(需手动释放),支持公平锁、可中断锁、超时锁,可绑定多个条件变量,灵活性高。 | 需要更复杂锁控制的场景(如尝试获取锁、可中断操作)。 |
| Semaphore | 基于许可证的同步,控制同时访问资源的线程数量,支持公平/非公平模式。 | 限制并发线程数(如数据库连接池)。 |
| CountDownLatch | 一次性同步工具,通过计数器等待其他线程完成,计数器归零后释放所有等待线程。 | 主线程等待多个子线程完成初始化任务。 |
| CyclicBarrier | 可重复使用的同步屏障,所有线程到达栅栏后继续执行,支持回调函数。 | 多线程分阶段协同工作(如并行计算分阶段汇总结果)。 |
| Phaser | 动态调整参与者数量,支持分层阶段同步,提供更灵活的到达/注销机制。 | 复杂分阶段任务(如动态增减线程的迭代计算)。 |
synchronized 对比 ReentrantLock:
synchronized是JVM实现的隐式锁,ReentrantLock是JDK代码实现的显式锁。ReentrantLock可通过tryLock()避免死锁,synchronized不支持。
CountDownLatch 对比 CyclicBarrier:
CountDownLatch计数器递减(一次性),CyclicBarrier计数器递增(可重复使用)。CyclicBarrier的reset()方法可重置,CountDownLatch不能重置。
Phaser:
- 支持动态注册(
register())/注销(arriveAndDeregister())参与者。 - 每个阶段(phase)可自定义同步策略,适合复杂任务编排。
CAS 和 AQS
以下是 CAS(Compare-And-Swap) 和 AQS(AbstractQueuedSynchronizer) 的对比总结表格:
| 特性 | CAS(Compare-And-Swap) | AQS(AbstractQueuedSynchronizer) |
|---|---|---|
| 核心思想 | 通过比较内存值与预期值是否一致来决定是否更新值。 | 通过 CLH队列(双向链表)管理线程的排队与唤醒机制。 |
| 实现方式 | 依赖底层硬件(CPU指令,如x86的cmpxchg)保证原子性。 | 基于模板方法模式,子类需重写 tryAcquire 和 tryRelease 等方法。 |
| 优点/缺点 | 优点:无锁,避免线程阻塞 缺点:高竞争时自旋开销大 | 优点:支持独占和共享模式,适用高竞争场景 缺点:实现复杂,需要处理线程中断和超时逻辑 |
| 典型应用 | AtomicInteger、AtomicReference | ReentrantLock、Semaphore、CountDownLatch |
| 应用场景 | 1. 实现无锁数据结构(如AtomicInteger) 2. 自旋锁 | 构建锁(如ReentrantLock)、同步工具(如Semaphore、CountDownLatch) |
CAS的ABA问题
- 问题:线程 A 修改变量和线程 B 修改变量的值相同,A 再次修改发现该变量未被修改过(实际上已经被修改过了)
- 解决方式:使用版本号(如
AtomicStampedReference)或时间戳标记变量状态。
AQS的核心机制
- 通过
state变量表示同步状态(如锁的持有次数)。 - 线程竞争失败后,会进入CLH队列并自旋检查前驱节点状态,避免频繁上下文切换。
JVM 相关
JVM 的组成
- ClassLoader(类加载器)
- Runtime Data Area(运行时数据区,内存分区)
- Stack(Java 虚拟机栈)
- Heap(Java 堆)
- Method Area(方法区)
- PC Register(程序计数器)
- Native Method Stack(本地方法栈)
- Execution Engine(执行引擎)
- Native InterFace(本地库接口)
运行流程:
- 类加载器:把Java代码转换为字节码
- 运行时数据区:把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是由执行引擎运行
- 执行引擎:将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口来实现整个程序的功能。
JVM 运行时数据区
| 区域 | 线程私有/共享 | 存储内容 | 异常类型 | 关键参数 |
|---|---|---|---|---|
| 程序计数器 | 私有 | 当前指令地址 | 无 | 无 |
| Java 虚拟机栈 | 私有 | 栈帧(局部变量、操作数栈等) | StackOverflowError/OOM | -Xss |
| 本地方法栈 | 私有 | Native 方法调用信息 | StackOverflowError/OOM | 无(与虚拟机栈合并) |
| 堆 | 共享 | 对象实例、数组 | OOM | -Xms, -Xmx |
| 方法区(元空间) | 共享 | 类信息、常量、静态变量、JIT代码 | OOM(JDK8 前 PermGen 相关) | -XX:MetaspaceSize 等 |
- Java堆:唯一存放java对象实例的空间,受垃圾回收器管理的区域
- 方法区:可以认为是java堆的一部分,两者可以被所有线程共享的,用于存储已被虚拟机加载的的信息,常量静态变量和即时编译器JIT编译后的代码。
- 虚拟机栈和本地方法栈:
- 栈一般指的是虚拟机栈,他是线程私有的,和线程的生命周期相同,方法执行时会在其中创建栈帧,包含存储局部变量表、操作数栈等信息。栈帧的入栈和出栈对应方法的调用和执行过程。
- 本地方法栈主要是为了本地方法服务的,在HotSpot虚拟机中将两者合二为一了。一般指的栈内存指的是java虚拟机栈的局部变量表部分。
- 程序计数器PC:存放了当前线程所执行的字节码的行号,因此每条线程都有独立的PC,在java虚拟机规范中,PC这部分所在的区域是唯一一个没有规定任何OOM异常的区域,因此该区域也不会进行GC(垃圾回收)
垃圾回收算法
| 算法 | 标记-清除 (Mark-Sweep) | 标记-复制 (Mark-Copy) | 标记-整理 (Mark-Compact) |
|---|---|---|---|
| 算法原理 | 1. 标记所有存活对象 2. 清除未标记的垃圾对象 | 1. 将内存分为两块,每次只用一块 2. 标记存活对象并复制到另一块 3. 清空当前块 | 1. 标记所有存活对象 2. 将存活对象向内存一端移动 3. 清理边界外的空间 |
| 优点/缺点 | 优点:执行速度快,内存利用率高 缺点:内存碎片化严重,分配大对象时可能失败 | 优点:无内存碎片, 分配效率高 缺点:内存浪费(需预留一半空间),复制存活对象开销大 | 优点:无内存碎片,内存利用率高(无需分区) 缺点:整理过程耗时,暂停时间较长 |
| 适用场景 | 老年代(存活对象多,碎片容忍) | 新生代(存活对象少,复制成本低) | 老年代(避免碎片,需连续内存) |
| JVM应用 | CMS 收集器的并发标记阶段 | Serial/ParNew 收集器的新生代 | Serial Old/CMS 的 Full GC 阶段 |
| 代表GC器 | CMS(并发标记清除) | Parallel Scavenge(复制算法) | G1/ZGC(局部整理) |
分代回收
- 新生代:多用标记-复制(如 Eden + Survivor 区)。
- 老年代:多用标记-清除或标记-整理(如 CMS 和 G1)。
G1 收集器将堆划分为多个 Region,局部使用复制或整理算法。
现代 GC
- G1 垃圾回收器:通过预测停顿时间,动态选择 Region 进行局部整理和复制。
- ZGC/Shenandoah 垃圾回收器:通过染色指针、读屏障等技术减少 STW 时间,实现并发整理。