Java编程思想学习笔记(8)

Java编程思想学习笔记(8)

Java多态

多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。

同时多态也是面向对象设计中,继抽象以及继承之后的第三大特性。

封装,是合并属性和行为创建一种新的数据类型,继承是建立数据类型之间的某种关系(is-a),而多态就是这种关系在实际场景的运用。

多态就是把做什么和怎么做分开了;其中,做什么是指调用的哪个方法,我是去吃饭(方法a)还是去睡觉(方法b),怎么做是指实现方案,如果我选择吃饭,那么我是吃米饭还是吃面条,”分开了“则是指两件事不在同一时间确定。

向上转型

对象既可以作为它本身的类型使用,也可以作为它的基类型使用,而这种把对某个对象的引用视为对其基类型的引用的做法就是向上转型。

例子:

public enum Note {


//    演奏乐符
    MIDDLE_C, C_SHARP, B_FLAT;

}

public class Instrument {

//    乐器基类
    public void play(Note n) {
        print("Instrument.play()");
    }


}

public class Wind extends Instrument{

//  Wind是一个具体的乐器
    // Redefine interface method:
    public void play(Note n) {
        System.out.println("Wind.play() " + n);
    }


}
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); // 向上转型
    }
}

Nusic,tune()接受一个Instrument引用,同时也接受任何到出自Instrument的类,在main()中,当一个Wind引用传递到tune时,不需要任何类型转换,这是因为Wind从Instrument继承而来。

直接用对应的对象类型

在上面例子中,如果让tune方法接受一个Wind引用作为自己的参数,似乎看起来更为直观,但是会引发一个问题:这个时候你就需要为系统中Instrument的每种类型都编写一个新的tune方法。

例子:

class Stringed extends Instrument {
        public void play(Note n) {
                print("Stringed.play() " + n);
        }
}


class Brass extends Instrument {
        public void play(Note n) {
                print("Brass.play() " + n);
        }
}


public class Music2 {
        public static void tune(Wind i) {
                i.play(Note.MIDDLE_C);
        }

        public static void tune(Stringed i) {
                i.play(Note.MIDDLE_C);
        }


        public static void tune(Brass i) {
                i.play(Note.MIDDLE_C);
    
        }
        public static void main(String[] args) {
                Wind flute = new Wind();
                Stringed violin = new Stringed();
                Brass frenchHorn = new Brass();
                tune(flute); // No upcasting
                tune(violin);
                tune(frenchHorn);
        }
}

可以看出,这么做的话你就需要更多的编程,每次添加类似tune方法的时候你都需要做大量的工作。

所以我们只写一个简单的方法,仅仅接收基类作为参数,而不是特殊的导出类,这么做情况不是变得更好吗。

深入理解

public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);

}

在上面这个方法中,它接收一个Instrument引用,那么在这种情况下,编译器怎么样才能知道这个instrument引用指向的是Wind对象呢?

实际上,编译器无法得知。

绑定

将一个方法调用同一个方法主体关联起来称为绑定

若在程序执行前进行绑定,就是前期绑定,比如C语言就只有一种方法调用,就是前期绑定。

而在运行时根据对象的类型进行绑定就是后期绑定,也叫做动态绑定或者运行时绑定。

Java中除了static方法和final方法之外,其它所有方法都是后期绑定,这意味着通常情况下,我们不必判定是否应该进行后期绑定---它会自动发生。

经典例子

在面向对象程序设计中,有一个经典例子就是“几何形状”,在这个例子中,有一个基类Shape,以及多个导出类,Circle,Square,Triangle。

继承图:

未命名文件.png

想要向上转型,只需:

Shape s = new Circle();

这里创建了一个Circle对象,并把得到的引用赋值给Shape,这样看似错误,但实际是OK的,因为通过继承,Circle就是一种Shape。

例子:

public class Shape {
        public void draw() {}
        public void erase() {}
}

public class Circle extends Shape {
    public void draw() { print("Circle.draw()"); }
    public void erase() { print("Circle.erase()"); }
} 

