Featured image of post Java 面试合集:OOP / 泛型 / SPI / String 家族

Java 面试合集:OOP / 泛型 / SPI / String 家族

Java 面试基础合集:面向对象三大要素、泛型机制、String/StringBuilder/StringBuffer 区别、SPI 加载机制、静态代理与动态代理、设计模式思想

本文写于 2014 年 9 月——JDK 1.8 刚发布半年,Lambda 表达式刚开始普及。

一、面向对象三大要素

1.1 封装、继承、多态

1
面向对象三要素:封装、继承、多态

封装

封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,或者叫接口。

  • 数据隐藏
  • 接口隔离
  • 降低耦合

继承

  • 继承基类的方法,并做出自己的扩展
  • 声明某个子类兼容于某基类(接口上完全兼容)
  • 外部调用者可无需关注其差别

多态

基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。多态实际上是依附于继承的第二种含义的

抽象

抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

1.2 面试追问

Q1:多态的必要条件?

  1. 继承
  2. 方法重写
  3. 父类引用指向子类对象
1
2
Animal animal = new Dog();  // 父类引用指向子类对象
animal.shout();  // 调用的是 Dog 的 shout 方法(运行时多态)

Q2:抽象类和接口的区别?

维度抽象类接口
方法实现可以有抽象方法 + 具体方法JDK 8+ 可有 default/static 方法
字段任意字段默认 public static final
多继承单继承多实现
设计意图is-a 关系(模板)has-a 能力(规范)

二、Java 泛型

2.1 泛型定义

1
2
3
Java 泛型(generics)是 JDK 5 中引入的一个新特性
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数

2.2 泛型的好处

1
2
3
1. 类型安全
2. 消除强制类型转换
3. 潜在的性能收益(值类型避免装箱)

2.3 泛型类型擦除

Q:泛型是编译期还是运行期? A:编译期。Java 泛型是伪泛型,编译后会擦除类型信息。

1
2
3
4
5
6
7
// 编译前
List<String> list = new ArrayList<>();
list.add("hello");

// 编译后(字节码)
List list = new ArrayList();
list.add("hello");

为什么擦除

  • 保持向后兼容(JDK 5 之前的代码也能跑)
  • 避免 JVM 指令集膨胀
  • 编译期类型检查已足够

2.4 面试常见陷阱

1
2
3
4
5
6
// Q:以下代码能否编译通过?
List<Integer> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass());  // true

// A:能编译,且输出 true(泛型擦除)

三、String / StringBuilder / StringBuffer

3.1 三大区别

维度StringStringBufferStringBuilder
可变不可变可变可变
线程安全安全(final)安全(synchronized)不安全
性能最低最高
场景少量字符串多线程 + 字符串拼接单线程 + 字符串拼接

3.2 实现原理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// String 不可变
public final class String {
    private final char value[];  // final 数组,不可变
    private final int hash;     // 缓存 hash
}

// StringBuilder / StringBuffer 可变
public final class StringBuilder extends AbstractStringBuilder {
    char[] value;  // 非 final,可变
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
}

3.3 性能对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 性能测试
String s = "";
StringBuilder sb = new StringBuilder();
StringBuffer sbSync = new StringBuffer();

// 100 万次拼接
for (int i = 0; i < 1_000_000; i++) {
    s += "a";      // ~5000 ms(每次创建新对象)
    sb.append("a");  // ~10 ms(原地修改)
    sbSync.append("a");  // ~30 ms(同步开销)
}

实战建议

  • 循环内字符串拼接:StringBuilder
  • 字符串常量拼接:编译器自动优化(final String s = "a" + "b""ab"
  • 工具类:Guava Joiner / JDK 8 String.join()

四、Java 代理

4.1 静态代理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 接口
interface UserService {
    void save(User user);
}

// 2. 目标类
class UserServiceImpl implements UserService {
    public void save(User user) { /* 业务 */ }
}

// 3. 静态代理(手动写代理类)
class UserServiceProxy implements UserService {
    private UserService target;
    public UserServiceProxy(UserService target) {
        this.target = target;
    }
    public void save(User user) {
        System.out.println("事务开始");
        target.save(user);
        System.out.println("事务提交");
    }
}

缺点:每个被代理类都需要手动写一个代理类,代码冗余

4.2 动态代理

JDK Proxy(基于接口)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    (proxyObj, method, args) -> {
        System.out.println("事务开始");
        Object result = method.invoke(target, args);
        System.out.println("事务提交");
        return result;
    }
);
proxy.save(user);

