文章目录

  • 小结
  • 原文
    • 组合语法
    • 继承语法
    • 委托
    • 结合组合与继承
    • 组合与继承的选择

小结

根据《On Java 8》: 第八章复用 总结

复用指的是代码复用,一般情况下有两种方式:组合、继承。还有一种是委托(了解就行)。

  • 组合:把一个对象的引用(object references)放置在一个新的类里,这就使用了组合。
  • 继承:派生类 extends 基类,派生类自动获得基类中的所有字段和方法,(还有方法重写)
  • 委托:Java 不直接支持的第三种重用关系称为委托。这介于继承和组合之间,因为你将一个成员对象放在正在构建的类中 (比如组合),但同时又在新类中公开来自成员对象的所有方法 (比如继承)

组合继承的选择:

  • 组合和继承都允许在新类中放置子对象(组合是显式的,而继承是隐式的)
  • 当你想在新类中包含一个已有类的功能时,使用组合,而非继承。也就是说,在新类中嵌入一个对象(通常是私有的),以实现其功能。新类的使用者看到的是你所定义的新类的接口,而非嵌入对象的接口。
  • 当使用继承时,使用一个现有类并开发出它的新版本。通常这意味着使用一个通用类,并为了某个特殊需求将其特殊化。稍微思考下,你就会发现,用一个交通工具对象来组成一部车是毫无意义的——车不包含交通工具,它就是交通工具这种 “是一个”的关系是用继承来表达的,而 “有一个 “的关系则用组合来表达。
  • 一种判断使用组合还是继承的最清晰的方法是问一问自己是否需要把新类向上转型为基类。如果必须向上转型,那么继承就是必要的,但如果不需要,则要进一步考虑是否该采用继承。

继承和组合都是从已有类型创建新类型。组合将已有类型作为新类型底层实现的一部分,继承复用的是接口。
使用继承时,派生类具有基类接口,因此可以向上转型为基类,这对于多态至关重要。

尽管在面向对象编程时极力强调继承,但在开始设计时,优先使用组合(或委托),只有当确实需要时再使用继承。组合更具灵活性。

原文

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

​ 对于像 C 语言等面向过程语言来说,“复用” 通常指的就是 “复制代码”。任何语言都可通过简单复制来达到代码复用的目的,但是这样做的效果并不好。Java 围绕 “类”(Class)来解决问题。我们可以直接使用别人构建或调试过的代码,而非创建新类、重新开始。
​ 如何在不污染源代码的前提下使用现存代码是需要技巧的。在本章里,你将学习到两种方式来达到这个目的:

  1. 第一种方式直接了当。在新类中创建现有类的对象。这种方式叫做 “组合”(Composition),通过这种方式复用代码的功能,而非其形式。
  2. 第二种方式更为微妙。创建现有类类型的新类。照字面理解:采用现有类形式,又无需在编码时改动其代码,这种方式就叫做 “继承”(Inheritance),编译器会做大部分的工作。继承是面向对象编程(OOP)的重要基础之一。更多功能相关将在多态(Polymorphism)章节中介绍。

组合与继承的语法、行为上有许多相似的地方(这其实是有道理的,毕竟都是基于现有类型构建新的类型)。在本章中,你会学到这两种代码复用的方法。

组合语法

在前面的学习中,“组合”(Composition)已经被多次使用。你仅需要把对象的引用(object references)放置在一个新的类里,这就使用了组合。例如,假设你需要一个对象,其中内置了几个 String 对象,两个基本类型(primitives)的属性字段,一个其他类的对象。对于非基本类型对象,将引用直接放置在新类中,对于基本类型属性字段则仅进行声明。

// reuse/SprinklerSystem.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Composition for code reuseclass WaterSource {private String s;WaterSource() {System.out.println("WaterSource()");s = "Constructed";}@Overridepublic String toString() { return s; }}public class SprinklerSystem {private String valve1, valve2, valve3, valve4;private WaterSource source = new WaterSource();private int i;private float f;@Overridepublic String toString() {return"valve1 = " + valve1 + " " +"valve2 = " + valve2 + " " +"valve3 = " + valve3 + " " +"valve4 = " + valve4 + "\n" +"i = " + i + " " + "f = " + f + " " +"source = " + source; // [1]}public static void main(String[] args) {SprinklerSystem sprinklers = new SprinklerSystem();System.out.println(sprinklers);}}
/* Output:
WaterSource()
valve1 = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = Constructed
*/

