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

Java类的继承与多态性

2021-04-055.9k 阅读

Java 类的继承

在 Java 编程中,继承是一项核心特性,它允许创建一个新类,这个新类基于已有的类,从而复用已有类的属性和方法。这种机制不仅提高了代码的复用性,还增强了代码的组织性和可维护性。

继承的基本概念

继承意味着一个类可以获取另一个类的成员(字段和方法)。被继承的类称为超类(superclass)、基类(base class)或父类(parent class),而继承的类称为子类(subclass)、派生类(derived class)或孩子类(child class)。

在 Java 中,使用 extends 关键字来实现继承。例如,假设有一个 Animal 类,它有一些通用的属性和方法,如 nameeat() 方法。现在我们想创建一个 Dog 类,它继承自 Animal 类,这样 Dog 类就自动拥有了 Animal 类的 name 属性和 eat() 方法。

class Animal {
    String name;

    void eat() {
        System.out.println(name + " is eating.");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println(name + " is barking.");
    }
}

在上述代码中,Dog 类通过 extends 关键字继承了 Animal 类。这意味着 Dog 类可以使用 Animal 类中的 name 属性和 eat() 方法,同时还可以定义自己特有的 bark() 方法。

继承的访问控制

在继承关系中,访问控制修饰符起着重要作用。Java 中有四种访问控制修饰符:privatedefault(默认,不写修饰符时即为 default)、protectedpublic

  1. private 修饰符:被 private 修饰的成员只能在定义它们的类内部被访问。子类无法直接访问父类的 private 成员。例如,在 Animal 类中,如果将 name 属性改为 private
class Animal {
    private String name;

    void eat() {
        System.out.println(name + " is eating.");
    }
}

class Dog extends Animal {
    void bark() {
        // 下面这行代码会报错,因为无法访问父类的 private 成员
        // System.out.println(name + " is barking.");
    }
}
  1. default 修饰符:默认访问修饰符允许在同一个包内的类访问。如果一个类或成员没有显式地声明访问修饰符,那么它就是 default 访问级别。例如,如果 Animal 类和 Dog 类在同一个包中,Dog 类可以访问 Animal 类的 default 成员。
package com.example;

class Animal {
    String name;

    void eat() {
        System.out.println(name + " is eating.");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println(name + " is barking.");
    }
}
  1. protected 修饰符protected 修饰的成员可以在同一个包内的类以及不同包中的子类访问。这使得子类可以访问父类中一些需要被保护的成员。例如,将 Animal 类的 name 属性改为 protected
class Animal {
    protected String name;

    void eat() {
        System.out.println(name + " is eating.");
    }
}

package com.anotherpackage;
import com.example.Animal;

class Dog extends Animal {
    void bark() {
        System.out.println(name + " is barking.");
    }
}
  1. public 修饰符public 修饰的成员可以被任何类访问,无论在哪个包中。这是最宽松的访问级别。例如,将 Animal 类的 name 属性和 eat() 方法都改为 public
class Animal {
    public String name;

    public void eat() {
        System.out.println(name + " is eating.");
    }
}

package com.anotherpackage;
import com.example.Animal;

class Dog extends Animal {
    void bark() {
        System.out.println(name + " is barking.");
    }
}

继承中的构造函数

在继承关系中,构造函数也有其特殊的行为。子类的构造函数默认会调用父类的无参构造函数。这是因为在创建子类对象时,首先需要初始化父类的状态。

例如,在 Animal 类中添加一个有参构造函数:

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void eat() {
        System.out.println(name + " is eating.");
    }
}

class Dog extends Animal {
    Dog(String name) {
        // 这里会隐式调用父类的无参构造函数,但是父类没有无参构造函数,会报错
        // 解决办法是在子类构造函数中显式调用父类的有参构造函数
        super(name);
    }

    void bark() {
        System.out.println(name + " is barking.");
    }
}

Dog 类的构造函数中,通过 super(name) 显式调用了父类 Animal 的有参构造函数。这样,在创建 Dog 对象时,父类的 name 属性会被正确初始化。

方法重写

当子类继承父类后,有时候需要对父类的某些方法进行不同的实现。这就用到了方法重写(method overriding)。方法重写要求子类中的方法与父类中的方法具有相同的方法签名(方法名、参数列表和返回类型)。

例如,在 Animal 类中有一个 makeSound() 方法,Dog 类可以重写这个方法来实现自己特有的叫声:

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

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

Dog 类的 makeSound() 方法上使用 @Override 注解,这是一个可选但强烈推荐的做法,它可以帮助编译器检查该方法是否确实重写了父类的方法。如果方法签名与父类不匹配,编译器会报错。

重写的规则

  1. 方法签名必须相同:方法名、参数列表和返回类型(在 Java 5.0 及以后,返回类型可以是父类方法返回类型的子类型,称为协变返回类型)必须与父类方法一致。例如:
