1. 什么是类的层次结构?
在编程中(以及其他领域),层次结构——是一种树形结构:树的顶部是“通用”的实体(基类),下面——是更具体的实体(子类)。在 Java 中,类的层次结构通过继承来构建:每个子类本身也可以作为其他子类的父类,依次类推。
类比:
- 动物(Animal)是通用的类。
- 哺乳动物(Mammal)是动物的特例。
- 狗(Dog)是哺乳动物的特例。
- 而具体的“沙里克”——就是 Dog 类的一个对象。
在代码中看起来如下:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
类的层次结构允许在“上层”描述通用属性和行为,而在“下层”描述细节。这使代码更有逻辑,也能避免重复。
层次结构示意图
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. 如何构建层次结构:思路与实践
辨别共性与特性
核心规则:基类应包含对其所有后代都适用的内容;而所有独有的内容——放到子类中。
示例:
- 所有动物都会呼吸和进食——因此,方法 breathe() 和 eat() 应该在 Animal 类中。
- 只有鸟会飞——因此,方法 fly() 应在 Bird 类中,而不是在 Animal 中。
- 只有狗会叫——方法 bark() 应在 Dog 类中。
示例:动物
// 基类
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " 在吃。");
}
void makeSound() {
System.out.println(name + " 发出声音。");
}
}
// 子类:哺乳动物
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " 用奶喂幼崽。");
}
}
// 子类:狗
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " 在汪汪叫:汪汪!");
}
void wagTail() {
System.out.println(name + " 摇尾巴。");
}
}
// 子类:猫
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " 在喵喵叫:喵!");
}
void purr() {
System.out.println(name + " 打呼噜。");
}
}
// 子类:鸟
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " 在飞。");
}
@Override
void makeSound() {
System.out.println(name + " 在叽喳叫:叽喳叽喳!");
}
}
在 main 中的调用:
public class ZooDemo {
public static void main(String[] args) {
Dog buddy = new Dog("沙里克");
Cat kitty = new Cat("穆尔卡");
Bird sparrow = new Bird("麻雀");
buddy.eat(); // 沙里克 在吃。
buddy.makeSound(); // 沙里克 在汪汪叫:汪汪!
buddy.feedMilk(); // 沙里克 用奶喂幼崽。
buddy.wagTail(); // 沙里克 摇尾巴。
kitty.eat(); // 穆尔卡 在吃。
kitty.makeSound(); // 穆尔卡 在喵喵叫:喵!
kitty.feedMilk(); // 穆尔卡 用奶喂幼崽。
kitty.purr(); // 穆尔卡 打呼噜。
sparrow.eat(); // 麻雀 在吃。
sparrow.makeSound(); // 麻雀 在叽喳叫:叽喳叽喳!
sparrow.fly(); // 麻雀 在飞。
}
}
可视化:类树
| 类 | 父类 | 特性 |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. 更多生活化示例
几何图形
类的层次结构非常适合建模几何图形。
// 基类
class Shape {
void draw() {
System.out.println("绘制图形。");
}
}
// 圆
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("绘制半径为 " + radius + " 的圆");
}
}
// 矩形
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
void draw() {
System.out.println("绘制矩形 " + width + "x" + height);
}
}
用法:
public class ShapeDemo {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.draw(); // 绘制半径为 5.0 的圆
s2.draw(); // 绘制矩形 3.0x4.0
}
}
交通工具
class Vehicle {
void move() {
System.out.println("交通工具在移动。");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("汽车在路上行驶。");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("骑行者在蹬踏板。");
}
}
main:
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // 汽车在路上行驶。
v2.move(); // 骑行者在蹬踏板。
用户
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " 登录系统。"); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " 删除了用户 " + user);
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " 完成了一次购买。"); }
}
4. 有用的细节
不要滥用继承。 继承是用于“is-a”关系的工具。如果你要表达“猫是动物”,使用继承。若是“猫包含尾巴”,应使用组合(has-a)。
不好:
class Engine { /* ... */ }
class Car extends Engine { /* 汽车就是发动机吗?不,这是组合! */ }
好:
class Car {
Engine engine; // 汽车包含发动机
}
不要把层次搞得过深。 层次越多——就越难维护和理解代码。通常 2–3 层已经足以覆盖大多数任务。
也不要过于“扁平”。 如果你有 20 个类都直接继承自一个基类,可能需要重新审视架构。
为“图省事”而继承。 有时会想“不管逻辑,先把方法或字段拿来再说”。这是走向混乱的开始!如果 class A 和 class B 在逻辑上并无关系,不要仅仅为了几个方法就让 extends 发生在 B 和 A 之间。
违反里氏替换原则。 如果子类无法在不带来意外的前提下替代父类使用——说明你的层次结构设计有问题。
代码重复。 如果你发现“哎,这个方法在好几个类里都复制了一份”,那也许就该把它上移到基类中了。
5. 构建类层次结构时的常见错误
错误 No.1:没有“is-a”语义的继承。
如果你使用继承只是为了获得方法或字段的访问权,而不是因为子类确实“是”父类的一种,那么你的代码很快就会变成一锅粥。比如,“汽车继承发动机”——这不是 is-a,而是 has-a。
错误 No.2:忽略子类的独特特性。
如果所有子类看起来都一样,且没有增加任何新内容,那么可能根本不需要层次结构——使用一个类即可。
错误 No.3:在子类中重复代码。
如果你把同样的实现复制到多个子类中,应该考虑把它上移到父类。
错误 №4:层次过于复杂或过深。
多层级的层次结构难以维护和测试。越简单——越好!
错误 №5:重写方法却不使用 @Override 注解。
没有注解就很容易在方法签名上出错,这样方法并不会被正确重写,而是会被当成新方法。请始终使用 @Override!
GO TO FULL VERSION