MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java多态中继承与方法重写的关系

2021-07-187.4k 阅读

Java 多态中继承与方法重写的基础概念

继承的基本概念

在 Java 中,继承是一种重要的特性,它允许一个类获取另一个类的属性和方法。通过继承,我们可以创建一个新类(子类或派生类),这个新类基于一个已有的类(父类或基类)。使用 extends 关键字来实现继承关系。例如:

class Animal {
    String name;
    void eat() {
        System.out.println("Animal is eating.");
    }
}

class Dog extends Animal {
    // Dog 类继承自 Animal 类
    void bark() {
        System.out.println("Dog is barking.");
    }
}

在上述代码中,Dog 类继承了 Animal 类,因此 Dog 类拥有了 Animal 类的 name 属性和 eat() 方法。这使得代码具有更好的复用性,我们无需在 Dog 类中重复编写 eat() 方法的代码。

继承不仅可以传递属性和方法,还体现了一种 “is - a” 的关系。在这里,Dog 类 “is - a” Animal,这是一种自然的层级关系体现。

方法重写的概念

方法重写发生在继承关系中,当子类对从父类继承而来的方法不满意,希望提供自己的实现时,就可以进行方法重写。重写的方法需要满足以下几个条件:

  1. 方法签名必须相同:包括方法名、参数列表和返回类型(在 Java 5.0 之后,返回类型可以是父类方法返回类型的子类,称为协变返回类型)。
  2. 访问修饰符不能更严格:例如,如果父类方法是 public,子类重写的方法不能是 privateprotected

以下是一个方法重写的示例:

class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

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

在这个例子中,Cat 类继承自 Animal 类,并对 makeSound() 方法进行了重写。@Override 注解是可选的,但使用它可以让编译器帮助我们检查是否正确重写了方法。如果不小心写错了方法签名,编译器会报错,提高了代码的正确性。

继承与方法重写在多态中的角色

多态的基本概念

多态是 Java 面向对象编程的三大特性之一(另外两个是封装和继承)。多态允许我们使用父类类型的变量来引用子类的对象,并根据对象的实际类型来调用相应的方法。这使得代码更加灵活和可维护。

例如,有一个 Animal 类和它的子类 DogCat

class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

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

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

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Dog();
        Animal animal2 = new Cat();

        animal1.makeSound();
        animal2.makeSound();
    }
}

在上述代码中,animal1animal2 都是 Animal 类型的变量,但分别引用了 DogCat 的对象。当调用 makeSound() 方法时,实际执行的是 DogCat 类中重写的方法,而不是 Animal 类中的原始方法。这就是多态的体现,程序能够根据对象的实际类型来决定调用哪个方法。

继承为多态提供基础

继承是多态的基础。如果没有继承关系,就无法实现多态。因为多态依赖于父类与子类之间的 “is - a” 关系。通过继承,子类获得了父类的特征,并且可以在需要的时候对这些特征进行扩展或修改。

在前面的例子中,如果 DogCat 类没有继承自 Animal 类,就不能将它们的对象赋值给 Animal 类型的变量。正是因为继承,使得 DogCat 类 “is - a” Animal,从而满足了多态的前提条件。

继承不仅传递了属性和方法,还传递了类型信息。这意味着子类对象可以在任何需要父类对象的地方使用,为多态的实现提供了可能。例如,我们可以创建一个方法,它接受 Animal 类型的参数,然后传入 DogCat 的对象:

class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

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

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

public class Main {
    static void animalSound(Animal animal) {
        animal.makeSound();
    }

    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();

        animalSound(dog);
        animalSound(cat);
    }
}

在这个例子中,animalSound() 方法接受 Animal 类型的参数,这得益于继承关系,DogCat 对象都可以作为参数传递进去,从而实现了多态。

方法重写实现多态的动态绑定

方法重写是实现多态的关键步骤。当使用父类类型的变量引用子类对象并调用重写方法时,Java 虚拟机(JVM)会根据对象的实际类型在运行时动态地决定调用哪个方法,这就是动态绑定。

在前面的例子中,animal1.makeSound()animal2.makeSound() 的调用,JVM 在运行时会根据 animal1animal2 实际引用的对象类型(DogCat)来调用相应的 makeSound() 方法。

动态绑定是多态的核心机制,它使得程序在运行时能够根据对象的实际类型做出正确的行为。这与静态绑定不同,静态绑定是在编译时就确定了调用哪个方法,而动态绑定增加了程序的灵活性和扩展性。

例如,如果我们添加一个新的子类 Bird 继承自 Animal,并对 makeSound() 方法进行重写:

class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

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

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

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

public class Main {
    static void animalSound(Animal animal) {
        animal.makeSound();
    }

    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Bird bird = new Bird();

        animalSound(dog);
        animalSound(cat);
        animalSound(bird);
    }
}

在这个扩展的例子中,即使 animalSound() 方法没有改变,我们只需要添加新的子类并正确重写 makeSound() 方法,就可以在运行时动态地调用新子类的方法,体现了多态的灵活性和扩展性。

继承与方法重写的深入探讨

继承中的访问控制与方法重写

在继承中,访问控制修饰符对方法重写有着重要的影响。如前文所述,子类重写方法的访问修饰符不能比父类更严格。这是因为多态的机制要求父类类型的变量能够正确地调用子类重写的方法。

例如,如果父类方法是 public,子类重写的方法也必须是 public。如果子类将重写方法定义为 private,那么当使用父类类型的变量调用该方法时,就会出现错误,因为 private 方法是无法从外部访问的,这就破坏了多态的机制。

以下是一个错误示例:

class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

class Dog extends Animal {
    // 错误:访问修饰符比父类更严格
    private void makeSound() {
        System.out.println("Woof!");
    }
}

在这个例子中,Dog 类对 makeSound() 方法的重写是错误的,因为 privatepublic 更严格。编译器会报错,提示 “method does not override or implement a method from a supertype”。

如果父类方法是 protected,子类重写方法可以是 protectedpublic。因为 protected 允许子类访问,而 public 则提供了更广泛的访问权限,都符合多态的要求。

方法重写与静态方法

静态方法不能被重写,尽管子类可以定义与父类静态方法具有相同签名的方法,但这并不是真正的重写。静态方法属于类,而不是对象,在编译时就确定了调用哪个静态方法,不会发生动态绑定。

例如:

class Animal {
    static void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

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

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.makeSound();
    }
}

在这个例子中,animal.makeSound() 调用的是 Animal 类的 makeSound() 静态方法,而不是 Dog 类的。这是因为静态方法的调用是基于引用类型,而不是对象的实际类型。如果希望通过子类对象调用子类的静态方法,应该使用子类的类名来调用,例如 Dog.makeSound()

方法重写与 final 关键字

final 关键字在继承和方法重写中有特殊的作用。如果一个方法被声明为 final,则不能被重写。这是为了防止子类对该方法进行修改,保证方法的行为一致性。

例如:

class Animal {
    final void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

class Dog extends Animal {
    // 错误:无法重写 final 方法
    @Override
    void makeSound() {
        System.out.println("Woof!");
    }
}

在这个例子中,Dog 类试图重写 Animal 类的 final 方法 makeSound(),编译器会报错,提示 “cannot override; method is final”。

同样,如果一个类被声明为 final,则不能被继承。这在一些情况下很有用,比如 String 类就是 final 类,保证了 String 对象的不可变性,避免了子类对其行为的意外修改。

继承与方法重写中的构造函数

在继承关系中,子类的构造函数会默认调用父类的无参构造函数。这是因为在创建子类对象时,首先需要初始化父类的部分,然后再初始化子类特有的部分。

例如:

class Animal {
    Animal() {
        System.out.println("Animal constructor");
    }
}

class Dog extends Animal {
    Dog() {
        System.out.println("Dog constructor");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
    }
}

在这个例子中,当创建 Dog 对象时,会先调用 Animal 类的构造函数,输出 “Animal constructor”,然后再调用 Dog 类的构造函数,输出 “Dog constructor”。

如果父类没有无参构造函数,子类的构造函数必须使用 super 关键字显式调用父类的构造函数,并传递相应的参数。例如:

class Animal {
    int age;
    Animal(int age) {
        this.age = age;
        System.out.println("Animal constructor with age: " + age);
    }
}

class Dog extends Animal {
    Dog(int age) {
        super(age);
        System.out.println("Dog constructor");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog(5);
    }
}

在这个例子中,Dog 类的构造函数使用 super(age) 调用了 Animal 类带参数的构造函数,确保父类的 age 属性得到正确初始化。

在方法重写中,构造函数不会被重写。构造函数的名称与类名相同,子类的构造函数是独立于父类构造函数的,它们有着不同的作用和调用方式。

继承与方法重写的实际应用场景

图形绘制系统中的应用

在一个简单的图形绘制系统中,我们可以利用继承和方法重写实现多态。假设有一个 Shape 类作为父类,它有一个 draw() 方法用于绘制图形。然后有 CircleRectangleTriangle 等子类继承自 Shape 类,并各自重写 draw() 方法来实现具体的绘制逻辑。

abstract class Shape {
    abstract void draw();
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle.");
    }
}

class Rectangle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a rectangle.");
    }
}

class Triangle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a triangle.");
    }
}

public class Main {
    static void drawShape(Shape shape) {
        shape.draw();
    }

    public static void main(String[] args) {
        Circle circle = new Circle();
        Rectangle rectangle = new Rectangle();
        Triangle triangle = new Triangle();

        drawShape(circle);
        drawShape(rectangle);
        drawShape(triangle);
    }
}

在这个例子中,drawShape() 方法接受 Shape 类型的参数,通过多态可以传入不同子类的对象,实现不同图形的绘制。这使得代码更加灵活,易于扩展。如果我们需要添加新的图形类型,只需要创建新的子类继承自 Shape 类并重写 draw() 方法即可,而不需要修改 drawShape() 方法的代码。

游戏开发中的应用

在游戏开发中,继承和方法重写也有着广泛的应用。例如,有一个 Character 类作为游戏角色的基类,它有一些通用的方法,如 move()attack() 等。然后有 WarriorMageArcher 等子类继承自 Character 类,并根据各自的特点重写这些方法。

class Character {
    void move() {
        System.out.println("Character is moving.");
    }

    void attack() {
        System.out.println("Character is attacking.");
    }
}

class Warrior extends Character {
    @Override
    void move() {
        System.out.println("Warrior is running.");
    }

    @Override
    void attack() {
        System.out.println("Warrior is slashing with a sword.");
    }
}

class Mage extends Character {
    @Override
    void move() {
        System.out.println("Mage is teleporting.");
    }

    @Override
    void attack() {
        System.out.println("Mage is casting a spell.");
    }
}

class Archer extends Character {
    @Override
    void move() {
        System.out.println("Archer is sneaking.");
    }

    @Override
    void attack() {
        System.out.println("Archer is shooting an arrow.");
    }
}

public class Main {
    static void performActions(Character character) {
        character.move();
        character.attack();
    }

    public static void main(String[] args) {
        Warrior warrior = new Warrior();
        Mage mage = new Mage();
        Archer archer = new Archer();

        performActions(warrior);
        performActions(mage);
        performActions(archer);
    }
}

在这个游戏角色的例子中,performActions() 方法可以对不同类型的角色执行通用的动作,通过继承和方法重写实现了每个角色独特的行为,增加了游戏的趣味性和多样性。同时,这种设计也使得代码结构清晰,易于维护和扩展。如果需要添加新的角色类型,只需要创建新的子类并按照需求重写相应的方法即可。

企业级应用开发中的应用

在企业级应用开发中,继承和方法重写常用于框架设计和业务逻辑实现。例如,在一个基于 Spring 框架的企业级应用中,可能会有一个 BaseService 类,它包含了一些通用的业务操作方法,如数据持久化、事务管理等。然后各个具体的业务服务类,如 UserServiceProductService 等继承自 BaseService 类,并根据具体业务需求重写部分方法。

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
class BaseService {
    @Transactional
    void save(Object object) {
        // 通用的数据持久化逻辑
        System.out.println("Saving object in base service.");
    }
}