class Animal {
    Animal getInstance() {
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    Dog getInstance() {
        return new Dog();
    }
}
  1. 访问权限不能更严格:子类重写方法的访问权限不能比父类被重写方法的访问权限更严格。例如,如果父类方法是 protected,子类重写方法不能是 private,但可以是 protectedpublic
class Animal {
    protected void doSomething() {
        System.out.println("Animal is doing something.");
    }
}

class Dog extends Animal {
    @Override
    public void doSomething() {
        System.out.println("Dog is doing something.");
    }
}
  1. 不能重写 final 方法:如果父类方法被声明为 final,子类不能重写该方法。例如:
class Animal {
    final void finalMethod() {
        System.out.println("This is a final method.");
    }
}

class Dog extends Animal {
    // 下面这行代码会报错,因为不能重写 final 方法
    // @Override
    // void finalMethod() {
    //     System.out.println("This will not work.");
    // }
}
  1. 不能重写 static 方法:虽然子类可以定义与父类 static 方法同名的 static 方法,但这不是方法重写,而是方法隐藏。例如:
class Animal {
    static void staticMethod() {
        System.out.println("Animal's static method.");
    }
}

class Dog extends Animal {
    static void staticMethod() {
        System.out.println("Dog's static method.");
    }
}

在这里,Dog 类的 staticMethod() 方法隐藏了 Animal 类的 staticMethod() 方法。调用 static 方法时,取决于引用的类型,而不是对象的实际类型。

Java 类的多态性

多态性是 Java 面向对象编程的另一个重要特性。它允许使用一个父类类型的变量来引用不同子类类型的对象,并根据对象的实际类型调用相应的方法。多态性使得代码更加灵活和可扩展。

多态性的概念

多态性可以分为两种类型:编译时多态性和运行时多态性。

  1. 编译时多态性(静态多态性):编译时多态性通过方法重载(method overloading)实现。方法重载是指在同一个类中定义多个方法,这些方法具有相同的方法名,但参数列表不同(参数个数、参数类型或参数顺序不同)。编译器在编译时根据调用方法时提供的参数来确定要调用的具体方法。

例如:

class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    double add(double a, double b) {
        return a + b;
    }
}

Calculator 类中,有两个 add 方法,一个接受两个 int 类型的参数,另一个接受两个 double 类型的参数。编译器会根据调用 add 方法时传入的参数类型来决定调用哪个方法。

  1. 运行时多态性(动态多态性):运行时多态性通过方法重写和向上转型实现。向上转型是指将子类对象赋值给父类类型的变量。在运行时,Java 虚拟机(JVM)会根据对象的实际类型来调用相应的方法。

例如:

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

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

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

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() 方法时,JVM 会根据对象的实际类型(DogCat)来调用相应的重写方法,输出“Dog barks.”和“Cat meows.”。

向上转型和向下转型

  1. 向上转型:向上转型是将子类对象赋值给父类类型的变量。这是安全的,因为子类对象是父类对象的一种特殊类型。例如:
Dog dog = new Dog();
Animal animal = dog;

这里,dogDog 类型的对象,animalAnimal 类型的变量,将 dog 赋值给 animal 就是向上转型。此时,animal 变量只能访问 Animal 类中定义的方法和属性,即使实际指向的是 Dog 对象。

  1. 向下转型:向下转型是将父类类型的变量转换为子类类型的变量。这需要进行显式的类型转换,并且在运行时可能会抛出 ClassCastException 异常,除非该父类变量实际指向的是子类对象。例如:
Animal animal = new Dog();
Dog dog = (Dog) animal;

在上述代码中,首先将 Dog 对象向上转型为 Animal 类型的变量 animal,然后再将 animal 向下转型为 Dog 类型的变量 dog。由于 animal 实际指向的是 Dog 对象,所以这个向下转型是成功的。

但是,如果 animal 实际指向的不是 Dog 对象,就会抛出 ClassCastException 异常。例如:

Animal animal = new Cat();
Dog dog = (Dog) animal; // 这里会抛出 ClassCastException

为了避免 ClassCastException 异常,可以在向下转型之前使用 instanceof 关键字进行类型检查。instanceof 运算符用于检查一个对象是否是某个特定类型的实例。例如:

Animal animal = new Cat();
if (animal instanceof Dog) {
    Dog dog = (Dog) animal;
} else {
    System.out.println("The animal is not a Dog.");
}

多态性的优势

  1. 代码的灵活性:多态性使得代码可以处理不同类型的对象,而不需要为每种类型编写特定的代码。例如,假设有一个 printSound 方法,它接受一个 Animal 类型的参数:
void printSound(Animal animal) {
    animal.makeSound();
}

这个方法可以接受任何 Animal 子类(如 DogCat 等)的对象,而不需要为每个子类编写单独的方法。这大大提高了代码的灵活性。

  1. 可扩展性:当需要添加新的子类时,多态性使得代码不需要进行大规模的修改。例如,添加一个新的 Bird 类继承自 Animal 类,只需要实现 makeSound 方法,printSound 方法就可以直接处理 Bird 对象,而不需要修改 printSound 方法的代码。