​ 这两个类中定义的一个方法是特殊的: toString()。每个非基本类型对象都有一个toString() 方法,在编译器需要字符串但它有对象的特殊情况下调用该方法。因此,在[1] 中,编译器看到你试图 “添加” 一个 WaterSource 类型的字符串对象。因为字符串只能拼接另一个字符串,所以它就先会调用 toString() 将 source 转换成一个字符串。然后,它可以拼接这两个字符串并将结果字符串传递给 System.out.println()。要对创建的任何类允许这种行为,只需要编写一个 toString() 方法。在 toString() 上使用 @Override 注释来告诉编译器,以确保正确地覆盖。@Override 是可选的,但它有助于验证你没有拼写错误 (或者更微妙地说,大小写字母输入错误)。类中的基本类型字段自动初始化为零,正如 object Everywhere 一章中所述。但是对象引用被初始化为 null,如果你尝试调用其任何一个方法,你将得到一个异常(一个运行时错误)。方便的是,打印 null 引用却不会得到异常。

​ 编译器不会为每个引用创建一个默认对象,这是有意义的,因为在许多情况下,这会导致不必要的开销。初始化引用有四种方法:

  1. 当对象被定义时。这意味着它们总是在调用构造函数之前初始化。
  2. 在该类的构造函数中。
  3. 在实际使用对象之前。这通常称为延迟初始化。在对象创建开销大且不需要每次
    都创建对象的情况下,它可以减少开销。
  4. 使用实例初始化。

以上四种实例创建的方法例子在这:

// reuse/Bath.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Constructor initialization with compositionclass Soap {private String s;Soap() {System.out.println("Soap()");s = "Constructed";}@Overridepublic String toString() { return s; }}public class Bath {private String // Initializing at point of definition:s1 = "Happy",s2 = "Happy",s3, s4;private Soap castille;private int i;private float toy;public Bath() {System.out.println("Inside Bath()");s3 = "Joy";toy = 3.14f;castille = new Soap();}// Instance initialization:{ i = 47; }@Overridepublic String toString() {if(s4 == null) // Delayed initialization:s4 = "Joy";return"s1 = " + s1 + "\n" +"s2 = " + s2 + "\n" +"s3 = " + s3 + "\n" +"s4 = " + s4 + "\n" +"i = " + i + "\n" +"toy = " + toy + "\n" +"castille = " + castille;}public static void main(String[] args) {Bath b = new Bath();System.out.println(b);}}
/* Output:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
*/

​ 在 Bath 构造函数中,有一个代码块在所有初始化发生前就已经执行了。当你不在定义处初始化时,仍然不能保证在向对象引用发送消息之前执行任何初始化——如果你试图对未初始化的引用调用方法,则未初始化的引用将产生运行时异常。

​ 当调用 toString() 时,它将赋值 s4,以便在使用字段的时候所有的属性都已被初始化。

继承语法

​ 继承是所有面向对象语言的一个组成部分。事实证明,在创建类时总是要继承,因为除非显式地继承其他类,否则就隐式地继承 Java 的标准根类对象(Object)。

​ 组合的语法很明显,但是继承使用了一种特殊的语法。当你继承时,你说,“这个新类与那个旧类类似。你可以在类主体的左大括号前的代码中声明这一点,使用关键字extends 后跟基类的名称。当你这样做时,你将自动获得基类中的所有字段和方法。这里有一个例子:

// reuse/Detergent.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Inheritance syntax & propertiesclass Cleanser {private String s = "Cleanser";public void append(String a) { s += a; }public void dilute() { append(" dilute()"); }public void apply() { append(" apply()"); }public void scrub() { append(" scrub()"); }@Overridepublic String toString() { return s; }public static void main(String[] args) {Cleanser x = new Cleanser();x.dilute(); x.apply(); x.scrub();System.out.println(x);}}public class Detergent extends Cleanser {// Change a method:@Overridepublic void scrub() {append(" Detergent.scrub()");super.scrub(); // Call base-class version}// Add methods to the interface:public void foam() { append(" foam()"); }// Test the new class:public static void main(String[] args) {Detergent x = new Detergent();x.dilute();x.apply();x.scrub();x.foam();System.out.println(x);System.out.println("Testing base class:");Cleanser.main(args);}}
/* Output:
Cleanser dilute() apply() Detergent.scrub() scrub()
foam()
Testing base class:
Cleanser dilute() apply() scrub()
*/

