JavaRush /Java 博客 /Random-ZH /多态性及其朋友
Viacheslav
第 3 级

多态性及其朋友

已在 Random-ZH 群组中发布
多态性是面向对象编程的基本原则之一。它允许您利用 Java 强类型的强大功能并编写可用且可维护的代码。关于他的说法已经很多了,但我希望每个人都能从这篇评论中得到一些新的东西。
多态性及其朋友 - 1

介绍

我想我们都知道Java编程语言属于Oracle。因此,我们的路径从以下站点开始:www.oracle.com。主页上有一个“菜单”。其中,“文档”部分有一个“Java”小节。所有与语言基本功能相关的内容都属于“Java SE 文档”,因此我们选择这一部分。文档部分将打开最新版本,但现在“正在寻找不同的版本?” 让我们选择选项:JDK8。在页面上我们会看到很多不同的选项。但我们对学习语言感兴趣:“ Java 教程学习路径”。在此页面上,我们会找到另一个部分:“学习 Java 语言”。这是最神圣的内容,来自 Oracle 的 Java 基础知识教程。Java 是一种面向对象编程语言 (OOP),因此即使在 Oracle 网站上学习该语言也是从讨论“面向对象编程概念”的基本概念开始的。从名称本身就可以清楚地看出,Java 专注于处理对象。从“什么是对象? ”小节中可以清楚地看出,Java 中的对象由状态和行为组成。想象一下我们有一个银行账户。账户中的金额是一种状态,而处理这种状态的方法是行为。对象需要以某种方式进行描述(告诉它们可能具有什么状态和行为),这种描述就是。当我们创建某个类的对象时,我们指定该类,这称为“对象类型”。因此,可以说 Java 是一种强类型语言,正如 Java 语言规范中“第 4 章类型、值和变量”部分所述。Java 语言遵循 OOP 概念并支持使用extends 关键字的继承。为什么要扩张?因为通过继承,子类继承了父类的行为和状态,并且可以补充它们,即 扩展基类的功能。还可以使用implements 关键字在类描述中指定接口。当一个类实现一个接口时,这意味着该类符合某种契约——程序员向环境的其余部分声明该类具有某种行为。例如,播放器有各种按钮。这些按钮是控制播放器行为的接口,行为会改变播放器的内部状态(例如音量)。在这种情况下,状态和行为作为描述将给出一个类。如果一个类实现了一个接口,那么该类创建的对象不仅可以通过类来描述,还可以通过接口来描述。让我们看一个例子:
public class MusicPlayer {

    public static interface Device {
        public void turnOn();
        public void turnOff();
    }

    public static class Mp3Player implements Device {
        public void turnOn() {
            System.out.println("On. Ready for mp3.");
        }
        public void turnOff() {
            System.out.println("Off");
        }
    }

    public static class Mp4Player extends Mp3Player {
        @Override
        public void turnOn() {
            System.out.println("On. Ready for mp3/mp4.");
        }
    }

    public static void main(String []args) throws Exception{
        // Какое-то устройство (Тип = Device)
        Device mp3Player = new Mp3Player();
        mp3Player.turnOn();
        // У нас есть mp4 проигрыватель, но нам от него нужно только mp3
        // Пользуемся им How mp3 проигрывателем (Тип = Mp3Player)
        Mp3Player mp4Player = new Mp4Player();
        mp4Player.turnOn();
    }
}
类型是一个非常重要的描述。它告诉我们将如何处理该对象,即 我们期望对象有什么行为。行为就是方法。因此,让我们了解一下这些方法。在 Oracle 网站上,方法在 Oracle 教程中有自己的部分:“定义方法”。从本文中要了解的第一件事:方法签名是方法的名称和参数的类型
多态性及其朋友 - 2
例如,当声明一个方法 public void method(Object o) 时,签名将是方法的名称和参数 Object 的类型。返回类型不包含在签名中。这很重要!接下来,让我们编译我们的源代码。众所周知,为此,代码必须保存在一个文件中,该文件的名称为类名,扩展名为 java.lang. Java 代码使用“ javac ”编译器编译成某种可以由 Java 虚拟机 (JVM) 执行的中间格式。这种中间格式称为字节码,包含在扩展名为 .class 的文件中。我们来运行命令进行编译:javac MusicPlayer.java java代码编译完成后,我们就可以执行它了。使用“ java ”实用程序启动,将启动java虚拟机进程来执行类文件中传递的字节码。让我们运行命令来启动应用程序:java MusicPlayer。我们将在屏幕上看到 println 方法的输入参数中指定的文本。有趣的是,将字节码放在扩展名为 .class 的文件中,我们可以使用“ javap ”实用程序查看它。让我们运行命令 <ocde>javap -c MusicPlayer:
多态性及其朋友 - 3
从字节码中我们可以看到,通过指定类的类型的对象调用方法是使用 进行的invokevirtual,并且编译器已经计算出应该使用哪个方法签名。为什么invokevirtual?因为有一个虚方法的调用(invoke翻译为调用)。什么是虚拟方法?这是一个在程序执行期间可以覆盖其主体的方法。简单地想象一下,您有一个特定密钥(方法签名)和方法主体(代码)之间的对应列表。并且键和方法体之间的这种对应关系可能会在程序执行过程中发生变化。因此该方法是虚拟的。默认情况下,在 Java 中,非静态、非最终和非私有的方法是虚拟的。正因为如此,Java 支持面向对象的多态编程原则。正如您可能已经了解的那样,这就是我们今天的评论的内容。

