Wrayの知识库 Wrayの知识库
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
  • Java章节编写规范
  • Java基础

    • Java概述
    • Java语法
    • 面向对象编程
      • 1. 类与对象
      • 2. 面向对象建模
      • 3. 封装
      • 4. 继承
        • 4.1 构造器调用顺序
        • 4.2 继承的风险
      • 5. 多态
        • 5.1 编译期类型与运行期类型
        • 5.2 字段没有多态
      • 6. 方法重载与重写
        • 6.1 方法重载
        • 6.2 方法重写
      • 7. 抽象类
      • 8. 接口
      • 9. 访问修饰符
      • 10. this、super、static、final
        • 10.1 this
        • 10.2 super
        • 10.3 static
        • 10.4 final
      • 11. 内部类
      • 12. 常见设计误区
        • 12.1 把继承当复用工具
        • 12.2 在构造器中调用可重写方法
        • 12.3 滥用 static
        • 12.4 对象只有 getter 和 setter
      • Tips 快问快答
    • Java数组
    • String字符串
    • 异常处理
    • 基本类型与包装类
    • Object类与常用方法
    • 泛型
    • 枚举与注解
    • 反射
    • 值传递与引用
    • Lambda表达式与函数式接口
    • Stream API
    • Optional与空值处理
    • 日期时间API
    • Java现代语法特性
  • Java集合

  • Java并发

  • Java IO

  • JVM

目录

面向对象编程

面向对象编程,简称 OOP,是 Java 组织复杂业务代码的核心方式。它不是简单地把函数塞进类里,而是用“对象”承载业务状态,用“方法”约束状态变化,用“类型体系”表达业务关系。

如果只记住封装、继承、多态三个词,很容易写出看似面向对象、实际难以维护的代码。真正需要掌握的是:

  • 如何从业务概念抽象出类。
  • 对象在内存中如何存在。
  • 继承和组合应该如何取舍。
  • 多态为什么能让代码面向扩展。
  • static、final、this、super 等关键字如何影响对象行为。
  • 抽象类、接口、内部类分别适合解决什么问题。

# 1. 类与对象

类是对象的模板,定义一类对象共同拥有的属性和行为。对象是类在运行时创建出来的具体实例。

public class User {
    private Long id;
    private String name;

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

    public String displayName() {
        return id + ":" + name;
    }
}

创建对象:

User user = new User(1L, "Tom");

对象关系可以理解为:

栈帧局部变量表
┌──────────────┐
│ user 引用     │
└──────┬───────┘
       │ 指向
       ▼
堆内存
┌──────────────────────┐
│ User 对象             │
│ id   = 1              │
│ name = "Tom"          │
└──────────┬───────────┘
           │ 类型信息
           ▼
方法区 / 元空间
┌──────────────────────┐
│ User.class 元数据     │
│ 字段、方法、构造器信息 │
└──────────────────────┘

注意:变量 user 本身不是对象,它保存的是对象引用。对象真正位于堆中。

# 2. 面向对象建模

面向对象建模的重点是识别“谁拥有数据、谁负责行为”。

例如订单系统中常见对象:

业务概念 可能的类 核心职责
订单 Order 保存订单状态、计算金额、判断能否取消
订单项 OrderItem 保存商品、数量、单价
用户 User 保存用户身份和基础信息
支付单 Payment 表达支付状态和支付流水
库存服务 InventoryService 扣减、回滚库存

一个常见原则是:数据和与数据强相关的行为应尽量放在一起。

不推荐的贫血写法:

public class Order {
    public BigDecimal amount;
    public String status;
}

public class OrderService {
    public boolean canCancel(Order order) {
        return "CREATED".equals(order.status);
    }
}

更好的写法:

public class Order {
    private BigDecimal amount;
    private OrderStatus status;

    public boolean canCancel() {
        return status == OrderStatus.CREATED;
    }

    public void cancel() {
        if (!canCancel()) {
            throw new IllegalStateException("当前订单状态不能取消");
        }
        this.status = OrderStatus.CANCELED;
    }
}

这样做的好处是业务规则不容易散落到各个 Service 里,对象自身就能维护状态合法性。

# 3. 封装

封装不是简单地把字段改成 private,而是隐藏内部实现,并通过方法维持对象不变量。

public class Account {
    private BigDecimal balance;

