JavaSE基础05-多态

Georgia ·
更新时间:2024-11-01
· 832 次阅读

1.多态中的向上转型

多态的好处:
松耦合:类与类之间或模块与模块之间的关联程度要低,修改一个类或模块时,尽量不影响到其他类或模块
高内聚:内聚性是指在程序设计中表示一个类具有单一的明确的目标程度;要做到责越单一,目标越明确,这样才能称为内聚程度高。
紧封装:在设计类的时候,尽量把数据成员设计为私有的,通过公共接口来操作私有数据成员。
多态是为了消除类之间的耦合关系,让两个类或模块的联系程度降低;松耦合的目的是为了提高可扩展性。

以下是实验代码:
我们定义的接口,接口参数接收的类型是顶层的(继承链中的基类),我们接收都对象都是子类的实例;而同样的方法我们在调用的时候,由具体的子类去实现,这就叫多态;把接口和实现分离,消除类型间的耦合关系。

package com.JavaSE03.demo01; enum Note{ MIDDLE_C,C_SHARP,B_FLAT; } class instrument{ public void play(Note n){ System.out.println("instrumen.play()"); } } public class Wind extends instrument { @Override public void play(Note n){ System.out.println("Wind.play()"); } } package com.JavaSE03.demo01; //使用了多态的思想 public class Music { public static void tune(instrument i){//基类,向上转型;只要是乐器类,都可以接收; i.play(Note.MIDDLE_C);//使用基类作为参数,而不是子类,这种是方式是多态所允许的;面向对象中的多态就是指多种形态; } public static void main(String[] args) { Wind flute =new Wind();//子类实例对象 tune(flute); } } //以下是没有使用多态思想的代码 package com.JavaSE03.demo01; class Stringed extends instrument{ @Override public void play(Note n){ System.out.println("Stringed.play()"); } } class Brass extends instrument{ @Override public void play(Note n){ System.out.println("Brass.play()"); } } //这是没有用多态,扩展性很差,一旦要扩展,就非常麻烦。 public class Music2 { public static void tune(Wind i){ i.play(Note.MIDDLE_C); } public static void tune(Brass i){ i.play(Note.MIDDLE_C); } public static void tune(Stringed i){ i.play(Note.MIDDLE_C); } public static void main(String[] args) { Wind flute =new Wind();//子类实例对象 tune(flute); } } 多态的缺陷

缺陷:“覆盖”私有方法
/

* privateOverride po=new Derived(); 此语句实现了向上转型,当执行方法的时候,会先从父类看有没有这个方法,然后看其子类有没有重写;子类没有重写则调用父类的方法;
* 这里是因为子类不能覆盖重写私有方法;所以在Dervied中的f(),是一个全新的方法,并没有重写父类的f(),所以这里先在父类中寻找f(),并执行(在有重写基类方法的情况下才会调用派生类的重写方法).
* 如果将程序入口定义在子类,则会报错;因为找不到f()方法;因为变量po是先从privateOverrid类中寻找f()方法,
* 而privateOverrid类中将其定义成private的,外界无法访问(对于类外来说相当于没有这个方法),则就没有重写这个说法,不会向下继续寻找;

package com.JavaSE03.demo02.demo02; //这里涉及前期绑定的问题,博文下一小节会谈到,这里现在这样理解即可。 public class privateOverride { private void f(){//在使用多态时,一定要注意谨慎private修饰符,只有非私有的方法,才能被覆盖重写 System.out.println("private f()"); } public static void main(String[] args) { privateOverride po=new Derived(); //这里默认输出 private.f() po.f();//我们本意是想执行子类的f(),为什么输出父类的f()? /** * privateOverride po=new Derived(); 此语句实现了向上转型,当执行方法的时候,会先从父类看有没有这个方法,然后看其子类有没有重写;子类没有重写则调用父类的方法; * 这里是因为子类不能覆盖重写私有方法;所以在Dervied中的f(),是一个全新的方法,并没有重写父类的f(),所以这里先在父类中寻找f(),并执行(在有重写基类方法的情况下才会调用派生类的重写方法). * 如果将程序入口定义在子类,则会报错;因为找不到f()方法;因为变量po是先从privateOverrid类中寻找f()方法, * 而privateOverrid类中将其定义成private的,外界无法访问(对于类外来说相当于没有这个方法),则就没有重写这个说法,不会向下继续寻找; */ } } class Derived extends privateOverride{ public void f(){ System.out.println("public f()"); } }

缺陷:域和静态方法
/

域(类的字段)是通过编译器操作访问,不具备多态的性质,根据声明的对象类型进行匹配。

