【Java 多态的表现形式】简单理解

您所在的位置:网站首页 clean的三种形式 【Java 多态的表现形式】简单理解

【Java 多态的表现形式】简单理解

2024-03-23 05:25| 来源: 网络整理| 查看: 265

多态

1、多态 多态是同一个行为具有多个不同表现形式或形态的能力 举个栗子:我们睡觉这个行为,你可以躺着睡,侧着睡,趴着睡等

2、多态实现的必要条件 我们以鸡的做法来举个栗子 首先,我们先给出一只鸡:

class Chicken{ public void live(){ System.out.println("这是一只鸡"); } }

2.1 子类必须继承父类

对于子类必须继承父类,我个人认为,是因为按照面向对象的五大基本原则所说的中的依赖倒置原则:抽象不依赖于具体,具体依赖于抽象。既然要实现多态,那么必定有一个作为"抽象"类来定义“行为”,以及若干个作为"具体"类来呈现不同的行为形式或形态。

所以我们给出的一个具体类——白切鸡类:

class BaiqieChicken extends Chicken{ }

但仅是定义一个白切鸡类是不够的,因为在此我们只能做到复用父类的属性和行为,而没有呈现出行为上的不同的形式或形态

2.2 必须有重写 重写,简单地理解就是重新定义的父类方法,使得父类和子类对同一行为的表现形式各不相同。我们用白切鸡类来举个栗子

class BaiqieChicken extends Chicken{ public void live(){ System.out.println("这是一只会被做成白切鸡的鸡"); } }

这样就实现了重写,鸡类跟白切鸡类在live()方法中定义的行为不同,鸡类是一只命运有着无限可能的鸡,而白切鸡类的命运就是做成一只白切鸡

但是为什么还要有“父类引用指向子类对象”这个条件呢?

2.3 父类引用指向子类对象

其实这个条件是面向对象的五大基本原则里面的里氏替换原则,简单说就是父类可以引用子类,但不能反过来

当一只鸡被选择做白切鸡的时候,它的命运就不是它能掌控的

Chicken c = new BaiqieChicken(); c.live();

运行结果:

这是一只会被做成白切鸡的鸡

为什么要有这个原则?因为父类对于子类来说,是属于“抽象”的层面,子类是“具体”的层面。“抽象”可以提供接口给“具体”实现,但是“具体”凭什么来引用“抽象”呢?而且“子类引用指向父类对象”是不符合“依赖倒置原则”的

所以,当一只白切鸡想回头重新选择自己的命运,抱歉,它已经在锅里,逃不出去了

BaiqieChicken bc = new Chicken(); //这句是运行不了的 bc.live();

3、多态的实现途径

多态的实现途径有三种:重写、重载、接口实现,虽然它们的实现方式不一样,但是核心都是:同一行为的不同表现形式

3.1 1. 重写 重写,指的是子类对父类方法的重新定义,但是子类方法的参数列表和返回值类型,必须与父类方法一致!所以可以简单的理解,重写就是子类对父类方法的核心进行重新定义

举个栗子:

class Chicken{ public void live(String lastword){ System.out.println(lastword); } } class BaiqieChicken extends Chicken{ public void live(String lastword){ System.out.println("这只白切鸡说:"); System.out.println(lastword); } }

这里白切鸡类重写了鸡类的live()方法,为什么说是重写呢?因为白切鸡类中live()方法的参数列表和返回值与父类一样,但方法体不一样了

3.2 重载 重载,指的是在一个类中有若干个方法名相同,但参数列表不同的情况,返回值可以相同也可以不同的方法定义场景。也可以简单理解成,同一行为(方法)的不同表现形式

举个栗子:

class BaiqieChicken extends Chicken{ public void live(){ System.out.println("这是一只会被做成白切鸡的鸡"); } public void live(String lastword){ System.out.println("这只白切鸡说:"); System.out.println(lastword); } }

这里的白切鸡类中的两个live()方法,一个无参一个有参,它们对于白切鸡类的live()方法的描述各不相同,但它们的方法名都是live。通俗讲,它们对于白切鸡鸡生的表现形式不同

3.3 接口实现

接口,是一种无法被实例化,但可以被实现的抽象类型,是抽象方法的集合,多用作定义方法集合,而方法的具体实现则交给继承接口的具体类来定义。所以,接口定义方法,方法的实现在继承接口的具体类中定义,也是对同一行为的不同表现形式

interface Chicken{ public void live(); } class BaiqieChicken implements Chicken{ public void live(){ System.out.println("这是一只会被做成白切鸡的鸡"); } } class ShousiChicken implements Chicken{ public void live(){ System.out.println("这是一只会被做成手撕鸡的鸡"); } }

从上面我们可以看到,对于鸡接口中的live()方法,白切鸡类和手撕鸡类都有自己对这个方法的独特的定义

4、动态绑定

4.1 程序绑定的概念

绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对java来说,绑定分为静态绑定和动态绑定;或者叫做前期绑定和后期绑定

4.2 静态绑定(早绑定 编译器绑定)

在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。例如:C。针对java可以理解为程序编译期的绑定;特别说明一点,java当中的方法只有final,static,private和构造方法是前期绑定

4.3 动态绑定(迟绑定 运行期绑定)

