CodeGym /课程 /JAVA 25 SELF /方法重写(overriding):与重载的区别

方法重写(overriding):与重载的区别

JAVA 25 SELF
第 18 级 , 课程 2
可用

1. 方法重写

方法重写(overriding)是指在子类中为父类已有的方法编写自己的版本。借此,程序在运行时(run-time)实现真正的多态。

通俗地说:
如果你有一个带方法的基类,并希望子类以自己的方式执行该方法,只需在子类中声明一个与父类方法具有相同签名的方法。当通过基类类型的引用调用该方法时,将执行对象实际类型中的方法版本。

生活中的例子。
想象你有一队动物,你让每个动物“发出声音”。对所有动物来说命令都是 makeSound(),但狗会汪汪叫,猫会喵喵叫,奶牛会哞哞叫。对代码来说,这是一次“相同方法”的调用,但结果不同——这就是重写的魔力!

重写的语法

基础示例

class Animal {
    void makeSound() {
        System.out.println("动物发出某种声音");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("汪!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("喵!");
    }
}

这里在类 Animal 中定义了方法 makeSound()。子类 DogCat 重写了这个方法,提供了各自的实现。

注解 @Override

在 Java 中,建议(也是一种好习惯)用 @Override 标注被重写的方法:

@Override
void makeSound() { ... }

这对代码运行不是必需的,但它可以:

  • 让编译器检查你是否真的在重写方法(而不是在方法名或参数上打错)。
  • 提升可读性——其他开发者一眼就能看出该方法是对父类方法的重写。

调用被重写的方法

Animal myDog = new Dog();
myDog.makeSound(); // 将输出:汪!

尽管变量类型是 Animal,它实际上指向的是一个 Dog 对象,因此会调用 Dog 类中的方法。这就是动态(后期)绑定。

2. 重写(overriding)与重载(overloading)的区别

重载(overloading)

  • 发生在同一个类中(或同一层级里“相邻”的类)。
  • 方法同名,但参数不同(类型、数量、顺序)。
  • 方法选择发生在编译期。
void print(int x) { ... }
void print(String s) { ... }

重写(overriding)

  • 发生在不同的类中:方法在父类中声明,在子类中被重写。
  • 方法具有相同的名称和签名(参数与返回类型)。
  • 方法选择发生在运行期(run-time),基于对象的实际类型。

对比表

重载(overloading) 重写(overriding)
发生位置 同一个类中 父类与子类之间
方法名 相同 相同
参数 不同 相同
返回类型 可以不同 必须相同或为其子类型
绑定/选择时机 编译期 运行期(run-time)
注解 不需要 @Override(推荐)

3. 方法重写的规则

重写很强大,但也有严格的规则。我们按顺序来说明。

方法签名必须一致

  • 方法名、参数类型与顺序必须与父类方法完全一致。
  • 返回类型必须一致,或是协变的(即父方法返回类型的子类型)。

协变返回类型示例:

class Animal {
    Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
    @Override
    Cat reproduce() { return new Cat(); } // OK,Cat 是 Animal 的子类型
}

访问修饰符

子类中重写方法的访问级别不能比父类方法更严格。 如果父类方法是 public,那么在子类中也必须保持 public。不能把它降为 protectedprivate

示例:

class Parent {
    public void greet() { }
}
class Child extends Parent {
    // void greet() { } // 错误!默认访问级别(package-private,包可见)比 public 更严格
    @Override
    public void greet() { } // OK
}

异常

  • 重写方法不能抛出父类方法声明中不存在的新的受检异常(checked exceptions)。
  • 可以抛出更少或相同的异常。

示例:

class Parent {
    void doWork() throws IOException { }
}
class Child extends Parent {
    @Override
    void doWork() throws FileNotFoundException { } // OK,FileNotFoundException 是 IOException 的子类型
    // void doWork() throws SQLException { } // 错误!父类未声明 SQLException
}

静态方法不能被重写

静态方法可以被隐藏(hidden),但不能被重写。 如果你在子类中声明了一个与父类相同签名的静态方法,这并不是重写!它只是隐藏方法,而非多态。

class Animal {
    static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
    static void info() { System.out.println("Dog"); }
}

调用 Dog.info() 会输出 "Dog",但如果通过一个 Animal 类型的变量调用,将会调用 Animal.info()。这不是多态!

final 方法不能被重写

如果父类中的方法被声明为 final,尝试重写它会导致编译错误。

class Animal {
    final void sleep() { }
}
class Dog extends Animal {
    // @Override
    // void sleep() { } // 错误!不能重写 final 方法
}

4. 实用示例

我们通过实践来看看重写如何工作,以及它与重载有何不同。

示例 1:类 Shape 及其子类

class Shape {
    void draw() {
        System.out.println("绘制图形");
    }
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("绘制圆形");
    }
}

class Rectangle extends Shape {
    @Override
    void draw() {
        System.out.println("绘制矩形");
    }
}

使用多态:

public class Main {
    public static void main(String[] args) {
        Shape s1 = new Circle();
        Shape s2 = new Rectangle();
        s1.draw(); // 绘制圆形
        s2.draw(); // 绘制矩形
    }
}

尽管变量被声明为 Shape 类型,实际调用的却是对象所属类中的方法。

示例 2:与重载的区别

class Printer {
    void print(String s) {
        System.out.println("字符串:" + s);
    }

    void print(int n) {
        System.out.println("数字:" + n);
    }
}

这里两个方法都叫 print,但参数不同——这是重载,而不是重写。

5. 重写与调用父类方法(super

有时在重写的方法里,你想先执行父类逻辑,再添加自己的逻辑。这时可以使用关键字 super

class Animal {
    void makeSound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        super.makeSound(); // 调用父类方法
        System.out.println("汪!");
    }
}

调用 new Dog().makeSound() 将输出:

动物发出声音
汪!

动态绑定(late binding)如何工作

当你通过基类类型的引用调用方法时,Java 会在运行期查看该引用实际指向的对象类型,并调用该对象所属类中定义的方法版本。

Animal a = new Cat();
a.makeSound(); // 将调用 Cat.makeSound(),而不是 Animal.makeSound()

这正是 Java 多态的基础。

6. 这与您的应用有什么关系

在我们的教学应用中(例如员工管理系统),你可以创建一个基类 Employee,其中包含方法 work(),而子类 ManagerDeveloper 可以用各自的方式实现该方法:

class Employee {
    void work() {
        System.out.println("员工在工作");
    }
}

class Manager extends Employee {
    @Override
    void work() {
        System.out.println("经理在管理");
    }
}

class Developer extends Employee {
    @Override
    void work() {
        System.out.println("开发者在写代码");
    }
}

现在你可以把所有员工存放在同一个数组或列表中:

Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
    e.work(); // 每个对象都有各自的输出!
}

7. 重写方法的常见错误

错误 1:方法名或参数拼写错误。 如果你在方法名或其参数上打错,你不是在重写方法,而是在创建一个新方法。结果,多态不会生效。这就是为什么要始终使用 @Override——编译器会立刻提示你。

错误 2:使用更严格的访问修饰符。 如果父类方法是 public,而你在子类中将其声明为 protected 或不写修饰符——会得到编译错误。

错误 3:尝试重写 staticfinal 方法。 静态方法不能被重写,而 final 方法完全不允许被重写。如果你尝试这样做——编译器会阻止你。

错误 4:将返回类型改为不兼容的类型。 如果子类方法的返回类型与父类不一致(且不是其子类型),编译器将不允许你进行重写。

错误 5:添加新的受检异常(checked)。 重写方法不能抛出父类方法声明中不存在的新的受检异常。否则会产生编译错误。

错误 6:忘记使用 super 如果在重写的方法中你想保留父类的部分行为,别忘了显式调用 super.methodName()。Java 不会替你自动调用。

现在你已经了解了方法重写的工作方式、它与重载的区别,以及它如何帮助我们在 Java 中实现多态。下一讲我们将看看如何在实践中使用多态——配合集合、数组与真实业务场景!

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION