本文写于 2014 年 9 月——JDK 1.8 刚发布半年,Lambda 表达式刚开始普及。
一、面向对象三大要素
1.1 封装、继承、多态
封装:
封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,或者叫接口。
继承:
- 继承基类的方法,并做出自己的扩展
- 声明某个子类兼容于某基类(接口上完全兼容)
- 外部调用者可无需关注其差别
多态:
基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。多态实际上是依附于继承的第二种含义的。
抽象:
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
1.2 面试追问
Q1:多态的必要条件?
- 继承
- 方法重写
- 父类引用指向子类对象
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 三大区别
| 维度 | String | StringBuffer | StringBuilder |
|---|
| 可变 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(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 整体机制图]
当服务的提供者提供了一种接口的实现之后:
- 在 classpath 下的
META-INF/services/ 目录里创建一个以服务接口命名的文件 - 文件内容是这个接口的具体实现类
- 通过
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");
}
}
}
|
输出:
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 的区别?
| 维度 | API | SPI |
|---|
| 调用方 | 调用者主动 | 服务提供方主动 |
| 实现位置 | 调用方内部 | 服务提供方 jar 包 |
| 典型场景 | 调用 SDK 方法 | 数据库驱动加载 |
Q2:ServiceLoader 是线程安全的吗?
部分线程安全。load 方法线程安全,但 iterator 迭代过程中如果服务实现发生变化可能产生 ConcurrentModificationException。
Q3:Spring Factories 机制与 JDK SPI 的区别?
| 维度 | JDK SPI | Spring Factories |
|---|
| 配置文件 | META-INF/services/接口全限定名 | META-INF/spring.factories |
| 加载方式 | ServiceLoader.load | SpringFactoriesLoader.loadFactoryNames |
| 多键支持 | 不支持 | 支持(一个 key 多个 value) |
| 触发时机 | ServiceLoader.load 时 | Spring 启动时 |
六、写在最后
面试建议:
- 基础题看似简单,追问要警惕——OOP 三要素可以追问多态、泛型可以追问类型擦除、SPI 可以追问与 API 区别
- 准备"业务场景题"——比如"如何用 SPI 设计一个可插拔的日志框架"
- “为什么这样设计"比"是什么"更重要——比如"为什么 String 设计成不可变”
参考资料