​ 这演示了一些特性。首先,在 Cleanser 的 append() 方法中,使用 += 操作符将字符串连接到 s,这是 Java 设计人员 “重载” 来处理字符串的操作符之一 (还有 + )。

​ 第二,Cleanser 和 Detergent 都包含一个 main() 方法。你可以为每个类创建一个 main() ; 这允许对每个类进行简单的测试。当你完成测试时,不需要删除 main();你可以将其留在以后的测试中。即使程序中有很多类都有 main() 方法,惟一运行的只有在命令行上调用的 main()。这里,当你使用 java Detergent 时候,就调用了Detergent.main()。但是你也可以使用 java Cleanser 来调用 Cleanser.main(),即使 Cleanser 不是一个公共类。即使类只具有包访问权,也可以访问 public main()。

​ 在这里,Detergent.main() 显式地调用 Cleanser.main(),从命令行传递相同的参数 (当然,你可以传递任何字符串数组)。

​ Cleanser 中的所有方法都是公开的。请记住,如果不使用任何访问修饰符,则成员默认为包访问权限,这只允许包内成员访问。因此,如果没有访问修饰符,那么包内的任何人都可以使用这些方法。例如,Detergent 就没有问题。但是,如果其他包中的类继承 Cleanser,则该类只能访问 Cleanser 的公共成员。因此,为了允许继承,一般规则是所有字段为私有,所有方法为公共。(protected 成员也允许派生类访问; 你以后会知道的。) 在特定的情况下,你必须进行调整,但这是一个有用的指南。