多态性

在 Oracle 网站的官方教程中,有一个单独的部分:“多态性”。让我们使用Java在线编译器来看看多态性在Java中是如何工作的。例如,我们有一些代表Java中数字的抽象类Number 。它允许什么?他拥有一些所有继承人都会拥有的基本技术。继承 Number 的人字面意思是:“我是一个数字,你可以作为一个数字与我一起工作。” 例如,对于任何后继,您可以使用 intValue() 方法来获取其 Integer 值。如果你看一下Number的java api,你会发现该方法是抽象的,也就是说,Number的每个后继者都必须自己实现这个方法。但这给我们带来了什么?让我们看一个例子:
public class HelloWorld {

    public static int summ(Number first, Number second) {
        return first.intValue() + second.intValue();
    }

    public static void main(String []args){
        System.out.println(summ(1, 2));
        System.out.println(summ(1L, 4L));
        System.out.println(summ(1L, 5));
        System.out.println(summ(1.0, 3));
    }
}
从示例中可以看出,由于多态性,我们可以编写一个接受任何类型参数作为输入的方法,该方法将是 Number 的后代(我们无法获取 Number,因为它是一个抽象类)。与播放器示例的情况一样,在本例中我们说我们想要使用某些东西,例如 Number。我们知道任何一个数字都必须能够提供它的整数值。这对我们来说就足够了。我们不想深入了解特定对象的实现细节,而是希望通过 Number 的所有后代通用的方法来处理该对象。我们可用的方法列表将由编译时的类型决定(正如我们之前在字节码中看到的那样)。在这种情况下,我们的类型将是 Number。从示例中可以看到,我们传递了不同类型的不同数字,即 summ 方法将接收 Integer、Long 和 Double 作为输入。但它们的共同点是它们都是抽象 Number 的后代,因此在 intValue 方法中重写了它们的行为,因为 每个特定类型都知道如何将该类型转换为 Integer。这种多态性是通过所谓覆盖来实现的,英文为Overriding。
多态性及其朋友 - 4
重写或动态多态性。因此,我们首先保存包含以下内容的 HelloWorld.java 文件:
public class HelloWorld {
    public static class Parent {
        public void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
让我们做javac HelloWorld.java并且javap -c HelloWorld
多态性及其朋友 - 5
正如您所看到的,在包含方法调用的行的字节码中,指示了对调用方法的相同引用invokevirtual (#6)。我们开始做吧java HelloWorld。正如我们所看到的,变量parent和child是用Parent类型声明的,但是根据分配给变量的对象(即对象的类型)来调用实现本身。在程序执行期间(他们也说在运行时),JVM根据对象的不同,在调用使用相同签名的方法时,执行不同的方法。也就是说,使用相应签名的密钥,我们首先收到一个方法体,然后收到另一个方法体。取决于变量中的对象。在程序执行时确定将调用哪个方法也称为后期绑定或动态绑定。也就是说,签名和方法体之间的匹配是动态执行的,具体取决于调用该方法的对象。当然,您不能覆盖类的静态成员(Class member),以及访问类型为 private 或 Final 的类成员。@Override 注释也可以为开发人员提供帮助。它可以帮助编译器理解此时我们将重写祖先方法的行为。如果我们在方法签名中犯了错误,编译器会立即告诉我们。例如:
public static class Parent {
        public void method() {
            System.out.println("parent");
        }
}
public static class Child extends Parent {
        @Override
        public void method(String text) {
            System.out.println("child");
        }
}
编译时不会出现错误:错误:方法不会覆盖或实现超类型中的方法
多态性及其朋友 - 6
重新定义还与“协方差”的概念相关。让我们看一个例子:
public class HelloWorld {
    public static class Parent {
        public Number method() {
            return 1;
        }
    }
    public static class Child extends Parent {
        @Override
        public Integer method() {
            return 2;
        }
    }

    public static void main(String[] args) {
        System.out.println(new Child().method());
    }
}
尽管表面上很深奥,但其意义归结为这样一个事实:当重写时,我们不仅可以返回祖先中指定的类型,还可以返回更具体的类型。例如,祖先返回 Number,我们可以返回 Integer - Number 的后代。这同样适用于方法的 throws 中声明的异常。继承人可以重写该方法并改进抛出的异常。但它们无法扩展。也就是说,如果父进程抛出 IOException,那么我们可以抛出更精确的 EOFException,但不能抛出 Exception。同样,您不能缩小范围,也不能施加额外的限制。例如,您不能添加静态。
多态性及其朋友 - 7

隐藏

还有一种说法叫“隐瞒”。例子:
public class HelloWorld {
    public static class Parent {
        public static void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public static void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
如果你仔细想想,这是一个非常明显的事情。类的静态成员属于该类,即 到变量的类型。因此,如果 child 的类型为 Parent,那么该方法将在 Parent 上调用,而不是在 child 上,这是合乎逻辑的。如果我们像之前那样查看字节码,我们将看到静态方法是使用 invokestatic 调用的。这向 JVM 解释说,它需要查看类型,而不是像 invokevirtual 或 invokeinterface 那样查看方法表。
多态性及其朋友 - 8

重载方法

我们在 Java Oracle 教程中还看到了什么?在前面学习的“定义方法”一节中,有一些关于重载的内容。这是什么?在俄语中,这是“方法重载”,这样的方法被称为“重载”。所以,方法重载。乍一看,一切都很简单。让我们打开一个在线Java编译器,例如tutorialspoint online java compiler
public class HelloWorld {

	public static void main(String []args){
		HelloWorld hw = new HelloWorld();
		hw.say(1);
		hw.say("1");
	}

	public static void say(Integer number) {
		System.out.println("Integer " + number);
	}
	public static void say(String number) {
		System.out.println("String " + number);
	}
}
所以,这里一切看起来都很简单。正如 Oracle 教程中所述,重载方法(在本例中为 say 方法)在传递给方法的参数数量和类型方面有所不同。您不能声明相同名称和相同数量的相同类型的参数,因为 编译器将无法区分它们。值得注意的是一件非常重要的事情:
多态性及其朋友 - 9
也就是说,当重载时,编译器会检查正确性。这很重要。但是编译器实际上如何确定需要调用某个方法呢?它使用Java语言规范中描述的“最具体方法”规则:“ 15.12.2.5.选择最具体方法”。为了演示它是如何工作的,我们以 Oracle 认证专业 Java 程序员为例:
public class Overload{
  public void method(Object o) {
    System.out.println("Object");
  }
  public void method(java.io.FileNotFoundException f) {
    System.out.println("FileNotFoundException");
  }
  public void method(java.io.IOException i) {
    System.out.println("IOException");
  }
  public static void main(String args[]) {
    Overload test = new Overload();
    test.method(null);
  }
}
举个例子:https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... 如您所见,我们正在通过该方法为空。编译器尝试确定最具体的类型。对象不适合,因为 一切都是从他那里继承的。前进。有两类例外。我们看一下java.io.IOException,发现“Direct Known Subclasses”中有一个FileNotFoundException。也就是说,事实证明 FileNotFoundException 是最具体的类型。因此,结果将是字符串“FileNotFoundException”的输出。但是,如果我们将 IOException 替换为 EOFException,则结果表明我们在类型树中的层次结构的同一级别上有两个方法,也就是说,对于这两个方法,IOException 都是父方法。编译器将无法选择调用哪个方法,并会抛出编译错误:reference to method is ambiguous。再举一个例子:
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
它将输出 1。这里没有问题。int... 类型是一个 vararg https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html ,实际上只不过是“语法糖”,实际上是一个 int 。 .. 数组可以读取为 int[] 数组。如果我们现在添加一个方法:
public static void method(long a, long b) {
	System.out.println("2");
}
那么它不会显示 1,而是 2,因为 我们传递 2 个数字,并且 2 个参数比 1 个数组更匹配。如果我们添加一个方法:
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
那么我们仍然会看到 2。因为在这种情况下,基元比 Integer 中的装箱更精确匹配。但是,如果我们执行,method(new Integer(1), new Integer(2));它将打印 3。 Java 中的构造函数与方法类似,并且由于它们也可以用于获取签名,因此与重载方法一样适用相同的“重载解析”规则。Java 语言规范在“ 8.8.8. 构造函数重载”中告诉了我们这一点。方法重载 = 早期绑定(又名静态绑​​定) 您经常会听到早期绑定和晚期绑定,也称为静态绑定或动态绑定。它们之间的区别非常简单。早期是编译,晚期是程序执行的时刻。因此,早期绑定(静态绑定)就是在编译时决定对谁调用哪个方法。那么,后期绑定(动态绑定)就是在程序执行时决定直接调用哪个方法。正如我们之前看到的(当我们将 IOException 更改为 EOFException 时),如果我们重载方法,使编译器无法理解在哪里进行哪个调用,那么我们将得到一个编译时错误:对方法的引用不明确。英语中的ambiguously一词翻译过来的意思是不明确的或不确定的、不精确的。事实证明,重载是早期绑定,因为 检查是在编译时执行的。为了证实我们的结论,让我们打开Java语言规范中的“ 8.4.9.重载”章节:
多态性及其朋友 - 10
事实证明,在编译期间,有关参数类型和数量的信息(在编译时可用)将用于确定方法的签名。如果该方法是对象的方法之一(即实例方法),则将在运行时使用动态方法查找(即动态绑定)确定实际的方法调用。为了更清楚地说明这一点,让我们举一个与前面讨论的类似的例子:
public class HelloWorld {
    public void method(int intNumber) {
        System.out.println("intNumber");
    }
    public void method(Integer intNumber) {
        System.out.println("Integer");
    }
    public void method(String intNumber) {
        System.out.println("Number is: " + intNumber);
    }

    public static void main(String args[]) {
        HelloWorld test = new HelloWorld();
        test.method(2);
    }
}
让我们将此代码保存到文件 HelloWorld.java 并使用以下命令对其进行编译。javac HelloWorld.java 现在让我们通过执行以下命令来查看编译器在字节码中写入的内容:javap -verbose HelloWorld
多态性及其朋友 - 11
如前所述,编译器已确定将来将调用某些虚拟方法。也就是说,方法体将在运行时定义。但在编译时,编译器在这三种方法中选择了最合适的一种,因此它显示了数字:"invokevirtual #13"
多态性及其朋友 - 12
这是什么样的methodref?这是该方法的链接。粗略地说,这是一些线索,在运行时,Java 虚拟机实际上可以确定要查找执行哪个方法。更多详细内容可以参见超级文章:《JVM如何在内部处理方法重载和重写》。

总结

所以,我们发现Java作为一种面向对象的语言,是支持多态的。多态性可以是静态的(静态绑定),也可以是动态的(动态绑定)。通过静态多态性(也称为早期绑定),编译器可以确定应调用哪个方法以及在何处调用。这允许使用诸如过载之类的机制。通过动态多态性,也称为后期绑定,基于先前计算的方法签名,将在运行时根据使用哪个对象(即调用哪个对象的方法)来计算方法。可以使用字节码来了解这些机制的工作原理。重载会查看方法签名,并在解决重载时选择最具体(最准确)的选项。重写查看类型以确定哪些方法可用,并根据对象调用方法本身。以及有关该主题的材料: #维亚切斯拉夫
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION