Bootstrap

菜鸟Java面向对象 2. Java 重写(Override)与重载(Overload)

Java 重写(Override)与重载(Overload)

1. 重写(Override)

1. 概念解释:

重写(Override)是指子类定义了一个与其父类中具有相同名称、参数列表和返回类型的方法,并且子类方法的实现覆盖了父类方法的实现。
即外壳不变,核心重写!
(或接口不变,内核更新)

2. 好处说明

重写的好处在于子类可以根据需要,定义特定于自己的行为
也就是说子类能够根据需要实现父类的方法
这样,在使用子类对象调用该方法时,将执行子类中的方法而不是父类中的方法

3. 异常规则处理

重写方法不能抛出新的检查异常或者比被重写方法申明 更加宽泛的异常。(编译上可以,逻辑上不允许)

例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,抛出 IOException 异常或者 IOException 的子类异常。


当一个方法在父类中声明了检查异常(checked exception),例如 IOException,这意味着在调用这个方法时,调用方必须要么使用try-catch块来处理异常,要么将异常通过throws子句传播到更高层次的调用者。

现在考虑一个情况,如果子类重写了父类的方法,并且在重写的方法中抛出了一个比父类方法申明的异常更加宽泛的异常,比如 Exception,这将会引起问题。因为调用方可能依赖于父类方法声明的异常类型来进行异常处理。如果子类方法抛出的异常类型更加宽泛,那么调用方可能会收到意料之外的异常,这可能会破坏代码的健壮性和可维护性。

举个例子来说明:

// 父类
class Parent {
    // 父类方法声明了 IOException 异常
    void doSomething() throws IOException {
        // 父类方法可能会抛出 IOException 异常
    }
}

// 子类
class Child extends Parent {
    // 子类重写父类方法,但是抛出了 Exception 异常,比父类宽泛
    @Override
    void doSomething() throws Exception {
        // 子类方法实现
    }
}

// 调用方
public class Main {
    public static void main(String[] args) {
        Parent obj = new Child();
        try {
            obj.doSomething(); // 子类方法抛出了 Exception 异常
        } catch (IOException e) {
            // 调用方期待捕获的是 IOException 异常,但实际收到了更加宽泛的 Exception 异常
            e.printStackTrace();
        }
    }
}

在这个例子中,调用方期待捕获的是 IOException 异常,但是由于子类方法抛出了更加宽泛的 Exception 异常,导致调用方收到了意料之外的异常。这可能会破坏代码的健壮性和可维护性。

因此,Java的设计约定是,在重写父类方法时,子类方法的异常声明不能比父类方法的异常声明更加宽泛,只能抛出父类方法声明的异常类型或其子类异常。


在面向对象原则里,重写意味着可以重写任何现有方法。实例如下:

//TestDog.java 文件代码:
class Animal{
   public void move(){ // 定义动物类的移动方法
      System.out.println("动物可以移动"); // 输出动物可以移动
   }
}
 
class Dog extends Animal{ // Dog 类继承自 Animal 类
   public void move(){ // 重写了父类的 move 方法
      System.out.println("狗可以跑和走"); // 输出狗可以跑和走
   }
}
 
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // 创建 Animal 对象
      Animal b = new Dog(); // 创建 Dog 对象,但是引用类型是 Animal
 
      a.move(); // 调用 Animal 类的 move 方法,输出动物可以移动
 
      b.move(); // 调用 Dog 类的 move 方法,输出狗可以跑和走
   }
}

以上实例编译运行结果如下:

动物可以移动
狗可以跑和走

在上面的例子中可以看到,尽管 b 属于 Animal 类型,但是它运行的是 Dog 类的 move方法。

这是由于在编译阶段,只是检查参数的引用类型。

然而在运行时,Java 虚拟机(JVM)指定对象的类型并且运行该对象的方法。

因此在上面的例子中,之所以能编译成功,是因为 Animal 类中存在 move 方法,然而运行时,运行的是特定对象的方法。

思考以下例子:

//TestDog.java 文件代码:
class Animal{
   public void move(){ // 定义动物类的移动方法
      System.out.println("动物可以移动"); // 输出动物可以移动
   }
}
 
class Dog extends Animal{ // Dog 类继承自 Animal 类
   public void move(){ // 重写了父类的 move 方法
      System.out.println("狗可以跑和走"); // 输出狗可以跑和走
   }
   public void bark(){ // 定义狗类的吠叫方法
      System.out.println("狗可以吠叫"); // 输出狗可以吠叫
   }
}
 
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // 创建 Animal 对象
      Animal b = new Dog(); // 创建 Dog 对象,但是引用类型是 Animal
 
      a.move(); // 调用 Animal 类的 move 方法,输出动物可以移动
 
      b.move(); // 调用 Dog 类的 move 方法,输出狗可以跑和走
      b.bark(); // 调用 Dog 类的 bark 方法,输出狗可以吠叫
   }
}

以上实例编译运行结果如下:

TestDog.java:30: cannot find symbol
symbol  : method bark()
location: class Animal
                b.bark();
                 ^

该程序将抛出一个编译错误,因为b的引用类型Animal没有bark方法。


problem: 
        The method bark() is undefined for the type Animal

        at Animal.TestDog.main(TestDog.java:27)

这个错误说明了在 Animal 类中没有定义名为 bark 的方法,而在主方法中却尝试调用了这个方法。让我们来修复这个问题:

class Animal{
   public void move(){ // 定义动物类的移动方法
      System.out.println("动物可以移动"); // 输出动物可以移动
   }
}
 
class Dog extends Animal{ // Dog 类继承自 Animal 类
   public void move(){ // 重写了父类的 move 方法
      System.out.println("狗可以跑和走"); // 输出狗可以跑和走
   }
   public void bark(){ // 定义狗类的吠叫方法
      System.out.println("狗可以吠叫"); // 输出狗可以吠叫
   }
}
 
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // 创建 Animal 对象
      Animal b = new Dog(); // 创建 Dog 对象,但是引用类型是 Animal
 
      a.move(); // 调用 Animal 类的 move 方法,输出动物可以移动
 
      b.move(); // 调用 Dog 类的 move 方法,输出狗可以跑和走
      if (b instanceof Dog) { // 检查 b 是否为 Dog 类的实例
          ((Dog)b).bark(); // 如果是 Dog 类的实例,强制转换为 Dog 类并调用 bark 方法
      }
   }
}

就是把

b.bark(); // 调用 Dog 类的 bark 方法,输出狗可以吠叫

修改成

if (b instanceof Dog) { // 检查 b 是否为 Dog 类的实例
	((Dog)b).bark(); // 如果是 Dog 类的实例,强制转换为 Dog 类并调用 bark 方法
	}

在这个修改后的版本中,我们在调用 b.bark() 之前使用了 instanceof 运算符来检查 b 是否为 Dog 类的实例。如果是,则进行强制类型转换并调用 bark 方法。这样就可以避免在 Animal 类中调用 bark 方法而产生编译错误。


2. 方法的重写规则

  1. 参数列表必须相同: 重写方法的参数列表必须与被重写方法完全相同,包括参数的类型、顺序和数量。

  2. 返回类型可以不同: 重写方法的返回类型可以与被重写方法不同,但是必须是被重写方法返回值的派生类。在Java 5及更早版本中,返回类型必须相同。在Java 7及更高版本中,可以不同。

  3. 访问权限不能低于父类: 重写方法的访问权限不能比父类中被重写方法的访问权限更低,因为子类不能降低父类的访问权限。。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。

  4. 只能重写父类的成员方法: 只有父类中的成员方法才能被子类重写,不能重写父类的成员变量、静态方法等。

  5. final 方法不能被重写: 被声明为 final 的方法不能被子类重写,因为 final 方法是不可改变的。

  6. static 方法不能被重写: 被声明为 static 的方法不能被子类重写,但是可以被再次声明。

  7. 包内和包外的限制: 如果子类和父类在同一个包中,子类可以重写父类的所有方法,除了被声明为 private 和 final 的方法。如果不在同一个包中,子类只能重写父类声明为 public 和 protected 的非 final 方法。

  8. 异常处理规则: 重写的方法可以抛出任何非强制异常,但是不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常。

  9. 构造方法不能被重写: 构造方法是用来创建对象的特殊方法,不能被子类重写。

  10. 不能继承的类不能重写: 如果一个类不能被继承(比如被声明为 final),那么它的方法也不能被重写。

这些规则确保了方法重写的合理性和一致性,遵循这些规则可以避免出现一些常见的错误和不一致性。


3. Super 关键字的使用

当需要在子类中调用父类的被重写方法时,要使用 super 关键字。

//TestDog.java 文件代码:
class Animal{
   public void move(){ // 定义动物类的移动方法
      System.out.println("动物可以移动"); // 输出动物可以移动
   }
}
 
class Dog extends Animal{ // Dog 类继承自 Animal 类
   public void move(){ // 重写了父类的 move 方法
      super.move(); // 调用父类的 move 方法
      System.out.println("狗可以跑和走"); // 输出狗可以跑和走
   }
}
 
public class TestDog{
   public static void main(String args[]){
 
      Animal b = new Dog(); // 创建 Dog 对象,但是引用类型是 Animal
 
      b.move(); // 调用 Dog 类的 move 方法,输出动物可以移动和狗可以跑和走
   }
}

以上实例编译运行结果如下:

动物可以移动
狗可以跑和走

4. 重载(Overload)

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

最常用的地方就是构造器的重载。

重载规则:

  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。

实例

public class Overloading {
    public int test(){ // 定义一个返回类型为 int 的 test 方法,无参数
        System.out.println("test1"); // 输出 test1
        return 1; // 返回值为 1
    }
 
    public void test(int a){ // 定义一个无返回值的 test 方法,参数为一个 int 类型的变量 a
        System.out.println("test2"); // 输出 test2
    }   
 
    // 以下两个方法参数类型顺序不同
    public String test(int a, String s){ // 定义一个返回类型为 String 的 test 方法,参数为一个 int 类型的变量 a 和一个 String 类型的变量 s
        System.out.println("test3"); // 输出 test3
        return "returntest3"; // 返回值为 "returntest3"
    }   
 
    public String test(String s, int a){ // 定义一个返回类型为 String 的 test 方法,参数为一个 String 类型的变量 s 和一个 int 类型的变量 a
        System.out.println("test4"); // 输出 test4
        return "returntest4"; // 返回值为 "returntest4"
    }   
 
    public static void main(String[] args){
        Overloading o = new Overloading(); // 创建 Overloading 对象
        System.out.println(o.test()); // 调用 test 方法,输出 test1,然后打印返回值 1
        o.test(1); // 调用带有一个 int 参数的 test 方法,输出 test2
        System.out.println(o.test(1, "test3")); // 调用带有一个 int 和一个 String 参数的 test 方法,输出 test3,然后打印返回值 "returntest3"
        System.out.println(o.test("test4", 1)); // 调用带有一个 String 和一个 int 参数的 test 方法,输出 test4,然后打印返回值 "returntest4"
    }
}


5. 重写与重载之间的区别

区别点重载方法重写方法
参数列表必须修改一定不能修改
返回类型可以修改一定不能修改
异常可以修改可以减少或删除,一定不能抛出新的或者更广的异常
访问可以修改一定不能做更严格的限制(可以降低限制)

总结

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  • (1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • (2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • (3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

;