【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
编译器不会为每个引用创建一个默认对象,这是有意义的,因为在许多情况下,这会导致不必要的开销。初始化引用有四种方法:
- 当对象被定义时。这意味着它们总是在调用构造函数之前初始化。
- 在该类的构造函数中。
- 在实际使用对象之前。这通常称为延迟初始化。在对象创建开销大且不需要每次都创建对象的情况下,它可以减少开销。
- 使用实例初始化。
代码如下:
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()
*/
这演示了一些特性。首先,在 Cleanser 的 append()
方法中,使用 +=
操作符将字符串连接到 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 对象,你可以调用 Cleanser 和 Detergent 中可用的所有方法 (如 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。注意到,类 Orc 的 toString()
方法也使用了基类的版本。
向上转型
继承真正的用处
并不是为了给新类提供方法,而是声明新类是已有类的一种类型。
示例如下:
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 类型的引用。但是,在 Wind 的 main()
方法里,tune()
方法却传入了一个 Wind 引用。
这种把 Wind 引用转换为 Instrument 引用的行为称作向上转型。
final关键字
final 数据
是永不改变的常量,并且在运行时就已经初始化的值。
一个被 static 和 final 同时修饰的属性只会占用一段不能改变的存储空间
注意:
对于基本类型,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 基本变量命名全部使用大写,单词之间用下划线分隔。
注意:
由上例中的 i4 和 INT_5 可以看出,它们在运行时才会赋值随机数。
INT_5 的值并没有因为创建了第二个 FinalData 对象而改变,这是因为它是 static 的,在加载时已经被初始化,并不是每次创建新对象时都初始化。
,v2 是 final 的并不意味着你不能修改它的值。因为它是引用,所以只是说明它不能指向一个新的对象。
空白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 类
当说一个类是 final (final 关键字在类定义之前),就意味着它不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类。
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形式,因为你几乎不可能预测到未来有没有人复用这个类或者方法