大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
继承、接口与多态
这一课你将能做到
- 用 extends 实现继承并正确调用 super,用 @Override 重写方法而不踩「重载冒充重写」的坑
- 解释多态的发生机制(父类引用指向子类对象 + 动态绑定),并手写验证
- 写出「接口 + 实现类」结构——这正是本站后端
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
}
}
讲清楚要点
- Java 单继承、多实现:一个类只能 extends 一个父类,但可以 implements 多个接口。想复用多份能力,用接口组合。
- 重写要求签名完全一致(方法名、参数、返回值兼容),访问权限只能放宽不能收紧。
@Override注解让编译器帮你检查——不写它,参数写错就变成重载,运行时静默出错,最难排查。 - super 两条用法:
super(...)调父类构造器(必须第一行);super.method()调父类被重写前的版本。 - 多态三条件:继承/实现关系 + 父类引用指向子类对象 + 重写方法。缺一不可。
- 抽象类 vs 接口的选择:有公共字段/公共实现要复用 → 抽象类;纯行为约定、需要多继承效果 → 接口。后端惯例:Service 层用接口 + 实现类,模板方法用抽象类。
- 面向接口编程:方法参数、字段类型尽量声明为接口/父类(
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 不重写 hashCode | HashSet/HashMap 行为异常 | 两个一起重写,或改用 record |
| 接口实现类漏实现某方法 | 编译错误(除非该方法是 default) | 补全所有抽象方法,类声明 abstract 可暂缓 |
卡点急救
- @Override 报「方法不会覆盖或实现超类型的方法」:签名不一致——参数类型、个数或名字写错了。逐字对照父类方法。
- 「没有默认构造器」相关编译错:父类只有有参构造器时,子类每个构造器必须显式
super(参数)。 - 调用的方法明明存在却编译错:引用类型太「窄」或太「宽」。引用是 Animal 就调不了 fetch——加 instanceof 模式匹配转型,或把方法上提到父类。
- HashSet 去重失败/HashMap get 不到:忘了重写 hashCode。补上,或类直接改 record。
- 接口的 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 代理对象与原对象同接口,调用方无感知。
自测三问
Animal a = new Dog(); a.speak();编译期和运行期各看什么?如果 Dog 没有 fetch 方法但 Animal 有声明呢?- 抽象类里可以有非抽象方法吗?接口里可以有方法体吗(哪个 JDK 版本起)?
- 只重写 equals 不重写 hashCode,往 HashSet 里放两个「相等」的对象会怎样?
小结一句话
extends 复用与重写、implements 签契约、多态让「父类引用调子类实现」;接口 + 实现类是 Spring Service 层的标准形态,@Override 和 equals/hashCode 配套是两处不能省的纪律。
延伸
- Oracle:接口与继承
- 下一课:集合框架入门——List/Set/Map 三大件,把本课示例里的 Map 换成真正的 HashMap。