Lihu Zhai
Software Architect 位于 Ruijie Networks

Java 多态

已在 China 群组中发布
与 OOP 相关的问题是应聘 IT 公司 Java 开发人员职位技术面试中不可或缺的一部分。在本文中,我们将讨论 OOP 的一个原理:多态。我们将重点介绍在面试中经常被问到的问题,并提供几个例子进行说明。

Java 的多态是什么?

多态是程序以相同的方式处理具有相同接口的对象的能力,而不需要关于对象的具体类型的信息。如果让你回答什么是多态,你很可能要对你的理解进行阐释。在不引发一堆其他问题的情况下,再一次向面试官展示你对此的理解。 Java 多态 - 1你可以从这样一个事实开始:OOP 方法涉及到基于对象之间的交互来构建 Java 程序,而对象之间的交互是基于类的。类是以前编写的蓝图(模板),用于在程序中创建对象。此外,类始终包含一个特定的类型,在规范的编程风格下,它有一个表明其用途的名称。 此外,可以注意到,由于 Java 是强类型语言,所以当声明变量时,程序代码必须始终指定对象类型。此外,严格类型化提高了代码的安全性和可靠性,即使在编译时,也可以防止由于类型不兼容而导致的错误(例如,试图用一个数字除一个字符串)。自然,编译器必须“知道”声明的类型——它可以是 JDK 中的一个类,也可以是我们自己创建的一个类。 向面试官指出,我们的代码不仅可以使用声明中指出的类型的对象,还可以使用它的后代。这是很重要的一点:我们可以将许多不同的类型作为一个类型使用(前提是这些类型是从一个基本类型派生的)。这也意味着,如果我们声明一个类型为超类的变量,那么就可以将它的一个后代的实例赋给该变量。 举个例子,面试官会对你赞赏有加。选择某个可以被几个类(的基类)共享的类并让一些类继承它。 基类:

public class Dancer {
    private String name;
    private int age;

    public Dancer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void dance() {
        System.out.println(toString() + "我像其他人一样跳舞。");
    }

    @Override
    public String toString() {
        return "我是" + name + "。我 " + age + " 岁。";
    }
}
在子类中,重写基类的方法:

public class ElectricBoogieDancer extends Dancer {
    public ElectricBoogieDancer(String name, int age) {
        super(name, age);
    }
//重写基类的方法
    @Override
    public void dance() {
        System.out.println(toString () + "我跳电动布吉舞!");
    }
}

public class Breakdancer extends Dancer {

    public Breakdancer(String name, int age) {
        super(name, age);
    }
//重写基类的方法
    @Override
    public void dance() {
        System.out.println(toString() + "我跳霹雳舞!");
    }
}
多态的一个例子,以及如何在程序中使用这些对象:

public class Main {

    public static void main(String[] args) {
        Dancer dancer = new Dancer("弗雷德", 18);

        Dancer breakdancer = new Breakdancer("杰伊", 19); // 拓宽到基类型的转换
        Dancer electricBoogieDancer = new ElectricBoogieDancer("玛西娅", 20); // 拓宽到基类型的转换

        List<dancer> disco = Arrays.asList(dancer, breakdancer, electricBoogieDancer);
        for (Dancer d : disco) {
            d.dance(); // 调用多态方法
        }
    }
}
main 方法中,显示下面几行

