Java常见面试题7

Java 常见面试题(七)

适合人群 :正在准备 Java 后端面试的求职者

技术栈 :Java 21

涵盖方向 :SPI 机制、日期时间 API、接口默认方法、方法引用与函数式接口、Atomic 原子类

阅读建议:每道题按「面试官问法 → 求职者回答 → 核心知识点」三段式展开,建议先自己回答再对照


一、什么是 Java SPI 机制?JDBC 驱动是怎么加载的?

面试官会怎么问

"了解 Java 的 SPI 机制吗?它和 API 有什么区别?"

追问:"JDBC 驱动是怎么通过 SPI 加载的?"

再追问:"SPI 和 Dubbo SPI 有什么区别?"

求职者该怎么答

SPI 的概念

复制代码
SPI(Service Provider Interface)是 Java 提供的服务发现机制
- API:接口提供方定义并实现,调用方直接使用
- SPI:接口提供方定义接口,调用方(或服务使用方)提供实现

典型场景:
- JDBC:Java 定义数据库访问接口,各数据库厂商提供驱动实现
- SLF4J:定义日志接口,Logback/Log4j 提供实现
- Dubbo:定义扩展点接口,各模块提供实现

SPI 的核心思想:
  解耦!框架定义标准接口,第三方可以提供实现,框架自动发现并加载

SPI 的使用步骤

java 复制代码
// 1. 定义接口(服务提供方)
public interface Payment {
    void pay(BigDecimal amount);
}

// 2. 提供服务实现(第三方实现)
// 实现类 1:支付宝支付
public class AlipayPayment implements Payment {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("使用支付宝支付: " + amount);
    }
}

// 实现类 2:微信支付
public class WechatPayment implements Payment {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("使用微信支付: " + amount);
    }
}

// 3. 创建 SPI 配置文件
// 在 META-INF/services/ 目录下创建文件
// 文件名:接口全限定名(如 com.example.Payment)
// 文件内容:实现类全限定名(每行一个)
// META-INF/services/com.example.Payment:
// com.example.impl.AlipayPayment
// com.example.impl.WechatPayment

// 4. 使用 ServiceLoader 加载实现
ServiceLoader<Payment> loader = ServiceLoader.load(Payment.class);
for (Payment payment : loader) {
    payment.pay(new BigDecimal("100"));
}
// 输出:
// 使用支付宝支付: 100
// 使用微信支付: 100

JDBC 驱动的 SPI 加载

java 复制代码
// 传统方式(需要手动加载驱动):
Class.forName("com.mysql.cj.jdbc.Driver"); // 手动加载
Connection conn = DriverManager.getConnection(url, user, password);

// 现代方式(Java 6+ 自动加载):
Connection conn = DriverManager.getConnection(url, user, password);
// 不需要 Class.forName!自动发现并加载驱动

// 底层原理:
// 1. DriverManager 类中有静态代码块
static {
    loadInitialDrivers();
}

private static void loadInitialDrivers() {
    // 2. 使用 ServiceLoader 加载所有 Driver 实现
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    
    // 3. 遍历所有 Driver,触发类加载
    for (Driver driver : loadedDrivers) {
        // 每个 Driver 实现类都有静态代码块注册自己
        // 如:com.mysql.cj.jdbc.Driver 的静态代码块
        // static {
        //     DriverManager.registerDriver(new Driver());
        // }
    }
}

// 4. 获取连接时,DriverManager 遍历所有已注册的 Driver
public static Connection getConnection(String url, String user, String password) {
    for (DriverInfo di : registeredDrivers) {
        if (di.driver.acceptsURL(url)) {
            return di.driver.connect(url, info);
        }
    }
    throw new SQLException("No suitable driver found");
}

// 5. MySQL 驱动的 SPI 配置
// mysql-connector-java.jar 中的 META-INF/services/java.sql.Driver:
// com.mysql.cj.jdbc.Driver

SPI 的优缺点

复制代码
优点:
1. 解耦:框架和实现分离,符合开闭原则
2. 可扩展:第三方可以提供实现,无需修改框架代码
3. 自动发现:ServiceLoader 自动加载所有实现

缺点:
1. 性能问题:每次都要遍历所有实现,不能按需加载
2. 线程安全问题:ServiceLoader 不是线程安全的
3. 功能有限:只能遍历所有实现,不能根据条件选择特定实现
4. 实例化问题:每次都创建新实例,不能复用

改进方案:
- Dubbo SPI:支持按需加载、IOC、AOP、自适应
- Spring 的 @Service + @Autowired:更灵活的依赖注入

SPI 的实际应用场景

java 复制代码
// 1. JDBC 驱动加载(最经典的 SPI 应用)
// 2. SLF4J 日志框架绑定
//    - SLF4J 定义日志接口
//    - Logback/Log4j 提供实现
//    - META-INF/services/org.slf4j.spi.SLF4JServiceProvider
// 3. Java 加密扩展(JCE)
//    - Java 定义加密接口
//    - 各安全厂商提供实现
// 4. Dubbo 扩展点
//    - Dubbo 定义大量 SPI 接口(Protocol、Cluster、LoadBalance 等)
//    - 各模块提供实现,支持按需加载和自适应

