Java知识库问答案例

wen java案例 2

本文目录导读:

Java知识库问答案例

  1. 基础语法篇
  2. 面向对象篇
  3. 集合框架篇
  4. 异常处理篇
  5. JVM与性能篇
  6. Java 8/11/17新特性篇
  7. 设计模式场景案例
  8. 综合面试场景题

我为你整理了一份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()返回常量池引用

⚠️ 注意事项

  1. 重写 equals() 时必须重写 hashCode(),否则HashMap等集合会异常
  2. 基本类型包装类(如 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;    // 指向下一个节点(处理冲突)
}

工作流程

  1. 计算哈希值
// HashMap中的hash方法
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    // 高16位和低16位异或,让分布更均匀
}
  1. 确定桶下标index = (n - 1) & hash,其中n是数组长度(2的幂次方)
  2. 插入冲突处理
    • 冲突少 → 链表(头插法转为尾插法,JDK 8)
    • 链表长度 ≥ 8 且数组长度 ≥ 64 → 转为红黑树
    • 树节点数 ≤ 6 → 转回链表
  3. 扩容机制(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 线程耗尽 降低线程数/检查线程泄漏

定位步骤

  1. 启动时加JVM参数
java -Xms512m -Xmx512m \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/logs/heapdump.hprof \
     -Xloggc:/logs/gc.log \
     YourApplication
  1. 使用MAT分析dump文件
// 查找大对象
// 1. 打开heapdump.hprof
// 2. 分析"Dominator Tree"(支配树)
// 3. 查看Leak Suspects(泄漏嫌疑对象)
  1. 模拟排查案例
// 常见内存泄漏示例:静态集合持有对象造成泄漏
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(强引用,对象)
         └── ...

泄漏发生过程

  1. ThreadLocal对象被外部置为null(无强引用)
  2. Key成为弱引用,会被GC回收
  3. 但Value仍然被Entry强引用着
  4. 若线程长期存活(如线程池中的线程),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();
        }
    }
}

预防措施

  1. 每次用完调用 remove() 方法
  2. 使用 try-with-resources 模式确保清理
  3. ThreadLocalValue中不要存放大型对象
  4. 避免在长生命周期线程(如线程池)中使用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 懒加载,线程安全 实现复杂,有反射风险
饿汉式 简单,线程安全 未使用也加载,浪费内存
枚举 绝对安全,简单 不能懒加载
静态内部类 懒加载,线程安全 有反射风险,不可序列化

抱歉,评论功能暂时关闭!