• 【《On Java 8》学习之路——复用】知识点整理分享



    本文是对《On Java 8》即《Java编程思想》第五版的知识点汇总整理,仅供学习分享。

    代码复用是面向对象编程(OOP)最具魅力的原因之一

    任何语言都可通过简单复制来达到代码复用的目的,但是这样做的效果并不好。Java 围绕“类”(Class)来解决问题。我们可以直接使用别人构建或调试过的代码,而非创建新类、重新开始。

    如何在不污染源代码的前提下使用现存代码:

    【1】在新类中创建现有类的对象。这种方式叫做“组合”(Composition),通过这种方式复用代码的功能

    【2】采用现有类形式,又无需在编码时改动其代码,这种方式就叫做“继承”(Inheritance),编译器会做大部分的工作。继承是面向对象编程(OOP)的重要基础之一。


    复用


    组合语法

    • @Override 是可选的,但它有助于验证你没有拼写错误 (或者更微妙地说,大小写字母输入错误)
    • 编译器不会为每个引用创建一个默认对象,这是有意义的,因为在许多情况下,这会导致不必要的开销
    • 初始化引用有四种方法:
      1. 当对象被定义时。这意味着它们总是在调用构造函数之前初始化。
      2. 在该类的构造函数中。
      3. 在实际使用对象之前。这通常称为延迟初始化。在对象创建开销大且不需要每次都创建对象的情况下,它可以减少开销。
      4. 使用实例初始化。
    • 如果你试图对未初始化的引用调用方法,则未初始化的引用将产生运行时异常。

    继承语法

    • 在创建类时总是要继承,因为除非显式地继承其他类,否则就隐式地继承 Java 的标准根类对象(Object)

    • 在类主体的左大括号前的代码中声明这一点,使用关键字 extends 后跟基类的名称。将自动获得基类中的所有字段和方法

    • super 关键字引用了当前类继承的“超类”(基类)

    初始化基类

    • 当你创建派生类的对象时,它包含基类的子对象。这个子对象与你自己创建基类的对象是一样的。只是从外部看,基类的子对象被包装在派生类的对象中。

    • 必须正确初始化基类子对象 : 通过调用基类构造函数在构造函数中执行初始化,该构造函数具有执行基类初始化所需的所有适当信息和特权。Java 自动在派生类构造函数中插入对基类构造函数的调用

    • 构造从基类“向外”进行,因此基类在派生类构造函数能够访问它之前进行初始化。即使不为派生类创建构造函数,编译器也会合成一个无参数构造函数,调用基类构造函数。

    带参数的构造函数

    • 如果没有无参数的基类构造函数,或者必须调用具有参数的基类构造函数,则必须使用 super 关键字和适当的参数列表显式地编写对基类构造函数的调用
    • 对基类构造函数的调用必须是派生类构造函数中的第一个操作

    委托

    • Java不直接支持的第三种重用关系称为委托。这介于继承和组合之间,因为你将一个成员对象放在正在构建的类中(比如组合),但同时又在新类中公开来自成员对象的所有方法(比如继承)

    • 示例代码如下:

    方法被转发到底层 control 对象,因此接口与继承的接口是相同的。但是,你对委托有更多的控制,因为你可以选择只在成员对象中提供方法的子集。

    public class SpaceShipControls {
      void up(int velocity) {}
      void down(int velocity) {}
      void left(int velocity) {}
      void right(int velocity) {}
      void forward(int velocity) {}
      void back(int velocity) {}
      void turboBoost() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    public class SpaceShipDelegation {
      private String name;
      private SpaceShipControls controls =
        new SpaceShipControls();
      public SpaceShipDelegation(String name) {
        this.name = name;
      }
      // Delegated methods:
      public void back(int velocity) {
        controls.back(velocity);
      }
      public void down(int velocity) {
        controls.down(velocity);
      }
      public void forward(int velocity) {
        controls.forward(velocity);
      }
      public void left(int velocity) {
        controls.left(velocity);
      }
      public void right(int velocity) {
        controls.right(velocity);
      }
      public void turboBoost() {
        controls.turboBoost();
      }
      public void up(int velocity) {
        controls.up(velocity);
      }
      public static void main(String[] args) {
        SpaceShipDelegation protector =
          new SpaceShipDelegation("NSEA Protector");
        protector.forward(100);
      }
    }
    
    • 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

    结合组合与继承

    • Java 没有 C++ 中析构函数的概念,析构函数是在对象被销毁时自动调用的方法。

    • 在Java中,通常是忘掉而不是销毁对象,从而允许垃圾收集器根据需要回收内存。

    • 你无法知道垃圾收集器何时会被调用,甚至它是否会被调用。因此,如果你想为类清理一些东西,必须显式地编写一个特殊的方法来完成它,并确保客户端程序员知道他们必须调用这个方法。

    • 必须注意基类和成员对象清理方法的调用顺序,以防一个子对象依赖于另一个子对象。

    • 如果 Java 基类的方法名多次重载,则在派生类中重新定义该方法名不会隐藏任何基类版本。不管方法是在这个级别定义的,还是在基类中定义的,重载都会起作用

    • @Override 注释防止你意外地重载。


    组合与继承的选择

    • 组合和继承都允许在新类中放置子对象(组合是显式的,而继承是隐式的)

    • 当你想在新类中包含一个已有类的功能时使用组合,而非继承。在新类中嵌入一个对象(通常是私有的),以实现其功能。新类的使用者看到的是你所定义的新类的接口,而非嵌入对象的接口。

    • 成员对象隐藏了具体实现,所以这是安全的。当用户知道你正在组装一组部件时,会使得接口更加容易理解。

    • 当使用继承时,使用一个现有类并开发出它的新版本。通常这意味着使用一个通用类,并为了某个特殊需求将其特殊化


    protected

    • 关键字 protected 作用:想把一个事物尽量对外界隐藏,而允许派生类的成员访问。它表示“就类的用户而言,这是 private 的。但对于任何继承它的子类或在同一包中的类,它是可访问的。”(protected 也提供了包访问权限)
    • 尽管可以创建 protected 属性,但是最好的方式是将属性声明为 private 以一直保留更改底层实现的权利。

    向上转型

    • 继承最重要的方面不是为新类提供方法。它是新类与基类的一种关系。可以表述为“新类是已有类的一种类型”。
    • 因为继承保证了基类的所有方法在派生类中也是可用的,所以任意发送给该基类的消息也能发送给派生类。
    • Wind 引用转换为 Instrument 引用的行为称作向上转型:如下例所示
    class Instrument {
        public void play() {}
    
        static void tune(Instrument i) {
            // ...
            i.play();
        }
    }
    
    // Wind objects are instruments
    // because they have the same interface:
    public class Wind extends Instrument {
        public static void main(String[] args) {
            Wind flute = new Wind();
            Instrument.tune(flute); // Upcasting
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 因为是从一个更具体的类转化为一个更一般的类,所以向上转型永远是安全
    • 在向上转型期间,类接口只可能失去方法,不会增加方法。这就是为什么编译器在没有任何明确转型或其他特殊标记的情况下,仍然允许向上转型
    • 尽管在教授 OOP 的过程中我们多次强调继承,但这并不意味着要尽可能使用它。恰恰相反,尽量少使用它,除非确实使用继承是有帮助的。
    • 一种判断使用组合还是继承的最清晰的方法:问一问自己是否需要把新类向上转型为基类。如果必须向上转型,那么继承就是必要的

    final关键字

    final 数据

    • 根据上下文环境,Java 的关键字 final 的含义有些微的不同,但通常它指的是“这是不能被改变的”。

    • 许多编程语言都有某种方法告诉编译器有一块数据是恒定不变的。恒定是有用的,如:

      1. 一个永不改变的编译时常量。——编译器可以把常量带入计算中,在编译时计算,减少了一些运行时的负担
      2. 一个在运行时初始化就不会改变的值。
    • 在 Java 中,永不改变的编译时常量必须是基本类型,而且用关键字 final 修饰。你必须在定义常量的时候进行赋值。

    • 一个被 staticfinal 同时修饰的属性只会占用一段不能改变的存储空间。

    • 对于对象引用,final 使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。但是,对象本身是可以修改的

    • 按照惯例,带有恒定初始值的 final static 基本变量(即编译时常量)命名全部使用大写,单词之间用下划线分隔。

    • 空白 final 指的是没有初始化值的 final 属性

    • 编译器确保空白 final 在使用前必须被初始化。这样既能使一个类的每个对象的 final 属性值不同,也能保持它的不变性。

    • 必须在定义时或在每个构造器中执行 final 变量的赋值操作。这保证了 final 属性在使用前已经被初始化过。

    final 参数

    • 在参数列表中,将参数声明为 final 意味着在方法中不能改变参数指向的对象或基本变量,只能读取而不能修改参数。这个特性主要用于传递数据给匿名内部类

    final 方法

    • 使用 final 方法的原因:

      【1】第一个原因是给方法上锁,防止子类通过覆写改变方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变。【2】第二个原因是效率。在早期的 Java 实现中,如果将一个方法指明为 final,就是同意编译器把对该方法的调用转化为内嵌调用。当编译器遇到 final 方法的调用时,就会很小心地跳过普通的插入代码以执行方法的调用机制,而用方法体内实际代码的副本替代方法调用。这消除了方法调用的开销。但是如果一个方法很大代码膨胀,你也许就看不到内嵌带来的性能提升,因为内嵌调用带来的性能提高被花费在方法里的时间抵消了。

    • 有很长一段时间,使用 final 来提高效率都被阻止。你应该让编译器和 JVM 处理性能问题,只有在为了明确禁止覆写方法时才使用 final

    • 类中所有的 private 方法都隐式地指定为 final。因为不能访问 private 方法,所以不能覆写它。

    • 如果一个方法是 private 的,它就不是基类接口的一部分。它只是隐藏在类内部的代码,且恰好有相同的命名而已。但是如果你在派生类中以相同的命名创建了 publicprotected 或包访问权限的方法,这些方法与基类中的方法没有联系,你没有覆写方法,只是在创建新的方法而已。

    final 类

    • 当说一个类是 finalfinal 关键字在类定义之前),就意味着它不能被继承。因为类的设计永远不需要改动,或者是出于安全考虑不希望它有子类。

    • 由于 final 类禁止继承,类中所有的方法都被隐式地指定为 final,所以没有办法覆写它们


    类初始化和加载

    • 每个类的编译代码都存在于它自己独立的文件中。该文件只有在使用程序代码时才会被加载
    • 一般可以说“类的代码在首次使用时加载“。这通常是指创建类的第一个对象,或者是访问了类的 static 属性或方法
    • 构造器也是一个 static 方法尽管它的 static 关键字是隐式的。
    • 一个类当它任意一个 static 成员被访问时,就会被加载。
    • 所有的 static 对象和 static 代码块在加载时按照文本的顺序(在类中定义的顺序)依次初始化。
    • static 变量只被初始化一次。

    复用总结

    • 继承和组合都是从已有类型创建新类型。组合将已有类型作为新类型底层实现的一部分,继承复用的是接口(如果一个方法是 private 的,它就不是基类接口的一部分。****)
    • 使用继承时,派生类具有基类接口,因此可以向上转型为基类,这对于多态至关重要
    • 在开始设计时,优先使用组合(或委托),只有当确实需要时再使用继承。
    • 通过对成员类型使用继承的技巧,可以在运行时改变成员的类型和行为。因此,可以在运行时改变组合对象的行为
    • 每个类有特定的用途,而且既不应太大(包括太多功能难以复用),也不应太小(不添加其他功能就无法使用)
    • 如果设计变得过于复杂,通过将现有类拆分为更小的部分而添加更多的对象,通常是有帮助的。
  • 相关阅读:
    如何获取跑腿App源码并定制化你的业务
    支持向量机SVM
    Shiro学习笔记(1)——shiro入门
    聊聊自动化测试路上会遇到的挑战~
    ant Design的table组件结合h函数实现合并行
    idea 集成 git 后使用的常用命令
    uImage的制作过程详解
    按键中断实验
    Linux驱动开发——PCI设备驱动
    Android ArrayMap源码解析
  • 原文地址:https://blog.csdn.net/wondersfan/article/details/128207562