// 自定义 SPI 示例:插件系统
public interface Plugin {
    String name();
    void execute();
}

public class PluginManager {
    private List<Plugin> plugins = new ArrayList<>();
    
    public PluginManager() {
        // 自动加载所有插件
        ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
        for (Plugin plugin : loader) {
            plugins.add(plugin);
        }
    }
    
    public void executeAll() {
        for (Plugin plugin : plugins) {
            System.out.println("执行插件: " + plugin.name());
            plugin.execute();
        }
    }
}

// 第三方开发插件:
// 1. 实现 Plugin 接口
// 2. 在 META-INF/services/ 下创建配置文件
// 3. 打包为 jar,放入 classpath
// 4. PluginManager 自动发现并加载

核心知识点总结

  • SPI 概念:接口定义方和使用方分离,自动发现并加载实现
  • 使用步骤:定义接口 → 提供实现 → 配置 META-INF/services → ServiceLoader 加载
  • JDBC 加载:DriverManager 通过 ServiceLoader 自动加载所有 Driver
  • 优缺点:解耦可扩展,但性能差、功能有限
  • 改进方案:Dubbo SPI(按需加载、IOC、AOP)

二、Java 8+ 的日期时间 API,LocalDate 和 LocalDateTime 怎么用?

面试官会怎么问

"Java 8 的日期时间 API 了解吗?和 Date/Calendar 有什么区别?"

追问:"LocalDate、LocalTime、LocalDateTime 有什么区别?"

再追问:"怎么计算两个日期之间的差值?怎么处理时区?"

求职者该怎么答

旧版日期 API 的问题

java 复制代码
// Date 的问题:
// 1. 可变对象,线程不安全
Date date = new Date();
date.setTime(1234567890); // 可以修改,多线程共享时不安全

// 2. API 设计混乱
date.getYear();  // 返回的是减去 1900 的值(如 2024 年返回 124)
date.getMonth(); // 返回 0-11(0 表示一月)
date.getDay();   // 返回星期几(0-6)

// 3. 时区处理困难
// Date 内部存储 UTC 时间戳,但 toString() 用默认时区格式化
// 容易混淆

// Calendar 的问题:
// 1. 也是可变的,线程不安全
// 2. API 复杂,月份从 0 开始
// 3. 性能差

// Java 8 新 API 的设计原则:
// 1. 不可变(Immutable):线程安全
// 2. 清晰的职责分离:日期、时间、日期时间、时区
// 3. 流式 API:链式调用
// 4. 支持 ISO-8601 标准

新版日期时间 API

java 复制代码
// LocalDate ------ 日期(年月日,无时间)
LocalDate date = LocalDate.of(2026, 7, 7);  // 2026-07-07
LocalDate today = LocalDate.now();          // 当前日期
LocalDate parsed = LocalDate.parse("2026-07-07"); // 从字符串解析

// 常用操作
date.plusDays(7);     // 加 7 天
date.minusMonths(1);  // 减 1 个月
date.withYear(2027);  // 设置年份
date.getDayOfWeek();  // 星期几
date.lengthOfMonth(); // 当月天数
date.isLeapYear();    // 是否闰年

// LocalTime ------ 时间(时分秒,无日期)
LocalTime time = LocalTime.of(14, 30, 0);  // 14:30:00
LocalTime now = LocalTime.now();            // 当前时间
LocalTime parsed = LocalTime.parse("14:30:00");

// 常用操作
time.plusHours(2);
time.minusMinutes(30);
time.getHour();
time.getMinute();

// LocalDateTime ------ 日期 + 时间(无时区)
LocalDateTime dateTime = LocalDateTime.of(2026, 7, 7, 14, 30, 0);
LocalDateTime now = LocalDateTime.now();
LocalDateTime parsed = LocalDateTime.parse("2026-07-07T14:30:00");

// 常用操作
dateTime.plusDays(7).plusHours(2);
dateTime.toLocalDate(); // 提取日期
dateTime.toLocalTime(); // 提取时间

// ZonedDateTime ------ 带时区的日期时间
ZonedDateTime zoned = ZonedDateTime.now(); // 默认时区
ZonedDateTime shanghai = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

// 时区转换
ZonedDateTime converted = shanghai.withZoneSameInstant(ZoneId.of("America/New_York"));

日期时间计算

java 复制代码
// 1. 计算两个日期之间的差值
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 7, 7);

// Period(日期差:年月日)
Period period = Period.between(start, end);
System.out.println(period.getYears());   // 0
System.out.println(period.getMonths());  // 6
System.out.println(period.getDays());    // 6
System.out.println(period.toTotalMonths()); // 6

// 2. 计算两个时间之间的差值
LocalTime startTime = LocalTime.of(9, 0);
LocalTime endTime = LocalTime.of(17, 30);

