在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特性。多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。在一开始接触多态这个词的时候,我们或许会因为这个词本身而感到困惑,如果我们把多态改称作“动态绑定”,相信很多人就能理解他的深层含义。通常的,我们把动态绑定也叫做后期绑定,运行时绑定。
(一)方法调用绑定
1.绑定概念
通常,我们将一个方法调用同一个方法主体关联起来称作绑定。如果在程序执行前进行绑定,我们将这种绑定方法称作前期绑定。在面向过程语言中,比如c,这种方法是默认的也是唯一的。如果我们在java中采用前期绑定,很有可能编译器会因为在这庞大的继承实现体系中去绑定哪个方法而感到迷惑。解决的办法就是动态绑定,这种后期绑定的方法,在运行的时候根据对象的类型进行绑定。
在java中,动态绑定是默认的行为。但是在类中,普通的方法会采用这种动态绑定的方法,也有一些情况并不会自然的发生动态绑定。
2.final修饰
如果一个属性被final修饰,则含义是:在初始化之后不能被更改。
如果一个方法被final修饰,含义则是不能被覆盖。我们常常喜欢从宏观的角度这样说,但是我们真正的被final修饰的方法为什么不能被覆盖呢?因为final修饰词其实实际上关闭了动态绑定。在java中被final修饰的内容不能采用动态绑定的方法,不能动态绑定就没有多态的概念,自然也就不能被覆盖。
3.“覆盖”私有方法
其实我们很少把方法设定为私有。如果我们将private方法“覆盖”掉,其实我们获得的只是一个新的方法。完全和父类没关系了。这一点要注意,或许面试的时候会被问到:在子类中“覆盖”父类私有方法是被允许而不报错的,只不过完全是两个没关系的方法罢了。
4.域与静态方法
当我们了解了多态性之后可能会认为所有的事物都是可以多态地发生。其实并不是,如果我们直接访问某个域,这个访问会在编译期进行解析,我们可以参考下面的例子:
package Polymorphic;/** * * @author QuinnNorris * 域不具有多态性 */public class polymorphics { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Super sup = new Sub(); System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField()); Sub sub = new Sub(); System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField()); } } class Super { public int field = 0; public int getField() { return field; } } class Sub extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } }
输出结果:
sup.field = 0, sup.getField() = 1 sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
这个例子告诉我们,当我们调用一个方法时,去选择执行哪个方法的主体是运行时动态选择的。但是当我们直接访问实例域的时候,编译器直接按照这个对象所表示的类型来访问。于此情况完全相同的还有静态方法。所以我们可以做出这种总结:
普通方法:根据对象实体的类型动态绑定
域和静态方法:根据对象所表现的类型前期绑定
通俗地讲,普通的方法我们看new后面的是什么类型;域和静态方法我们看=前面声明的是什么类型。
尽管这看来好像是一个非常容易让人混悬哦的问题。但是在实践中,实际上从来(或者说很少)不会发生。首先,那些不把实例域设置为private的程序员基本上已经全都被炒鱿鱼了(实例域很少被修饰成public)。其次我们很少会将自己在子类中创建的域设置成和父类一样的名字。
(二)构造器与多态
通常,构造器是一个很独特的存在。涉及到多态的时候也是如此。尽管构造器并不具有多态性(实际上他们是有static来修饰的,尽管该static是被隐式声明的),但是我们还是有必要理解一下构造器的工作原理。
1.构造器的调用顺序
父类的构造器总是在子类构造器调用的过程中被调用,而且按照继承层次逐渐向上的链接,以使每个父类的构造器都能被正确的调用。这样做是很有必要的,因为构造器有一项特殊的任务,检查对象是否被正确的构造。子类方法只能访问自己的成员,不能访问父类中的成员。只有基类的构造器才具有恰当的权限对自己的元素进行初始化。因此必须要让每个构造器都能得到调用,否则不能构造出正确的完整的对象。
package Polymorphic; public class Father { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub new F(); } }class A { A() { System.out.println("A"); } }class B extends A { B() { System.out.println("B"); } }class C extends B { C() { System.out.println("C"); } }class D { D() { System.out.println("D"); } }class E { E() { System.out.println("E"); } }class F extends C { private D d = new D(); private E e = new E(); F() { System.out.println("F"); } }
输出结果:
A B C D E F
看似偶然的“ABCDEF”的输出结果,实际上是我们精心安排的。
这个例子非常直观的说明了构造器的调用法则,有以下三个步骤:
调用父类构造器。这个步骤会反复递归进去,直到最祖先的类,依次向下调用构造器。
按声明顺序调用成员的初始化构造器方法。
调用子类构造器的主体。
可能我说了这个顺序,大家马上就会想到super。是的没错,super()确实可以显示的调用父类中自己想要调用的构造方法,但是super()必须放在构造器的第一行,这个是规定。我们的顺序是没有任何问题的,或者说其实在F的构造器中第一句是super()。只不过我们默认省略了。
(三)协变返回类型特性
java在se5中添加了协变返回类型,它表示在子类中的被覆盖方法可以返回父类这个方法的返回类型的某种子类。
package Polymorphic;/** * * @author QuinnNorris * 协变返回类型 */public class covariant { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub A b = new B(); b.getC().print(); A a = new A(); a.getC().print(); } }class A{ public C getC() { return new C(); } }class B extends A{ public D getC(){ return new D(); } }class C{ public void print(){ System.out.println("C"); } }class D extends C{ public void print(){ System.out.println("D"); } }
输出结果:
D C
在上面的例子中,D类是继承于C类的,B类是继承于A类的,所以在B类覆盖的getC方法中,可以将返回类型协变成,C类的某个子类(D类)的类型。
(四)继承设计
通常,继承并不是我们的首选,能用组合的方法尽量用组合,这种手段更灵活,如果你的代码中is-a和is-like-a过多,你就应该考虑考虑是不是该换成has-a一些了。一条通用的准则是:用继承表达行为间的差异,并用字段表达状态上的变化。
而且在用继承的时候,我们会经常涉及到向上转型和向下转型。在java中,所有的转型都会得到检查。即使我们只是进行一次普通的加括号的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作”运行时类型识别(RTTI)“。
在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特性。多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。在一开始接触多态这个词的时候,我们或许会因为这个词本身而感到困惑,如果我们把多态改称作“动态绑定”,相信很多人就能理解他的深层含义。通常的,我们把动态绑定也叫做后期绑定,运行时绑定。
(一)方法调用绑定
1.绑定概念
通常,我们将一个方法调用同一个方法主体关联起来称作绑定。如果在程序执行前进行绑定,我们将这种绑定方法称作前期绑定。在面向过程语言中,比如c,这种方法是默认的也是唯一的。如果我们在java中采用前期绑定,很有可能编译器会因为在这庞大的继承实现体系中去绑定哪个方法而感到迷惑。解决的办法就是动态绑定,这种后期绑定的方法,在运行的时候根据对象的类型进行绑定。
在java中,动态绑定是默认的行为。但是在类中,普通的方法会采用这种动态绑定的方法,也有一些情况并不会自然的发生动态绑定。
2.final修饰
如果一个属性被final修饰,则含义是:在初始化之后不能被更改。
如果一个方法被final修饰,含义则是不能被覆盖。我们常常喜欢从宏观的角度这样说,但是我们真正的被final修饰的方法为什么不能被覆盖呢?因为final修饰词其实实际上关闭了动态绑定。在java中被final修饰的内容不能采用动态绑定的方法,不能动态绑定就没有多态的概念,自然也就不能被覆盖。
3.“覆盖”私有方法
其实我们很少把方法设定为私有。如果我们将private方法“覆盖”掉,其实我们获得的只是一个新的方法。完全和父类没关系了。这一点要注意,或许面试的时候会被问到:在子类中“覆盖”父类私有方法是被允许而不报错的,只不过完全是两个没关系的方法罢了。
4.域与静态方法
当我们了解了多态性之后可能会认为所有的事物都是可以多态地发生。其实并不是,如果我们直接访问某个域,这个访问会在编译期进行解析,我们可以参考下面的例子:
package Polymorphic;/** * * @author QuinnNorris * 域不具有多态性 */public class polymorphics { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Super sup = new Sub(); System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField()); Sub sub = new Sub(); System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField()); } } class Super { public int field = 0; public int getField() { return field; } } class Sub extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } }
输出结果:
sup.field = 0, sup.getField() = 1 sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
这个例子告诉我们,当我们调用一个方法时,去选择执行哪个方法的主体是运行时动态选择的。但是当我们直接访问实例域的时候,编译器直接按照这个对象所表示的类型来访问。于此情况完全相同的还有静态方法。所以我们可以做出这种总结:
普通方法:根据对象实体的类型动态绑定
域和静态方法:根据对象所表现的类型前期绑定
通俗地讲,普通的方法我们看new后面的是什么类型;域和静态方法我们看=前面声明的是什么类型。
尽管这看来好像是一个非常容易让人混悬哦的问题。但是在实践中,实际上从来(或者说很少)不会发生。首先,那些不把实例域设置为private的程序员基本上已经全都被炒鱿鱼了(实例域很少被修饰成public)。其次我们很少会将自己在子类中创建的域设置成和父类一样的名字。
(二)构造器与多态
通常,构造器是一个很独特的存在。涉及到多态的时候也是如此。尽管构造器并不具有多态性(实际上他们是有static来修饰的,尽管该static是被隐式声明的),但是我们还是有必要理解一下构造器的工作原理。
1.构造器的调用顺序
父类的构造器总是在子类构造器调用的过程中被调用,而且按照继承层次逐渐向上的链接,以使每个父类的构造器都能被正确的调用。这样做是很有必要的,因为构造器有一项特殊的任务,检查对象是否被正确的构造。子类方法只能访问自己的成员,不能访问父类中的成员。只有基类的构造器才具有恰当的权限对自己的元素进行初始化。因此必须要让每个构造器都能得到调用,否则不能构造出正确的完整的对象。
package Polymorphic; public class Father { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub new F(); } }class A { A() { System.out.println("A"); } }class B extends A { B() { System.out.println("B"); } }class C extends B { C() { System.out.println("C"); } }class D { D() { System.out.println("D"); } }class E { E() { System.out.println("E"); } }class F extends C { private D d = new D(); private E e = new E(); F() { System.out.println("F"); } }
输出结果:
A B C D E F
看似偶然的“ABCDEF”的输出结果,实际上是我们精心安排的。
这个例子非常直观的说明了构造器的调用法则,有以下三个步骤:
调用父类构造器。这个步骤会反复递归进去,直到最祖先的类,依次向下调用构造器。
按声明顺序调用成员的初始化构造器方法。
调用子类构造器的主体。
可能我说了这个顺序,大家马上就会想到super。是的没错,super()确实可以显示的调用父类中自己想要调用的构造方法,但是super()必须放在构造器的第一行,这个是规定。我们的顺序是没有任何问题的,或者说其实在F的构造器中第一句是super()。只不过我们默认省略了。
(三)协变返回类型特性
java在se5中添加了协变返回类型,它表示在子类中的被覆盖方法可以返回父类这个方法的返回类型的某种子类。
package Polymorphic;/** * * @author QuinnNorris * 协变返回类型 */public class covariant { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub A b = new B(); b.getC().print(); A a = new A(); a.getC().print(); } }class A{ public C getC() { return new C(); } }class B extends A{ public D getC(){ return new D(); } }class C{ public void print(){ System.out.println("C"); } }class D extends C{ public void print(){ System.out.println("D"); } }
输出结果:
D C
在上面的例子中,D类是继承于C类的,B类是继承于A类的,所以在B类覆盖的getC方法中,可以将返回类型协变成,C类的某个子类(D类)的类型。
(四)继承设计
通常,继承并不是我们的首选,能用组合的方法尽量用组合,这种手段更灵活,如果你的代码中is-a和is-like-a过多,你就应该考虑考虑是不是该换成has-a一些了。一条通用的准则是:用继承表达行为间的差异,并用字段表达状态上的变化。
而且在用继承的时候,我们会经常涉及到向上转型和向下转型。在java中,所有的转型都会得到检查。即使我们只是进行一次普通的加括号的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作”运行时类型识别(RTTI)“。
以上就是java深入理解动态绑定的内容,更多相关内容请关注PHP中文网(www.php.cn)!