    public Account(BigDecimal openingBalance) {
        if (openingBalance.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("开户金额不能为负数");
        }
        this.balance = openingBalance;
    }

    public void withdraw(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("提现金额必须大于 0");
        }
        if (balance.compareTo(amount) < 0) {
            throw new IllegalStateException("余额不足");
        }
        this.balance = this.balance.subtract(amount);
    }

    public BigDecimal getBalance() {
        return balance;
    }
}

封装带来的收益:

收益 说明
控制状态变化 不让外部代码随意修改字段
保持业务不变量 例如余额不能为负数
降低耦合 外部只依赖公开方法,不依赖内部字段结构
方便演进 内部实现可以调整,调用方不一定需要修改

封装设计时要避免两种极端:

  • 所有字段都暴露 public,对象变成数据袋。
  • 无脑生成所有 setter,让任何状态都能被外部任意改写。

# 4. 继承

继承用于表达“子类是父类的一种”。Java 使用 extends 表示类继承。

public class Animal {
    public void eat() {
        System.out.println("eat");
    }
}

public class Dog extends Animal {
    public void bark() {
        System.out.println("bark");
    }
}

继承关系:

Object
  ▲
  │
Animal
  ▲
  │
Dog

Java 类只支持单继承:

class Dog extends Animal {
}

不能这样写:

class Dog extends Animal, Pet {
}

单继承让对象布局和方法查找更简单,也避免多继承中常见的菱形继承问题。需要多种能力组合时,应优先使用接口或组合。

# 4.1 构造器调用顺序

创建子类对象时,一定会先初始化父类部分,再初始化子类部分。

class Parent {
    Parent() {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    Child() {
        System.out.println("Child");
    }
}

执行:

new Child();

输出:

Parent
Child

初始化顺序可以简化理解为:

分配对象内存
  │
  ▼
初始化 Object
  │
  ▼
初始化父类字段和构造器
  │
  ▼
初始化子类字段和构造器
  │
  ▼
返回对象引用

# 4.2 继承的风险

继承会把父类实现细节暴露给子类。父类改动可能影响子类,因此不要为了复用几行代码就使用继承。

判断是否适合继承,可以问三个问题:

问题 如果答案是否定的
子类是否真的是父类的一种 不要继承
子类是否能遵守父类行为约定 不要继承
父类是否专门为继承设计 谨慎继承

很多业务系统中,组合比继承更稳:

public class OrderService {
    private final PriceCalculator priceCalculator;

    public OrderService(PriceCalculator priceCalculator) {
        this.priceCalculator = priceCalculator;
    }
}

组合关系表达“拥有一个能力”,继承关系表达“本身就是一种”。

# 5. 多态

多态指同一个父类型引用,在运行时可以指向不同子类对象,并调用不同实现。

interface PayChannel {
    void pay(BigDecimal amount);
}

class WeChatPay implements PayChannel {
    public void pay(BigDecimal amount) {
        System.out.println("微信支付:" + amount);
    }
}

class AliPay implements PayChannel {
    public void pay(BigDecimal amount) {
        System.out.println("支付宝支付:" + amount);
    }
}

public class PaymentService {
    public void pay(PayChannel channel, BigDecimal amount) {
        channel.pay(amount);
    }
}

调用链路:

PaymentService.pay(PayChannel channel)
           │
           │ 编译期只知道 channel 是 PayChannel
           ▼
运行时真实对象
   ┌──────────────┬──────────────┐
   │ WeChatPay    │ AliPay       │
   └──────┬───────┴──────┬───────┘
          │              │
          ▼              ▼
   调用微信支付实现   调用支付宝实现

多态的核心价值是让上层代码依赖抽象,而不是依赖具体实现。

# 5.1 编译期类型与运行期类型

Animal animal = new Dog();

这里有两个类型:

类型 值
编译期类型 Animal
运行期类型 Dog

编译期类型决定能调用哪些方法,运行期类型决定重写方法的实际执行版本。

animal.eat();  // 可以,Animal 定义了 eat
animal.bark(); // 编译失败,Animal 没有 bark

如果需要调用子类特有方法,必须向下转型:

if (animal instanceof Dog dog) {
    dog.bark();
}

向下转型前应先判断类型,否则可能抛出 ClassCastException。

# 5.2 字段没有多态

方法重写具有多态,字段访问没有多态。

class Parent {
    String name = "parent";
}

class Child extends Parent {
    String name = "child";
}

Parent p = new Child();
System.out.println(p.name); // parent

字段访问取决于引用的编译期类型,方法调用才会动态分派。

# 6. 方法重载与重写

# 6.1 方法重载

重载发生在同一个类中,方法名相同,参数列表不同。

public class Printer {
    public void print(String value) {
    }