Duration duration = Duration.between(startTime, endTime);
System.out.println(duration.toHours());   // 8
System.out.println(duration.toMinutes()); // 510

// 3. 计算两个日期时间之间的差值
LocalDateTime startDT = LocalDateTime.of(2026, 1, 1, 9, 0);
LocalDateTime endDT = LocalDateTime.of(2026, 7, 7, 17, 30);

Duration durationDT = Duration.between(startDT, endDT);
System.out.println(durationDT.toDays()); // 187

// Period vs Duration:
// Period:日期差(年月日),用于 LocalDate
// Duration:时间差(时分秒),用于 LocalTime/LocalDateTime/Instant

日期时间格式化

java 复制代码
// 1. 预定义格式
LocalDateTime dateTime = LocalDateTime.now();

dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// 2026-07-07T14:30:00

dateTime.format(DateTimeFormatter.ISO_DATE);
// 2026-07-07

dateTime.format(DateTimeFormatter.ISO_TIME);
// 14:30:00

// 2. 自定义格式
DateTimeFormatter customFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = dateTime.format(customFormatter);
// "2026-07-07 14:30:00"

// 3. 从字符串解析
String str = "2026-07-07 14:30:00";
LocalDateTime parsed = LocalDateTime.parse(str, customFormatter);

// 4. 本地化格式
DateTimeFormatter chineseFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String chineseFormatted = dateTime.format(chineseFormatter);
// "2026年07月07日 14:30:00"

// 5. DateTimeFormatter 是线程安全的(比 SimpleDateFormat 好)
// 可以定义为 static final 常量复用
private static final DateTimeFormatter FORMATTER = 
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

时区处理

java 复制代码
// 1. 获取所有时区
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
zoneIds.stream().filter(z -> z.contains("Asia")).forEach(System.out::println);
// Asia/Shanghai
// Asia/Tokyo
// Asia/Singapore
// ...

// 2. 时区转换
ZonedDateTime beijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime tokyo = beijing.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
ZonedDateTime newYork = beijing.withZoneSameInstant(ZoneId.of("America/New_York"));

// 3. LocalDateTime 转 ZonedDateTime
LocalDateTime localDT = LocalDateTime.now();
ZonedDateTime zonedDT = localDT.atZone(ZoneId.of("Asia/Shanghai"));

// 4. 时间戳与日期时间转换
// Instant 表示 UTC 时间戳
Instant instant = Instant.now(); // 当前时间戳
long timestamp = instant.toEpochMilli(); // 毫秒时间戳

// Instant 转 LocalDateTime
LocalDateTime fromInstant = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai"));

// LocalDateTime 转 Instant
Instant toInstant = localDT.toInstant(ZoneOffset.of("+8")); // 指定时区偏移

// 5. 数据库交互
// JDBC 4.2+ 支持直接存储/读取 Java 8 日期时间
PreparedStatement ps = conn.prepareStatement("INSERT INTO events (event_time) VALUES (?)");
ps.setObject(1, LocalDateTime.now()); // 直接存 LocalDateTime

ResultSet rs = stmt.executeQuery("SELECT event_time FROM events");
LocalDateTime time = rs.getObject(1, LocalDateTime.class); // 直接读取

实际项目中的典型用法

java 复制代码
// 场景1:计算用户年龄
public int calculateAge(LocalDate birthday) {
    return Period.between(birthday, LocalDate.now()).getYears();
}

// 场景2:判断是否是工作日
public boolean isWorkday(LocalDate date) {
    DayOfWeek dayOfWeek = date.getDayOfWeek();
    return dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY;
}

// 场景3:生成日期范围
public List<LocalDate> getDateRange(LocalDate start, LocalDate end) {
    List<LocalDate> dates = new ArrayList<>();
    for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
        dates.add(date);
    }
    return dates;
}

// 场景4:定时任务(计算下一次执行时间)
public LocalDateTime getNextExecutionTime() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime nextNoon = now.withHour(12).withMinute(0).withSecond(0);
    
    if (now.isAfter(nextNoon)) {
        // 今天中午已过,返回明天中午
        return nextNoon.plusDays(1);
    }
    return nextNoon;
}

// 场景5:跨时区会议安排
public void scheduleMeeting(LocalDateTime beijingTime, ZoneId participantZone) {
    ZonedDateTime beijingZoned = beijingTime.atZone(ZoneId.of("Asia/Shanghai"));
    ZonedDateTime participantTime = beijingZoned.withZoneSameInstant(participantZone);
    
    System.out.println("北京时间: " + beijingZoned);
    System.out.println("参与者时间: " + participantTime);
}

核心知识点总结

  • 新版 API:LocalDate(日期)、LocalTime(时间)、LocalDateTime(日期时间)、ZonedDateTime(带时区)
  • 与旧版区别:不可变、线程安全、职责清晰、流式 API
  • 日期差计算:Period(年月日)、Duration(时分秒)
  • 格式化:DateTimeFormatter(线程安全,比 SimpleDateFormat 好)
  • 时区处理:ZoneId、ZonedDateTime、Instant(UTC 时间戳)

