java编程思想 多形性(多态)与内部类

本文深入探讨Java中的多态性及其重要性,通过上溯造型和动态绑定的概念,解释了如何在代码中实现接口的统一处理,避免冗余的重复编写。文中以音乐类比,展示了如何通过基础类接口调用实现不同类型的对象操作,从而实现代码的可扩展性和可维护性。同时,讲解了方法调用的绑定机制,包括早期绑定与后期绑定(动态绑定)的区别,强调了Java中后期绑定的自动应用和重要性。
摘要由CSDN通过智能技术生成

作者是在写代码的过程中 看了看框架底层源码的时候看的有的懵逼,后悔当初java基础没学好,上班后CRUD粘贴工程师 工作半年之久 突然发现自己写的代码的少了点灵魂,后来感觉写的一坨屎一样,不在研究新技术了,发现新技术确实学了不用很快就会忘掉, 然后就开始买书学习了,很多人推荐我读一读《Java编程思想》这本书,网上的口碑也非常不错,于是就买来看看。这本书读起来比较生涩,不适合初学Java的小白读,毕竟是元老级别的经典之作,密密麻麻的长篇概述看着头疼,我尽可能的精简出我认为比较核心的字句写到笔记上,所以建议有一定的Java语言基础后再读这本书或者看看我的这篇笔记博客,希望大家都能有所收获!

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

多形性

“对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承。”
“多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与
“怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建
“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成
长”。
通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施
细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。
但多形性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类
型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可
被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多形性的方
法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这
种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。
在这一章中,大家要由浅入深地学习有关多形性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。同
时举一些简单的例子,其中所有无关的部分都已剥除,只保留与多形性有关的代码。

7.1 上溯造型

public class c07 {
    private int value;
    private c07(int val) { value = val; }
    public static final c07
            middleC = new c07(0),
            cSharp = new c07(1),
            cFlat = new c07(2);;
     // Etc.
     static class Instrument {
        public void play(c07 n) {
            System.out.println("Instrument.play()");
        }
    }
    // Wind objects are instruments
     // because they have the same interface:
    static class Wind extends Instrument {
        // Redefine interface method:
        @Override
        public void play(c07 n) {
            System.out.println("Wind.play()"+n.value);
        }
    }
        public static void tune(Instrument i) {
            // ...
            i.play(c07.middleC);
        }
        public static void main(String[] args) {
            Wind flute = new Wind();
            tune(flute); // Upcasting
        }
    }

其中,方法 Music.tune()接收一个Instrument 句柄,同时也接收从Instrument 衍生出来的所有东西。当一个Wind 句柄传递给 tune()的时候,就会出现这种情况。此时没有造型的必要。
这样做是可以接受的;
Instrument 里的接口必须存在于 Wind 中,因为Wind 是从Instrument 里继承得到的。
从 Wind 向Instrument的上溯造型可能“缩小”那个接口,但不可能把它变得比 Instrument 的完整接口还要小。

7.1.1 为什么要上溯造型

这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind 句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多。
但要注意:假如那样做,就需为系统内Instrument 的每种类型写一个全新的tune()。
假设按照前面的推论,加入 Stringed(弦乐)和 Brass(铜管)这两种Instrument(乐器):

public class c07 {

    private int value;

    private c07(int val) {
        value = val;
    }

    public static final c07
            middleC = new c07(0),
            cSharp = new c07(1),
            cFlat = new c07(2);

    // Etc.
   static class Instrument2 {
        public void play(c07 n) {
            System.out.println("Instrument2.play()");
        }
    }

    static class Wind2 extends Instrument2 {
        @Override
        public void play(c07 n) {
            System.out.println("Wind2.play()");
        }
    }

    static class Stringed2 extends Instrument2 {
        @Override
        public void play(c07 n) {
            System.out.println("Stringed2.play()");
        }
    }

    static class Brass2 extends Instrument2 {
        @Override
        public void play(c07 n) {
            System.out.println("Brass2.play()");
        }
    }

    public static class Music2 {
        public static void tune(Wind2 i) {
            i.play(c07.middleC);
        }

        public static void tune(Stringed2 i) {
            i.play(c07.middleC);
        }

        public static void tune(Brass2 i) {
            i.play(c07.middleC);
        }

        public static void main(String[] args) {
            Wind2 flute = new Wind2();
            Stringed2 violin = new Stringed2();
            Brass2 frenchHorn = new Brass2();
            tune(flute); // No upcasting
            tune(violin);
            tune(frenchHorn);
        }
    } ///:
}

这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的Instrument2 类编写与类紧密相关的方法。
这意味着第一次就要求多得多的编程量。以后,假如想添加一个象tune()那样的新方法或者为
Instrument 添加一个新类型,仍然需要进行大量编码工作。
此外,即使忘记对自己的某个方法进行过载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。
但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的衍生类,岂不是会简单得多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计的。
这正是“多形性”大显身手的地方。然而,大多数程序员(特别是有程序化编程背景的)对于多形性的工作原理仍然显得有些生疏。

