【java编程思想第五版笔记】第八章复用(更新完毕)

  • 继承语法
  • 初始化基类
  • 带参数的构造函数
  • 委托
  • 应用的场景
  • 结合组合与继承
  • 名称隐藏
  • 组合与继承的选择
  • 组合和继承的区别
  • 组合使用的场景
  • 使用继承的场景
  • protected
  • 向上转型
  • final关键字
  • 空白final
  • final参数
  • final 类


最后更新时间:2020.04.28

本笔记大部分基于《on java 8》整理, 另外初学者一枚,大家多多关照,有错误可以在下面说出来 谢谢大家

第⼋章 复⽤

代码复⽤是⾯向对象编程(OOP)最具魅⼒的原因之⼀。

面向过程和面向对象复用的区别:

对于像 C 语⾔等⾯向过程语⾔来说,“复⽤”通常指的就是“复制代码”

Java 围绕“类”(Class)来解决问题。我们可以直接使⽤别⼈构建或

调试过的代码,⽽⾮创建新类、重新开始。

前提:在不污染源代码的前提下使⽤现存代码

两种方法:组合和继承

他们都是基于现有类型构建新的类型

组合的语法

概念:把对象的引⽤(object references)放置在⼀个新的类⾥,这就使⽤了组合。

举例如下:假设你需要⼀个对象,其中内置了⼏个 String 对象,两个基本类

型(primitives)的属性字段,⼀个其他类的对象。

注:

对于⾮基本类型对象,

将引⽤直接放置在新类中,对于基本类型属性字段则仅进⾏声明。

代码如下:

class WaterSource {
  private String s;
  WaterSource() {
    System.out.println("WaterSource()");
    s = "Constructed";
  }
  @Override
  public String toString() { return s; }
}

public class SprinklerSystem {
  private String valve1, valve2, valve3, valve4;
  private WaterSource source = new WaterSource();
  private int i;
  private float f;
  @Override
  public 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);
  }
}
/*输出结果:
WaterSource()
valve1 = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = Constructed
*/

关于上面的程序的解析

仅限于个人理解

如果没有设置初始值,那么,引用的初始化结果为null,基础类型为int 的初始化结果是0,float类型的初始化结果是0.0

其中最重要的是source = Constructed的运行逻辑是new SprinklerSystem(),又因为在这个初始化块中声明了s的值是"Constructed",所以最后输出source = Constructed

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

  1. 当对象被定义时。这意味着它们总是在调用构造函数之前初始化。
  2. 在该类的构造函数中。
  3. 在实际使用对象之前。这通常称为延迟初始化。在对象创建开销大且不需要每次都创建对象的情况下,它可以减少开销。
  4. 使用实例初始化。
    代码如下:
class Soap {
  private String s;
  Soap() {
    System.out.println("Soap()");
    s = "Constructed";
  }
  @Override
  public 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()");//1
    s3 = "Joy";
    toy = 3.14f;
    castille = new Soap();
  }
  // Instance initialization:
  { i = 47; }
  @Override
  public 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);
  }
}
/* 输出结果:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
*/

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

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

继承语法

继承是面向对象的三大特性之一,每个类都会继承,如果它们不显式继承其他类,那么肯定它们隐式继承(Object)类,也是所有java类的祖类。

组合和继承的区别就是继承使用了一种特殊的语法——使用关键字 extends 后跟基类的名称。

class 派生类 extends 基类

之后派生类就会拥有基类的所有字段和方法,无需再次声明。

注:java不允许菱形继承

代码示例如下

class 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()");
  }

  @Override
  public 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:
  @Override
  public 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()
*/

这演示了一些特性。首先,在 Cleanserappend() 方法中,使用 += 操作符将字符串连接到 s,这是 Java 设计人员“重载”来处理字符串的操作符之一 ,专门为了处理字符串而进行重载的。

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

Cleanser 的接口中有一组方法: append()dilute()apply()scrub()toString()。因为 Detergent 是从 Cleanser 派生的(通过 extends 关键字),即使你没有在 Detergent 中看到所有这些方法的显式定义,它也会在其接口中自动获取所有这些方法。

