第8章:面向对象(下)——继承与多态
第7章学会了造"设计图"(类)。这一章讲设计图之间的关系。
📌 代码说明:标注"可复制运行"的代码块包含完整的
main方法。没标注的是概念示意片段。
一个故事开头
你的奶茶店生意越来越好了。现在菜单上不光有奶茶,还有水果茶、咖啡、冰沙。
每种饮品都有自己的做法,但也共享很多共同点:都有名字和价格,都能被制作和展示。
// 如果每个饮品都自己写一个类:
public class BubbleTea {
String name;
double price;
void make() { /* 加珍珠、加茶... */ }
void display() { /* 展示信息 */ }
}
public class FruitTea {
String name;
double price;
void make() { /* 加水果、加茶... */ }
void display() { /* 展示信息 */ }
}
// 名字、价格、display() 方法重复写了三遍能不能把"共同的部分"提取出来,每种饮品只写自己"独特的部分"?**
能。这就是继承。
1. 继承——提取共同点
// 父类(基类):所有饮品共同的部分
public class Beverage {
String name;
double price;
void display() {
System.out.println(name + " — " + price + "元");
}
}
// 子类(派生类):奶茶专有
public class MilkTea extends Beverage {
String topping; // 加料
void addTopping(String t) {
topping = t;
System.out.println("加了 " + t);
}
}
// 使用
MilkTea mt = new MilkTea();
mt.name = "波波奶茶"; // 继承自 Beverage
mt.price = 15.0; // 继承自 Beverage
mt.display(); // 继承自 Beverage
mt.addTopping("珍珠"); // 自己的方法extends = 继承。子类自动拥有父类的所有非 private 字段和方法。
2. 方法重写——子类改父类的做法
父类的方法可能不适合子类。子类可以**重写(Override)**它。
public class Beverage {
String name;
double price;
void make() {
System.out.println("制作 " + name + "...");
}
}
public class MilkTea extends Beverage {
@Override // 注解:告诉编译器"我在重写父类方法"
void make() {
System.out.println("取杯 → 加茶底 → 加奶 → 加料 → 封口");
System.out.println(name + " 做好了!");
}
}
public class IceBlended extends Beverage {
@Override
void make() {
System.out.println("取杯 → 加冰 → 加原料 → 打碎 → 装杯");
System.out.println(name + " 做好了!");
}
}@Override 不是必须的,但强烈建议写——它能帮编译器检查你是不是真的在重写(写错方法名会立刻报错)。
3. super——喊爸爸
子类里可以用 super 访问父类的字段或方法。
public class Beverage {
String name;
Beverage(String name) {
this.name = name;
}
void display() {
System.out.println(name);
}
}
public class MilkTea extends Beverage {
String topping;
MilkTea(String name, String topping) {
super(name); // 调父类的构造方法——必须在子类构造器的第一行
this.topping = topping;
}
@Override
void display() {
super.display(); // 先执行父类的 display
System.out.println("加料:" + topping);
}
}super = "父类"。 当子类重写了方法但还想用父类版本时,用 super.方法名()。
4. 多态——同一件事,不同做法
Beverage b1 = new MilkTea("波波奶茶", "珍珠");
Beverage b2 = new IceBlended("芒果冰沙");
b1.make(); // 调的是 MilkTea 的 make()
b2.make(); // 调的是 IceBlended 的 make()父类类型的变量,可以指向子类的对象。调用方法时,执行的是实际对象所属类的方法。
这就是"多态"——同一行代码 make(),不同的对象有不同的表现。
实际用途
public class OrderSystem {
public static void processOrder(Beverage b) {
// 不管传进来的是 MilkTea 还是 IceBlended,都能处理
b.display();
b.make();
System.out.println("订单完成");
}
public static void main(String[] args) {
processOrder(new MilkTea("波波奶茶", "珍珠"));
processOrder(new IceBlended("芒果冰沙"));
}
}这就是多态的强大之处:写一个方法,能处理所有子类型的对象。新增一种饮品时,不用改 processOrder。
5. 抽象类——只有定义,没有实现
有些类太"概念"了,不应该被直接创建对象。
"饮品"是个概念——你从菜单上点不了"一杯饮品",你只能点"波波奶茶"、"杨枝甘露"这种具体的东西。
// 抽象类——不能直接 new Beverage()
abstract class Beverage {
String name;
Beverage(String name) {
this.name = name;
}
// 抽象方法——只有声明,没有方法体
abstract void make();
// 普通方法——子类可以直接用
void display() {
System.out.println(name);
}
}
// 子类必须实现 make(),否则编译不通过
class MilkTea extends Beverage {
MilkTea(String name) {
super(name);
}
@Override
void make() {
System.out.println("制作奶茶");
}
}
// Beverage b = new Beverage("test"); // ❌ 编译错误!抽象类不能 new抽象类 = 半成品设计图——它规定了子类必须有什么方法(抽象方法),但实现由子类自己完成。
6. 接口——纯规定
如果说抽象类是"半成品",接口就是"合同"——它只规定"你必须有什么方法",完全不管你怎么实现。
// 接口——定义能力
interface Sellable {
double getPrice(); // 必须实现
void displayInfo(); // 必须实现
}
interface Discountable {
double applyDiscount(double rate); // 必须实现
}
// 类可以实现多个接口
class Merchandise implements Sellable, Discountable {
String name;
double price;
Merchandise(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public double getPrice() {
return price;
}
@Override
public void displayInfo() {
System.out.println(name + " — " + price + "元");
}
@Override
public double applyDiscount(double rate) {
return price * rate;
}
}接口 vs 抽象类
| 抽象类 | 接口 | |
|---|---|---|
| 能干什么 | 定义通用代码 + 规定必须实现的方法 | 只规定必须实现什么 |
| 能继承几个 | 只能继承一个父类 | 可以实现多个接口 |
| 有构造方法 | 有 | 没有 |
| 字段 | 可以有普通字段 | 只能有常量(public static final) |
| 什么时候用 | 多个类共享代码 + 行为 | 只关心"有什么能力" |
7. Java 8+ 的接口新特性
interface Greetable {
void greet(); // 抽象方法
// default 方法:接口里带有默认实现的普通方法
default void sayGoodbye() {
System.out.println("再见!");
}
// static 方法
static void printWelcome() {
System.out.println("欢迎光临!");
}
}
class Customer implements Greetable {
@Override
public void greet() {
System.out.println("你好!");
}
// sayGoodbye() 不用重写——直接用接口的默认版本
}
Customer c = new Customer();
c.greet(); // 你好!
c.sayGoodbye(); // 再见!(来自接口的 default 方法)
Greetable.printWelcome(); // 欢迎光临!(静态方法通过接口名调用)8. 完整例子:奶茶店菜单系统
import java.util.ArrayList;
// 抽象类
abstract class Beverage {
protected String name;
protected double price;
Beverage(String name, double price) {
this.name = name;
this.price = price;
}
abstract void make();
void display() {
System.out.println(name + " — " + price + "元");
}
}
// 接口
interface Customizable {
void addTopping(String topping);
void adjustSweetness(int level);
}
// 具体子类
class MilkTea extends Beverage implements Customizable {
private String topping = "";
private int sweetness = 5;
MilkTea(String name, double price) {
super(name, price);
}
@Override
void make() {
System.out.println("制作" + name + ":取杯→加茶→加奶→加料→封口");
}
@Override
public void addTopping(String topping) {
this.topping = topping;
System.out.println("已加料:" + topping);
}
@Override
public void adjustSweetness(int level) {
this.sweetness = level;
System.out.println("甜度调整为:" + level + "分");
}
}
class FruitTea extends Beverage {
FruitTea(String name, double price) {
super(name, price);
}
@Override
void make() {
System.out.println("制作" + name + ":取杯→加水果→加茶→加冰");
}
}
// 主程序
public class MenuSystem {
public static void main(String[] args) {
ArrayList<Beverage> menu = new ArrayList<>();
menu.add(new MilkTea("波波奶茶", 15.0));
menu.add(new MilkTea("抹茶拿铁", 16.0));
menu.add(new FruitTea("柠檬茶", 12.0));
menu.add(new FruitTea("百香果绿", 14.0));
// 多态遍历
for (Beverage b : menu) {
b.display();
b.make();
System.out.println("---");
}
// 向下转型——调子类专属方法
MilkTea mt = new MilkTea("特制奶茶", 18.0);
mt.addTopping("珍珠"); // 来自接口
mt.adjustSweetness(3); // 来自接口
mt.make();
}
}本章小结
- 继承(extends)——子类自动拥有父类的非 private 成员,提取公共代码
- 方法重写(@Override)——子类覆盖父类的方法实现
- super——在子类里调用父类的构造方法或成员
- 多态——父类引用指向子类对象,运行时决定调哪个方法
- 抽象类(abstract)——不能 new,可以定义抽象方法来强迫子类实现
- 接口(interface)——纯合同,规定"有什么能力",一个类可以实现多个
- Java 8+ 接口增强——default 方法给接口加上默认实现
✅ 验收标准
完成本章后,你应该能:
- [ ] 用
extends实现继承 - [ ] 用
@Override重写父类方法 - [ ] 用
super调用父类构造器和方法 - [ ] 理解多态——父类引用指向子类对象
- [ ] 区分抽象类和接口的选择场景
📌 常见卡点
- 子类构造器第一行必须调用父类构造器(显式 `super()` 或隐式调用无参)
- 方法重写不能缩小访问权限——`public` 不能改成 `protected`
- 静态方法不能重写——只能隐藏
- 抽象类不能实例化
🔜 现在不需要理解
- final 类和方法的作用
- 接口的 private 方法(Java 9+)
- 菱形继承问题——Java 通过接口避免
- 密封类(Java 17+)
🧪 练习
1. 画继承图:画一个类继承图(在纸上或脑子里),包含 Animal(抽象类,有 void makeSound() 抽象方法)→ Dog 和 Cat,它们分别实现 makeSound()。
2. 写接口:定义一个 Playable 接口,包含 void play() 方法。让 Dog 实现它(输出"狗在玩球")。
3. 多态:写一个方法 static void playWith(Playable p),调用 p.play()。传一个 Dog 对象进去,看看调用的是哪个类的 play()。