class Bird extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bird chirps.");
    }
}
  1. 提高代码的可维护性:多态性使得代码结构更加清晰,每个类只负责自己的功能实现。通过父类类型的变量来操作子类对象,可以减少代码中的重复部分,提高代码的可维护性。

多态性的实现原理

在 Java 中,运行时多态性是通过动态方法调度(dynamic method dispatch)实现的。当一个对象调用一个重写的方法时,JVM 会在运行时根据对象的实际类型来确定调用哪个类的方法。

JVM 使用方法表(method table)来实现动态方法调度。每个类都有一个方法表,其中包含了该类及其父类中所有非 private、非 static、非 final 方法的入口地址。当对象调用一个方法时,JVM 首先根据对象的实际类型找到对应的方法表,然后在方法表中查找要调用的方法的入口地址,并调用该方法。

例如,在前面的 AnimalDogCat 类的例子中,DogCat 类都有自己的方法表。当 animal1.makeSound() 被调用时,JVM 根据 animal1 实际指向的 Dog 对象,在 Dog 类的方法表中找到 makeSound 方法的入口地址并调用它;当 animal2.makeSound() 被调用时,JVM 根据 animal2 实际指向的 Cat 对象,在 Cat 类的方法表中找到 makeSound 方法的入口地址并调用它。

多态性在实际应用中的案例

  1. 图形绘制系统:假设有一个图形绘制系统,其中有 Shape 类作为父类,CircleRectangleTriangle 类继承自 Shape 类。每个子类都重写了 draw 方法来实现自己的绘制逻辑。
class Shape {
    void draw() {
        System.out.println("Drawing a shape.");
    }
}

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 GraphicsSystem {
    public static void main(String[] args) {
        Shape[] shapes = {new Circle(), new Rectangle(), new Triangle()};
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}

在上述代码中,通过 Shape 类型的数组可以存储不同类型的图形对象,并通过调用 draw 方法实现多态绘制。

  1. 游戏角色系统:在一个游戏中,有 Character 类作为父类,WarriorMageThief 类继承自 Character 类。每个子类都有自己独特的 attack 方法。
class Character {
    void attack() {
        System.out.println("Character attacks.");
    }
}

class Warrior extends Character {
    @Override
    void attack() {
        System.out.println("Warrior attacks with a sword.");
    }
}

class Mage extends Character {
    @Override
    void attack() {
        System.out.println("Mage casts a spell.");
    }
}

class Thief extends Character {
    @Override
    void attack() {
        System.out.println("Thief attacks stealthily.");
    }
}

public class Game {
    public static void main(String[] args) {
        Character[] characters = {new Warrior(), new Mage(), new Thief()};
        for (Character character : characters) {
            character.attack();
        }
    }
}

在这个游戏角色系统中,通过多态性可以方便地管理和操作不同类型的角色,根据角色的实际类型调用相应的攻击方法。

通过理解和掌握 Java 类的继承与多态性,开发者可以编写出更加灵活、可扩展和可维护的代码,这对于构建大型、复杂的 Java 应用程序至关重要。无论是在企业级开发、移动应用开发还是其他领域,继承和多态性都是不可或缺的重要特性。在实际编程中,合理运用继承和多态性可以提高代码的质量和开发效率,同时也能更好地满足业务需求的变化。

在使用继承时,要注意合理设计继承层次结构,避免过度继承导致代码的复杂性增加。同时,在方法重写和多态性的实现中,要严格遵循相关的规则和约定,以确保程序的正确性和稳定性。通过不断地实践和优化,开发者能够更加熟练地运用继承与多态性,打造出高质量的 Java 软件产品。

在处理多态性时,还需要注意性能问题。虽然动态方法调度在大多数情况下不会带来明显的性能开销,但在一些对性能要求极高的场景下,如高性能计算或实时系统中,可能需要对代码进行优化。例如,可以通过适当的设计模式或使用 final 方法来减少动态方法调度的次数,从而提高性能。

另外,在团队开发中,对于继承和多态性的使用要保持清晰的文档和沟通。因为不同的开发者可能对继承层次和多态行为有不同的理解,良好的文档和沟通可以避免因误解而导致的代码错误和维护困难。

在 Java 类库中,继承和多态性也被广泛应用。例如,java.util.List 接口有多个实现类,如 ArrayListLinkedList。通过使用 List 类型的变量,可以实现多态地操作不同类型的列表,而不需要关心具体的实现类。这体现了 Java 类库设计中对继承和多态性的巧妙运用,为开发者提供了极大的便利。

总之,Java 类的继承与多态性是 Java 语言的核心特性,深入理解和掌握它们对于成为一名优秀的 Java 开发者至关重要。通过不断地学习和实践,开发者可以充分发挥这些特性的优势,编写出更加高效、灵活和健壮的 Java 程序。