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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 语言基础/课时/继承接口与多态
RawBlame
课时目录14 节

Java 语言基础

  1. 01Java 与 JVM 是什么
  2. 02基本语法与 main
  3. 03基本类型与引用类型
  4. 04条件与循环
  5. 05方法与参数
  6. 06数组
  7. 07类与封装
  8. 08继承接口与多态
  9. 09集合框架入门
  10. 10字符串
  11. 11异常处理
  12. 12文件 IO 入门
  13. 13Maven 与项目结构
  14. 14综合练习与 dsa-java 分工

继承接口与多态

extends、implements、@Override。

40 分钟

继承、接口与多态

这一课你将能做到

  1. 用 extends 实现继承并正确调用 super,用 @Override 重写方法而不踩「重载冒充重写」的坑
  2. 解释多态的发生机制(父类引用指向子类对象 + 动态绑定),并手写验证
  3. 写出「接口 + 实现类」结构——这正是本站后端 UserService 接口 + UserServiceImpl 实现的标准形态,也是 Spring 依赖注入工作的根基

为什么要学

上一课你把数据和逻辑装进了类。但当系统变大,问题来了:普通用户和管理员都是用户,逻辑重复怎么消除?支付有微信、支付宝多种实现,调用方怎么做到「不关心具体是谁」?

答案就是继承(代码复用)与接口(行为约定)+ 多态(同一调用、不同实现)。本站后端的分层架构完全建立在这套机制上:

UserController ──调用──▶ UserService(接口) ◀──实现── UserServiceImpl
                                                     │
                                          换实现只改一行 @Service 注解,
                                          Controller 一行都不用动——这就是多态的价值

不懂多态,你永远只能「抄懂」Spring 代码而无法「读懂」它。

先建立图像

            Animal(抽象父类)          ──「是什么」的复用
           /        \
       Dog(extends)  Cat(extends)   ──继承共性、重写个性
            \        /
       接口 Pet(implements)          ──「能做什么」的约定

Animal a = new Dog();   ← 父类引用指向子类对象
a.speak();              ← 运行时执行的是 Dog 的 speak(动态绑定)

一句话区分两根支柱:extends 回答「我是谁」(is-a),implements 回答「我会什么」(can-do)。

跟着做

步骤 1:继承与方法重写

class Animal {
    protected String name;           // protected:子类可见,包外不可见

    public Animal(String name) {
        this.name = name;
        System.out.println("Animal 构造器执行");
    }

    public void speak() {
        System.out.println(name + " 发出声音");
    }

    public void sleep() {
        System.out.println(name + " 在睡觉");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);                  // 必须第一行:先把父类部分造好
        System.out.println("Dog 构造器执行");
    }

    @Override                         // 强制编译器校验「真的重写了吗」
    public void speak() {
        System.out.println(name + " 汪汪汪");
    }

    // 子类新增能力
    public void fetch() {
        System.out.println(name + " 衔回飞盘");
    }
}

public class InheritDemo {
    public static void main(String[] args) {
        Dog d = new Dog("旺财");
        d.speak();    // 重写后的版本:旺财 汪汪汪
        d.sleep();    // 原样继承父类:旺财 在睡觉
        d.fetch();    // 子类独有
    }
}

输出顺序体现构造链:先父后子。

步骤 2:多态实证

class Cat extends Animal {
    public Cat(String name) { super(name); }

    @Override
    public void speak() { System.out.println(name + " 喵喵喵"); }
}

public class PolyDemo {
    public static void main(String[] args) {
        // 父类引用,指向不同子类对象
        Animal[] zoo = { new Dog("旺财"), new Cat("咪咪"), new Dog("大黄") };

        for (Animal a : zoo) {
            a.speak();       // 同一行代码,运行时各走各的实现 → 多态
            // a.fetch();    // 编译错!引用类型 Animal 没有 fetch 方法
        }
    }
}

输出:

旺财 汪汪汪
咪咪 喵喵喵
大黄 汪汪汪

机制:编译期看「引用类型」决定能调什么方法;运行期看「实际对象类型」决定执行谁的方法。这叫动态绑定。

步骤 3:instanceof 与类型转换(JDK 17 模式匹配)

