Java-八股
文章目录
-
- String、StringBuffer、StringBuilder的区别
- 接口和抽象类的区别
- Java常见的异常类有哪些
- 说一说Java面向对象三大特性
- 说一说你对Java多态的理解
- Java重写和重载的区别
- final关键字有什么作用
- == 和equals的区别
- Java的集合类有哪些,那些是线程安全的,那些是线程不安全的?
- ArrayList 和 Array 有什么区别?
- ArrayList 和 LinkedList 的区别是什么?
- ArrayList 扩容机制
- 介绍一下HashMap
- HashMap的底层结构
- Put的流程
- Get的流程
- 扩容的流程
- 解决Hash冲突的方法有哪些?HashMap 是如何解决 hash 冲突的
- HashMap 为什么是线程不安全的? 如何实现线程安全
- ConcurrentHashMap 如何保证线程安全
- HashMap和ConcurrentHashMap的区别
- HashSet 和 HashMap 的区别
- HashMap 和 HashTable 的区别
- Java 创建线程有哪几种方式?
- 1. 继承 Thread 类
- 2. 实现 Runnable 接口
- 3. Lambda 表达式
- 线程start和run的区别
- 你知道Java中有哪些锁吗
- 说一说你对 synchronized 的理解
- synchronized和lock的区别是什么
- synchronized和ReentrantLock的区别是什么
- volatile 关键字的作用有那些?
- volatile 与synchronized 的对比
- 写一下单例模式
- JDK8有哪些新特性
- 为什么要有线程池?
- 说一说线程池有哪些常用参数
- BIO、NIO、AIO 的区别
- Java 内存区域有哪些部分
- 介绍一下什么是强引用、软引用、弱引用、虚引用
- 有哪些垃圾回收算法
- 有哪些垃圾回收器
- 类加载机制介绍一下
- 介绍一下双亲委派机制
String、StringBuffer、StringBuilder的区别
背景
它们都是操作字符串的
是什么
| String | StringBuffer | StringBuilder | |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(因为不可变) | 安全(方法用 synchronized 修饰) | 不安全 |
| 适用环境 | 字符串不常变化 | 多线程下字符串拼接 | 单线程下字符串拼接 |
为什么
为什么 String 不可变?
字符数组 final,每次修改都会新建对象
为什么 StringBuffer 是线程安全的?
它的 append()、insert() 等方法都用 synchronized 修饰,保证多线程下操作同一对象时数据不混乱。
为什么 StringBuilder 线程不安全却更好?
StringBuffer / StringBuilder:内部是一个可扩展的字符数组,修改时直接操作该数组,避免频繁创建新对象。
接口和抽象类的区别
背景
在面向对象设计中,我们需要两种层次的抽象:
-
定义规范(协议):只声明“能做什么”,不关心“怎么做”。
-
提供共同部分的实现(模板):抽取子类的公共代码,同时强制子类完成某些细节。
Java 用接口实现前者,用抽象类实现后者。
是什么
| 抽象类 | 接口 | |
|---|---|---|
| 定义 | 定义的是什么, 是动物, 还是交通工具 | 定义的是能力和行为, 比如可飞行, 可奔跑 |
| 本质 | 本质是一个类, 可以有成员变量, 构造方法, 具体方法和抽象方法 | 极简的抽象类, 通常只包含抽象方法, 接口只声明方法, 不包含实例变量 |
| 继承 | 抽象类只支持单继承, 一个类只能继承一个抽象类 | 接口支持多继承, 一个类可以实现多个接口 |
| 使用场景 | 当要多个相关类提供基类, 用抽象类 | 当需要用跨不同的类的行为时, 用接口 |
Java常见的异常类有哪些
背景
Java的异常都是继承 Throwable
是什么
| 运行时异常 | 编译时的异常 |
|---|---|
| 不强制处理 | 强制处理(catch,throws) |
| 代码逻辑错误(空指针, 数组越界等), 就是程序员的bug | 外部问题(IO, 数据库等), 这部分异常编译器要求必须处置 |
为什么
设计理念
运行时异常的原因是代码逻辑错误, 所以应该修复bug, 而不是到处catch
受检异常, 是外部问题, 程序员无法避免, 所以编译器要求强制考虑这种情况, 要么处理(catch), 要么上报(throws)
说一说Java面向对象三大特性
| 封装 | 继承 | 多态 | |
|---|---|---|---|
| 背景(核心目的) | 保护数据 | 复用代码 | 灵活拓展 |
| 是什么 | 将对象的**属性(数据)和行为(方法)**捆绑在一起,并隐藏内部实现细节,只对外暴露必要的访问接口(如 getter/setter) |
子类通过 extends 复用父类的非私有属性和方法,并可以扩展或重写父类行为 |
父类引用可以指向子类对象,调用方法时会执行子类的实现。 |
| 总结 | 把该藏的藏起来,该露的露出来 | 继承父类的能力 | 一个接口,多种实现 |
说一说你对Java多态的理解
多态是指同一个行为具有多种表现形式,即父类引用可以指向子类对象,调用方法时会执行子类的实现。
简单理解:一个接口(或父类),多种实现。
// 多态的体现:父类引用指向子类对象
Animal a1 = new Dog(); // 编译时类型是Animal,运行时类型是Dog
Animal a2 = new Cat(); // 编译时类型是Animal,运行时类型是Cat
a1.speak(); // 输出"汪汪汪"(实际调用Dog的方法)
a2.speak(); // 输出"喵喵喵"(实际调用Cat的方法)
Java重写和重载的区别
方法重载就是编译时多态
在编译时就确定调用哪个方法, 根据参数类型,个数, 顺序决定
class Calculator {
// 方法重载:方法名相同,参数不同
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
编译时就知道调用哪个方法(看参数类型和数量)
方法重写就是运行时多态
运行时才确定调用哪个方法, 根据对象的实际类型决定
Animal animal; // 编译时类型是Animal
// 运行时才确定调用哪个类的speak()
animal = new Dog();
animal.speak(); // 输出"汪汪汪"
animal = new Cat();
animal.speak(); // 输出"喵喵喵"
final关键字有什么作用
背景
在编程中,有些数据, 类, 方法一旦确定就不希望被修改。Java 提供 final 关键字来实现这些“不可变”约束。
是什么
| 修饰目标 | 作用 | 示例 |
|---|---|---|
| 类 | 不能被继承 | final class String {} |
| 方法 | 不能被重写(override) | public final void method() {} |
| 变量 | 值不能被修改(常量) | final int MAX_SIZE = 100; |
为什么
修饰类:保证类的核心逻辑不被子类篡改,如 String 类,保证字符串的安全性和不可变性
修饰方法:防止子类重写关键方法,确保行为一致
修饰变量:固定值,提升代码可读性,防止误修改,配合 static 可定义全局常量
== 和equals的区别
背景
“相等”有两个维度:是否是同一个对象(内存地址相同),还是逻辑上内容相同。
是什么
| == | equals() | |
|---|---|---|
| 比较的是内存地址 | 比较的是内容 | |
| 基本类型:比较数值是否相等 引用类型:比较两个引用是否指向同一个对象 |
是 Object 类中的方法,默认行为等同于 ==(比较地址) 需要子类重写才有意义,例如 String、Integer 等已重写,用于比较内容是否相等 |
|
| 使用场景 | 判断两个引用是否指向同一个对象时 | 当需要判断两个对象逻辑上相等时 |
为什么
为什么重写 equals() 必须重写 hashCode():因为 HashMap、HashSet 等集合先通过 hashCode() 定位存储位置,再用 equals() 判断是否相等。如果两者不一致,会导致集合中元素重复或无法找到
Java的集合类有哪些,那些是线程安全的,那些是线程不安全的?
一、背景
Java 集合框架在设计时,为了性能考虑,大部分实现都是线程不安全的。但实际开发中既有单线程也有多线程场景,因此需要区分哪些是线程安全的,哪些不是。
二、是什么
线程安全的集合(位于 java.util.concurrent 包):
-
List:
Vector(古老,全表锁)、CopyOnWriteArrayList(读多写少场景) -
Set:
CopyOnWriteArraySet、ConcurrentSkipListSet -
Map:
Hashtable(古老)、ConcurrentHashMap(推荐) -
Queue:
ConcurrentLinkedQueue、ArrayBlockingQueue、LinkedBlockingQueue线程不安全的集合(位于
java.util包): -
List:
ArrayList、LinkedList -
Set:
HashSet、LinkedHashSet、TreeSet -
Map:
HashMap、LinkedHashMap、TreeMap
三、为什么
为什么大部分集合线程不安全?
- 性能考虑:同步会带来性能开销,单线程场景不需要
- 设计选择:把选择权交给开发者,按需选择同步方案
ArrayList 和 Array 有什么区别?
一、ArrayList 和 Array 的区别
底层
- Array:一块连续的内存空间,每个元素占固定大小,通过偏移量直接访问
- ArrayList:基于数组实现, 有自动扩容机制
是什么
| 对比项 | Array | ArrayList |
|---|---|---|
| 特点 | 固定,不可变 | 动态,可自动扩容 |
| 使用 | 只有length属性 | 提供 add(), remove() 等丰富方法 |
| 场景 | 性能更高,适合长度固定、性能敏感场景 | 使用更灵活,适合需要动态增删的场景 |
ArrayList 和 LinkedList 的区别是什么?
背景
两者都是 List 的实现,但底层数据结构不同,性能表现各异。
是什么
| 对比项 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | O(1),快 | O(n),慢 |
| 中间插入/删除 | O(n),慢(需移动元素) | O(1),快(只改指针) |
| 内存占用 | 连续内存,较省 | 每个节点多存前后指针,较耗 |
| 使用场景 | 查询多时 | 中间的增删多 |
-
为什么实际开发 ArrayList 用得多:查询场景远多于增删场景,且数组对 CPU 缓存友好,综合性能更好
使用建议
-
查询多 →
ArrayList -
中间增删多 →
LinkedList -
大多数情况直接选
ArrayList
ArrayList 扩容机制
背景
ArrayList 底层基于数组实现,但数组长度固定。为了支持动态添加元素,需要自动扩容机制。
是什么
1. 初始容量
- JDK 1.8 后:无参构造初始容量为 0(懒加载)
- 第一次
add()时扩容到 10 - 也可通过构造器指定初始容量
2. 扩容规则
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍
- 每次扩容为原容量的 1.5 倍
- 10 → 15 → 22 → 33 → 49 …
3. 扩容过程
- 计算新容量(1.5 倍)
- 创建新数组
- 用
Arrays.copyOf()复制原数组元素 - 原数组被 GC 回收
为什么
1. 为什么是 1.5 倍?
- 不是 2 倍:避免内存浪费
- 不是 1.1 倍:避免频繁扩容影响性能
- 在时间和空间之间取得平衡
2. 为什么采用懒加载(初始容量 0)?
- 节省内存:很多 ArrayList 实际使用元素很少
- 避免提前分配不必要的内存
3. 性能建议
- 能预知元素数量时,指定初始容量
- 减少扩容次数,提升性能
// 不推荐:可能多次扩容
ArrayList<String> list = new ArrayList<>();
// 推荐:指定容量,避免扩容
ArrayList<String> list = new ArrayList<>(10000);
一句话总结
ArrayList 在容量不足时,会创建一个 1.5 倍大小的新数组并复制元素;能预知数据量时建议指定初始容量以减少扩容开销。
介绍一下HashMap
一、背景
需要存储键值对并快速查找,数组和链表都无法同时满足,HashMap 结合两者优点,提供 O(1) 的平均查询速度。
二、是什么
底层结构
数组 + 链表 + 红黑树
方法
put, 就是插入键值对, 平均时间复杂度:O(1)
扩容机制, 当元素个数大于阈值时, 就会出发扩容, 新容量 = 旧容量 × 2, 所有元素重新分配
get, 根据键获取值, 平均时间复杂度:O(1)
注意事项
- 线程不安全
- 键对象必须重写
hashCode()和equals()
三、为什么
1 为什么引入红黑树?
- 解决链表过长导致查询退化为 O(n) 的问题
- 红黑树查询 O(log n),性能更好
- 阈值 8 基于泊松分布,概率极低
2. 重写 equals 为什么要重写 hashCode?
- HashMap 先用 hashCode 定位,再用 equals 判断
- 只重写 equals 会导致相同对象 hash 不同,存到不同位置,逻辑错误
HashMap的底层结构
数组每个元素是一个桶, 桶中有三种情况,
没有元素(null), 有元素(链表或者红黑树)
- 链表长度 > 8 且数组长度 ≥ 64 时,链表转红黑树
- 红黑树节点 < 6 时,退化为链表
Put的流程
- 计算 hash:
(key.hashCode()) ^ (h >>> 16) - 计算索引:
(n - 1) & hash - 该位置为空 → 直接插入
- 不为空 → 判断 key 是否相等,相等则覆盖
- 是红黑树 → 红黑树插入
- 是链表 → 遍历链表,尾插法插入,插入后判断是否转红黑树
- 元素个数 > 阈值 → 扩容
Get的流程
- 计算 hash 和索引
- 找到位置后,先判断第一个节点
- 不匹配则按链表或红黑树查找
扩容的流程
- 触发:元素个数 > 阈值
- 新容量 = 旧容量 × 2
- JDK 1.8 优化:元素要么在原位置,要么在原位置 + 旧容量
- 无需重新计算 hash,效率更高
1. 为什么容量是 2 的幂?
HashMap 容量为 2 的幂,是为了用位运算 (n-1) & hash 替代取模提升性能,同时扩容时只需位运算判断新增位,无需重新计算 hash,大幅提升扩容效率。
- 扩容时容量翻倍,元素的新位置只需判断
(hash & oldCap) == 0- 为 0 → 位置不变
- 不为 0 → 原位置 + 旧容量
- 不需要重新计算 hash 值,只需一次位运算,效率极高
2. 为什么加载因子是 0.75?
- 时间和空间的平衡
- 太大(1.0):冲突多,查询慢
- 太小(0.5):频繁扩容,浪费空间
解决Hash冲突的方法有哪些?HashMap 是如何解决 hash 冲突的
解决哈希冲突的方法主要有以下两种:
- 链地址法:在数组的每个位置维护一个链表。当发生冲突时,新的元素会被添加到链表的尾部。
- 开放寻址法:当发生冲突时,根据某种探测算法在哈希表中寻找下一个空闲位置来存储元素。
Java中的 HashMap使用链地址法解决hash冲突。
为什么?
- 对负载因子更宽容
- 开放寻址法:负载因子 > 0.7 时性能暴跌(探测越来越长)
- 链地址法:负载因子可以到 0.75 甚至更高,性能下降平缓
- 删除操作简单
- 开放寻址法:删除要留“墓碑标记”,否则查找会断,久了堆积垃圾
- 链地址法:直接从链表删除,干净利落
- 不怕冲突聚集
- 开放寻址法:容易产生“扎堆”现象(一个冲突带出一串冲突)
- 链地址法:冲突的节点各自挂在链表里,互不干扰
HashMap 为什么是线程不安全的? 如何实现线程安全
背景
设计初衷是为了追求单线程下的极致性能,因此没有加同步机制。
是什么
HashMap 的所有方法都没有加锁,也没有使用 CAS 等并发控制手段。
为什么
实现线程安全推荐使用 ConcurrentHashMap,它在 JDK 1.8 中采用 CAS + synchronized 锁桶的方式
ConcurrentHashMap 如何保证线程安全
线程安全主要通过 CAS 和 synchronized 来保证
具体来说有三点:
-
当往空桶里放数据时,用 CAS 无锁插入,效率高
-
当桶不为空(发生哈希冲突)时,用 synchronized 锁住这个桶的头节点,然后操作链表或红黑树
-
读操作完全不加锁,因为 Node 的 value 和 next 是 volatile 的
不同的桶可以同时读写,只有操作同一个桶的写线程才需要竞争锁,所以性能非常好。
HashMap和ConcurrentHashMap的区别
HashSet 和 HashMap 的区别
HashMap 和 HashTable 的区别
Java 创建线程有哪几种方式?
1. 继承 Thread 类
// 最简单:继承 + 重写run
Thread t = new Thread() {
public void run() {
System.out.println("线程执行");
}
};
t.start();
2. 实现 Runnable 接口
// 最简单:Runnable + Thread
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("线程执行");
}
});
t.start();
3. Lambda 表达式
// 最简单:Lambda一行搞定
Thread t = new Thread(() -> System.out.println("线程执行"));
t.start();
线程start和run的区别
start() 会创建新线程并执行,run() 只是普通方法调用,不会创建新线程。
你知道Java中有哪些锁吗
说一说你对 synchronized 的理解
是什么
synchronized 是 Java 内置的锁关键字,保证同一时刻只有一个线程能执行被它修饰的代码。
怎么用
可以锁方法,也可以锁代码块。锁实例方法锁的是 this,锁静态方法锁的是 Class 对象。
特点
它是可重入的,同一个线程可以多次拿到同一个锁,不会死锁。
底层原理我了解不深,JDK 1.6 之后有锁升级优化
synchronized和lock的区别是什么
synchronized和ReentrantLock的区别是什么
volatile 关键字的作用有那些?
- 保证可见性, 一个线程修改了变量,其他线程立刻能看到最新值,不会从本地缓存读旧值。
- 禁止指令重排序, 防止编译器优化, 保证代码按顺序执行
volatile 与synchronized 的对比
写一下单例模式
1. 饿汉式(最简单、最安全)
/**
* 饿汉式单例
* 优点:线程安全,实现简单
* 缺点:可能造成资源浪费(不管用不用都创建)
*/
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
2. 懒汉式 + 双重检查锁(最经典)
/**
* 懒汉式 + 双重检查锁(DCL)
* 优点:延迟加载,线程安全,性能好
* 关键点:volatile 禁止指令重排序
*/
class Singleton {
// volatile 防止指令重排序,保证线程安全
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查:避免不必要的加锁
if (instance == null) {
// 加锁保证线程安全
synchronized (Singleton.class) {
// 第二次检查:防止重复创建
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
JDK8有哪些新特性
为什么要有线程池?
说一说线程池有哪些常用参数
BIO、NIO、AIO 的区别
Java 内存区域有哪些部分
介绍一下什么是强引用、软引用、弱引用、虚引用
有哪些垃圾回收算法
有哪些垃圾回收器
类加载机制介绍一下
介绍一下双亲委派机制
更多推荐




所有评论(0)