【《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() {}
}
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);
  }
}

结合组合与继承

  • 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
    }
}
  • 因为是从一个更具体的类转化为一个更一般的类,所以向上转型永远是安全
  • 在向上转型期间,类接口只可能失去方法,不会增加方法。这就是为什么编译器在没有任何明确转型或其他特殊标记的情况下,仍然允许向上转型
  • 尽管在教授 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 的,它就不是基类接口的一部分。****)
  • 使用继承时,派生类具有基类接口,因此可以向上转型为基类,这对于多态至关重要
  • 在开始设计时,优先使用组合(或委托),只有当确实需要时再使用继承。
  • 通过对成员类型使用继承的技巧,可以在运行时改变成员的类型和行为。因此,可以在运行时改变组合对象的行为
  • 每个类有特定的用途,而且既不应太大(包括太多功能难以复用),也不应太小(不添加其他功能就无法使用)
  • 如果设计变得过于复杂,通过将现有类拆分为更小的部分而添加更多的对象,通常是有帮助的。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值