    public void print(int value) {
    }

    public void print(String value, int count) {
    }
}

重载判断依据:

能否区分重载 示例
参数个数不同 print(String) 与 print(String, int)
参数类型不同 print(String) 与 print(int)
参数顺序不同 print(String, int) 与 print(int, String)

返回值不同不能构成重载:

int value() { return 1; }
long value() { return 1L; } // 编译失败

# 6.2 方法重写

重写发生在父子类之间,子类重新实现父类方法。

class Animal {
    public void sound() {
        System.out.println("some sound");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("bark");
    }
}

重写规则:

规则 说明
方法名相同 必须与父类方法一致
参数列表相同 参数类型、数量、顺序一致
返回值兼容 可以是父类返回值的子类型
访问权限不能更严格 public 不能重写成 protected
异常不能更宽 不能抛出比父类更宽的受检异常
private 方法不能重写 子类只是定义了同名新方法
static 方法不能重写 只能隐藏
final 方法不能重写 编译失败

建议重写时始终加 @Override,让编译器帮你检查。

# 7. 抽象类

抽象类使用 abstract 修饰,不能直接实例化。

public abstract class AbstractMessageSender {
    public void send(String message) {
        check(message);
        doSend(message);
    }

    private void check(String message) {
        if (message == null || message.isBlank()) {
            throw new IllegalArgumentException("message 不能为空");
        }
    }

    protected abstract void doSend(String message);
}

抽象类适合表达“有共同状态或共同流程,但部分步骤由子类决定”的场景。

模板方法结构:

抽象父类定义流程
┌──────────────────────────┐
│ send                     │
│  1. check                │
│  2. doSend 抽象步骤       │
│  3. record               │
└────────────┬─────────────┘
             │
             ▼
子类提供差异实现
┌──────────────┬──────────────┐
│ SmsSender    │ EmailSender  │
└──────────────┴──────────────┘

抽象类可以包含:

  • 成员变量。
  • 构造器。
  • 普通方法。
  • 抽象方法。
  • 静态方法。

抽象方法只能出现在抽象类或接口中。

# 8. 接口

接口用于定义能力或协议。

public interface Repository<T> {
    T findById(Long id);

    void save(T entity);
}

接口适合表达“某个对象具备某种能力”。例如:

接口 表达能力
Runnable 可以被线程执行
Comparable 可以比较大小
Serializable 可以序列化
AutoCloseable 可以自动关闭资源

Java 8 之后,接口可以包含默认方法和静态方法:

public interface Named {
    String name();

    default String displayName() {
        return "[" + name() + "]";
    }

    static boolean isValid(String name) {
        return name != null && !name.isBlank();
    }
}

接口与抽象类对比:

对比项 接口 抽象类
关系含义 具备某种能力 是某类对象
多继承 一个类可实现多个接口 一个类只能继承一个抽象类
状态 不适合保存实例状态 可以保存实例状态
构造器 没有构造器 有构造器
使用场景 能力抽象、SPI、策略 模板方法、共享基础实现

一般经验:

  • 优先面向接口编程。
  • 需要共享状态和流程时再考虑抽象类。
  • 接口默认方法适合做兼容演进,不适合塞入大量业务逻辑。

# 9. 访问修饰符

Java 有四种访问级别:

修饰符 当前类 同包 子类不同包 任意位置
private 可以 不可以 不可以 不可以
默认 可以 可以 不可以 不可以
protected 可以 可以 可以 不可以
public 可以 可以 可以 可以

设计建议:

  • 字段优先 private。
  • 构造器根据创建方式选择 public、private 或包级访问。
  • 工具类可使用 private 构造器避免实例化。
  • 业务 API 不要随意 public,暴露出去就会成为兼容性负担。

# 10. this、super、static、final

# 10.1 this

this 表示当前对象。

常见用途:

public class User {
    private String name;

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

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

this 可以区分成员变量和局部变量,也可以在链式调用中返回当前对象。

# 10.2 super

super 表示父类部分。

class Child extends Parent {
    Child(String name) {
        super(name);
    }