7.2 深入理解

对于Music.java 的困难性,可通过运行程序加以体会。输出是 Wind.play()。这当然是我们希望的输出,但它看起来似乎并不愿按我们的希望行事。请观察一下tune()方法:

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

它接收 Instrument 句柄。所以在这种情况下,编译器怎样才能知道 Instrument 句柄指向的是一个 Wind,而不是一个Brass 或Stringed 呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定”这个主题。

7.2.1 方法调用的绑定

将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C 编译器只有一种方法调用,那就是“早期绑定”。
上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个 Instrument 句柄的前提下,编译器不知道具体该调用哪个方法。
解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
Java 中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。
为什么要把一个方法声明成final 呢?正如上一章指出的那样,它能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为final 方法调用生成效率更高的代码。

7.2.2 产生正确的行为

知道Java 里绑定的所有方法都通过后期绑定具有多形性以后,就可以相应地编写自己的代码,令其与基础类沟通。此时,所有的衍生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消息发给一个对象,让对象自行判断要做什么事情。”
在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常都用它说明问题。但很不幸的是,它可能误导初学者认为 OOP 只是为图形化编程设计的,这种认识当然是错误的。
形状例子有一个基础类,名为 Shape;另外还有大量衍生类型:Circle(圆形),Square(方形),
Triangle(三角形)等等。大家之所以喜欢这个例子,因为很容易理解“圆属于形状的一种类型”等概念。下面这幅继承图向我们展示了它们的关系
在这里插入图片描述
上溯造型可用下面这个语句简单地表现出来:
Shape s = new Circle();
在这里,我们创建了Circle 对象,并将结果句柄立即赋给一个Shape。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,Circle 属于Shape 的一种。
因此编译器认可上述语句,不会向我们提示一条出错消息。
当我们调用其中一个基础类方法时(已在衍生类里覆盖):s.draw();
同样地,大家也许认为会调用 Shape 的 draw(),因为这毕竟是一个 Shape 句柄。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是 Circle.draw() ,因为后期绑定已经介入(多形性)。下面这个例子从一个稍微不同的角度说明了问题:

/**
 * @author guoshunli
 * @version 1.0
 * @description: TODO
 * @date 2021/9/1 13:57
 */
public class Shapes {

    static class Shape {
        void draw() {
        }

        void erase() {
        }
    }

    static class Circle extends Shape {
        @Override
        void draw() {
            System.out.println("Circle.draw()");
        }
        @Override
        void erase() {
            System.out.println("Circle.erase()");
        }
    }

    static class Square extends Shape {
        @Override
        void draw() {
            System.out.println("Square.draw()");
        }
        @Override
        void erase() {
            System.out.println("Square.erase()");
        }
    }

    static class Triangle extends Shape {
        @Override
        void draw() {
            System.out.println("Triangle.draw()");
        }
        @Override
        void erase() {
            System.out.println("Triangle.erase()");
        }
    }

        public static Shape randShape() {
            switch ((int) (Math.random() * 3)) {
                default:
                    ;
                case 0:
                    return new Circle();
                case 1:
                    return new Square();
                case 2:
                    return new Triangle();
            }
        }

        public static void main(String[] args) {
            Shape[] s = new Shape[9];
            // 用形状填充数组:
            for (int i = 0; i < s.length; i++) {
                //请注意上溯造型是在每个 return 语句里发生的。这个语句取得指向一个 Circle,Square 或者Triangle 的句柄
                s[i] = randShape();
            }
            /***
             * Shape 句柄的一个数组,其中的数据通过对randShape()的调用填入。在这个时候,我们知道
             * 自己拥有Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,
             * 并为每个元素调用 draw()的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的
             * 编译器毋需获得任何特殊的情报 对draw()的所有调用都是通过动态绑定进行的
             */
            for (int i = 0; i < s.length; i++) {
                s[i].erase();
            }
        }
     ///:
}

针对从 Shape 衍生出来的所有东西,Shape 建立了一个通用接口——也就是说,所有(几何)形状都可以描绘和删除。
衍生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。
在主类 Shapes 里,包含了一个static 方法,名为 randShape()。它的作用是在每次调用它时为某个随机选择的Shape 对象生成一个句柄。请注意上溯造型是在每个 return 语句里发生的。这个语句取得指向一个Circle,Square 或者Triangle 的句柄,并将其作为返回类型 Shape 发给方法。所以无论什么时候调用这个方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的 Shape 句柄。
main()包含了 Shape 句柄的一个数组,其中的数据通过对randShape()的调用填入。在这个时候,我们知道自己拥有Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,并为每个元素调用 draw()的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的
那样:
Circle.draw()
Triangle.draw()
Circle.draw()
Circle.draw()
Circle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Square.draw()
当然,由于几何形状是每次随机选择的,所以每次运行都可能有不同的结果。之所以要突出形状的随机选择,是为了让大家深刻体会这一点:为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。
对draw()的所有调用都是通过动态绑定进行的。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值