CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 进阶/课时/OOP 进阶
RawBlame
课时目录12 节

Java 进阶

  1. 01Java 进阶概览
  2. 02枚举与模式
  3. 03Set/Queue/Stack
  4. 04OOP 进阶
  5. 05方法链与绑定
  6. 06Record 与不可变
  7. 07模块系统
  8. 08并发进阶
  9. 09依赖注入概念
  10. 10Java Web 框架
  11. 11Quarkus 入门
  12. 12综合练习

OOP 进阶

重载/覆盖、封装、对象生命周期。

30 分钟

OOP 进阶

这一课你将能做到

  1. 一句话区分重载与覆盖,并判断任意一个方法调用发生的是编译期绑定还是运行期绑定
  2. 用 final 字段、构造器校验、不可变视图写出「外部改不坏」的领域类
  3. 说清 equals 与 hashCode 的契约,以及抽象类与接口的选型标准

为什么要学

写 CRUD 时感觉不到 OOP,代码要活过三个月时它才开始伤人:Service 里塞满 public 字段、任何人都能把订单状态改成任意值、内部 List 引用被外层 clear() 掉——这些不是「设计美学」问题,是线上事故的源头。

面试角度:重载 vs 覆盖、equals/hashCode 契约、抽象类 vs 接口,是 Java 面试出场率最高的三连问,本课答案按能背的标准写。Spring 的 Bean 体系(第 9 课)也建立在这些概念上:单例 Bean 为什么要求无状态,就是对象生命周期知识的直接应用。

先建立图像

重载 overload:同一个类里,同名不同参,编译期拍板
    save(Lesson)              ┐
    save(Lesson, boolean)     ┘ 编译器按参数列表选一个

覆盖 override:子类重新实现父类的同名同参方法,运行期拍板
    Animal a = new Dog();
    a.speak();   ← 声明类型是 Animal,实际执行的是 Dog.speak()

对象生命周期:

new → 堆上分配 → 被引用持有(局部变量/字段/静态字段/容器)
                      ↓
             所有引用断开(出栈、置 null、容器移除)
                      ↓
             不可达 → GC 择机回收(时机由 JVM 决定,不可控)

关键直觉:GC 看的是「引用链是否可达」,不是「你觉得它没用了」。静态 Map 只进不出,就是最典型的内存泄漏。

跟着做

步骤 1:完整示例——接口、抽象类、封装、equals/hashCode

新建 OopDemo.java,完整可运行:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class OopDemo {

    /** 接口:定义「能力」,谁都能实现 */
    interface Payable {
        boolean pay(long amountFen);

        default String channelName() { return getClass().getSimpleName(); }
    }

    /** 抽象类:抽取公共骨架,模板方法固定流程 */
    static abstract class PaymentChannel implements Payable {
        protected final String merchantId;

        protected PaymentChannel(String merchantId) {
            this.merchantId = Objects.requireNonNull(merchantId);
        }

        /** 模板方法:流程固定为 final,差异留给子类 */
        public final String payAndLog(long amountFen) {
            boolean ok = pay(amountFen);
            return channelName() + (ok ? " 支付成功 " : " 支付失败 ") + amountFen + " 分";
        }
    }

    static class WechatPay extends PaymentChannel {
        WechatPay(String merchantId) { super(merchantId); }

        @Override
        public boolean pay(long amountFen) {
            return amountFen > 0; // 模拟调用微信下单接口
        }
    }

    /** 封装:对外只读,内部列表不外泄可变引用 */
    static class Order {
        private final long id;
        private final List<String> skuCodes = new ArrayList<>();

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

        void addSku(String sku) {
            if (sku == null || sku.isBlank()) throw new IllegalArgumentException("sku 非法");
            skuCodes.add(sku);
        }

        public long id() { return id; }

        /** 不可变视图:外部 clear() 会抛 UnsupportedOperationException */
        public List<String> skuCodes() { return Collections.unmodifiableList(skuCodes); }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Order other)) return false;
            return id == other.id;
        }
        @Override
        public int hashCode() { return Objects.hash(id); }
    }

    public static void main(String[] args) {
        PaymentChannel channel = new WechatPay("m-1001");
        System.out.println(channel.payAndLog(9900)); // 运行期绑定到 WechatPay.pay

        Order order = new Order(1L);
        order.addSku("SKU-1");
        System.out.println(order.skuCodes());
        try {
            order.skuCodes().clear();
        } catch (UnsupportedOperationException e) {
            System.out.println("封装生效:外部无法清空内部列表");
        }

        System.out.println(new Order(1L).equals(new Order(1L))); // true:按业务键判等
    }
}