三、Java 接口的默认方法和静态方法是什么?有什么用?

面试官会怎么问

"Java 8 的接口默认方法了解吗?为什么要引入默认方法?"

追问:"默认方法和抽象方法有什么区别?"

再追问:"多个默认方法冲突时怎么解决?"

求职者该怎么答

默认方法的引入原因

java 复制代码
// Java 8 之前,接口只能有抽象方法
// 如果要给接口添加新方法,所有实现类都要实现,否则编译报错
// 问题:破坏已有的实现

// 例子:Java 8 给 Collection 接口添加 stream() 方法
// 如果 stream() 是抽象方法,所有 Collection 实现类都要修改
// 这不可能!Java 集合框架有大量实现类,包括第三方的

// 解决方案:默认方法
// 接口可以提供方法的默认实现,实现类可以选择性覆盖

// 默认方法的语法:
public interface List<E> extends Collection<E> {
    // 默认方法
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        // ...
    }
    
    // 默认方法
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }
    
    // 抽象方法(必须实现)
    E get(int index);
}

默认方法的使用

java 复制代码
// 1. 定义接口,包含默认方法
public interface Animal {
    // 抽象方法(必须实现)
    String getName();
    
    // 默认方法(可选实现)
    default void eat() {
        System.out.println(getName() + " is eating");
    }
    
    // 默认方法
    default void sleep() {
        System.out.println(getName() + " is sleeping");
    }
}

// 2. 实现类可以选择是否覆盖默认方法
public class Lion implements Animal {
    @Override
    public String getName() {
        return "Lion";
    }
    
    // 覆盖默认方法
    @Override
    public void eat() {
        System.out.println("Lion is eating meat");
    }
    
    // 不覆盖 sleep(),使用默认实现
}

// 3. 使用
Animal lion = new Lion();
lion.eat();   // "Lion is eating meat"(覆盖的方法)
lion.sleep(); // "Lion is sleeping"(默认方法)

// 4. 默认方法的好处:
// - 可以在不破坏现有实现的情况下扩展接口
// - 实现类可以选择性覆盖
// - 提供向后兼容性

多个默认方法冲突的解决

java 复制代码
// 场景1:类继承 + 接口实现冲突
interface A {
    default void hello() {
        System.out.println("Hello from A");
    }
}

interface B {
    default void hello() {
        System.out.println("Hello from B");
    }
}

// 编译错误:类 C 从 A 和 B 继承了冲突的默认方法
// class C implements A, B { } // ❌ 编译错误

// 解决:覆盖冲突方法
class C implements A, B {
    @Override
    public void hello() {
        // 方式1:自己实现
        System.out.println("Hello from C");
        
        // 方式2:调用 A 的默认实现
        A.super.hello();
        
        // 方式3:调用 B 的默认实现
        B.super.hello();
    }
}

// 场景2:类方法优先于接口默认方法
interface D {
    default void hello() {
        System.out.println("Hello from D");
    }
}

class Base {
    public void hello() {
        System.out.println("Hello from Base");
    }
}

class Derived extends Base implements D {
    // 不需要覆盖,Base 的 hello() 优先于 D 的默认方法
}

// 使用
Derived d = new Derived();
d.hello(); // "Hello from Base"(类方法优先)

// 冲突解决规则:
// 1. 类中定义的方法优先于接口默认方法
// 2. 更具体的接口优先(子接口优先于父接口)
// 3. 如果多个接口冲突,必须覆盖并使用 Super 调用

接口的静态方法

java 复制代码
// Java 8 引入,接口可以定义静态方法
public interface MathUtils {
    // 静态方法
    static int add(int a, int b) {
        return a + b;
    }
    
    // 静态方法
    static int multiply(int a, int b) {
        return a * b;
    }
}

// 使用:通过接口名调用
int sum = MathUtils.add(3, 5);
int product = MathUtils.multiply(3, 5);

// 静态方法不能被实现类继承或覆盖
// 静态方法属于接口本身,不属于实现类

// 实际应用:工具方法
public interface StringUtils {
    static boolean isEmpty(String str) {
        return str == null || str.isEmpty();
    }
    
    static boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }
}

// 使用
StringUtils.isEmpty("hello"); // false
StringUtils.isNotEmpty("");   // false

// 静态方法 vs 默认方法:
// 静态方法:属于接口,通过接口名调用,不能被覆盖
// 默认方法:属于实现类实例,通过实例调用,可以被覆盖

默认方法的底层原理

java 复制代码
// 默认方法的实现:
// 编译后,默认方法变成普通的实例方法
// 实现类如果没有覆盖,编译器会生成一个桥接方法调用接口的默认实现

// 例如:
interface I {
    default void hello() {
        System.out.println("Hello from I");
    }
}

class C implements I {
    // 没有覆盖 hello()
}