但是如果从基类里面的继承过来的方法不满意的话,可以覆写这个方法,只需要定义一个同名方法即可,不过你最好加上 @Override注解,如 scrub() 方法就进行了重写。

那么,可以把继承看作是复用类。如在 scrub() 中所见,可以使用基类中定义的方法并修改它。在这里,你可以在新类中调用基类的该方法。但是在 scrub() 内部,不能简单地调用 scrub(),因为这会产生递归调用。为了解决这个问题,Java的 super 关键字引用了当前类继承的“超类”(基类)。因此表达式 super.scrub() 调用方法 scrub() 的基类版本。

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

初始化基类

派生类和基类都有相同的接口,但是继承并不只是复制基类的接口,派生类还包括基类的子对象,这个这个子对象与你自己创建基类的对象是一样的。只是看起来像被包裹在派生类的对象中一样,只有外形的区别。

正确初始化基类子对象,只有一种方法

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

理解起来似乎有点麻烦,我们来上代码

class Art {
  Art() {
    System.out.println("Art constructor");
  }
}

class Drawing extends Art {
  Drawing() {
    System.out.println("Drawing constructor");
  }
}

public class Cartoon extends Drawing {
  public Cartoon() {
    System.out.println("Cartoon constructor");
  }
  public static void main(String[] args) {
    Cartoon x = new Cartoon();
  }
}
/* Output:
Art constructor
Drawing constructor
Cartoon constructor
*/

由输出结果可知:

构造从基类“向外”进行,因此基类在派生类构造函数能够访问它之前进行初始化。

这个和剥洋葱刚好是相反的。构造从内向外进行,即使 Cartoon 没有创建构造函数,编译器也会自动为你合成一个无参数构造函数,调用基类构造函数。

带参数的构造函数

以上的所有例子都是无参的,所以编译器会自动帮你创造一个无参的构造器,并调用他们。但是必须调用具有参数的基类构造函数,则必须使用 super 关键字和适当的参数列表显式地编写对基类构造函数的调用,其中super必须写在类的第一行。

如果没有在 BoardGame 构造函数中调用基类构造函数,编译器找不到 Game() 的构造函数,会进行报错。

代码如下:

class Game {
  Game(int i) {
    System.out.println("Game constructor");
  }
}

class BoardGame extends Game {
  BoardGame(int i) {
    super(i);
    System.out.println("BoardGame constructor");
  }
}

public class Chess extends BoardGame {
  Chess() {
    super(11);
    System.out.println("Chess constructor");
  }
  public static void main(String[] args) {
    Chess x = new Chess();
  }
}
/* Output:
Game constructor
BoardGame constructor
Chess constructor
*/

委托

委派:一个对象请求另一个对象的功能,捕获一个操作并将其发送到另一个对象。

应用的场景

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

例如,宇宙飞船需要一个控制模块:

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
DerivedSpaceShip extends SpaceShipControls {
  private String name;
  public DerivedSpaceShip(String name) {
    this.name = name;
  }
  @Override
  public String toString() { return name; }
  public static void main(String[] args) {
    DerivedSpaceShip protector =
        new DerivedSpaceShip("NSEA Protector");
    protector.forward(100);
  }
}

不过有一个问题,DerivedSpaceShip 并不是真正的“一种” SpaceShipControls ,一艘宇宙飞船包含了 **SpaceShipControls **,同时 SpaceShipControls 中的所有方法都暴露在宇宙飞船中。

委托解决了这个难题:

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 来自动生成。

结合组合与继承

class 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 Kinfe extends Utensil {
  Kinfe(int i) {
    super(i);
    System.out.println("Kinfe constructor");
  }
}

class Custom {
  Custom(int i) {
    System.out.println("Custom  constructor");
  }
}

public class PlaceSetting extends Custom {
  private Spoon sp;
  private Fork frk;
  private Kinfe kn;
  private DinnerPlate p1;

