1. 继承中的错误
继承是 OOP 的基石之一,但也是初学者最容易踩坑的主题之一。让我们拆解典型错误,并学习如何避免。
没有调用基类构造函数 (super(...))
当你创建子类时,需要记住:基类可能要求通过构造函数进行特定初始化。如果基类没有无参构造函数,那么在子类构造函数中必须显式通过 super(...) 调用基类构造函数。
错误示例:
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
// 错误!Animal 没有无参构造函数
public Dog() {
// super(); // 编译器会自动插入 super(),但基类并不存在这样的构造函数!
}
}
如何修复:
class Dog extends Animal {
public Dog(String name) {
super(name); // 一切正常!
}
}
说明:
如果基类只有带参构造函数,编译器不会自动添加无参构造函数。这是编译错误的常见原因。
试图继承 final 类或重写 final 方法
在 Java 中可以将类或方法声明为 final。这意味着:
- 该类不能被继承。
- 该方法不能在子类中被重写(override)。
错误示例:
final class Cat {}
// 编译错误!
class Tiger extends Cat {
// ...
}
class Animal {
public final void sleep() {
System.out.println("Zzz...");
}
}
class Dog extends Animal {
// 编译错误!
@Override
public void sleep() {
System.out.println("Dog is sleeping...");
}
}
说明:
如果你看到错误 "cannot inherit from final"、"cannot override final method" —— 请检查修饰符!
违反 Liskov 替换原则 (Liskov Substitution Principle)
听起来吓人,但实际上意味着:子类对象必须能像基类对象那样行为,不破坏程序逻辑。常见错误是以与基类期望不一致的方式重写方法。
示例:
class Bird {
public void fly() {
System.out.println("我在飞!");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("企鹅不会飞!");
}
}
问题是什么?
针对 Bird 编写的代码期望任何鸟都会飞。但如果传入 Penguin,程序可能会崩溃。
更好的做法:
在这种情况下应重审继承层次,或改用接口/组合。
2. 方法重载(overloading)中的错误
重载是指在同一个类中有多个同名但参数不同的方法。看似简单,但也容易踩坑。
把重写写成了重载(签名出错)
新手常常想重写(override)父类方法,却不小心改了参数。结果变成了重载,而不是重写——多态不起作用!
错误示例:
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
// 原本想重写,却变成了重载!
public void makeSound(String extra) {
System.out.println("Bark! " + extra);
}
}
问题:
调用 dog.makeSound() 将调用父类的方法,而不是你新增的那个。
调用 dog.makeSound("loudly") 会调用重载版本,但多态不起作用!
最佳实践:
请使用注解 @Override —— 如果你的签名错了,编译器会立刻提示。
@Override
public void makeSound() { /* ... */ }
重载的不显而易见行为(自动类型转换、调用歧义)
当参数可同时匹配多个重载时,Java 可能选择与你期望不同的方法。
public class OverloadDemo {
public void print(int x) {
System.out.println("int: " + x);
}
public void print(double x) {
System.out.println("double: " + x);
}
}
OverloadDemo demo = new OverloadDemo();
demo.print(5); // int: 5
demo.print(5.0); // double: 5.0
demo.print(5L); // long -> double: double: 5.0
问题:
如果调用 demo.print(5L),Java 会选择 print(double x)(因为 long 比起转为 int,更倾向被转换为 double)。
如果同时存在参数类型为 Object、Integer、int 的重载,用 null 调用可能会导致编译错误:“reference to print is ambiguous”。
仅以返回类型区分同名方法(编译错误)
在 Java 中,不能声明两个仅返回类型不同而参数列表相同的同名方法!
public class Demo {
// 编译错误!
public int foo() { return 1; }
public String foo() { return "hello"; }
}
说明:
用于重载匹配的方法签名是“名称 + 参数”。返回类型不参与比较。编译器无法判断你想调用哪一个。
3. 最佳实践
为避免在继承和重载上踩坑,请遵循以下建议:
对需要重写的方法始终使用 @Override 注解
这不仅提高可读性,还能防止签名错误。一旦你不小心改了参数或方法名,编译器会立即告知。
@Override
public void makeSound() {
System.out.println("Bark!");
}
清晰地区分重载与重写
- 重写(override):改变父类方法的行为——签名必须完全一致。
- 重载(overload):添加同名但参数不同的新方法。
直观对比表:
| 重载(overloading) | 重写(overriding) | |
|---|---|---|
| 位置 | 同一类/层级内 | 在子类中 |
| 方法名 | 相同 | 相同 |
| 参数 | 不同 | 相同 |
| 返回类型 | 可以不同 | 必须相同/可协变 |
| 注解 | 非必需 | @Override 推荐 |
不要滥用重载
如果一个方法有太多重载,代码会变得难读且混乱。若重载过多,最好使用参数对象或 Builder 模式。
4. 示例:宠物管理系统
假设你要做一个简单的宠物管理系统。你有一个基类 Pet,以及子类 Cat 和 Dog。
public class Pet {
private String name;
public Pet(String name) {
this.name = name;
}
public void speak() {
System.out.println(name + " 发出不明的声音。");
}
}
public class Cat extends Pet {
public Cat(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(getName() + " 说:喵!");
}
// 错误:Pet 中没有 getName() 方法!
}
典型错误:
在尝试重写方法时,我们调用了基类中不存在的方法。应当添加一个 getter:
public class Pet {
private String name;
public Pet(String name) { this.name = name; }
public String getName() { return name; }
public void speak() { System.out.println(name + " 发出不明的声音。"); }
}
现在一切正常,我们可以使用多态:
Pet myPet = new Cat("Barsik");
myPet.speak(); // Barsik 说:喵!
5. 继承与重载的常见错误
错误 1:忘记调用 super(...)。
如果基类构造函数中有重要逻辑而你没有调用,程序可能出现意外行为,甚至无法通过编译。
错误 2:重写错了方法。
本想修改父类方法的行为,结果却添加了一个名称相近或参数不同的新方法。结果:旧方法照旧被调用,而你的新方法没人用。
错误 3:尝试重写 final 方法。
Java 不允许这么做,这正是它的用处!若看到编译错误——请检查是否有 final。
错误 4:把方法重载到难以辨认。
当你有 10 个 calculate 重载,连自己都分不清调用的是哪一个时——该停下来考虑重构了。
错误 5:违反 Liskov 替换原则。
如果子类改变了基类行为的语义,整个架构都可能“跑偏”。例如,若你有类 Shape 及其方法 getArea(),而子类 BrokenShape 返回 -1,这可能引发奇怪的 bug。
GO TO FULL VERSION