// 编译后:
class C implements I {
    // 编译器自动生成(如果实现类没有覆盖)
    // public void hello() {
    //     I.hello(this); // 调用接口的静态方法
    // }
}

// 接口中的默认方法编译后变成:
// 1. 一个静态方法(实际逻辑)
// 2. 一个实例方法(桥接方法)

// 这就是为什么默认方法可以被覆盖:
// 实现类可以覆盖实例方法

实际项目中的典型用法

java 复制代码
// 场景1:扩展已有接口(向后兼容)
public interface Repository<T> {
    // 原有方法
    T findById(Long id);
    List<T> findAll();
    
    // Java 8 新增的默认方法
    default Optional<T> findOptionalById(Long id) {
        return Optional.ofNullable(findById(id));
    }
    
    // 默认方法
    default boolean existsById(Long id) {
        return findById(id) != null;
    }
}

// 场景2:提供通用实现
public interface Validator<T> {
    boolean isValid(T value);
    
    // 默认方法:组合多个验证器
    default Validator<T> and(Validator<T> other) {
        return value -> this.isValid(value) && other.isValid(value);
    }
    
    default Validator<T> or(Validator<T> other) {
        return value -> this.isValid(value) || other.isValid(value);
    }
}

// 使用
Validator<String> notEmpty = s -> s != null && !s.isEmpty();
Validator<String> minLength = s -> s.length() >= 3;
Validator<String> maxLength = s -> s.length() <= 10;

Validator<String> combined = notEmpty.and(minLength).and(maxLength);
combined.isValid("hello"); // true
combined.isValid("hi");    // false(太短)

// 场景3:接口提供工具方法
public interface CollectionUtils {
    static <T> boolean isEmpty(Collection<T> collection) {
        return collection == null || collection.isEmpty();
    }
    
    static <T> boolean isNotEmpty(Collection<T> collection) {
        return !isEmpty(collection);
    }
}

核心知识点总结

  • 默认方法:接口提供方法的默认实现,实现类可选覆盖
  • 引入原因:向后兼容,可以在不破坏现有实现的情况下扩展接口
  • 冲突解决:类方法优先 > 更具体的接口 > 必须覆盖时使用 Super 调用
  • 静态方法:属于接口本身,通过接口名调用,不能被覆盖
  • 底层原理:默认方法编译后变成静态方法 + 桥接方法

四、方法引用和函数式接口,你真的理解了吗?

面试官会怎么问

"方法引用了解吗?和 Lambda 有什么区别?"

追问:"方法引用有哪几种类型?"

再追问:"函数式接口是什么?@FunctionalInterface 有什么作用?"

求职者该怎么答

方法引用的概念

java 复制代码
// 方法引用是 Lambda 的语法糖,更简洁
// 当 Lambda 体只是调用一个已有方法时,可以用方法引用替代

// Lambda 写法
list.forEach(e -> System.out.println(e));

// 方法引用写法
list.forEach(System.out::println);

// 两者等价,但方法引用更简洁、可读性更好

方法引用的四种类型

java 复制代码
// 1. 静态方法引用:ClassName::staticMethod
// Lambda
Function<String, Integer> parseInt = s -> Integer.parseInt(s);
// 方法引用
Function<String, Integer> parseInt = Integer::parseInt;

// 2. 实例方法引用(特定对象):instance::method
String str = "hello";
// Lambda
Supplier<String> getUpper = () -> str.toUpperCase();
// 方法引用
Supplier<String> getUpper = str::toUpperCase;

// 3. 实例方法引用(任意对象):ClassName::method
// Lambda
Function<String, String> toUpper = s -> s.toUpperCase();
// 方法引用
Function<String, String> toUpper = String::toUpperCase;
// 这里 toUpper.apply("hello") 等价于 "hello".toUpperCase()

// 4. 构造函数引用:ClassName::new
// Lambda
Function<String, StringBuilder> createBuilder = s -> new StringBuilder(s);
// 方法引用
Function<String, StringBuilder> createBuilder = StringBuilder::new;

// 总结:
// 静态方法引用:类名::方法名
// 特定对象的方法引用:对象::方法名
// 任意对象的方法引用:类名::方法名(第一个参数是调用者)
// 构造函数引用:类名::new

方法引用的实际用法

java 复制代码
// 场景1:集合排序
List<String> names = Arrays.asList("Bob", "Alice", "Charlie");

// Lambda
names.sort((a, b) -> a.compareTo(b));
// 方法引用
names.sort(String::compareTo);

// 场景2:集合转换
List<String> list = Arrays.asList("1", "2", "3");

// Lambda
List<Integer> nums = list.stream().map(s -> Integer.parseInt(s)).collect(Collectors.toList());
// 方法引用
List<Integer> nums = list.stream().map(Integer::parseInt).collect(Collectors.toList());

// 场景3:创建对象
// Lambda
Function<String, User> createUser = name -> new User(name);
// 方法引用
Function<String, User> createUser = User::new;

// 场景4:条件判断
List<String> words = Arrays.asList("hello", "", "world", " ");

