Skip to content

Java面向对象

面向对象是 Java 的核心编程思想。它通过类和对象组织代码,把数据和行为封装到一起,降低复杂系统的理解成本。

为什么需要面向对象

如果只用过程式代码,数据和操作数据的逻辑容易分散在各个方法里。项目一大,就会出现“谁都能改数据、谁也不知道规则在哪里”的问题。面向对象把状态和行为放到同一个对象里,通过方法保护状态变化,让代码更接近业务概念。

mermaid
flowchart TD
    A["过程式写法"] --> B["数据到处传"]
    B --> C["规则分散在多个方法"]
    C --> D["状态容易被非法修改"]
    E["面向对象写法"] --> F["对象封装状态和行为"]
    F --> G["通过方法表达业务规则"]
    G --> H["对象始终保持合法状态"]

三大特性

特性说明
封装隐藏内部细节,对外暴露稳定接口
继承子类复用父类字段和方法
多态同一个父类引用可以指向不同子类对象

类和对象

java
public class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void sayHello() {
        System.out.println("hello " + name);
    }
}

创建对象:

java
User user = new User("Tom");
user.sayHello();

对象创建流程

mermaid
flowchart TD
    A["new 关键字"] --> B["检查类是否已加载"]
    B --> C["为对象分配堆内存"]
    C --> D["成员变量零值初始化"]
    D --> E["执行字段显式赋值和代码块"]
    E --> F["执行构造方法"]
    F --> G["返回对象引用"]

更底层的对象内存内容可以看 HotSpot 虚拟机对象探秘

对象创建不是“调用构造方法这么简单”。构造方法执行前,对象内存已经分配,并且字段已经有默认零值。

示例:

java
class Order {
    private long id;
    private String status = "CREATED";

    public Order(long id) {
        this.id = id;
    }
}

创建 new Order(1001L) 时可以理解为:

  1. 检查 Order 类是否已加载。
  2. 在堆里分配一块对象内存。
  3. id 先是 0status 先是 null
  4. 执行字段显式赋值,status = "CREATED"
  5. 执行构造方法,id = 1001L
  6. 把对象引用返回给变量。

如果构造方法里把 this 提前传给其他线程或全局变量,对方可能看到还没构造完整的对象。这也是为什么构造期间不要让 this 逃逸。

对象在内存中怎么关联

对象变量保存的是引用,不是对象本身。多个变量可以指向同一个对象。

java
User a = new User("Tom");
User b = a;
b.setName("Jerry");
System.out.println(a.getName()); // Jerry
mermaid
flowchart TD
    A["变量 a"] --> C["堆中的 User 对象"]
    B["变量 b"] --> C
    C --> D["name = Jerry"]

这解释了为什么把对象传到方法里,方法可以修改对象字段。也解释了为什么共享可变对象在多线程或多模块之间很危险:谁拿到引用,谁就可能改状态。

封装

封装不是简单把字段改成 private,而是把对象内部状态保护起来,只允许通过有意义的方法修改。

java
import java.math.BigDecimal;

public class Account {
    private BigDecimal balance;

    public void withdraw(BigDecimal amount) {
        if (balance == null) {
            throw new IllegalStateException("账户余额未初始化");
        }
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("金额必须大于0");
        }
        if (balance.compareTo(amount) < 0) {
            throw new IllegalStateException("余额不足");
        }
        this.balance = this.balance.subtract(amount);
    }
}

这样可以避免外部代码随意修改余额,保证对象状态合法。

封装的核心是“状态只能通过规则变化”。如果只暴露 setBalance,就等于把规则交给外部调用者:

java
public class BadAccount {
    public BigDecimal balance;
}

外部可以这样写:

java
account.balance = new BigDecimal("-999999");

对象立刻进入非法状态。更好的建模方式是把业务动作放进对象:

java
import java.math.BigDecimal;

public class Account {
    private BigDecimal balance;

    public Account(BigDecimal initialBalance) {
        if (initialBalance == null || initialBalance.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("初始余额不能为负");
        }
        this.balance = initialBalance;
    }

    public void deposit(BigDecimal amount) {
        checkPositive(amount);
        this.balance = this.balance.add(amount);
    }

    public void withdraw(BigDecimal amount) {
        checkPositive(amount);
        if (this.balance.compareTo(amount) < 0) {
            throw new IllegalStateException("余额不足");
        }
        this.balance = this.balance.subtract(amount);
    }

    public BigDecimal getBalance() {
        return balance;
    }

    private void checkPositive(BigDecimal amount) {
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("金额必须大于 0");
        }
    }
}

这段代码比 getter/setter 多,但它把“余额不能为负、提款不能超额、金额必须大于 0”的规则放在对象内部。调用方不需要记住所有细节,只能通过合法方法改变状态。

继承

继承适合表达“is-a”关系。例如 Cat 是 Animal。

java
public class Animal {
    public void eat() {}
}

public class Cat extends Animal {
    public void catchMouse() {}
}

继承不要滥用。继承层级过深会让代码难理解,也会把父类变化传递给所有子类。