后期绑定:在运行时根据具体对象的类型进行绑定 若一种语言实现了后期绑定,同时必须提供一些机制在运行期间判断对象的类型,并分别调用适当的方法。也就是说编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。可以这样认为:它们都要在对象中安插某些特殊类型的信息

4.4 动态绑定的过程

虚拟机提取对象的实际类型的方法表

虚拟机搜索方法签名

调用方法

4.5 关于绑定相关的总结

在java中,几乎所有的方法都是后期绑定,在运行时动态绑定方法属于子类还是基类。但也有特殊,针对static方法和final方法由于不能被继承,因此在编译时就可以确定他们的值,他们是属于前期绑定。特别说明的一点,private声明的方法和成员变量不能被子类继承,所有的private方法都被隐式的指定为final的(由此我们知道:将方法声明为final类型的 一是为了防止方法被覆盖,二是为了有效的关闭java中的动态绑定) java当中的向上转型或者说多态是借助于动态绑定实现的,所以理解动态绑定,也就搞定向上转型和多态

静态绑定:程序运行前方法已被绑定。即Java中编译期进行的绑定。 动态绑定:程序运行时根据具体对象的类型进行绑定

Parent p = new Children();

具体过程如下:

1:编译器检查对象的声明类型和方法名。假设我们调用x.f(args)方法,并且x已经被声明为C类的对象,那么编译器会列举出C类中所有的名称为f的方法和从C类的超类继承过来的f方法

2:接下来编译器检查方法调用中提供的参数类型。如果在所有名称为f 的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就调用这个方法,这个过程叫做“重载解析”

3:当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。假设实际类型为D(C的子类),如果D类定义了f(String)那么该方法被调用,否则就在D的超类中搜寻方法f(String),依次类推

(重要)再先来看stack overflow上的一个问题:

import java.util.*; import java.lang.*; import java.io.*; class A { int x = 5; } class B extends A { int x = 6; } class SubCovariantTest extends CovariantTest { public B getObject() { System.out.println("sub getobj"); return new B(); } } public class CovariantTest { public A getObject() { System.out.println("ct getobj"); return new A(); } public static void main (String[] args) throws java.lang.Exception { CovariantTest c1 = new SubCovariantTest(); System.out.println(c1.getObject().x); } }

答案是:

sub getobj 5

这不科学!既然c1.getObject()执行的是SubCovariantTest中的getObject()方法,那么应该返回的是B对象,所以应该输出6才对啊。 这个解释听上去很有道理,但忽略了Java中的静态绑定和动态绑定的知识

Java中程序分为编译和解释两个阶段。 也就是说,Java文件被编译成class文件时,已经对其中的方法和域根据类信息进行了一次绑定(静态绑定)。 而运行时方法又会根据运行时对象信息进行另外一次绑定(动态绑定),也就说我们常说的多态

进一步理解两种绑定

class A { int x = 5; public void doSomething() { System.out.println("A.doSomething()"); } } class B extends A { int x = 6; public void doSomething() { System.out.println("B.doSomething()"); } } public class Main { public static void main(String args[]) { A a=new B(); System.out.println(a.x); a.doSomething(); } }

Output:

5 B.doSomething()

结果在意料当中,我们来看下反编译Main.class文件后的信息:

//javap -c Main Compiled from "Main.java" public class test.Main { public test.Main(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class test/B 3: dup 4: invokespecial #3 // Method test/B."":()V 7: astore_1 8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_1 12: getfield #5 // Field test/A.x:I 15: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 18: aload_1 19: invokevirtual #7 // Method test/A.doSomething:()V 22: return }

通过12:和19:后的注释我们可以知道,编译后的class文件中,域x和方法doSomething都是和A类绑定在了一起。而在程序执行时,doSomething方法会再根据运行的对象类型进行第二次的动态绑定,从执行了B类中的方法

解答问题 回到最开始问题。 我们反编译下CovariantTest.class文件:

//javap -c CovariantTest Compiled from "CovariantTest.java" public class test.CovariantTest { public test.CovariantTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public test.A getObject(); Code: 0: new #2 // class test/A 3: dup 4: invokespecial #3 // Method test/A."":()V 7: areturn public static void main(java.lang.String[]); Code: 0: new #4 // class test/SubCovariantTest 3: dup 4: invokespecial #5 // Method test/SubCovariantTest."":()V 7: astore_1 8: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_1 12: invokevirtual #7 // Method getObject:()Ltest/A; 15: getfield #8 // Field test/A.x:I 18: invokevirtual #9 // Method java/io/PrintStream.println:(I)V 21: return }

从12:和15:可以看出,在编译期,c1跟类CovariantTest绑定在一起,所以c1.getObject()编译后认为是类A,c1.getObject().x编译后便成为了A.x。 又因为Java中域是静态绑定的,所以程序运行时便不会根据运行时对象类型来确定,所有最后输出了5

java因为什么要对属性要采取静态的绑定方法。这是因为静态绑定是有很多的好处,它可以让我们在编译期就发现程序中的错误,而不是在运行期。这样就可以提高程序的运行效率!而对方法采取动态绑定是为了实现多态,多态是java的一大特色。多态也是面向对象的关键技术之一,所以java是以效率为代价来实现多态这是很值得的

==重载的本质是在编译期就会根据参数的静态类型来决定重载方法的版本重写的本质在运行期确定接收者的实际类型 ==



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3