@Service
class UserService extends BaseService {
    @Override
    @Transactional
    void save(Object object) {
        // 对用户数据进行额外的验证等操作后再调用父类的 save 方法
        System.out.println("Validating user data...");
        super.save(object);
    }
}

@Service
class ProductService extends BaseService {
    @Override
    @Transactional
    void save(Object object) {
        // 对产品数据进行特殊处理后再调用父类的 save 方法
        System.out.println("Processing product data...");
        super.save(object);
    }
}

在这个企业级应用的例子中,UserServiceProductService 继承自 BaseService,并根据自身业务需求重写了 save() 方法。通过这种方式,既复用了 BaseService 中的通用逻辑,又能针对不同业务场景进行定制化处理。同时,Spring 框架的事务管理注解 @Transactional 也可以在子类重写方法中继续使用,保证了业务操作的事务一致性。这种基于继承和方法重写的设计模式在企业级应用开发中能够提高代码的复用性和可维护性,加快开发速度。

继承与方法重写的常见问题与解决方法

重写方法的签名不一致问题

在进行方法重写时,最常见的错误之一就是重写方法的签名与父类方法不一致。这可能导致编译器报错,提示 “method does not override or implement a method from a supertype”。

例如,在以下代码中:

class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

class Dog extends Animal {
    // 错误:方法签名不一致,多了一个参数
    void makeSound(int volume) {
        System.out.println("Woof! with volume: " + volume);
    }
}

在这个例子中,Dog 类的 makeSound(int volume) 方法与 Animal 类的 makeSound() 方法签名不一致,因此不是重写。要解决这个问题,需要确保重写方法的方法名、参数列表和返回类型(或协变返回类型)与父类方法完全一致。

访问控制修饰符错误

如前文所述,子类重写方法的访问修饰符不能比父类更严格。如果违反了这个规则,编译器也会报错。

例如:

class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

class Dog extends Animal {
    // 错误:访问修饰符比父类更严格
    private void makeSound() {
        System.out.println("Woof!");
    }
}

要解决这个问题,需要将子类重写方法的访问修饰符改为与父类相同或更宽松的访问级别,例如将 private 改为 public

静态方法与实例方法混淆

在继承关系中,容易混淆静态方法和实例方法的重写规则。静态方法不能被重写,而实例方法可以。如果错误地认为静态方法可以被重写,可能会导致程序运行结果不符合预期。

例如:

class Animal {
    static void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

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

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.makeSound();
    }
}

在这个例子中,animal.makeSound() 调用的是 Animal 类的静态方法,而不是 Dog 类的。要正确调用 Dog 类的静态方法,应该使用 Dog.makeSound()。如果希望实现类似重写的效果,应该将静态方法改为实例方法。

调用父类被重写的方法

在子类重写方法中,有时需要调用父类被重写的方法。可以使用 super 关键字来实现这一点。

例如:

class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound.");
    }
}

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

在这个例子中,Dog 类的 makeSound() 方法先调用了父类的 makeSound() 方法,然后输出了自己的特定声音。这在一些情况下很有用,比如需要在子类重写方法中先执行父类的通用逻辑,再添加子类特有的逻辑。

总结

继承与方法重写是 Java 多态的重要组成部分。继承为多态提供了基础,通过建立类之间的层级关系,使得子类能够复用父类的属性和方法,并在需要时进行扩展。方法重写则是实现多态的关键,通过在子类中提供与父类方法相同签名的方法,实现了动态绑定,使得程序能够根据对象的实际类型在运行时调用正确的方法。

在实际应用中,继承和方法重写广泛应用于各种领域,如图形绘制、游戏开发、企业级应用开发等,它们提高了代码的复用性、灵活性和可维护性。然而,在使用过程中也需要注意一些常见问题,如重写方法的签名一致性、访问控制修饰符的正确性、静态方法与实例方法的区别等,避免出现错误。

通过深入理解继承与方法重写的关系,开发者能够更好地运用 Java 的多态特性,编写出更加健壮、灵活和高效的程序。