原理:JDK 在运行时动态生成 $Proxy0.class 字节码。

CGLIB 动态代理(基于继承)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// CGLIB 通过继承目标类生成子类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    System.out.println("事务开始");
    Object result = proxy.invokeSuper(obj, args);
    System.out.println("事务提交");
    return result;
});
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
proxy.save(user);

原理:基于 ASM 字节码工具,生成被代理类的子类。

Spring AOP 选择

  • 目标类有接口:用 JDK Proxy(性能更好)
  • 目标类无接口:用 CGLIB
  • Spring Boot 2.0+ 默认使用 CGLIB

五、SPI 机制(Service Provider Interface)

5.1 什么是 SPI

1
2
3
SPI(Service Provider Interface),是 JDK 内置的一种服务提供发现机制
可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用
比如 java.sql.Driver 接口,不同厂商可以针对同一接口做出不同的实现

核心思想解耦 + 可插拔

5.2 SPI 整体机制

![SPI 整体机制图]

当服务的提供者提供了一种接口的实现之后:

  1. 在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件
  2. 文件内容是这个接口的具体实现类
  3. 通过 java.util.ServiceLoader 加载

5.3 实战示例

1. 定义接口

1
2
3
public interface Search {
    public List<String> searchDoc(String keyword);
}

2. 文件系统实现

1
2
3
4
5
6
7
public class FileSearch implements Search {
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("文件搜索 " + keyword);
        return null;
    }
}

3. 数据库实现

1
2
3
4
5
6
7
public class DatabaseSearch implements Search {
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("数据库搜索 " + keyword);
        return null;
    }
}

4. META-INF/services 配置

新建文件 META-INF/services/com.cainiao.ys.spi.learn.Search

1
com.cainiao.ys.spi.learn.FileSearch

5. 测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class TestCase {
    public static void main(String[] args) {
        ServiceLoader<Search> s = ServiceLoader.load(Search.class);
        Iterator<Search> iterator = s.iterator();
        while (iterator.hasNext()) {
            Search search = iterator.next();
            search.searchDoc("hello world");
        }
    }
}

输出

1
文件搜索 hello world

5.4 SPI 的应用

1. JDBC DriverManager

在 JDBC 4.0 之前,开发有连接数据库的时候,通常会用 Class.forName("com.mysql.jdbc.Driver") 先加载数据库相关的驱动,然后再进行获取连接等操作。而 JDBC 4.0 之后不需要用 Class.forName("com.mysql.jdbc.Driver") 来加载驱动,直接获取连接就可以了,现在这种方式就是使用了 Java 的 SPI 扩展机制来实现。

MySQL 实现

mysql-connector-java-6.0.6.jar 中可以找到 META-INF/services 目录,文件名为 java.sql.Driver,内容是 com.mysql.cj.jdbc.Driver

PostgreSQL 实现

postgresql-42.0.0.jar 中也有同样文件,内容是 org.postgresql.Driver

2. Spring Boot 自动配置

META-INF/spring.factories 文件:

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.MyAutoConfiguration

3. Dubbo 扩展点

Dubbo 的所有扩展点(Filter / LoadBalance / Protocol 等)都基于 SPI 实现。

5.5 SPI 面试高频追问

Q1:SPI 和 API 的区别?

维度APISPI
调用方调用者主动服务提供方主动
实现位置调用方内部服务提供方 jar 包
典型场景调用 SDK 方法数据库驱动加载

Q2:ServiceLoader 是线程安全的吗?

部分线程安全。load 方法线程安全,但 iterator 迭代过程中如果服务实现发生变化可能产生 ConcurrentModificationException

Q3:Spring Factories 机制与 JDK SPI 的区别?

维度JDK SPISpring Factories
配置文件META-INF/services/接口全限定名META-INF/spring.factories
加载方式ServiceLoader.loadSpringFactoriesLoader.loadFactoryNames
多键支持不支持支持(一个 key 多个 value)
触发时机ServiceLoader.loadSpring 启动时

六、写在最后

面试建议

  • 基础题看似简单,追问要警惕——OOP 三要素可以追问多态、泛型可以追问类型擦除、SPI 可以追问与 API 区别
  • 准备"业务场景题"——比如"如何用 SPI 设计一个可插拔的日志框架"
  • “为什么这样设计"比"是什么"更重要——比如"为什么 String 设计成不可变”

参考资料

使用 Hugo 构建
主题 StackJimmy 设计