继承真正适合的是稳定的“is-a”关系,而不是为了复用几行代码。

错误示例:为了复用折扣字段,让所有订单都继承 DiscountOrder

java
class DiscountOrder {
    protected int discountRate;
}

class NormalOrder extends DiscountOrder {
}

问题是普通订单未必有折扣,这样会把不属于它的状态强塞进去。更好的方式是组合:

java
class DiscountPolicy {
    private int discountRate;
}

class Order {
    private DiscountPolicy discountPolicy;
}

组合比继承更灵活,因为对象可以选择是否拥有某种能力,也可以在运行期替换策略。继承一旦建立,父类变化会影响所有子类。

重写、重载和动态绑定

重载发生在编译期,重写发生在运行期多态。

java
class PayService {
    public void pay(String orderNo) {
        System.out.println("按订单号支付");
    }

    public void pay(long orderId) {
        System.out.println("按订单 ID 支付");
    }
}

这是重载:方法名相同,参数列表不同。编译器根据变量的静态类型和参数类型决定调用哪个方法。

重写示例:

java
class Animal {
    public void speak() {
        System.out.println("animal");
    }
}

class Cat extends Animal {
    @Override
    public void speak() {
        System.out.println("cat");
    }
}

public class PolymorphismDemo {
    public static void main(String[] args) {
        Animal animal = new Cat();
        animal.speak(); // cat
    }
}

执行过程:

mermaid
flowchart TD
    A["编译期看到变量类型 Animal"] --> B["确认 Animal 有 speak 方法"]
    B --> C["运行期对象真实类型是 Cat"]
    C --> D["动态绑定到 Cat.speak"]

字段没有多态,静态方法也不参与运行期动态绑定。多态主要针对实例方法。

多态

java
Animal animal = new Cat();
animal.eat();

编译时看左边类型,运行时执行右边真实对象的方法。多态是很多框架扩展点的基础。

多态的价值是让调用方依赖抽象,而不是依赖具体实现。

java
interface MessageSender {
    void send(String target, String content);
}

class SmsSender implements MessageSender {
    public void send(String target, String content) {
        System.out.println("发送短信:" + target + " " + content);
    }
}

class EmailSender implements MessageSender {
    public void send(String target, String content) {
        System.out.println("发送邮件:" + target + " " + content);
    }
}

class NoticeService {
    private final MessageSender sender;

    NoticeService(MessageSender sender) {
        this.sender = sender;
    }

    public void notifyUser(String target, String content) {
        sender.send(target, content);
    }
}

NoticeService 不关心是短信还是邮件,只依赖 MessageSender。以后增加站内信、企业微信、钉钉,只要实现同一个接口,不需要修改通知业务主流程。

接口和抽象类

对比项接口抽象类
关系表示能力表示抽象父类
多实现支持不支持,只能单继承
状态通常不保存对象状态可以保存字段
场景定义规范、扩展点提取公共逻辑

更细一点:

问题接口抽象类
表达什么能力、规范、扩展点一类对象的共同父类
是否可多继承一个类可实现多个接口一个类只能继承一个抽象类
是否适合保存状态不适合保存实例状态可以保存公共字段
JDK 8 默认方法支持 default 方法普通方法天然支持
典型例子RunnableComparator、Repository 接口模板方法基类、公共 Service 基类

选择口诀:

  1. 定义能力和协议,优先接口。
  2. 需要复用公共状态和流程,可以考虑抽象类。
  3. 如果只是复用工具方法,优先组合或工具类,不要强行继承。

抽象类模板方法 Demo

抽象类适合固定流程、开放部分步骤。

java
abstract class AbstractImportTask {
    public final void execute(String filePath) {
        checkFile(filePath);
        Object data = parse(filePath);
        validate(data);
        save(data);
    }

    private void checkFile(String filePath) {
        if (filePath == null || filePath.trim().isEmpty()) {
            throw new IllegalArgumentException("文件路径不能为空");
        }
    }

    protected abstract Object parse(String filePath);

    protected abstract void validate(Object data);

    protected abstract void save(Object data);
}

这里 execute 固定导入流程,子类只实现解析、校验、保存。final 防止子类破坏整体流程。这就是模板方法思想。

贫血模型和充血模型

很多项目里实体类只有字段和 getter/setter,所有规则都写在 Service 里,这叫贫血模型。

贫血模型示例:

java
class Order {
    private String status;

    public void setStatus(String status) {
        this.status = status;
    }
}

class OrderService {
    public void pay(Order order) {
        if (!"CREATED".equals(order.getStatus())) {
            throw new IllegalStateException("只有待支付订单才能支付");
        }
        order.setStatus("PAID");
    }
}

问题是任何地方都可以绕过 OrderService 直接 setStatus("PAID")。充血模型会把核心状态变化放到对象里:

java
enum OrderStatus {
    CREATED,
    PAID,
    CANCELLED
}

class Order {
    private OrderStatus status;

