多态的好处:
松耦合:类与类之间或模块与模块之间的关联程度要低,修改一个类或模块时,尽量不影响到其他类或模块
高内聚:内聚性是指在程序设计中表示一个类具有单一的明确的目标程度;要做到责越单一,目标越明确,这样才能称为内聚程度高。
紧封装:在设计类的时候,尽量把数据成员设计为私有的,通过公共接口来操作私有数据成员。
多态是为了消除类之间的耦合关系,让两个类或模块的联系程度降低;松耦合的目的是为了提高可扩展性。
以下是实验代码:
我们定义的接口,接口参数接收的类型是顶层的(继承链中的基类),我们接收都对象都是子类的实例;而同样的方法我们在调用的时候,由具体的子类去实现,这就叫多态;把接口和实现分离,消除类型间的耦合关系。
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();
}
}
持续更新中。