public class Square extends Shape {
        public void draw() { print("Square.draw()"); }
        public void erase() { print("Square.erase()"); }
} 


public class Triangle extends Shape {
        public void draw() { print("Triangle.draw()"); }
        public void erase() { print("Triangle.erase()"); }
}

public class RandomShapeGenerator {
        private Random rand = new Random(47);

//    向上转型是在return语句发生的,每个return语句获得一个指向具体图形的引用
//    并将其以Shape类型从next方法中发生出去
//    所以无论什么时候调用next方法,我们都不可能知道具体类型到底是什么
//    因为我们获取的是一个通用的Shape引用
        public Shape next() {
            switch(rand.nextInt(3)) {
                    default:
                    case 0: return new Circle();
                    case 1: return new Square();
                    case 2: return new Triangle();
            }
        }
}


public class Shapes {
    private static RandomShapeGenerator gen =
    new RandomShapeGenerator();

    public static void main(String[] args) {
            Shape[] s = new Shape[9];
            // Fill up the array with shapes:
            for(int i = 0; i < s.length; i++)
                    s[i] = gen.next();
            // Make polymorphic method calls:
            for(Shape shp : s)
                shp.draw();
    }
}

在main方法中,我们只知道拥有一些Shape,除此之外不会知道其他更多的了。当我们遍历数组,调用draw方法时,与类型有关的特定行为就会自动正确发生。

上面这个例子说明,在编译时,编译器不需要获得任何特殊信息就能进行正确的调用,对draw方法的所有调用都是通过动态绑定进行的。

扩展性

例子:



class Instrument {
        void play(Note n) { print("Instrument.play() " + n); }
        String what() { return "Instrument"; }
        void adjust() { print("Adjusting Instrument"); }
}


class Wind extends Instrument {
        void play(Note n) { print("Wind.play() " + n); }
        String what() { return "Wind"; }
        void adjust() { print("Adjusting Wind"); }
} 


class Percussion extends Instrument {
        void play(Note n) { print("Percussion.play() " + n); }
        String what() { return "Percussion"; }
        void adjust() { print("Adjusting Percussion"); }
}

class Stringed extends Instrument {
        void play(Note n) { print("Stringed.play() " + n); }
    String what() { return "Stringed"; }
    void adjust() { print("Adjusting Stringed"); }
}


class Brass extends Wind {
        void play(Note n) { print("Brass.play() " + n); }
        void adjust() { print("Adjusting Brass"); }
}


class Woodwind extends Wind {
        void play(Note n) { print("Woodwind.play() " + n); }
        String what() { return "Woodwind"; }
} 


public class Music3 {
    // Doesn’t care about type, so new types
    // added to the system still work right:
    public static void tune(Instrument i) {
        // ...
        i.play(Note.MIDDLE_C);
    }

    public static void tuneAll(Instrument[] e) {
            for(Instrument i : e)
            tune(i);
    }
    public static void main(String[] args) {
            // Upcasting during addition to the array:
            Instrument[] orchestra = {
                    new Wind(),
                    new Percussion(),
                    new Stringed(),
                    new Brass(),
                    new Woodwind()
            };
            tuneAll(orchestra);
    }
}

为乐器系统添加更多的类型,而不用改动tune方法。tune方法完全可以忽略它周围代码所发生的全部变化,依旧正常运行,

“覆盖”私有方法

下面这个例子:


public class PrivateOverride {
    private void f() { print("private f()"); }
    public static void main(String[] args) {
            PrivateOverride po = new Derived();
            po.f();
    }
}


class Derived extends PrivateOverride {

        public void f() { print("public f()"); }
} 

期望输出的是public f(),但是private方法被自动修饰为final,而且对导出类是屏蔽的,所以在Derived类中的f方法是一个全新的方法。既然基类中的f方法在在子类Derived中不可见,那么也不能被重载。

域与静态方法

例子:


public class Super {