运行:java OopDemo.java。

步骤 2:重载在编译期、覆盖在运行期

新建 OverrideDemo.java:

public class OverrideDemo {

    static class Repo {
        void save(Object entity) { System.out.println("save(Object)"); }
        void save(String entity) { System.out.println("save(String)"); } // 重载
    }

    static class Lesson {
        @Override
        public String toString() { return "Lesson{...}"; } // 覆盖
    }

    public static void main(String[] args) {
        Repo repo = new Repo();
        repo.save("lesson-1");   // save(String):编译期按字面类型选最具体的重载

        Object o = "lesson-1";
        repo.save(o);            // save(Object):编译期只看引用类型,不看实际对象

        Object lesson = new Lesson();
        System.out.println(lesson); // Lesson{...}:toString 是运行期按实际对象分派
    }
}

注意第二条输出:o 的实际对象是 String,但走的是 save(Object)——重载是编译期行为,与多态无关。

步骤 3(选做):JUnit 验证 equals 契约

把 Order 提为独立文件后,同包下写测试:

import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;

class OrderTest {

    @Test
    void sameIdShouldBeEqual() {
        assertEquals(new Order(1L), new Order(1L));
    }

    @Test
    void equalObjectsShouldOccupyOneSetSlot() {
        Set<Order> set = new HashSet<>();
        set.add(new Order(1L));
        set.add(new Order(1L));
        assertEquals(1, set.size());
    }
}

讲清楚要点

  1. 重载是编译期行为:调用哪个重载由「编译期引用/字面类型」决定,与运行期实际对象无关;参数列表必须不同,仅返回类型不同不构成重载。
  2. 覆盖是运行期行为:JVM 按实际对象类型分派(虚方法调用),这是多态的底层机制;覆盖要求方法签名一致,返回类型可以协变(父类返回 Object、子类可返回 String)。
  3. @Override 必写:它不是语法要求,而是保险丝——签名拼错时编译器立刻报错,否则就静默变成一个意外的重载。
  4. 封装三件套:private final 字段 + 构造器校验 + 对外返回不可变视图。Collections.unmodifiableList 是视图(原列表变了它跟着变),List.copyOf 是真拷贝,二者语义不同,按需选择。
  5. equals/hashCode 契约:equals 相等 ⇒ hashCode 必须相等(反之不要求);重写 equals 必须重写 hashCode,否则值相等的对象散落在不同哈希桶,HashSet 出现「重复」、HashMap 的 get 拿不到值。
  6. 抽象类 vs 接口:共享状态、构造逻辑、模板方法 → 抽象类;定义行为契约、希望无关类也能实现、需要「多继承」效果 → 接口。「接口表达 can-do,抽象类表达 is-a」仍是最可靠的判断句式。
  7. 对象存活只看引用链:局部变量出栈、字段置 null、容器 remove 都会断引用;静态集合长期持有对象是内存泄漏最常见形态,可用 WeakHashMap 或定期清理应对。