Dancer breakdancer = new Breakdancer("杰伊", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("玛西娅", 20);
声明超类的变量,并给它分配给作为其后代之一实例的对象。你很可能会问,为什么编译器不会对赋值运算符左右两边声明的类型不一致感到惊讶 — 毕竟,Java 是强类型语言。因为这里使用的是拓宽类型转换 — 对对象的引用被视为对其基类的引用。此外,在代码中遇到这样的结构后,编译器会自动隐式地执行转换。 示例代码显示在赋值运算符左侧声明的类型 (Dancer) 有多种形式(类型),这些类型在右侧声明(BreakdancerElectricBoogieDancer)。相对于超类中定义的一般功能(dance 方法),每个形式都可以有自己独特的行为。也就是说,在超类中声明的方法可能在它的后代中以不同的方式实现。在这种情况下,我们处理方法重写,这正是创建多种形式(行为)的原因。通过运行 main 方法中的代码可以了解这一点: 程序输出:
我是弗雷德。我 18 岁。我像其他人一样跳舞。 我是杰伊。我 19 岁。我跳霹雳舞! 我是玛西娅。我 20 岁。我跳电动布吉舞!
如果我们不重写子类中的方法,那么我们不会得到不同的行为。例如,如果我们在 Breakdancerelectricboogidancer 类中注释掉 dance 方法,那么程序的输出将是这样的:
我是弗雷德。我 18 岁。我像其他人一样跳舞。 我是杰伊。我 19 岁。我像其他人一样跳舞。 我是玛西娅。我 20 岁。我像其他人一样跳舞。
这意味着创建 BreakdancerElectricBoogieDancer 类是没有意义的。 多态原理具体体现在哪里?如果不知道对象的具体类型,它在程序中的什么地方使用呢?在我们的示例中,当对 Dancer d 对象调用 dance() 方法时,就会发生这种情况。在 Java 中,多态意味着程序不需要知道对象是 Breakdancer 还是 ElectricBoogieDancer。重要的是它是 Dancer 类的后代。 如果你提到后代,你应该注意到 Java 中的继承不仅仅是扩展,还包括实现。现在是时候提到 Java 不支持多重继承了— 每种类型可以有一个父类(超类)和无限数量的后代(子类)。相应地,接口用于向类添加多组函数。 与子类(继承)相比,接口与父类的耦合更少。它们被广泛使用。在 Java 中,接口是一个引用类型,所以程序可以声明一个接口类型的变量。现在举个例子。 创建接口:

public interface CanSwim {
    void swim();
}
为了清楚起见,我们将采用各种不相关的类,并让它们实现接口:

public class Human implements CanSwim {
    private String name;
    private int age;

    public Human(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void swim() {
        System.out.println(toString()+" 我游泳时带着充气管子。");
    }

    @Override
    public String toString() {
        return "我是" + name + "。我 " + age + " 岁。";
    }

}

public class Fish implements CanSwim {
    private String name;

    public Fish(String name) {
        this.name = name;
    }

    @Override
    public void swim() {
        System.out.println("我是一条鱼。我叫 " + name + "。我通过摆动我的鳍来游泳。");

    }

public class UBoat implements CanSwim {

    private int speed;

    public UBoat(int speed) {
        this.speed = speed;
    }

    @Override
    public void swim() {
        System.out.println("我是一艘靠旋转螺旋推进器游过水面的潜艇。我的速度是 " + speed + " 节。");
    }
}
main 方法:

public class Main {

    public static void main(String[] args) {
        CanSwim human = new Human("约翰", 6);
        CanSwim fish = new Fish("鲸");
        CanSwim boat = new UBoat(25);

        List<swim> swimmers = Arrays.asList(human, fish, boat);
        for (Swim s : swimmers) {
            s.swim();
        }
    }
}
调用接口中定义的多态方法的结果会向我们展示实现该接口的类型的行为差异。在所示的例子中,这些是 swim 方法显示的不同字符串。 在研究了所示的例子之后,面试官可能会问为什么在 main 方法中运行这个代码

for (Swim s : swimmers) {
            s.swim();
}
导致子类中定义的重写方法被调用。当程序运行时,如何选择方法的预期实现?要回答这些问题,你需要解释后期(动态)绑定。绑定意味着在方法调用和它的特定类实现之间建立映射。本质上,代码决定了将执行类中定义的三个方法中的哪一个。Java 默认使用后期绑定,即绑定发生在运行时,而不是像早期绑定那样发生在编译时。这意味着当编译器编译这段代码时

for (Swim s : swimmers) {
            s.swim();
}
它不知道哪个类(HumanFishUboat)包含调用 swim 方法时将执行的代码。由于采用了动态绑定机制(在运行时检查对象的类型并为该类型选择正确的实现),这只有在程序执行时才能确定。 如果你被问到这是如何实现的,你可以回答,当加载和初始化对象时,JVM 在内存中构建表,并将变量与其值关联起来,将对象与其方法关联起来。如此,如果一个类被继承或者实现了一个接口,首先要做的就是检查被重写的方法是否存在。如果有,就将这些方法绑定到这个类型。否则,对匹配方法的搜索将移到更高一级的类(父类),依此类推,直到移到多级层次结构中的根。 当谈到 OOP 中的多态及其在代码中的实现时,我们注意到使用抽象类和接口来提供基类的抽象定义是一个很好的实践。这种实践遵循抽象原则 — 识别公共行为和属性并将其放入抽象类中,或者只识别公共行为并将其放入接口中。 设计和创建基于接口和类继承的对象层次结构是实现多态所必需的。 关于 Java 中的多态性和创新,我们注意到从 Java 8 开始,当创建抽象类和接口时,可以使用 default 关键字来编写基类中抽象方法的默认实现。 例如:

public interface CanSwim {
    default void swim() {
        System.out.println("我刚刚游泳了");
    }
}
有时面试官会问,基类中的方法必须如何声明才能不违反多态原则。答案很简单:这些方法不能是 staticprivatefinal 方法private 使方法只能在类中使用,所以你不能在子类中对其重写。static 将方法与类而不是任何对象相关联,所以超类的方法始终被调用。final 使方法不可变并对子类隐藏。

多态的好处有哪些?

你也很可能会被问到多态对我们有什么好处。你可以简单地回答这个问题,而不必纠结于令人不安的细节:
  1. 多态使替换类实现成为可能。测试就是建立在此基础之上。
  2. 它利于可扩展性,使得创建一个可以在未来进行构建的基础变得更加容易。在现有类型的基础上添加新类型是扩展 OOP 程序功能的最常见方式。
  3. 它允许你将具有相同类型或行为的对象组合到一个集合或数组中,并统一处理(在所示的例子中,我们强制每个人 dance()swim()) :)
  4. 创建新类型的灵活性:你可以选择方法的父实现,或者在子类中重写它。
Java 多态 - 2

结语

多态是一个非常重要和广泛的主题。在本文关于 Java 中 OOP 内容几乎一半的主题都与多态有关,这是 Java 语言基础不可或缺的一部分。在面试时,你肯定要对该原则进行说明。如果你不知道或者不了解,面试大概就无法继续。所以不要偷懒 — 在面试前评估你掌握的知识,必要时复习一下。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION