Java面向对象设计常见误区包括:把类当容器、继承当复用、方法当过程;应坚持封装、优先接口与组合、构造函数确保不可变性、避免过度抽象。

Java面向对象设计最常见的误区,不是语法写错,而是把“类”当容器、“继承”当复用、“方法”当过程——结果代码越来越难改、测试越来越难写、协作越来越痛苦。
把类当成数据结构或工具包来用
很多新手看到一个业务实体(比如 Order),第一反应是定义一堆 public 字段 + 一堆静态工具方法,美其名曰“简洁”。这直接破坏封装,也让后续加校验、审计、序列化逻辑无从下手。
典型表现:
-
Order类里全是public String orderId;,外部直接读写 - 所有跟订单相关的逻辑都塞进
OrderUtils静态类,和Order毫无关系 - 字段可变但没 setter 校验,导致
order.setAmount(-100)居然能通过编译并运行
正确做法:
立即学习“Java免费学习笔记(深入)”;
- 字段默认
private,提供有约束的setAmount(BigDecimal amount) - 把“计算应付金额”“生成订单号”等行为作为
Order自身的方法,而非丢给工具类 - 如果真需要工具逻辑(如跨多个领域对象的批量处理),应明确命名、限定作用域,不滥用
static
滥用继承代替组合或接口实现
一看到“猫是动物”“狗是动物”,就急着建 Animal 父类,再让子类重写 makeSound()。问题在于:一旦出现“机器人狗”(会叫但不是生物)、“电子猫”(有 UI 但不会抓老鼠),继承树立刻崩塌。
更隐蔽的问题是:为了共享字段或方法,在无关类之间硬拉出一个“父类”,比如让 Report 和 Notification 都继承 BaseMessage,只因为它们都有 title 和 sendTime —— 这是典型的“继承泄露”。
建议优先考虑:
- 用接口定义能力(
interface Soundable、interface Sendable) - 用组合委托行为(
class RobotDog { private final Speaker speaker; }) - 只有当子类**真正属于父类的一种**,且父类能完整描述其不变契约时,才用继承
忽略构造函数的语义与不可变性
新手常把构造函数当成“初始化字段的普通方法”,随便加一堆可选参数、允许传 null、甚至在构造中调用可被重写的方法——这会导致对象创建失败、状态不一致、子类构造出错。
常见错误示例:
public class User {
private String name;
public User(String name) {
this.name = name;
init(); // ❌ 构造中调用非 final 方法,子类重写后可能访问未初始化字段
}
protected void init() { /* ... */ }
}安全做法:
- 构造函数只做必要赋值,不做复杂逻辑或外部调用
- 字段尽量设为
final,配合全参构造或 Builder 模式保证构建后不可变 - 拒绝
null参数,用Objects.requireNonNull(name, "name must not be null")明确契约 - 避免在构造中启动线程、打开文件、发 HTTP 请求等副作用操作
过度设计抽象层,却忘了真实需求
刚学完策略模式,就给所有 if-else 套一层 StrategyFactory;刚了解模板方法,就把三个相似方法强行抽出一个抽象基类,哪怕它们未来根本不会扩展。
这类设计看似“规范”,实则带来三重成本:
- 新增一个简单分支要改 4 个类(接口 + 实现 + 工厂 + 配置)
- 新人读代码时要在抽象层和具体实现间反复跳转,反而看不清主干逻辑
- 单元测试要 mock 接口、注入实现,而原本一个私有方法加个测试就够了
判断是否需要抽象的朴素标准:
- 当前已有至少两个不同实现,并且它们确实需要被统一调度?
- 这个变化点在未来 3 个月内大概率会增加新分支?
- 去掉抽象层后,重复代码是否真的难以维护(而非只是“看起来不够优雅”)?
大多数时候,先写具体实现,等第二个相似场景出现时再提炼,比一开始就画好类图靠谱得多。