// Lambda
List<String> nonEmpty = words.stream().filter(s -> !s.isEmpty()).collect(Collectors.toList());
// 方法引用
List<String> nonEmpty = words.stream().filter(String::isEmpty).collect(Collectors.toList());
// 注意:这里逻辑反了,应该是 s -> !s.isEmpty(),但方法引用不能直接取反

// 场景5:方法引用作为参数
// Lambda
Optional<String> opt = Optional.of("hello");
opt.ifPresent(s -> System.out.println(s));
// 方法引用
opt.ifPresent(System.out::println);

函数式接口

java 复制代码
// 函数式接口:只有一个抽象方法的接口
// 可以有默认方法、静态方法,但抽象方法只能有一个

@FunctionalInterface
public interface Runnable {
    void run();
}

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    // 还有 equals(),但是是 Object 的方法,不算
}

// @FunctionalInterface 注解的作用:
// 1. 标记这是函数式接口
// 2. 编译器检查:如果不是函数式接口(有多个抽象方法),编译报错

@FunctionalInterface
public interface BadInterface {
    void method1();
    void method2(); // ❌ 编译错误:多个抽象方法
}

// Java 8 内置的常用函数式接口:
接口 方法 说明 示例
Predicate<T> boolean test(T t) 判断 s -> s.isEmpty()
Function<T,R> R apply(T t) 转换 s -> Integer.parseInt(s)
Consumer<T> void accept(T t) 消费 s -> System.out.println(s)
Supplier<T> T get() 生产 () -> new User()
BiFunction<T,U,R> R apply(T t, U u) 双参转换 (a, b) -> a + b
UnaryOperator<T> T apply(T t) 一元操作 s -> s.toUpperCase()
BinaryOperator<T> T apply(T t, T u) 二元操作 (a, b) -> a + b
java 复制代码
// 使用内置函数式接口
// Predicate
Predicate<String> isEmpty = String::isEmpty;
isEmpty.test(""); // true

// Function
Function<String, Integer> parse = Integer::parseInt;
parse.apply("123"); // 123

// Consumer
Consumer<String> print = System.out::println;
print.accept("hello");

// Supplier
Supplier<User> userFactory = User::new;
User user = userFactory.get();

// 自定义函数式接口
@FunctionalInterface
public interface Converter<F, T> {
    T convert(F from);
}

// 使用
Converter<String, Integer> converter = Integer::parseInt;
int num = converter.convert("123");

方法引用和 Lambda 的选择

java 复制代码
// 什么时候用方法引用?
// 1. Lambda 体只是调用一个已有方法
// 2. 方法引用更简洁、可读性更好

// 什么时候用 Lambda?
// 1. 需要复杂逻辑
// 2. 需要多个参数
// 3. 方法引用不能表达(如取反)

// 例子:
// Lambda(复杂逻辑)
list.filter(s -> s != null && s.length() > 3 && s.startsWith("A"));

// 方法引用(简单调用)
list.forEach(System.out::println);

// Lambda(取反)
list.filter(s -> !s.isEmpty());

// 方法引用(不能取反)
// list.filter(String::isEmpty); // 逻辑反了

核心知识点总结

  • 方法引用:Lambda 的语法糖,更简洁
  • 四种类型:静态方法引用、特定对象方法引用、任意对象方法引用、构造函数引用
  • 函数式接口:只有一个抽象方法的接口,用 @FunctionalInterface 标记
  • 内置接口:Predicate、Function、Consumer、Supplier、BiFunction 等
  • 选择原则:简单调用用方法引用,复杂逻辑用 Lambda

五、Atomic 原子类与 CAS,无锁并发是怎么实现的?

面试官会怎么问

"了解 Atomic 原子类吗?它们是怎么保证原子性的?"

追问:"CAS 是什么?有什么问题?"

再追问:"AtomicInteger 和 synchronized 有什么区别?"

求职者该怎么答

Atomic 原子类的概念

java 复制代码
// Atomic 原子类:线程安全的操作类
// 使用 CAS(Compare-And-Swap)实现无锁并发
// 比 synchronized 性能更高

// 常用的 Atomic 类:
// - AtomicInteger:原子整数
// - AtomicLong:原子长整数
// - AtomicBoolean:原子布尔值
// - AtomicReference:原子引用
// - AtomicStampedReference:带版本号的原子引用(解决 ABA 问题)
// - AtomicMarkableReference:带标记的原子引用

// 使用示例:
AtomicInteger count = new AtomicInteger(0);

// 原子操作
count.get();           // 获取值
count.set(10);         // 设置值
count.getAndSet(20);   // 获取旧值,设置新值
count.getAndIncrement(); // 自增,返回旧值
count.incrementAndGet(); // 自增,返回新值
count.getAndAdd(5);    // 加 5,返回旧值
count.addAndGet(5);    // 加 5,返回新值

// 线程安全
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> count.incrementAndGet());
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println(count.get()); // 1000(线程安全)