package com.JavaSE03.demo02.demo02; 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 getSuperfiled(){return super.getField();} } public class FieldAccess { public static void main(String[] args) { Super sup = new sub(); //这里我们实际拿到的域的值是基类的;因为任何域(类的字段)的访问操作,都是编译器执行的, 它不是多态的; // 但方法是多态的;根据new出来的对象类型,在运行时,通过动态绑定,选择适当的方法进行调用。 //输出结果:sup.field=0 sup.getField()=1 System.out.println("sup.field="+sup.field+" sup.getField()="+sup.getField()); System.out.println("-------------------------分割线----------------------------------"); sub sub = new sub();//这里当然是派生类的域 System.out.println("sub.field="+sub.field+" sub.getField()="+sub.getField()+" sub.getUpserfiled="+sub.getSuperfiled()); /** 这是完整的实验结果 sup.field=0 sup.getField()=1 -------------------------分割线---------------------------------- sub.field=1 sub.getField()=1 sub.getUpserfiled=0 */ } }

这里先了解Java面向对象编程中的两个概念前期绑定和后期绑定

https://www.cnblogs.com/jstarseven/articles/4631586.html 参考博文

前期绑定(静态绑定):在编译时期程序就已经知道了方法是哪一个类中的方法;
后期绑定(动态绑定):在运行时,根据对象类型进行绑定;要使用后期绑定,必须要有某种机制,判断对象的类型,从而调用恰当的方法;
static final private 三种方法是前期绑定;其他的所有方法都是后期绑定
前期绑定决定是声明的是哪一个类,然后去调用该类的方法;
后期绑定决定于new的对象是哪一个类,然后去调用该类中的方法;
所以多态的性质(仅对方法而言),只针对非static、private、final的方法起作用。

什么?你问什么是声明的类型?什么是new出来对象的类型?
e.g. Super sup = new Sub();
这里 Super类 就是声明的类型、new 的对象是Sub类;等式前面是声明,后面是对象的类型;
但sup对象是Super类的,因为使用了向上转型,把new Sub()这一对象,向上转为Super类型。
希望大家能够分清楚这几个基本概念。同时由于我的水平有限,欢迎大家批评指正,互相促进。

因此静态方法也不具备多态的性质。因为static方法是通过前期绑定,那么前期绑定是通过声明的类型决定调用的方法,而多态的声明类型一般都是基类(超类);

package com.JavaSE03.demo02.demo02; 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()); /** * 以下是实验结果:说明静态方法不具备多态性质 * Base staticGet() * Derived dynamicGet() */ } } 构造器与多态

构造器的运行顺序

先说结论:基类的构造器总是在子类的构造过程中被调用,而且按照继承层次,逐渐向上链接,直到最顶层。如果没有明确指定调用某个基类构造器,则会默认调用无参构造器,若不存在无参构造器,则会编译报错。而在一个类的构造器执行之前,它的实例成员是要被初始化的,而且是在构造器执行之前初始化。
简单总结:
step1.找到基类构造器执行,根据继承链往上寻找
step2.若基类中有变量成员,则先初始化;注意,若变量成员有静态的,则先初始化静态,否则按照声明先后顺序
step3.逐层执行,当所有的基类构造器执行完,且实例类中的变量成员都初始化完毕,实例类的构造器才执行。

以下是实验代码

package com.JavaSE03.demo03; class Bread{ Bread(){ System.out.println("Bread()"); } } class Cheese{ Cheese(){ System.out.println("Cheese()"); } } class Lettuce{ Lettuce(){ System.out.println("Lettuce()"); } } class Meal{ Meal(){ System.out.println("Meal()"); } } class Lunch extends Meal{ Lunch(){ System.out.println("Lunch"); } } class PortableLunch extends Lunch{ PortableLunch (){ System.out.println("PortableLunch()"); } } public class Sandwich extends PortableLunch{ private Bread bread = new Bread(); private Cheese cheese=new Cheese(); private Lettuce lettuce=new Lettuce(); public Sandwich(){ System.out.println("SandWich"); } public static void main(String[] args) { new Sandwich(); } }

继承和清理

继承中的清理工作:一般情况下,不需要我们手写清理代码,但需要的时候要小心谨慎,防止出错。子类的清理,有可能会调用基类的某些方法,不恰当的清理顺序可能会出一些未知的错误,所以我们一定要注意清理的先后顺序;一般来说,要遵循:先创建,后清理,后创建,先清理。

以下是实验代码