    public int field = 0;
    public int getField() { return field; }
    
}

public 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(); // Upcast
        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());
    }


}

上面这个例子中,当Sub对象转型为Super引用时,任何域的访问操作都是由编译器解析的,因此不是多态的。

如果某个方法是静态的,那么它就不具有多态性

public class StaticSuper {

    public static String staticGet() {
        return "Base staticGet()";
    }
    public String dynamicGet() {
        return "Base dynamicGet()";
    }

    
}

public 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(); // Upcast
        System.out.println(sup.staticGet());
        System.out.println(sup.dynamicGet());
    }


}

构造器与多态

通常,构造器不同于其它方法,涉及到多态时也是如此。构造器是不具有多态性的

构造器的调用顺序

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接。使得每个基类的构造器都能得到调用。

例子:

public class Meal {

    Meal() { print("Meal()"); }
}

public class Cheese {
    Cheese() { print("Cheese()"); }
}

public class Lettuce {

    Lettuce() { print("Lettuce()"); }
}

public class Bread {

    Bread() { print("Bread()"); }
}

public class Lunch extends Meal{

    Lunch() { print("Lunch()"); }


}

public class PortableLunch extends Lunch{

    PortableLunch() { print("PortableLunch()");}

}

public class Sandwich extends PortableLunch{

    private Bread b = new Bread();

    private Cheese c = new Cheese();

    private Lettuce l = new Lettuce();

    public Sandwich() { print("Sandwich()"); }


    public static void main(String[] args) {
        new Sandwich();
    }


}


例子中最重要的是Sandwich,它反映了三层继承,以及三个成员对象。

可以看到复杂对象调用构造器要遵照下面的顺序:

  • 1 调用基类构造器,不断反复递归下去

  • 2 按照声明顺序调用成员的初始化方法

  • 3 调用导出类构造器的主体

构造器内部的多态方法的行为

构造器调用的层次结构带来一个问题:如果在一个构造器内部调用正在构造的对象的某个动态绑定方法,会发生什么?

例子:

public class Glyph {

    void draw() { print("Glyph.draw()"); }
    Glyph() {
        print("Glyph() before draw()");
        draw();
        print("Glyph() after draw()");
    }


}


public class RoundGlyph extends Glyph{

    private int radius = 1;

    RoundGlyph(int r) {
        radius = r;
        print("RoundGlyph.RoundGlyph(), radius = " + radius);
    }
    void draw() {
        print("RoundGlyph.draw(), radius = " + radius);
    }


}


public class PolyConstructors {

    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}




输出:

Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5

在Glyph的构造器中,我们调用了draw方法,因为这个是动态绑定方法的缘故,我们就会调用导出类RoundGlyph中的draw方法,但是这个方法操纵的成员radius还没初始化,所以就体现出问题了,结果中第一次输出radius为0。

所以初始化的实际过程是:

  • 1 在其他任何事物之前,将分配给对象的存储空间初始化成二进制的零

  • 2 如前所述调用基类构造器

  • 3 按照声明的顺序调用成员的初始化方法

  • 4 调用导出类的构造器主体

协变返回类型

在面向对象程序设计中,协变返回类型指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更 "狭窄" 的类型。

Java 5.0添加了对协变返回类型的支持,即子类覆盖(即重写)基类方法时,返回的类型可以是基类方法返回类型的子类。协变返回类型允许返回更为具体的类型。

例子:

import java.io.ByteArrayInputStream;
import java.io.InputStream;

class Base
{
    //子类Derive将重写此方法,将返回类型设置为InputStream的子类
   public InputStream getInput()
   {
      return System.in;
   }
}
public  class Derive extends Base
{
    
    @Override
    public ByteArrayInputStream getInput()
    {
        
        return new ByteArrayInputStream(new byte[1024]);
    }
    public static void main(String[] args)
    {
        Derive d=new Derive();
        System.out.println(d.getInput().getClass());
    }
}
/*程序输出:
class java.io.ByteArrayInputStream
*/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容