CAS 的原理

java 复制代码
// CAS(Compare-And-Swap):比较并交换
// 一种无锁的原子操作

// CAS 的三个操作数:
// V:内存地址
// E:预期值(Expected)
// N:新值(New)

// CAS 操作:
// 如果 V 的值等于 E,则将 V 的值更新为 N
// 否则不做操作
// 返回是否成功

// 伪代码:
boolean cas(V, E, N) {
    if (V == E) {
        V = N;
        return true;
    }
    return false;
}

// AtomicInteger 的 incrementAndGet() 源码(简化):
public final int incrementAndGet() {
    for (;;) { // 自旋
        int current = get();          // 获取当前值
        int next = current + 1;       // 计算新值
        if (compareAndSet(current, next)) { // CAS 操作
            return next;
        }
        // 如果 CAS 失败,循环重试
    }
}

// compareAndSet() 底层使用 Unsafe 类
// Unsafe 提供了硬件级别的原子操作
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

// CAS 的特点:
// 1. 无锁:不需要加锁,性能更高
// 2. 乐观:假设没有冲突,直接操作
// 3. 自旋:如果失败,循环重试

CAS 的问题

java 复制代码
// 1. ABA 问题
// 线程 A 读取值为 A
// 线程 B 修改为 B
// 线程 C 修改回 A
// 线程 A CAS 成功,但值已经被修改过

// 解决:使用 AtomicStampedReference(带版本号)
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(1, 0);
// 参数1:初始值,参数2:初始版本号

int stamp = ref.getStamp(); // 获取版本号
ref.compareAndSet(1, 2, stamp, stamp + 1); // CAS 时检查版本号

// 2. 自旋开销
// 如果 CAS 长时间失败,会一直自旋,消耗 CPU

// 3. 只能保证单个变量的原子性
// 多个变量的操作需要加锁或使用 AtomicReference 封装对象

// 例子:
AtomicInteger x = new AtomicInteger(0);
AtomicInteger y = new AtomicInteger(0);

// 不安全:x 和 y 的操作不是原子的
x.set(1);
y.set(2);

// 解决:封装成一个对象
class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

AtomicReference<Point> point = new AtomicReference<>(new Point(0, 0));
point.set(new Point(1, 2)); // 原子操作

Atomic 类 vs synchronized

java 复制代码
// 对比:
// AtomicInteger:使用 CAS,无锁,性能高
// synchronized:使用锁,有锁竞争和上下文切换开销

// 性能对比(1000 次自增):
// AtomicInteger:约 10ms
// synchronized:约 50ms

// 使用场景:
// AtomicInteger:简单计数、状态标记、CAS 操作
// synchronized:复杂逻辑、多个变量、需要阻塞等待

// 代码对比:
// AtomicInteger
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

// synchronized
private int count = 0;
public synchronized void increment() {
    count++;
}

// Atomic 的优点:
// 1. 无锁,性能更高
// 2. 不会阻塞,不会死锁
// 3. 细粒度,只操作单个变量

// synchronized 的优点:
// 1. 可以保护多个变量
// 2. 可以保护复杂逻辑
// 3. 失败时阻塞等待,不会自旋消耗 CPU

LongAdder:高性能计数器

java 复制代码
// LongAdder 是 Java 8 引入的高性能计数器
// 内部使用多个 Cell 分散竞争,比 AtomicLong 性能更高

// 适用场景:高并发统计(如访问量、计数)

LongAdder adder = new LongAdder();
adder.increment();
adder.add(10);
long sum = adder.sum(); // 获取当前值

// LongAdder vs AtomicLong:
// AtomicLong:所有线程竞争一个 value,竞争激烈时性能下降
// LongAdder:内部维护 Cell[],每个线程更新不同的 Cell,最后汇总

// LongAdder 的底层结构:
// ┌──────────────────────────────────────────────────────┐
// │  long baseCount                                        │
// │  Cell[] cells                                          │
// │  ├── Cell[0].value                                     │
// │  ├── Cell[1].value                                     │
// │  └── ...                                               │
// └──────────────────────────────────────────────────────┘

// 线程 A 更新 cells[0]
// 线程 B 更新 cells[1]
// 线程 C 更新 cells[2]
// 最后 sum() = baseCount + sum(cells)

// 性能对比(高并发场景):
// AtomicLong:约 100ms
// LongAdder:约 20ms(5 倍性能提升)

// 注意:
// LongAdder 不是原子操作,sum() 可能不是最新值
// 适用于统计场景,不适用于需要精确值的场景

实际项目中的典型用法

java 复制代码
// 场景1:计数器
public class RequestCounter {
    private final LongAdder counter = new LongAdder();
    
    public void increment() {
        counter.increment();
    }
    
    public long getCount() {
        return counter.sum();
    }
}

// 场景2:状态标记
public class ServiceStatus {
    private final AtomicBoolean running = new AtomicBoolean(true);
    