  public PlaceSetting(int i) {
    super(i);
    sp = new Spoon(i + 2);
    frk = new Fork(i + 3);
    kn = new Kinfe(i + 6);
    p1 = new DinnerPlate(i + 5);

    System.out.println(i);
    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
*/

注:个人认为要注意一点,i的值和本次程序结果无任何关系,也不会对结果进行任何干扰,i只是为了传参实验所用

名称隐藏

如果 Java 基类的方法名多次重载,即使你在派生类中重新定义该方法,也不会隐藏任何基类版本。无论它在哪定义的,重载都会起作用:

class Homer {
  char doh(char c) {
    System.out.println("doh(char)");
    return 'd';
  }
  float doh(float f) {
    System.out.println("doh(float)");
    return 1.0f;
  }
}

class Milhouse {}

class Bart extends Homer {
  void doh(Milhouse m) {
    System.out.println("doh(Milhouse)");
  }
}

public class Hide {
  public static void main(String[] args) {
    Bart b = new Bart();
    b.doh(1);
    b.doh('x');
    b.doh(1.0f);
    b.doh(new Milhouse());
  }
}
/* Output:
doh(float)
doh(char)
doh(float)
doh(Milhouse)
*/

如结果所示:Homer 的所有重载方法在 Bart 中都是可用的,哪怕Bart 引入了一种新的重载方法,也不会覆盖它。

组合与继承的选择

组合和继承的区别

相同点:组合和继承都允许在新类中放置子对象

不同点:组合是显式的,而继承是隐式的

如何选择

组合使用的场景

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

优点

新类的使用者看到的是你所定义的新类的接口,而非嵌入对象的接口。

成员对象隐藏了具体实现,是安全的。

使用方法:

只需将成员对象声明为 public 即可(可以把这当作“半委托”的一种)。

示例如下:

// reuse/Car.java
// Composition with public objects
class 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-door
    
    public 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);
    }
}

注:声明成员为 public 有助于客户端程序员理解如何使用类,且降低了类创建者面临的代码复杂度。但是,记住这是一个特例。通常来说,属性还是应该声明为 private

使用继承的场景

它是一个通用类,并且为了某个特殊需求将其特殊化。

简单来说:这种“是一个”的关系是用继承来表达的,而“有一个“的关系则用组合来表达。

protected

有的时候,在实际项目中,我们需要把一个事物尽量对外界隐藏,而允许派生类的成员访问。这时候我们就用上了 protected关键字

protected的含义

它表示“就类的用户而言,这是 private 的。但对于任何继承它的子类或在同一包中的类,它是可访问的。”

注意事项:

最好的方式是将属性声明为 private 以一直保留更改底层实现的权利。然后通过 protected 控制类的继承者的访问权限。

class Villain {
    private String name;
    
    protected void set(String nm) {
        name = nm;
    }
    
    Villain(String name) {
        this.name = name;
    }
    
    @Override
    public String toString() {
        return "I'm a Villain and my name is " + name;
    }
}

public class Orc extends Villain {
    private int orcNumber;
    
    public Orc(String name, int orcNumber) {
        super(name);
        this.orcNumber = orcNumber;
    }
    
    public void change(String name, int orcNumber) {
        set(name); // Available because it's protected
        this.orcNumber = orcNumber;
    }
    
    @Override
    public String toString() {
        return "Orc " + orcNumber + ": " + super.toString();
    }
    
    public static void main(String[] args) {
        Orc orc = new Orc("Limburger", 12);
        System.out.println(orc);
        orc.change("Bob", 19);
        System.out.println(orc);
    }
}
/*
output:
Orc 12: I'm a Villain and my name is Limburger
Orc 19: I'm a Villain and my name is Bob
*/

change() 方法可以访问 set() 方法,因为 set() 方法是 protected。注意到,类 OrctoString() 方法也使用了基类的版本。

向上转型

继承真正的用处

并不是为了给新类提供方法,而是声明新类是已有类的一种类型。

示例如下:

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
    }
}

tune() 方法接受了一个 Instrument 类型的引用。但是,在 Windmain() 方法里,tune() 方法却传入了一个 Wind 引用。

这种把 Wind 引用转换为 Instrument 引用的行为称作向上转型