​ Cleanser 的接口中有一组方法: append()、dilute()、apply()、scrub() 和toString()。因为 Detergent 是从 Cleanser 派生的 (通过 extends 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 Detergent 中看到所有这些方法的显式定义。那么,可以把继承看作是复用类。如在 scrub() 中所见,可以使用基类中定义的方法并修改它。在这里,你可以在新类中调用基类的该方法。但是在 scrub()内部,不能简单地调用 scrub(),因为这会产生递归调用。为了解决这个问题,Java 的super 关键字引用了当前类继承的 “超类”(基类)。因此表达式 super.scrub() 调用方法 scrub() 的基类版本。

继承时,你不受限于使用基类的方法。你还可以像向类添加任何方法一样向派生类添加新方法: 只需定义它。方法 foam() 就是一个例子。Detergent.main() 中可以看到,对于 Detergent 对象,你可以调用 Cleanser 和 Detergent 中可用的所有方法(如 foam() )。

委托

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

// reuse/SpaceShipControls.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.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() {}}建造宇宙飞船的一种方法是使用继承:// reuse/DerivedSpaceShip.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.public class DerivedSpaceShip extends SpaceShipControls {private String name;public DerivedSpaceShip(String name) {this.name = name;}@Overridepublic String toString() { return name; }public static void main(String[] args) {DerivedSpaceShip protector =new DerivedSpaceShip("NSEA Protector");protector.forward(100);}}

​ 然而,DerivedSpaceShip 并不是真正的 “一种” SpaceShipControls ,即使你“告诉” DerivedSpaceShip 调用 forward()。更准确地说,一艘宇宙飞船包含了 SpaceShipControls,同时 SpaceShipControls 中的所有方法都暴露在宇宙飞船中。委托解决了这个难题:

// reuse/SpaceShipDelegation.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.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);}}

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

​ 虽然 Java 语言不支持委托,但是开发工具常常支持。例如,上面的例子是使用JetBrains Idea IDE 自动生成的。

结合组合与继承

​ 你将经常同时使用组合和继承。下面的例子展示了使用继承和组合创建类,以及必要的构造函数初始化:

// reuse/PlaceSetting.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Combining composition & inheritanceclass Plate {Plate(int i) {System.out.println("Plate constructor");}}class DinnerPlate extends Plate {DinnerPlate(int i) {super(i);System.out.println("DinnerPlate constructor");}}class Utensil {Utensil(int i) {System.out.println("Utensil constructor");}}class Spoon extends Utensil {Spoon(int i) {super(i);System.out.println("Spoon constructor");}}class Fork extends Utensil {Fork(int i) {super(i);System.out.println("Fork constructor");}}class Knife extends Utensil {Knife(int i) {super(i);System.out.println("Knife constructor");}}// A cultural way of doing something:class Custom {Custom(int i) {System.out.println("Custom constructor");}}public class PlaceSetting extends Custom {private Spoon sp;private Fork frk;private Knife kn;private DinnerPlate pl;public PlaceSetting(int i) {super(i + 1);sp = new Spoon(i + 2);frk = new Fork(i + 3);kn = new Knife(i + 4);pl = new DinnerPlate(i + 5);System.out.println("PlaceSetting constructor");}public static void main(String[] args) {PlaceSetting x = new PlaceSetting(9);}}
/* Output:
Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
PlaceSetting constructor
*/

​ 尽管编译器强制你初始化基类,并要求你在构造函数的开头就初始化基类,但它并不监视你以确保你初始化了成员对象。注意类是如何干净地分离的。你甚至不需要方法重用代码的源代码。你最多只导入一个包。(这对于继承和组合都是正确的。)

组合与继承的选择

组合和继承都允许在新类中放置子对象(组合是显式的,而继承是隐式的)。你或许想知道这二者之间的区别,以及怎样在二者间做选择。

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

​ 有时让类的用户直接访问到新类中的组合成分是有意义的。只需将成员对象声明为 public 即可(可以把这当作 “半委托” 的一种)。成员对象隐藏了具体实现,所以这是安全的。当用户知道你正在组装一组部件时,会使得接口更加容易理解。下面的 car对象是个很好的例子:

// reuse/Car.java
// Composition with public objectsclass Engine {public void start() {}public void rev() {}public void stop() {}}class Wheel {public void inflate(int psi) {}}class Window {public void rollup() {}public void rolldown() {}}class Door {public Window window = new Window();public void open() {}public void close() {}}public class Car {public Engine engine = new Engine();public Wheel[] wheel = new Wheel[4];public Door left = new Door(), right = new Door(); // 2-doorpublic Car() {for (int i = 0; i < 4; i++) {wheel[i] = new Wheel();}}public static void main(String[] args) {Car car = new Car();car.left.window.rollup();car.wheel[0].inflate(72);}}

​ 因为在这个例子中 car 的组合也是问题分析的一部分(不是底层设计的部分),所以声明成员为 public 有助于客户端程序员理解如何使用类,且降低了类创建者面临的代码复杂度。但是,记住这是一个特例。通常来说,属性还是应该声明为 private。

当使用继承时,使用一个现有类并开发出它的新版本。通常这意味着使用一个通用类,并为了某个特殊需求将其特殊化。 稍微思考下,你就会发现,用一个交通工具对象来组成一部车是毫无意义的——车不包含交通工具,它就是交通工具这种 “是一个”的关系是用继承来表达的,而 “有一个 “的关系则用组合来表达。

《On Java 8》- 面向对象之代码复用(组合、继承、委托)相关推荐

  1. JavaScript面向对象——深入理解寄生组合继承

    JavaScript面向对象--深入理解寄生组合继承 之前谈到过组合继承,会有初始化两次实例方法/属性的缺点,接下来我们谈谈为了避免这种缺点的寄生组合继承 寄生组合继承: 思路:组合继承中,构造函数继 ...

  2. Java中面向对象的三大特征之一——继承

    继承 1.继承是类和类之间的一种关系java中的类和类之间的关系有很多中,继承只是其中一种,其他的还有依赖.组合.聚合等2.继承关系的俩个类,一个是子类,一个是父类子类也可以称为派生类,父类也可以称为 ...

  3. java 中组合与复用_Java 代码复用(组合与继承)

    java中的类都是围绕着类进行的.可以通过创建新类来复用代码,而不必从头编写.可以使用别人已经开发并调试好的类.此方法使用的窍门在于使用类而不破坏现有的程序代码.达到这一目的的方法有两种: 第一种方法 ...

  4. [转载] JAVA面向对象之代码块 继承 方法的重写 super关键字与重写toString()方法介绍

    参考链接: 可以重写Java中的私有方法吗 JAVA面向对象之代码块与继承 代码块分类 局部代码块 作用:限制变量生命周期 书写位置:在方法中 构造代码块 开发中很少使用 书写位置:类中  方法外 调 ...

  5. Rust 中的继承与代码复用

    Rust 中的继承与代码复用 在学习Rust过程中突然想到怎么实现继承,特别是用于代码复用的继承,于是在网上查了查,发现不是那么简单的. C++的继承 首先看看c++中是如何做的. 例如要做一个场景结 ...

  6. Java刷漆问题代码,@不负代码不负漆

    面向对象 1. 面向对象的三大特性封装 继承 多肽 1.1 原型链的知识原型链是面向对象的基础,是非常重要的部分.有以下几种知识: 2. 创建原型的几种方法 2.1 方式一:字面量var obj1 = ...

  7. python 类和对象_Python零基础入门学习33:类与面向对象编程:类的继承

    注:本文所有代码均经过Python 3.7实际运行检验,保证其严谨性. 本文字数约1300,阅读时间约为3分钟. Python面向对象编程 类的继承机制 如果一个类A继承自另一个类B,就把继承者类A称 ...

  8. JS基础--组合继承,寄生组合式继承

    以下内容总结自<JavaScript高级程序设计(第3版)> 一. 组合继承 组合继承使用原型链实现对原型属性和方法的继承,使用借用构造函数实现对实例属性的继承(引用类型的属性写在构造函数 ...

  9. Java面向对象与代码编写

    Java面向对象与代码编写 面向过程的思想和面向对象的思想 面向对象和面向过程的思想有着本质上的区别, 作为面向对象的思维来说,当你拿到一个问题时,你分析这个问题不再是第一步先做什么,第二步再做什么, ...

最新文章

  1. 快速构建Windows 8风格应用13-SearchContract构建
  2. vc2008使用技巧
  3. C# 全角半角相互转换
  4. 基于DNS实现智能化访问网站
  5. NodeJS-001-Nodejs学习文档整理(转-出自http://www.cnblogs.com/xucheng)
  6. Linux学习总结(65)——Linux 服务器安全强化的七个步骤
  7. 一个新基民的感叹:人心不足蛇吞象
  8. gem ruby on rails 安装出错GemNotFoundException
  9. ubuntu中使用.rpm
  10. 在Linux下群ping脚本,Linux下使用screen和ping命令对网络质量进行监控
  11. cad2016中选择全图字体怎么操作_在学习CAD的过程中,经常会遇到的10个问题,你遇到过吗...
  12. 圈小猫游戏与天使问题——容错值理论
  13. 简单快速将pdf转换成jpg的方法
  14. OSChina 周六乱弹 ——是不是傻!是不是傻!
  15. 手机免流量,还会是天方夜谭吗?
  16. 三年级计算机老师个人总结,三年级计算机教学工作总结
  17. vue实现ps辅助线功能
  18. magento 模块化开发_Magento中的PayPal信用卡令牌化
  19. 极客时间运维进阶训练营第二周作业
  20. picpick尺子像素大小精度不够准确_picpick尺子像素大小精度不够准确_相机的像素精度,物理定位精度,亚像素定位之间的关系和进行像素的固定误差累积......

热门文章

  1. TreeList 节点拖曳
  2. 斯坦福 AI Lab 主任 Chris Manning:人工智能研究的最新趋势和挑战
  3. 美国AMC数学竞赛的含金量如何?
  4. Python预测2022世界杯1/8决赛胜负
  5. 一千瓶酒有一瓶酒有毒药,问你最少用多少只老鼠可以找出那瓶毒酒? 老鼠毒发的时间在两小时内,要求在两个小时内找出毒酒。
  6. 平面几何----用梅涅劳斯定解20年一道高三数学模拟题
  7. wifidog 源码初分析(三)
  8. python江红第五章课后答案_第五章课后习题参考答案
  9. 2022-2028全球汽车压力传感器行业发展现状调研及投资前景分析报告
  10. java.lang.IllegalArgumentException: Index for header ‘XXX‘ is 1 but CSVRecord only has 1 value