public class InstanceofDemo {
    public static void main(String[] args) {
        Animal a = new Dog("旺财");

        // 老写法:判断 + 强转,两步
        if (a instanceof Dog) {
            Dog d = (Dog) a;
            d.fetch();
        }

        // JDK 16+ 模式匹配:判断 + 绑定变量一步到位(本站后端常用)
        if (a instanceof Dog d) {
            d.fetch();       // d 已自动转型
        }

        // 危险操作:ClassCastException
        Animal c = new Cat("咪咪");
        if (c instanceof Dog d2) {      // 不会进这个分支
            d2.fetch();
        } else {
            System.out.println("猫不是狗,强转前必须 instanceof");
        }
    }
}

步骤 4:抽象类——半成品模板

abstract class Payment {                      // 抽象类:不能 new
    protected String channel;

    public Payment(String channel) { this.channel = channel; }

    public abstract void pay(double amount);  // 抽象方法:只声明不实现,子类必须实现

    public void log(double amount) {          // 具体方法:公共逻辑放这
        System.out.printf("[%s] 支付 %.2f 元%n", channel, amount);
    }
}

class WechatPay extends Payment {
    public WechatPay() { super("微信"); }
    @Override
    public void pay(double amount) { log(amount); System.out.println("调用微信支付 API"); }
}

class AlipayPay extends Payment {
    public AlipayPay() { super("支付宝"); }
    @Override
    public void pay(double amount) { log(amount); System.out.println("调用支付宝 API"); }
}

public class AbstractDemo {
    public static void main(String[] args) {
        Payment[] channels = { new WechatPay(), new AlipayPay() };
        for (Payment p : channels) {
            p.pay(99.9);      // 新增支付方式无需改这里 → 开闭原则
        }
    }
}

步骤 5:接口——行为契约

interface Cacheable {                     // 接口:纯行为约定
    void put(String key, String value);   // 抽象方法,隐式 public abstract
    String get(String key);

    default String getOrDefault(String key) {   // JDK 8+ default 方法
        String v = get(key);
        return v != null ? v : "默认值";
    }
}

interface Expirable {
    void expireAfter(int seconds);
}

// 类可以实现多个接口(弥补 Java 单继承限制)
class RedisCache implements Cacheable, Expirable {
    private final java.util.Map<String, String> data = new java.util.HashMap<>();

    @Override
    public void put(String key, String value) { data.put(key, value); }

    @Override
    public String get(String key) {
        return data.get(key);
    }

    @Override
    public void expireAfter(int seconds) {
        System.out.println("设置 " + seconds + " 秒过期(模拟 Redis TTL)");
    }
}

public class InterfaceDemo {
    public static void main(String[] args) {
        Cacheable cache = new RedisCache();    // 用接口类型接收 → 面向接口编程
        cache.put("user:1001", "tom");
        System.out.println(cache.get("user:1001"));
        System.out.println(cache.getOrDefault("user:9999"));
        ((Expirable) cache).expireAfter(300);
    }
}

步骤 6:Object 三大方法速览

import java.util.Objects;

class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public String toString() { return "(" + x + "," + y + ")"; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                    // 同一对象快速返回
        if (!(o instanceof Point p)) return false;     // 类型不符直接 false
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() { return Objects.hash(x, y); }  // 重写 equals 必须配套重写
}

public class ObjectDemo {
    public static void main(String[] args) {
        Point p1 = new Point(1, 2), p2 = new Point(1, 2);
        System.out.println(p1 == p2);        // false:不同对象
        System.out.println(p1.equals(p2));   // true:内容相等
        System.out.println(p1.hashCode() == p2.hashCode());  // true
    }
}

讲清楚要点

  1. Java 单继承、多实现:一个类只能 extends 一个父类,但可以 implements 多个接口。想复用多份能力,用接口组合。
  2. 重写要求签名完全一致(方法名、参数、返回值兼容),访问权限只能放宽不能收紧。@Override 注解让编译器帮你检查——不写它,参数写错就变成重载,运行时静默出错,最难排查。
  3. super 两条用法:super(...) 调父类构造器(必须第一行);super.method() 调父类被重写前的版本。
  4. 多态三条件:继承/实现关系 + 父类引用指向子类对象 + 重写方法。缺一不可。
  5. 抽象类 vs 接口的选择:有公共字段/公共实现要复用 → 抽象类;纯行为约定、需要多继承效果 → 接口。后端惯例:Service 层用接口 + 实现类,模板方法用抽象类。
  6. 面向接口编程:方法参数、字段类型尽量声明为接口/父类(List<String> 而非 ArrayList<String>),调用方与实现解耦。