Java如何缓解平台特定的问题?Java通过JVM和标准库来实现平台无关性。1)使用字节码和JVM抽象操作系统差异;2)标准库提供跨平台API,如Paths类处理文件路径,Charset类处理字符编码;3)实际项目中使用配置文件和多平台测试来优化和调试。

java'splatformentenceenhancesenhancesmicroservicesharchitecture byferingDeploymentFlexible,一致性,可伸缩性和便携性。1)DeploymentFlexibilityAllowsibilityAllowsOllowsOllowSorlowsOllowsOllowsOllowSeStorunonAnyPlatformwithajvM.2)penterencyCrossServAccAcrossServAcrossServiCessImplifififiesDeevelopmentandeDe

GraalVM通过三种方式增强了Java的平台独立性:1.跨语言互操作,允许Java与其他语言无缝互操作;2.独立的运行时环境,通过GraalVMNativeImage将Java程序编译成本地可执行文件;3.性能优化,Graal编译器生成高效的机器码,提升Java程序的性能和一致性。

效率testjavaapplicationsforplatformcompatibility oftheSesteps:1)setUpautomatedTestingTestingActingAcrossMultPlatFormSusingCitoolSlikeSlikeJenkinSorgithUbactions.2)contuctualtemualtemalualTesteTESTENRETESTINGINREALHARTWARETOLEALHARDOELHARDOLEATOCATCHISSUSESUSEUSENINCIENVIRENTMENTS.3)schictcross.3)schoscross.3)

Java编译器通过将源代码转换为平台无关的字节码,实现了Java的平台独立性,使得Java程序可以在任何安装了JVM的操作系统上运行。

ByteCodeachievesPlatFormIndenceByByByByByByExecutedBoviratualMachine(VM),允许CodetorunonanyplatformwithTheApprepreprepvm.Forexample,Javabytecodecodecodecodecanrunonanydevicewithajvm

Java不能做到100%的平台独立性,但其平台独立性通过JVM和字节码实现,确保代码在不同平台上运行。具体实现包括:1.编译成字节码;2.JVM的解释执行;3.标准库的一致性。然而,JVM实现差异、操作系统和硬件差异以及第三方库的兼容性可能影响其平台独立性。

Java通过“一次编写,到处运行”实现平台独立性,提升代码可维护性:1.代码重用性高,减少重复开发;2.维护成本低,只需一处修改;3.团队协作效率高,方便知识共享。


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

禅工作室 13.0.1
功能强大的PHP集成开发环境

螳螂BT
Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

mPDF
mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

WebStorm Mac版
好用的JavaScript开发工具

VSCode Windows 64位 下载
微软推出的免费、功能强大的一款IDE编辑器