你可能会懵的地方

  • 单例 Bean 需要自己保证线程安全吗? Spring 默认 singleton,一个实例被所有请求线程共享。只要不在字段里存请求级状态(无状态),就天然线程安全;需要共享计数用 AtomicInteger 或 Redis,不要用普通成员变量。
  • 什么时候对象会被 GC? 唯一标准是「从 GC Roots 出发不可达」。GC Roots 包括栈帧局部变量、静态字段、JNI 引用等。System.gc() 只是建议,时机不可控。
  • 构造器里能调用可被覆盖的方法吗? 不能。子类构造尚未执行,其字段还是默认值,覆盖方法会读到脏数据;构造器里只调用 private 或 final 方法。
  • 为什么仅返回类型不同不构成重载? 方法签名 = 方法名 + 参数列表,返回类型不在签名内。编译器靠签名在编译期定位方法,只有返回类型不同时它无法抉择。
  • 领域对象和 Spring Bean 的生命周期有什么区别? Spring Bean 由容器创建、装配、销毁,默认单例;领域对象(Order、Lesson)由你的代码 new 出来、随引用消亡。框架托管的是「组件」,不是「业务数据」。

错法对照

错法问题正解
getter 直接返回内部 List 引用外部可 clear/add 破坏状态unmodifiableList 或 List.copyOf
重写 equals 不重写 hashCodeHashSet 出现重复、HashMap.get 失败成对重写
把 @Override 标在重载方法上编译报错@Override 只用于覆盖
构造器里调用可被覆盖的方法子类未初始化字段被读到构造器只调 private/final 方法
用 public 字段暴露状态失去修改控制权与校验点private 字段 + 访问器/行为方法

卡点急救

  1. HashSet 里对象 remove 不掉:存入后修改了参与 hashCode 的字段导致哈希值漂移。规则:放入哈希容器的不可变对象不修改;确需修改则先 remove 再改再 add。
  2. 子类「覆盖」了方法却不生效:签名没有完全一致,多半写成了重载;补上 @Override 让编译器把关。
  3. 构造器参数一多就写乱:超过 3~4 个参数改用 Builder(第 5 课)或 record(第 6 课),不要堆十几个重载构造器。

面试常问

  • 重载和覆盖的区别? 可背版:重载发生在同一个类(或继承可见)中,方法名相同、参数列表不同,编译期静态绑定,返回类型可不同;覆盖发生在子类与父类的实例方法之间,签名必须一致,运行期动态绑定,返回类型可协变,访问权限不能收窄,异常不能放大。@Override 用于编译期校验覆盖关系。
  • equals 与 hashCode 的契约是什么?为什么必须成对重写? 可背版:① equals 相等 ⇒ hashCode 必须相等;② hashCode 相等 ⇏ equals 相等(哈希冲突合法);③ equals 判等标准涉及的字段必须同时参与 hashCode 计算。HashMap 先比 hashCode 再比 equals,只重写 equals 会导致等值对象落进不同桶——HashSet 出现重复元素、get 返回 null。
  • 抽象类和接口怎么选? 需要共享字段、构造逻辑、模板方法(流程固定细节开放)→ 抽象类;只约定能力、实现类彼此无关、需要多实现 → 接口。Java 8 后接口有 default 方法,但「is-a 用抽象类、can-do 用接口」仍是首选判断;一个类可以 implements 多个接口,但只能 extends 一个抽象类。

自测三问

  1. Object o = "abc"; save(o) 与 save("abc") 调用的重载版本分别由什么决定?
  2. 为什么返回 unmodifiableList 还不够,有时要 List.copyOf?两者差异在哪?
  3. 静态 Map 缓存只进不出会引发什么问题?有哪两种应对手段?

小结一句话

重载看参数在编译期定,覆盖看对象在运行期定;封装决定谁能改你的状态,equals/hashCode 决定你在哈希世界里的身份。

延伸

  • java.lang.Object(equals/hashCode 契约)官方文档
  • 下一课:方法链与绑定
加载本节练习…
← 上一课Set/Queue/Stack下一课 →方法链与绑定