    public void pay() {
        if (this.status != OrderStatus.CREATED) {
            throw new IllegalStateException("只有待支付订单才能支付");
        }
        this.status = OrderStatus.PAID;
    }

    public void cancel() {
        if (this.status == OrderStatus.PAID) {
            throw new IllegalStateException("已支付订单不能直接取消");
        }
        this.status = OrderStatus.CANCELLED;
    }
}

不是所有项目都必须使用复杂领域模型,但核心业务规则不能完全散落在各处。至少要让关键状态变化有统一入口。

商业 Demo:订单状态流转

下面是一个更完整的状态流转 Demo,适合理解封装、多态、枚举和异常。

java
import java.math.BigDecimal;

enum OrderStatus {
    CREATED,
    PAID,
    DELIVERED,
    CANCELLED
}

public class Order {
    private final Long id;
    private final BigDecimal amount;
    private OrderStatus status;

    public Order(Long id, BigDecimal amount) {
        if (id == null) {
            throw new IllegalArgumentException("订单 ID 不能为空");
        }
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("订单金额必须大于 0");
        }
        this.id = id;
        this.amount = amount;
        this.status = OrderStatus.CREATED;
    }

    public void pay() {
        if (status != OrderStatus.CREATED) {
            throw new IllegalStateException("只有待支付订单才能支付");
        }
        this.status = OrderStatus.PAID;
    }

    public void deliver() {
        if (status != OrderStatus.PAID) {
            throw new IllegalStateException("只有已支付订单才能发货");
        }
        this.status = OrderStatus.DELIVERED;
    }

    public void cancel() {
        if (status == OrderStatus.DELIVERED) {
            throw new IllegalStateException("已发货订单不能取消");
        }
        this.status = OrderStatus.CANCELLED;
    }

    public OrderStatus getStatus() {
        return status;
    }
}

这个类的重点不是字段多少,而是对象自己保护状态:

  1. 创建订单时必须有合法 ID 和金额。
  2. 初始状态固定为 CREATED
  3. 支付、发货、取消都有状态前置条件。
  4. 外部不能直接 setStatus 绕过规则。

如果这些规则都写在外部 Service 的多个方法里,未来新增“退款、售后、部分发货”时很容易漏改。

线上问题排查

对象状态被非法修改

排查路径:

mermaid
flowchart TD
    A["对象状态异常"] --> B{"字段是否 public 或有随意 setter"}
    B -- "是" --> C["收敛修改入口,字段 private"]
    B -- "否" --> D{"状态变化是否分散在多个 Service"}
    D -- "是" --> E["封装领域方法或状态机"]
    D -- "否" --> F{"是否并发修改"}
    F -- "是" --> G["加锁、事务、乐观锁或不可变对象"]
    F -- "否" --> H["检查反射、序列化、ORM 映射"]

继承导致行为异常

常见原因:

  1. 子类重写父类方法,但父类构造方法中调用了可重写方法。
  2. 父类新增字段或逻辑,影响所有子类。
  3. 子类不满足父类语义,违反里氏替换原则。
  4. 继承只是为了复用代码,导致层级越来越深。

父类构造方法里不要调用可被子类重写的方法,因为此时子类字段可能还没初始化完成。

面试标准回答

面向对象三大特性是什么?
封装、继承、多态。封装把状态和行为放在对象内部,通过方法保护状态变化;继承表达稳定的 is-a 关系,复用父类能力;多态让父类或接口引用指向不同实现,调用时按真实对象执行方法。

封装为什么不只是 private + getter/setter?
真正的封装是让对象状态只能通过有业务含义的方法变化,并在方法里校验规则。如果只是给所有字段生成 setter,外部仍然能随意修改对象,业务规则会分散并且容易被绕过。

重载和重写区别是什么?
重载发生在同一个类中,方法名相同但参数列表不同,编译期决定调用哪个;重写发生在继承关系中,子类覆盖父类方法,运行期根据真实对象类型动态绑定。

接口和抽象类怎么选?
接口更适合定义能力、规范和扩展点,一个类可以实现多个接口;抽象类更适合提取一类对象的公共状态和固定流程。项目中通常优先接口和组合,谨慎使用继承。

为什么优先组合而不是继承?
继承会把子类和父类强绑定,父类变化会影响所有子类,层级深了难理解。组合可以按需持有某个能力对象,更灵活,也更符合“用什么组合什么”的业务建模方式。

设计建议

  1. 优先组合,谨慎继承。
  2. 字段尽量 private,通过方法表达业务行为。
  3. 接口不要过大,避免实现类被迫实现无关方法。
  4. equals 和 hashCode 要一起重写。
  5. 业务对象要保持状态合法,不要只做贫血 getter/setter。

面向对象设计和 设计模式 联系很紧密,建议结合阅读。

常见风险

风险后果建议
只写 getter/setter对象没有行为,业务规则散落在服务层把核心状态变化封装成方法
继承层级过深父类变化影响大量子类优先组合和接口
public 字段随意改对象状态不受控制字段私有化,通过方法校验
equals/hashCode 只重写一个集合去重、Map 查找异常两者一起重写