package com.JavaSE03.demo03; class Characteristic{ private String s; Characteristic(String s ){ this.s=s; System.out.println("creating Characteristic "+s); } protected void dispose(){ System.out.println("dispose Characteristic "); } } class Descruption{ private String s; Descruption(String s){ this.s=s; System.out.println("creating Descruption "+ s); } protected void dispose(){ System.out.println("creating dispose "); } } class LivingCreature{ private Characteristic p =new Characteristic("is alive"); private Descruption t = new Descruption("Basic Living Creature"); LivingCreature(){ System.out.println("LivingCreature()"); } protected void dispose(){ System.out.println("LivingCreature dispose"); //先创建的最后销毁,后创建的先销毁 t.dispose(); p.dispose(); } } class Animal extends LivingCreature{ private Characteristic p =new Characteristic("has heart"); private Descruption t = new Descruption("Animal not Vegetable"); Animal(){ System.out.println("Animal()"); } protected void dispose(){ System.out.println("Animal dispose"); //先创建的最后销毁,后创建的先销毁 t.dispose(); p.dispose(); super.dispose(); } } class Amphibian extends Animal{ private Characteristic p =new Characteristic("can live in water"); private Descruption t = new Descruption("Both water and land"); Amphibian(){ System.out.println("Amphibian()"); } protected void dispose(){ System.out.println("Amhibian dispose"); //先创建的最后销毁,后创建的先销毁 t.dispose(); p.dispose(); super.dispose(); } } public class Frog extends Amphibian{ private Characteristic p =new Characteristic("Croaks"); private Descruption t = new Descruption("Eats Bugs"); Frog(){ System.out.println("Frog()"); } protected void dispose(){ System.out.println("Frog dipose"); t.dispose(); p.dispose(); super.dispose(); } public static void main(String[] args) { Frog frog = new Frog(); System.out.println("------------------分割线-------------------"); frog.dispose(); } }

实例成员都知道自己的生命周期,当我们程序中存在共享成员(静态成员)的时候,清理工作就会变得复杂;静态成员是不受实例对象控制的,即使实例对象销毁了,静态成员还是可以访问的;

这个实验说明,创建顺序和销毁顺序刚好相反,而且说明了静态成员的声明周期问题,不难看出,当一个实例对象销毁时,静态成员还是存在的。

需要自己清理对象的时候,一定要加倍小心,避免引发不必要的麻烦!
以下是实验代码

package com.JavaSE03.demo03; class shared{ private int refcount=0;//引用计数器 private static long counter=0;//帮助生成程序id private final long id=counter++;//唯一表示程序的ID public shared (){ System.out.println("Creating "+this.toString()); } public void addRef(){ refcount++; } protected void dispose(){ if(--refcount ==0){//当refcount=0说明没有引用,可以销毁 System.out.println("Disposing "+this.toString());//模拟销毁操作 } } public String toString(){ return "shared "+this.id; } } class Composing{ private shared s; private static long counter=0; private final long id =counter++; public Composing(shared s){ System.out.println("Creating Composing"+this.id); this.s=s; this.s.addRef(); } public void dispose(){ System.out.println("Disposing Composing"+this.id); this.s.dispose(); } } public class ReferenceCounting { public static void main(String[] args) { shared s = new shared();//共享对象 //composings共用一个shared Composing [] composings = {new Composing(s),new Composing(s),new Composing(s),new Composing(s),new Composing(s)}; for (int i = 0; i < composings.length; i++) { //循环销毁组件 //当最后一个组件被销毁时,shred(共享对象)才会被销毁 composings[i].dispose(); } } }

构造器内部的多态方法的行为
实例方法都是动态绑定的。new一个对象的时候;首先会为对象分配存储空间并初始化为二进制的0。若有继承,便找到基类的构造器执行(基类中若有需要实例化的实例成员变量则先实例化),然后返回实例类,执行构造器;
在此案例中,执行构造器draw()时,由于draw()被派生类重写,而且 new 派生类 ,根据动态绑定的特性,此处基类构造器执行的是派生类重写覆盖的draw(),而执行被重写draw()时,派生类 int radius来不及被实例化,就被调用了。(建议先看试验代码,理解后再回头看此处的解析)
所以在下面的实验案例会输出。
Glyph() before draw()
RoundGlyph.draw().radius=0 —这里输出0,存储空间被初始化时的状态,来不及被初始化就被调用了
Glyph() after draw()
RoundGlyph.RoundGlyph(),radius=1
RoundGlyph.draw().radius=1

在编写构造器时,我们应该遵循这样的准则:用尽可能简单的方法使对象进入正常状态,即尽量不要在构造器里调用别的方法作一些初始化的工作;不是完全不可以,但是要避免。

package com.JavaSE03.demo03; class Glyph{ void draw(){ System.out.println("Glyph.draw()"); } Glyph(){ System.out.println("Glyph() before draw()"); //这里输出的是派生类的draw() //因为基类方法被重写了,我们new的是派生类对象,根据动态绑定的特性会导致派生类的方法把基类的方法重写覆盖了; //即使是在父类中执行draw(),执行的也是派生类重写过后的draw() //执行了派生类的draw(),但由于执行此方法时,派生类实例变量来不及被实例化,所以没有达到我们想要的结果 this.draw(); System.out.println("Glyph() after draw()"); } } class RoundGlyph extends Glyph{ private int radius =1;//没有被实例化之前,int变量初始值=0 RoundGlyph(int radius){ this.radius=radius; System.out.println("RoundGlyph.RoundGlyph(),radius="+this.radius); } void draw(){ System.out.println("RoundGlyph.draw().radius="+this.radius); } } public class PolyConstructors { public static void main(String[] args) { RoundGlyph roundGlyph = new RoundGlyph(1); roundGlyph.draw(); } }

持续更新中。


作者:这是自动生成的名字



多态 javase

需要 登录 后方可回复, 如果你还没有账号请 注册新账号