    public void shutdown() {
        if (running.compareAndSet(true, false)) {
            // 只执行一次关闭逻辑
            System.out.println("Service shutting down...");
        }
    }
    
    public boolean isRunning() {
        return running.get();
    }
}

// 场景3:无锁栈
public class ConcurrentStack<T> {
    private final AtomicReference<Node<T>> top = new AtomicReference<>();
    
    public void push(T value) {
        Node<T> newNode = new Node<>(value);
        Node<T> oldTop;
        do {
            oldTop = top.get();
            newNode.next = oldTop;
        } while (!top.compareAndSet(oldTop, newNode));
    }
    
    public T pop() {
        Node<T> oldTop;
        Node<T> newTop;
        do {
            oldTop = top.get();
            if (oldTop == null) return null;
            newTop = oldTop.next;
        } while (!top.compareAndSet(oldTop, newTop));
        return oldTop.value;
    }
    
    private static class Node<T> {
        T value;
        Node<T> next;
        Node(T value) { this.value = value; }
    }
}

// 场景4:限流器
public class RateLimiter {
    private final AtomicInteger tokens;
    private final int maxTokens;
    
    public RateLimiter(int maxTokens) {
        this.maxTokens = maxTokens;
        this.tokens = new AtomicInteger(maxTokens);
    }
    
    public boolean tryAcquire() {
        int current;
        do {
            current = tokens.get();
            if (current <= 0) return false;
        } while (!tokens.compareAndSet(current, current - 1));
        return true;
    }
    
    public void release() {
        tokens.updateAndGet(current -> Math.min(current + 1, maxTokens));
    }
}

核心知识点总结

  • Atomic 原子类:使用 CAS 实现无锁并发,线程安全且性能高
  • CAS 原理:比较并交换,如果值匹配则更新,否则重试
  • CAS 问题:ABA 问题(用 AtomicStampedReference 解决)、自旋开销、只能保证单变量原子性
  • Atomic vs synchronized:Atomic 无锁高性能,synchronized 可保护多变量和复杂逻辑
  • LongAdder:高并发计数器,内部分散竞争,性能比 AtomicLong 更高

知识图谱总结

复制代码
Java 常见面试题(七)
│
├── SPI 机制
│   ├── 概念:接口定义方和使用方分离,自动发现实现
│   ├── 使用步骤:定义接口 → 提供实现 → META-INF/services → ServiceLoader
│   ├── JDBC 加载:DriverManager 通过 ServiceLoader 自动加载 Driver
│   └── 优缺点:解耦可扩展,但性能差、功能有限
│
├── 日期时间 API(Java 8+)
│   ├── 新版 API:LocalDate、LocalTime、LocalDateTime、ZonedDateTime
│   ├── 与旧版区别:不可变、线程安全、职责清晰
│   ├── 日期差计算:Period(年月日)、Duration(时分秒)
│   └── 格式化:DateTimeFormatter(线程安全)
│
├── 接口默认方法与静态方法
│   ├── 默认方法:接口提供默认实现,实现类可选覆盖
│   ├── 引入原因:向后兼容,扩展接口不破坏现有实现
│   ├── 冲突解决:类方法优先 > 更具体接口 > Super 调用
│   └── 静态方法:属于接口,通过接口名调用
│
├── 方法引用与函数式接口
│   ├── 方法引用:Lambda 的语法糖,四种类型
│   ├── 函数式接口:只有一个抽象方法,@FunctionalInterface 标记
│   ├── 内置接口:Predicate、Function、Consumer、Supplier
│   └── 选择原则:简单调用用方法引用,复杂逻辑用 Lambda
│
└── Atomic 原子类与 CAS
    ├── Atomic 类:使用 CAS 实现无锁并发
    ├── CAS 原理:比较并交换,自旋重试
    ├── CAS 问题:ABA、自旋开销、单变量原子性
    └── LongAdder:高并发计数器,分散竞争
相关推荐
^yi1 小时前
【C++】类和对象(全)
开发语言·c++
带刺的坐椅2 小时前
代码审查 Agent Harness 实战:AI 自动 Code Review
java·ai·llm·agent·solon·soloncode·harness
可儿·四系桜2 小时前
经纬度转北斗网格位置码(非极地区域,编码篇)
java·物联网
唐青枫2 小时前
Java ActiveMQ 实战指南:从 JMS 队列、主题到 Spring Boot 消息处理
java
Dovis(誓平步青云)2 小时前
《C/C+++ Boost 轻量级搜索引擎实战:架构流程、技术栈与工程落地指南——HTML文件的清洗(上篇)》
开发语言·数据库·c++·搜索引擎·开源
布朗克1682 小时前
Go 入门到精通-13-指针与内存
开发语言·c++·golang·指针与内存
码兄科技9 小时前
Java AI智能体开发实战:从零构建智能对话系统指南
java·开发语言·人工智能
折哥的程序人生 · 物流技术专研10 小时前
第8篇:六大设计原则在工厂模式中的体现
java·设计模式·设计原则·工厂模式·编程进阶·扩充系列