本文目录导读:

我为你整理了一份Java知识库问答案例,涵盖基础语法、面向对象、集合框架、并发编程、JVM与性能、新特性等核心领域,每个案例都包含典型问题与详细解答,并附代码示例。
基础语法篇
案例1:String、StringBuilder、StringBuffer的区别
❓ 问题:请详细解释String、StringBuilder、StringBuffer三者的区别,并说明各自的适用场景。
✅ 解答:
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变(final) | 可变 | 可变 |
| 线程安全 | 安全(不可变天然安全) | 不安全 | 安全(synchronized) |
| 性能 | 最差(频繁拼接产生大量对象) | 最优 | 较优(有锁开销) |
| 适用场景 | 不变的场景 | 单线程下频繁字符串拼接 | 多线程下需要安全的字符串拼接 |
深层解析:
// String的"+"拼接底层原理(JDK 8)
String str = "a" + "b" + "c";
// 编译器优化为:String str = "abc";
// 循环拼接场景
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // ⚠️ 实际上每次循环都创建一个新的StringBuilder
}
// 优化建议:在循环外显式声明StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
🔧 附加知识点:
- JDK 9+之后,String底层由
char[]改为byte[]+coder(编码标识),节省空间 - 字符串常量池在JDK 7+从方法区移到堆中
案例2:== 与 equals() 的区别
❓ 问题: 与 equals() 有什么区别?使用时有哪些注意事项?
✅ 解答:
- :比较的是内存地址(基本类型比较值,引用类型比较引用地址)
equals():Object类中的方法,默认实现就是 ,但很多类会重写为比较
深入案例:
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false,两个不同对象
System.out.println(s1.equals(s2)); // true,内容相同
String s3 = "hello"; // 放入常量池
String s4 = "hello";
System.out.println(s3 == s4); // true,指向常量池同一对象
String s5 = s3 + "!";
String s6 = "hello!";
System.out.println(s5 == s6); // false,s5通过运行期拼接产生新对象
System.out.println(s5.intern() == s6); // true,intern()返回常量池引用
⚠️ 注意事项:
- 重写
equals()时必须重写hashCode(),否则HashMap等集合会异常 - 基本类型包装类(如
Integer)的 比较时,-128~127之间有缓存
Integer a = 127, b = 127; System.out.println(a == b); // true(缓存) Integer c = 128, d = 128; System.out.println(c == d); // false(超出缓存范围,创建新对象)
面向对象篇
案例3:抽象类与接口的选择
❓ 问题:在什么情况下应该使用抽象类?什么情况下应该使用接口?请结合设计场景说明。
✅ 解答:
设计原则:
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 关系 | "is-a"(是一个) | "has-a"(有某种能力) |
| 字段 | 可以有实例变量 | 默认 public static final |
| 构造器 | 有 | 无 |
| 多继承 | 单继承 | 可多实现 |
| 默认方法 | 可以 | Java 8+可以 |
实战选择场景:
// ✅ 用接口:定义行为能力
public interface Flyable {
void fly();
}
public interface Swimmable {
void swim();
}
// 鸟类可以"有"游泳能力(如鸭子),也可以"有"飞行能力
public class Duck implements Flyable, Swimmable {
@Override
public void fly() { /* ... */ }
@Override
public void swim() { /* ... */ }
}
// ✅ 用抽象类:提取共同属性和公共逻辑
public abstract class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
// 所有动物都会叫,但实现方式不同
public abstract void makeSound();
// 所有动物共同的休眠逻辑
public void sleep() {
System.out.println(name + " is sleeping...");
}
}
public class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override
public void makeSound() {
System.out.println("汪汪!");
}
}
经验法则:
- 需要默认实现共享时 → 抽象类
- 需要多实现/解耦行为时 → 接口
- Java 8+的默认方法让接口有了"轻量级抽象类"的特性,但要谨慎使用(避免菱形问题)
案例4:深拷贝与浅拷贝的区别及实现
❓ 问题:Java中的浅拷贝和深拷贝有什么区别?请实现一个深拷贝案例。
✅ 解答:
- 浅拷贝:拷贝对象时,基本类型字段会拷贝新值,但引用类型字段共享同一个引用
- 深拷贝:引用类型字段也会递归地创建新副本
实现方案:
// 方式一:实现 Cloneable 接口(需重写clone)
class Address implements Cloneable {
String city;
Address(String city) { this.city = city; }
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class User implements Cloneable {
String name;
Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
User user = (User) super.clone(); // 浅拷贝
user.address = (Address) address.clone(); // 深度拷贝引用字段
return user;
}
}
// 方式二:序列化实现深拷贝(推荐)
import java.io.*;
public static <T> T deepCopy(T object) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
oos.flush();
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
return (T) ois.readObject();
}
集合框架篇
案例5:HashMap的底层原理(高频面试)
❓ 问题:详细描述HashMap的底层实现原理,包括存储结构、哈希冲突解决方案、扩容机制。
✅ 解答:
存储结构(JDK 8+):
数组 + 链表 + 红黑树
- 数据节点(Node):
static class Node<K,V> {
final int hash; // 哈希值
final K key; // 键
V value; // 值
Node<K,V> next; // 指向下一个节点(处理冲突)
}
工作流程:
- 计算哈希值:
// HashMap中的hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// 高16位和低16位异或,让分布更均匀
}
- 确定桶下标:
index = (n - 1) & hash,其中n是数组长度(2的幂次方) - 插入冲突处理:
- 冲突少 → 链表(头插法转为尾插法,JDK 8)
- 链表长度 ≥ 8 且数组长度 ≥ 64 → 转为红黑树
- 树节点数 ≤ 6 → 转回链表
- 扩容机制(resize):
- 默认容量:16,负载因子:0.75
- 当
size > 容量 * 负载因子时触发扩容,容量翻倍 - 2的幂次方扩容,利用
位运算高效重新分配位置
// 新增元素的核心流程(简化)
public V put(K key, V value) {
if (table == null) resize(); // 首次使用时初始化
int index = (n - 1) & hash(key); // 计算桶位置
if (table[index] == null) { // 无冲突,直接放
table[index] = newNode(hash, key, value, null);
} else {
// 遍历链表/红黑树,找到位置插入或替换
// 链表长度>=8且数组长度>=64 → 树化
}
if (++size > threshold) resize(); // 检查扩容
}
JDK 7 vs JDK 8 区别:
| 特点 | JDK 7 | JDK 8 |
|---|---|---|
| 数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 插入方式 | 头插法(并发死循环) | 尾插法 |
| 扩容后位置 | 重新hash计算 | 原位置 或 原位置+旧容量 |
| 扰动函数 | 4次 | 1次(高16位异或低16位) |
案例6:ConcurrentHashMap原理
❓ 问题:ConcurrentHashMap是如何实现线程安全的?相比Hashtable有哪些优势?
✅ 解答:
JDK 8的ConcurrentHashMap实现:
- 采用 CAS + synchronized 实现安全(放弃分段锁)
// 关键put操作(简化)
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// CAS CAS自旋尝试
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 使用CAS原子操作放入节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) {
break;
}
} else if (f.hash == MOVED) { // 扩容
tab = helpTransfer(tab, f);
} else {
synchronized (f) { // 锁住链表头节点
// 进行链表/红黑树操作
}
}
}
// 判断是否需要转红黑树
}
优势对比:
| 维度 | Hashtable | ConcurrentHashMap |
|---|---|---|
| 锁粒度 | 锁整个表 | 锁单个桶(节点) |
| 并发性 | 极低 | 极高(读不加锁,写锁桶) |
| null值 | 不允许 | 不允许 |
| 迭代 | fail-fast | 弱一致性(迭代器不抛异常) |
异常处理篇
案例7:受检异常与非受检异常
❓ 问题:Checked Exception和Unchecked Exception有什么区别?如何处理自定义异常?
✅ 解答:
异常体系图:
Throwable
├── Error(错误,不可捕获,如OOM)
└── Exception
├── RuntimeException(非受检异常,编译不强制)
│ ├── NullPointerException
│ ├── ClassCastException
│ └── IllegalArgumentException
└── 其他必须处理的受检异常(编译期强制)
├── IOException
├── SQLException
└── ClassNotFoundException
Spring事务处理注意:
// 默认情况下,Spring声明式事务只在RuntimeException时回滚
@Transactional
public void transfer(Long fromId, Long toId, int amount) {
// 业务操作,可能抛出RuntimeException
// 注意:受检异常默认不回滚!需要 rollbackFor 指定
}
// 修正
@Transactional(rollbackFor = Exception.class)
public void transfer(...) { ... }
最佳实践:
// 自定义异常继承RuntimeException(推荐)
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}
// 枚举错误码
public enum ErrorCode {
USER_NOT_FOUND(1001, "用户不存在"),
INSUFFICIENT_BALANCE(1002, "余额不足");
private final int code;
private final String message;
// 构造器、getter...
}
JVM与性能篇
案例8:OOM(OutOfMemoryError)定位与分析
❓ 问题:当线上应用报OOM异常时,如何定位和解决?
✅ 解答:
OOM类型与原因分析:
| OOM类型 | 触发场景 | 解决方向 |
|---|---|---|
Java heap space |
堆内存不足(对象太多) | 增大堆内存/排查内存泄漏 |
GC overhead limit exceeded |
GC耗时过长(>98%时间GC) | 检查堆大小与对象生命周期 |
Metaspace |
元空间满(类加载过多) | 增大Metaspace/排查动态代理 |
Unable to create new native thread |
线程耗尽 | 降低线程数/检查线程泄漏 |
定位步骤:
- 启动时加JVM参数:
java -Xms512m -Xmx512m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-Xloggc:/logs/gc.log \
YourApplication
- 使用MAT分析dump文件:
// 查找大对象 // 1. 打开heapdump.hprof // 2. 分析"Dominator Tree"(支配树) // 3. 查看Leak Suspects(泄漏嫌疑对象)
- 模拟排查案例:
// 常见内存泄漏示例:静态集合持有对象造成泄漏
public class MemoryLeakDemo {
private static final List<Object> cache = new ArrayList<>();
public void addData() {
// 不断放入但不清理
for (int i = 0; i < 10000; i++) {
cache.add(new Byte[1024 * 1024]); // 1MB
}
// 问题:cache是static,一直持有引用
}
}
// 修复:使用WeakReference / 定时清理 / 限制容量
案例9:ThreadLocal的内存泄漏问题
❓ 问题:ThreadLocal为什么会造成内存泄漏?如何避免?
✅ 解答:
原理分析:
- ThreadLocal的值存放在Thread的
ThreadLocalMap中 - Entry的Key是弱引用(
WeakReference<ThreadLocal>),但 Value是强引用
Thread(线程对象)
│
└── ThreadLocalMap(线程属性)
│
├── Entry[0]: Key(ThreadLocal-弱引用) → Value(强引用,对象)
├── Entry[1]: Key(ThreadLocal-弱引用) → Value(强引用,对象)
└── ...
泄漏发生过程:
- ThreadLocal对象被外部置为null(无强引用)
- Key成为弱引用,会被GC回收
- 但Value仍然被Entry强引用着
- 若线程长期存活(如线程池中的线程),Value永远无法释放 → 内存泄漏
最佳实践:
public class ThreadLocalDemo {
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String format(Date date) {
try {
return dateFormat.get().format(date);
} finally {
// ✅ 每次用完后remove,防止泄漏
dateFormat.remove();
}
}
}
预防措施:
- 每次用完调用
remove()方法 - 使用
try-with-resources模式确保清理 - ThreadLocalValue中不要存放大型对象
- 避免在长生命周期线程(如线程池)中使用ThreadLocal
Java 8/11/17新特性篇
案例10:Stream API的实战使用
❓ 问题:有一个员工列表,需要筛选出薪资超过5000的员工,按部门分组,并统计每个部门的平均薪资,请用Stream实现。
✅ 解答:
// 员工实体
public class Employee {
private String name;
private String department;
private double salary;
// 构造器、getter、setter...
}
// 核心Stream操作
List<Employee> employees = getEmployees();
Map<String, Double> avgSalaryByDept = employees.stream()
.filter(e -> e.getSalary() > 5000) // 1. 过滤条件
.collect(Collectors.groupingBy( // 2. 按部门分组
Employee::getDepartment, // 分组键
Collectors.averagingDouble( // 3. 每组求平均工资
Employee::getSalary
)
));
// 进阶操作:找每个部门薪资最高的员工
Map<String, Optional<Employee>> highestPaidByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparingDouble(Employee::getSalary))
));
// 并行流注意(数据量大且操作无状态才使用)
double avgSalary = employees.parallelStream()
.mapToDouble(Employee::getSalary)
.average()
.orElse(0.0);
常用Stream API汇总:
| 操作类型 | 方法 | 作用 |
|---|---|---|
| 中间操作 | filter |
过滤条件 |
| 中间操作 | map |
转换元素 |
| 中间操作 | flatMap |
扁平化流 |
| 中间操作 | distinct |
去重 |
| 中间操作 | sorted |
排序 |
| 终止操作 | collect |
收集结果 |
| 终止操作 | reduce |
归约(求和等) |
| 终止操作 | forEach |
遍历 |
| 终止操作 | count |
计数 |
案例11:Java 8的CompletableFuture实现异步编程
❓ 问题:如何使用CompletableFuture实现多个远程调用的并行执行,并合并结果?
✅ 解答:
public class AsyncDemo {
private final ExecutorService executor =
Executors.newFixedThreadPool(10);
// 场景:并行调用3个远程服务,最终汇总
public CompletableFuture<OrderDetail> getOrderDetail(Long orderId) {
// 步骤1:并行获取基础信息
CompletableFuture<OrderBase> baseFuture =
CompletableFuture.supplyAsync(() ->
orderService.getBase(orderId), executor);
CompletableFuture<List<Item>> itemsFuture =
CompletableFuture.supplyAsync(() ->
itemService.getItems(orderId), executor);
CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(() ->
userService.getUserByOrder(orderId), executor);
// 步骤2:合并所有结果
return baseFuture
.thenCombineAsync(itemsFuture, (base, items) -> {
base.setItems(items);
return base;
}, executor)
.thenCombineAsync(userFuture, (base, user) -> {
base.setUser(user);
return base;
}, executor);
}
// 异常处理
public CompletableFuture<String> fetchWithRetry() {
return CompletableFuture.supplyAsync(() -> {
// 业务逻辑
if (Math.random() > 0.5) {
throw new RuntimeException("临时错误");
}
return "成功";
}, executor)
.exceptionally(ex -> {
// 异常时重试或返回默认值
System.out.println("发生异常: " + ex.getMessage());
return "降级结果";
})
.orTimeout(5, TimeUnit.SECONDS); // 超时控制
}
}
设计模式场景案例
案例12:策略模式 + 工厂模式实际应用
❓ 问题:不同支付方式(支付宝、微信、银联)的处理逻辑不同,如何设计实现?
✅ 解答:
// 1. 统一支付策略接口
public interface PaymentStrategy {
PayResult pay(PayRequest request);
String getChannel(); // 渠道标识
}
// 2. 支付宝策略
@Component
public class AlipayStrategy implements PaymentStrategy {
@Override
public PayResult pay(PayRequest request) {
// 支付宝特有逻辑
return new PayResult("ALIPAY_SUCCESS", "支付成功");
}
@Override
public String getChannel() { return "alipay"; }
}
// 微信策略
@Component
public class WechatStrategy implements PaymentStrategy {
@Override
public PayResult pay(PayRequest request) {
// 微信特有逻辑
return new PayResult("WECHAT_SUCCESS", "支付成功");
}
@Override
public String getChannel() { return "wechat"; }
}
// 3. 策略工厂(根据渠道自动选择)
@Component
public class PaymentStrategyFactory {
private final Map<String, PaymentStrategy> strategyMap;
// Spring注入所有策略beans
@Autowired
public PaymentStrategyFactory(List<PaymentStrategy> strategies) {
strategyMap = strategies.stream()
.collect(Collectors.toMap(PaymentStrategy::getChannel, s -> s));
}
public PaymentStrategy getStrategy(String channel) {
PaymentStrategy strategy = strategyMap.get(channel);
if (strategy == null) {
throw new UnsupportedOperationException("不支持的支付渠道: " + channel);
}
return strategy;
}
}
// 使用
@Service
public class PaymentService {
@Autowired
private PaymentStrategyFactory factory;
public PayResult executePay(PayRequest request) {
return factory.getStrategy(request.getChannel()).pay(request);
}
}
优点:
- 开闭原则:新增支付方式只需添加新实现类
- 消除大量 if-else
- 单一职责:每个策略类职责清晰
综合面试场景题
案例13:手写单例模式并说明安全性
❓ 问题:请手写一个线程安全的单例模式,并分析其优缺点。
✅ 解答:
推荐标准写法(双重检查锁 DCL):
public class Singleton {
// volatile 防止指令重排序,保证可见性
private static volatile Singleton instance;
private Singleton() {
// 防止反射破坏单例
if (instance != null) {
throw new IllegalStateException("Already initialized");
}
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 锁
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
// 防止序列化破坏单例
protected Object readResolve() {
return getInstance();
}
}
为什么用volatile:
instance = new Singleton(); // 实际分为3步: // 1. 分配内存空间 // 2. 初始化Singleton对象 // 3. 将引用指向内存地址(此时instance非null) // 如果不加volatile,指令可能重排序为 1→3→2 // 其他线程可能在步骤2完成前就获取到未初始化的实例
枚举单例(绝对安全,推荐):
public enum SingletonEnum {
INSTANCE;
public void doSomething() {
// 业务逻辑
}
}
// 枚举天然防反射、防序列化
优缺点对比:
| 方式 | 优点 | 缺点 |
|---|---|---|
| DCL | 懒加载,线程安全 | 实现复杂,有反射风险 |
| 饿汉式 | 简单,线程安全 | 未使用也加载,浪费内存 |
| 枚举 | 绝对安全,简单 | 不能懒加载 |
| 静态内部类 | 懒加载,线程安全 | 有反射风险,不可序列化 |