    @Override
    public void run() {
        super.run();
        System.out.println("child run");
    }
}

super() 必须作为构造器第一行语句。

# 10.3 static

static 表示属于类,而不是属于某个对象。

public class IdGenerator {
    private static long sequence = 0;

    public static long nextId() {
        return ++sequence;
    }
}

static 常用于:

  • 工具方法。
  • 常量。
  • 类级共享状态。
  • 静态工厂方法。

慎用可变静态字段,因为它是全局共享状态,容易带来并发和测试隔离问题。

# 10.4 final

final 可以修饰变量、方法和类:

位置 含义
final 变量 只能赋值一次
final 方法 不能被子类重写
final 类 不能被继承
public final class Money {
    private final BigDecimal amount;

    public Money(BigDecimal amount) {
        this.amount = amount;
    }
}

final 引用不能重新指向其他对象,但对象内部状态未必不可变:

final List<String> names = new ArrayList<>();
names.add("Tom");      // 可以
names = new ArrayList<>(); // 编译失败

# 11. 内部类

Java 支持在类内部定义类。

类型 定义位置 特点
成员内部类 类成员位置 依赖外部类实例
静态内部类 static 修饰 不依赖外部类实例
局部内部类 方法内部 作用域仅在方法内
匿名内部类 表达式中 常用于一次性实现接口

成员内部类:

public class Outer {
    private String name = "outer";

    class Inner {
        String value() {
            return name;
        }
    }
}

静态内部类:

public class Response {
    private String code;

    public static class Builder {
        public Response build() {
            return new Response();
        }
    }
}

匿名内部类:

Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("run");
    }
};

现代 Java 中,函数式接口场景通常优先使用 Lambda:

Runnable task = () -> System.out.println("run");

# 12. 常见设计误区

# 12.1 把继承当复用工具

如果只是为了复用代码,优先考虑组合。继承会引入父子类强耦合。

# 12.2 在构造器中调用可重写方法

class Parent {
    Parent() {
        init();
    }

    void init() {
    }
}

class Child extends Parent {
    private String name = "child";

    @Override
    void init() {
        System.out.println(name.length());
    }
}

父类构造器执行时,子类字段可能还没初始化完,调用子类重写方法容易产生隐藏 bug。

# 12.3 滥用 static

把业务状态放进静态字段,会导致状态全局共享,测试互相污染,并发场景难以控制。

# 12.4 对象只有 getter 和 setter

如果一个类只有字段访问方法,没有业务行为,它可能只是数据结构,不是真正的领域对象。并不是所有类都必须富有行为,但核心业务对象应尽量承载规则。

# Tips 快问快答

Q:Java 是纯面向对象语言吗? A:严格来说不是。Java 有 8 种基本类型,它们不是对象。但 Java 的主体编程模型是面向对象。

Q:接口和抽象类怎么选? A:表达能力或协议时选接口;需要共享状态、构造过程、模板流程时选抽象类。

Q:为什么推荐面向接口编程? A:接口能隔离调用方和实现方,让实现可以替换,例如测试时替换为 Mock,生产中替换为不同策略。

Q:多态的本质是什么? A:编译期看引用类型,运行期根据真实对象类型进行方法分派。

Q:重载是多态吗? A:重载通常被称为编译期多态,编译器根据参数列表决定调用哪个方法;重写是运行期多态。

Q:为什么字段没有多态? A:字段访问在编译期根据引用类型确定,不参与运行期动态分派。

Q:private 方法能重写吗? A:不能。父类 private 方法对子类不可见,子类同名方法只是一个新方法。

Q:static 方法能重写吗? A:不能。static 方法属于类,只能被隐藏,调用哪个版本取决于引用的编译期类型。

Q:final 修饰引用变量后对象就不可变了吗? A:不是。final 只限制引用不能重新赋值,不限制引用指向对象的内部状态变化。

Q:为什么不建议在构造器里调用可重写方法? A:因为子类对象尚未初始化完成,重写方法可能访问到未初始化字段,产生难排查的问题。

上次更新: 2026/06/24, 16:28:28
Java语法
Java数组

← Java语法 Java数组→

Copyright © 2023-2026 Wray | 鄂ICP备2024050235号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式