final关键字

final 数据

是永不改变的常量,并且在运行时就已经初始化的值。

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

注意:

对于基本类型,final 使数值恒定不变,而对于对象引用,final 使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。``但是,对象本身是可以修改`,Java 没有提供将任意对象设为常量的方法。

import java.util.*;

class Value {
    int i; // package access

    Value(int i) {
        this.i = i;
    }
}

public class FinalData {
    private static Random rand = new Random(47);
    private String id;

    public FinalData(String id) {
        this.id = id;
    }

    // Can be compile-time constants:
    private final int valueOne = 9;
    private static final int VALUE_TWO = 99;
    // Typical public constant:
    public static final int VALUE_THREE = 39;
    // Cannot be compile-time constants:
    private final int i4 = rand.nextInt(20);
    static final int INT_5 = rand.nextInt(20);
    private Value v1 = new Value(11);
    private final Value v2 = new Value(22);
    private static final Value VAL_3 = new Value(33);
    // Arrays:
    private final int[] a = {1, 2, 3, 4, 5, 6};

    @Override
    public String toString() {
        return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
    }

    public static void main(String[] args) {
        FinalData fd1 = new FinalData("fd1");
        //- fd1.valueOne++; // Error: can't change value
        fd1.v2.i++; // Object isn't constant
        fd1.v1 = new Value(9); // OK -- not final
        for (int i = 0; i < fd1.a.length; i++) {
            fd1.a[i]++; // Object isn't constant
        }
        //- fd1.v2 = new Value(0); // Error: Can't
        //- fd1.VAL_3 = new Value(1); // change reference
        //- fd1.a = new int[3];
        System.out.println(fd1);
        System.out.println("Creating new FinalData");
        FinalData fd2 = new FinalData("fd2");
        System.out.println(fd1);
        System.out.println(fd2);
    }
}
/* out put:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*/

规范:带有恒定初始值的 final static 基本变量命名全部使用大写,单词之间用下划线分隔。

注意:

由上例中的 i4INT_5 可以看出,它们在运行时才会赋值随机数。

INT_5 的值并没有因为创建了第二个 FinalData 对象而改变,这是因为它是 static 的,在加载时已经被初始化,并不是每次创建新对象时都初始化。

v2final 的并不意味着你不能修改它的值。因为它是引用,所以只是说明它不能指向一个新的对象。

空白final

空白 final 指的是没有初始化值的 final 属性。编译器确保空白 final 在使用前必须被初始化。

final参数

在参数列表中,将参数声明为 final 意味着在方法中不能改变参数指向的对象或基本变量:

你只能读取而不能修改参数。这个特性主要用于传递数据给匿名内部类。

代码示例如下:

class Gizmo {
    public void spin() {
        
    }
}

public class FinalArguments {
    void with(final Gizmo g) {
        //-g = new Gizmo(); // Illegal -- g is final
    }
    
    void without(Gizmo g) {
        g = new Gizmo(); // OK -- g is not final
        g.spin();
    }
    
    //void f(final int i) { i++; } // Can't change
    // You can only read from a final primitive
    int g(final int i) {
        return i + 1;
    }
    
    public static void main(String[] args) {
        FinalArguments bf = new FinalArguments();
        bf.without(null);
        bf.with(null);
    }
}

使用 final 方法的原因是给方法上锁,防止子类通过覆写改变方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变。

final 类

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

final 类的属性可以根据个人选择是或不是 final。这同样适用于不管类是否是 final 的内部 final 属性。然而,由于 final 类禁止继承,类中所有的方法都被隐式地指定为 final,所以没有办法覆写它们

class SmallBrain {}

final class Dinosaur {
    int i = 7;
    int j = 1;
    SmallBrain x = new SmallBrain();
    
    void f() {}
}

//- class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'
public class Jurassic {
    public static void main(String[] args) {
        Dinosaur n = new Dinosaur();
        n.f();
        n.i = 40;
        n.j++;
    }
}

注意:不要随意将一个类和方法写成final形式,因为你几乎不可能预测到未来有没有人复用这个类或者方法