你可能会懵的地方

  • 为什么不直接 new Dog(),要写 Animal a = new Dog()? 单行代码看不出好处,但方法参数、集合元素用 Animal 类型时,一段代码能处理所有动物——新增 Cat 零改动。解耦是目的,多态是手段。

  • 子类能继承父类的 private 成员吗? 物理上「继承」(对象内存里有),语法上「不可直接访问」。要访问走父类的 public/protected 方法。这也是封装在父子类间的体现。

  • default 方法是干嘛的? JDK 8 为接口演进而生:给已有接口加方法而不破坏所有实现类。如 Comparator.reversed()。实现类不重写就用默认实现。

  • 抽象类必须有抽象方法吗? 不必须。一个抽象方法都没有的抽象类也合法(纯粹禁止实例化)。反过来,含抽象方法的类必须声明为 abstract。

  • equals 和 hashCode 为什么要一起重写? 约定:equals 相等的对象必须有相同 hashCode。只重写 equals,两个「相等」的对象进了 HashSet/HashMap 后会因 hashCode 不同被分进不同桶,去重失效、get 查不到——集合阶段最经典的 bug。

  • 接口里能有字段吗? 只能有 public static final 常量。接口不是存状态的地方,状态放实现类。

错法对照

错法问题正解
重写方法参数类型写错变成重载,多态失效且无警告加 @Override 让编译器把关
new Animal("x")(抽象类)编译错误new 具体子类
子类构造器没调 super(...)编译错(父类无无参构造器时)第一行写 super(...)
(Dog) cat 强转ClassCastException先 instanceof 判断
只重写 equals 不重写 hashCodeHashSet/HashMap 行为异常两个一起重写,或改用 record
接口实现类漏实现某方法编译错误(除非该方法是 default)补全所有抽象方法,类声明 abstract 可暂缓

卡点急救

  1. @Override 报「方法不会覆盖或实现超类型的方法」:签名不一致——参数类型、个数或名字写错了。逐字对照父类方法。
  2. 「没有默认构造器」相关编译错:父类只有有参构造器时,子类每个构造器必须显式 super(参数)。
  3. 调用的方法明明存在却编译错:引用类型太「窄」或太「宽」。引用是 Animal 就调不了 fetch——加 instanceof 模式匹配转型,或把方法上提到父类。
  4. HashSet 去重失败/HashMap get 不到:忘了重写 hashCode。补上,或类直接改 record。
  5. 接口的 default 方法被实现类「撞签名」:按重写规则处理即可;实现两个接口出现同签名 default 时必须用 接口名.super.方法() 显式选择。

面试常问

  • 重载与重写的区别?(必考) 重载:同类中同名不同参,编译期静态绑定;重写:子类覆盖父类同签名方法,运行期动态绑定。重写受「两同两小一大」约束(方法名参数相同、返回值和异常更小/相同、访问权限更大或相同)。

  • 抽象类和接口的区别?(必考) 抽象类:单继承、可有状态(字段)与构造器、is-a 关系;接口:多实现、JDK 8 前纯抽象/8 后可 default 和 static 方法、9 后可私有方法、can-do 关系。选择:复用骨架用抽象类,定义能力用接口。

  • 多态的实现原理? 编译期静态检查引用类型的方法表,运行期 JVM 通过对象的实际类型的方法表(虚方法表 vtable)动态绑定到子类实现。

  • Java 为什么单继承? 多继承引发「菱形问题」(两个父类有同名成员时歧义)。Java 用接口多实现 + default 方法显式解决的方案规避了歧义。

  • Spring 里哪里体现多态? @Autowired UserService 注入的是接口,容器按类型找实现类 Bean(一个实现直接注入,多个实现配合 @Primary/@Qualifier);AOP 代理对象与原对象同接口,调用方无感知。

自测三问

  1. Animal a = new Dog(); a.speak(); 编译期和运行期各看什么?如果 Dog 没有 fetch 方法但 Animal 有声明呢?
  2. 抽象类里可以有非抽象方法吗?接口里可以有方法体吗(哪个 JDK 版本起)?
  3. 只重写 equals 不重写 hashCode,往 HashSet 里放两个「相等」的对象会怎样?

小结一句话

extends 复用与重写、implements 签契约、多态让「父类引用调子类实现」;接口 + 实现类是 Spring Service 层的标准形态,@Override 和 equals/hashCode 配套是两处不能省的纪律。

延伸

  • Oracle:接口与继承
  • 下一课:集合框架入门——List/Set/Map 三大件,把本课示例里的 Map 换成真正的 HashMap。
加载本节练习…
← 上一课类与封装下一课 →集合框架入门