Java基础 -- 多态
在Java中一个对象既可以作为它自己本身的类型使用,也可以作为它的父类类型使用。而把这种对某个对象的引用视为对其父类类型引用的做法被称作向上转型。
一 向上转型
下面我们看一个例子,有一个父类Instrument,派生的子类中Wind、Stringed、Brass。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | package polymorphism.music; class Instrument{ public void play() { System.out.println( "Instrument.play()" ); } } class Wind extends Instrument{ public void play() { System.out.println( "Wind.play()" ); } } class Stringed extends Instrument{ public void play() { System.out.println( "Stringed.play()" ); } } class Brass extends Instrument{ public void play() { System.out.println( "Brass.play()" ); } } public class Music { public static void tune(Instrument i) { i.play(); } public static void main(String[] args) { Wind w = new Wind(); Stringed s = new Stringed(); Brass b = new Brass(); tune(w); tune(s); tune(b); } } |
可以看到输出为:
1 2 3 | Wind.play() Stringed.play() Brass.play() |
在Music类中tune方法接受的是一个Instrument引用类型,但是运行结果却是调用的实际子类对象中的play()方法。编译器是怎样知道这个Instrument引用指向的是Wind对象、还是Brass、Stringed对象呢?实际上,编译器无法得知。为了理解这个问题,我们介绍一下绑定。
1、方法调用绑定
将一个方法调用与一个方法主体关联起来被称作绑定、如果在程序执行前就进行绑定,叫做前期绑定。如果是在运行时根据对象的类型进行绑定就是后期绑定(动态绑定)。
Java中除了static和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。
因此tune函数传入的是Wind对象引用,则tune函数调用时会绑定到Wind类中的play()函数。
二 多态案例
创建一个父类Shape,以及多个子类Circle、Square、Triangle。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | package polynorphism.shape; import java.util.*; class Shape{ public void draw() {} public void erase() {} } class Circle extends Shape{ public void draw() { System.out.println( "Circle.draw()" ); } public void erase(){ System.out.println( "Circle.erase()" ); } } class Square extends Shape{ public void draw() { System.out.println( "Square.draw()" ); } public void erase(){ System.out.println( "Square.erase()" ); } } class Triangle extends Shape{ public void draw() { System.out.println( "Triangle.draw()" ); } public void erase(){ System.out.println( "Triangle.erase()" ); } } public class RandomShapeGenerator { private Random rand = new Random( 47 ); public Shape next() { switch (rand.nextInt( 3 )) { default : case 0 : return new Circle(); case 1 : return new Square(); case 2 : return new Triangle(); } } public static void main(String[] args) { RandomShapeGenerator gen = new RandomShapeGenerator(); Shape[] s = new Shape[ 9 ]; for ( int i= 0 ;i< 9 ;i++) { s[i] = gen.next(); s[i].draw(); } } } |
输出:
1 2 3 4 5 6 7 8 9 | Triangle.draw() Triangle.draw() Square.draw() Triangle.draw() Square.draw() Triangle.draw() Square.draw() Triangle.draw() Circle.draw() |
可以看到当调用一个父类方法s[i].draw();虽然s[i]是Shape类型的引用,由于后期绑定的原因,它能够正确调用s[i]所指向对象(比如Circle对象)的draw()方法。
Shape类为所有从它那里继承而来的子类都建立了一个公用接口--也就是说,所有形状都可以描绘和擦除。子类通过覆盖这些定义,来为每种特殊类型的几何形状提供单独的行为。
1、缺陷:”覆盖“私有方法
我们尝试运行一下方法:
public class PrivateOverride { private void f() { System.out.println("private f()"); } public static void main(String[] args) { PrivateOverride po = new Derived(); po.f(); } } class Derived extends PrivateOverride{ public void f() { System.out.println("public f()"); } }
运行结果:
1 | private f() |
我们所期望的是输出public f(),但是由于private方法自动被识别认为是final方法,而且对子类是屏蔽的,因此在这种情况下,Derived 类中的f()方法是一个新的方法,和父类的f()方法没有任何关系;既然父类中的f()方法在子类中不可见,因此不能被重载和覆盖(重写),调用po.f()时则执行的是父类的f()方法。
因此总结下来:只有非private方法才可以被覆盖,但是还需要密切注意覆盖private方法的现象,这时编译器虽然不会报错,但是也不会按照我们所期望的来执行。确切来说,在子类中,对于父类的private方法,最好采用不同的名字。
2、缺陷:字段与静态方法
一旦你了解了多态,你可能就会开始认为所有事物都可以多态地发生,然而,只有普通的方法调用可以是多态的。例如:如果你直接访问某个字段,这个访问就将在编译期进行解析,由于多态是动态绑定的体现,所以就无法实现多态,如下程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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;} } public class FieldAccess { public static void main(String[] args) { 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() ); } } |
运行结果如下:
1 2 | sup.field = 0 , sup.getField() = 1 sub.field = 1 , sub.getField() = 1 , sub.getSuperField() = 0 |
当Sub对象转换为Super引用时,任何字段访问操作都将由编译器解析(即根据对象引用的类型判断调用哪个字段,所以sup.field指的是Super中的field,sub.field指的是Sub中的field),因此不是多态的。在本例中,为Super.field和Sub.field分配了不同的存储空间,这样Sub实际上包含了两个称为field的字段:它自己和它从Super处继承来的。然而,在引用Sub中的field时所产生的默认字段并非Super中的field,因此为了得到Super.field,必须显示的指明super.field。
如果某个方法是静态的,那么它的行为将不具有多态性。这是因为静态方法是与类,而并非单个对象相关联的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class StaticSuper{ public static String staticGet() { return "Base staticGet" ; } public String dynamicGet() { return "Base dynamicGet" ; } } class StaticSub extends StaticSuper{ public static String staticGet() { return "Derived staticGet" ; } public String dynamicGet() { return "Derived dynamicGet" ; } } public class StaticPolymorphism { public static void main(String[] args) { StaticSuper sup = new StaticSub(); System.out.println(sup.staticGet()); System.out.println(sup.dynamicGet()); } } |
输出结果:
1 2 | Base staticGet Derived dynamicGet |
三 构造器和多态
1、构造器调用顺序
父类的构造器总是在子类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个父类的构造器都能被调用。
如果创建一个子类对象,调用顺序:
- 初始化父类static字段;
- 初始化子类static字段;
- 初始化父类非static字段;
- 调用父类构造函数;
- 初始化子类非static字段;
- 掉用子类构造函数;
下面我们来看一个案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | class Meal{ Meal(){ System.out.println( "Meal()" ); } } class Bread{ Bread(){ System.out.println( "Bread()" ); } } class Cheese{ Cheese(){ System.out.println( "Cheese()" ); } } class Lettuce{ Lettuce(){ System.out.println( "Lettuce()" ); } } class Lunch extends Meal{ Lunch(){ System.out.println( "Lunch()" ); } } class ProtableLunch extends Lunch{ ProtableLunch(){ System.out.println( "ProtableLunch()" ); } } public class Sandwish extends ProtableLunch{ private Bread b = new Bread(); private Cheese c = new Cheese(); private Lettuce l = new Lettuce(); public Sandwish() { System.out.println( "Sandwish()" ); } public static void main(String[] args) { new Sandwish(); } } |
运行结果如下:
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了