设计原则面试高频题+Java代码实践
设计原则面试高频题+Java代码实践(吃透面试+落地开发)
结合面试高频场景,我会先整理核心面试题+标准答案(覆盖90%面试考点),再用Java代码逐一演示七大设计原则的落地用法,兼顾“面试答题”和“实际开发”双重需求。
一、设计原则高频面试题+标准答案
基础必问(80%面试官会问)
1. 请说说SOLID五大设计原则分别是什么,各自的核心思想?
答案:
SOLID是面向对象设计的核心原则,拆解为5个原则:
- 单一职责(SRP):一个类只负责一个职责,变更原因唯一,核心是“高内聚”,避免一个类承担过多功能导致修改牵一发而动全身;
- 开闭原则(OCP):对扩展开放、对修改关闭,核心是依赖抽象而非具体实现,新增功能通过扩展子类/实现类完成,不修改原有稳定代码;
- 里氏替换(LSP):子类可无缝替换父类且程序行为不变,核心是保证继承的合理性,子类不能破坏父类的约定(如不能重写父类非抽象方法、不能缩小方法的访问权限);
- 接口隔离(ISP):客户端不依赖不需要的接口,核心是将臃肿接口拆分为细粒度接口,避免接口变更影响无关客户端;
- 依赖倒置(DIP):高层模块不依赖低层模块,二者都依赖抽象;抽象不依赖细节,细节依赖抽象,核心是“面向接口编程”,解除模块间的强耦合。
2. 开闭原则是设计原则的核心目标,如何在代码中落地开闭原则?
答案:
开闭原则的核心是“抽象化”,落地关键有3点:
- 定义抽象层(接口/抽象类):将稳定的业务逻辑抽象为接口,封装不变的行为;
- 具体实现类扩展抽象层:新增功能时,编写新的实现类继承/实现抽象层,不修改原有实现;
- 依赖注入解耦:高层模块通过抽象层调用功能,而非直接依赖具体实现类。
举例:支付场景中,定义PayService接口,原有AlipayPayService实现支付宝支付,新增微信支付时,只需新增WechatPayService实现PayService,无需修改原有支付逻辑。
3. 里氏替换原则的核心要求是什么?举一个违反里氏替换的例子。
答案:
核心要求:所有引用父类的地方,替换为子类后程序的行为和逻辑不变,具体约束:
- 子类不能重写父类的非抽象方法;
- 子类不能扩大方法的前置条件(如父类方法参数是
Object,子类不能改为String); - 子类不能缩小方法的后置条件(如父类返回
List,子类不能返回ArrayList并限制元素类型); - 子类不能抛出父类未声明的异常(运行时异常除外)。
反例:
// 父类:鸟类定义飞的行为
class Bird {
public void fly() {
System.out.println("鸟类飞行");
}
}
// 子类:鸵鸟,违反里氏替换
class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("鸵鸟不会飞");
}
}
// 调用方:依赖父类,但替换子类后报错
public class Test {
public static void letBirdFly(Bird bird) {
bird.fly();
}
public static void main(String[] args) {
letBirdFly(new Ostrich()); // 抛出异常,违反里氏替换
}
}
修正方案:拆分抽象层,定义Flyable接口,只有会飞的鸟实现该接口,鸵鸟不实现。
4. 组合/聚合复用原则(CRP)和继承的区别?为什么优先组合?
答案:
| 维度 | 继承(is-a) | 组合/聚合(has-a) |
|---|---|---|
| 耦合程度 | 强耦合(父类变子类必变) | 弱耦合(只需调整依赖) |
| 复用灵活性 | 静态复用(编译期确定) | 动态复用(运行时可替换) |
| 代码冗余 | 易产生冗余(继承无关方法) | 无冗余(按需依赖) |
优先组合的原因:
- 避免继承的“侵入性”:子类会继承父类所有公开方法,即使不需要;
- 降低耦合:组合可在运行时替换依赖的对象,继承一旦确定无法修改;
- 符合开闭原则:新增功能只需替换组合的对象,无需修改原有类。
进阶扩展(面试加分题)
5. 迪米特法则(最少知识原则)如何落地?举代码例子说明。
答案:
迪米特法则核心:对象只和直接关联的对象通信,不访问“陌生人”的内部属性/方法,落地要点:
- 隐藏内部细节:通过公共方法封装内部依赖,不暴露成员变量;
- 减少间接调用:避免
A.getB().getC().doSomething()的链式调用; - 降低依赖范围:只依赖必要的对象,不依赖无关对象。
反例(违反迪米特):
// 订单类需要获取用户地址,直接访问用户的地址对象(间接依赖)
class User {
private Address address;
public Address getAddress() { return address; }
}
class Address {
private String detail;
public String getDetail() { return detail; }
}
class Order {
private User user;
// 违反:Order依赖了User的内部对象Address(陌生人)
public String getUserAddress() {
return user.getAddress().getDetail();
}
}
正例(符合迪米特):
class User {
private Address address;
// 封装内部细节,对外提供直接方法
public String getAddressDetail() {
return address.getDetail();
}
}
class Address {
private String detail;
public String getDetail() { return detail; }
}
class Order {
private User user;
// 只和直接朋友User通信,不接触Address
public String getUserAddress() {
return user.getAddressDetail();
}
}
6. 设计原则之间的关联是什么?实际开发中如何取舍?
答案:
- 关联关系:开闭原则是最终目标,单一职责、接口隔离、依赖倒置是实现开闭原则的手段;里氏替换是继承的约束(保证抽象层可用);迪米特法则是降低耦合的补充;组合复用是减少继承耦合的具体方式。
- 取舍原则:
- 拒绝过度设计:小项目/简单场景无需严格遵循所有原则(如一个工具类无需拆分为多个接口);
- 核心优先:优先保证“单一职责+开闭原则+依赖倒置”,这三个是降低维护成本的核心;
- 平衡复杂度:抽象层不宜过多(如一个功能拆多层接口会增加理解成本);
- 结合场景:高频变更的模块严格遵循原则,稳定模块可简化实现。
二、七大设计原则Java代码实践(落地版)
1. 单一职责原则(SRP)
场景:用户模块拆分,避免一个类既处理用户CRUD,又处理用户权限验证。
// 职责1:用户基础信息管理(单一职责)
class UserManager {
public void addUser(String username) {
System.out.println("新增用户:" + username);
}
public void deleteUser(String username) {
System.out.println("删除用户:" + username);
}
}
// 职责2:用户权限验证(单一职责)
class UserAuth {
public boolean checkPermission(String username) {
System.out.println("验证" + username + "的权限");
return true;
}
}
// 调用示例
public class SRPTest {
public static void main(String[] args) {
UserManager userManager = new UserManager();
UserAuth userAuth = new UserAuth();
userManager.addUser("张三");
userAuth.checkPermission("张三");
}
}
2. 开闭原则(OCP)
场景:支付系统扩展,新增微信支付无需修改原有支付宝代码。
// 抽象层:支付接口(稳定)
interface PayService {
void pay(double amount);
}
// 具体实现:支付宝支付(已上线,无需修改)
class AlipayPayService implements PayService {
@Override
public void pay(double amount) {
System.out.println("支付宝支付:" + amount + "元");
}
}
// 扩展实现:微信支付(新增,不修改原有代码)
class WechatPayService implements PayService {
@Override
public void pay(double amount) {
System.out.println("微信支付:" + amount + "元");
}
}
// 调用方:依赖抽象,支持扩展
public class OCPTest {
public static void main(String[] args) {
PayService alipay = new AlipayPayService();
PayService wechat = new WechatPayService();
alipay.pay(100); // 原有功能
wechat.pay(200); // 新增功能
}
}
3. 里氏替换原则(LSP)
场景:图形计算,子类矩形替换父类图形后逻辑不变。
// 父类:图形(定义通用行为)
abstract class Shape {
public abstract double getArea();
}
// 子类:圆形(符合里氏替换)
class Circle extends Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
// 子类:矩形(符合里氏替换)
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
}
// 调用方:依赖父类,替换子类后行为不变
public class LSPTest {
public static void printArea(Shape shape) {
System.out.println("图形面积:" + shape.getArea());
}
public static void main(String[] args) {
printArea(new Circle(2)); // 替换为圆形,正常计算
printArea(new Rectangle(3,4)); // 替换为矩形,正常计算
}
}
4. 接口隔离原则(ISP)
场景:动物行为接口拆分,避免无关方法强制实现。
// 细粒度接口:只包含单一行为
interface EatAble {
void eat();
}
interface FlyAble {
void fly();
}
interface SwimAble {
void swim();
}
// 鸟类:实现吃+飞(只依赖需要的接口)
class Bird implements EatAble, FlyAble {
@Override
public void eat() { System.out.println("鸟吃虫子"); }
@Override
public void fly() { System.out.println("鸟飞行"); }
}
// 鱼类:实现吃+游(只依赖需要的接口)
class Fish implements EatAble, SwimAble {
@Override
public void eat() { System.out.println("鱼吃虾米"); }
@Override
public void swim() { System.out.println("鱼游泳"); }
}
// 调用示例
public class ISPTest {
public static void main(String[] args) {
Bird bird = new Bird();
Fish fish = new Fish();
bird.eat();
bird.fly();
fish.eat();
fish.swim();
}
}
5. 依赖倒置原则(DIP)
场景:订单服务依赖数据层抽象,切换数据库无需修改业务代码。
// 抽象层:数据访问接口(高层/低层都依赖)
interface DataSource {
void save(String data);
}
// 低层实现:MySQL数据源
class MysqlDataSource implements DataSource {
@Override
public void save(String data) {
System.out.println("保存数据到MySQL:" + data);
}
}
// 低层实现:Oracle数据源(扩展)
class OracleDataSource implements DataSource {
@Override
public void save(String data) {
System.out.println("保存数据到Oracle:" + data);
}
}
// 高层模块:订单服务(依赖抽象,不依赖具体实现)
class OrderService {
private DataSource dataSource;
// 构造器注入,解耦具体数据源
public OrderService(DataSource dataSource) {
this.dataSource = dataSource;
}
public void saveOrder(String orderId) {
dataSource.save("订单ID:" + orderId);
}
}
// 调用示例:切换数据源只需替换实现类
public class DIPTest {
public static void main(String[] args) {
// 切换为MySQL
OrderService orderService1 = new OrderService(new MysqlDataSource());
orderService1.saveOrder("001");
// 切换为Oracle(无需修改OrderService)
OrderService orderService2 = new OrderService(new OracleDataSource());
orderService2.saveOrder("002");
}
}
6. 迪米特法则(LOD)
代码示例见上文面试题5,核心是“隐藏内部细节,只和直接朋友通信”。
7. 合成复用原则(CRP)
场景:汽车功能复用,用组合替代继承,避免继承带来的耦合。
// 被复用的功能类:发动机
class Engine {
public void start() {
System.out.println("发动机启动");
}
}
// 被复用的功能类:车轮
class Wheel {
public void rotate() {
System.out.println("车轮转动");
}
}
// 组合复用:汽车依赖发动机和车轮(而非继承)
class Car {
// 组合:持有依赖对象
private Engine engine = new Engine();
private Wheel wheel = new Wheel();
public void run() {
engine.start(); // 复用发动机功能
wheel.rotate(); // 复用车轮功能
System.out.println("汽车行驶");
}
}
// 扩展:公交车复用汽车功能(继续组合)
class Bus {
private Car car = new Car();
public void run() {
car.run(); // 复用汽车的run方法
System.out.println("公交车载客行驶");
}
}
// 调用示例
public class CRPTest {
public static void main(String[] args) {
Bus bus = new Bus();
bus.run();
}
}
三、总结
核心要点回顾
- SOLID是核心:单一职责(高内聚)、开闭原则(易扩展)、里氏替换(继承合理)、接口隔离(低耦合)、依赖倒置(面向接口)是面向对象设计的基石;
- 复用优先组合:组合/聚合比继承更灵活,能降低耦合,符合开闭原则;
- 原则是指导而非教条:实际开发中需平衡“设计合理性”和“实现复杂度”,小场景简化、高频变更场景严格遵循。
面试加分技巧
- 回答设计原则问题时,先讲定义,再讲核心思想,最后举代码例子(面试官最看重落地能力);
- 强调“设计原则的目标是降低变更成本”,而非“为了遵循原则而遵循”;
- 结合项目经验:比如“我在XX项目中,用依赖倒置原则重构了支付模块,新增支付方式时只需加实现类,无需修改原有业务代码”。
如果需要针对某一个原则的更复杂场景(如Spring框架中设计原则的应用)做深入讲解,我可以补充对应的代码和分析。
百流积聚,江河是也;文若化风,可以砾石。

浙公网安备 33010602011771号