funnycoding / blog

A Github Issue Blog
22 stars 0 forks source link

《On Java8》 8.复用 #3

Open funnycoding opened 4 years ago

funnycoding commented 4 years ago

第八章 复用

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

两种方式复用之前创建过的类: 1. 组合 2. 继承 ### 组合语法 类中置入要使用类的引用。 ```java private String s; // 对于引用类型,只需要声明引用就可以使用了。 ``` ```java // 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 reuse class WaterSource { private String s; // 声明一个字符串对象 s 的引用 WaterSource() { // 在构造函数中对该引用赋值指向具体的对象实例 System.out.println("WaterSource()"); s = "Constructed"; } @Override public String toString() { return s; } } public class SprinklerSystem { private String valve1, valve2, valve3, valve4; // 声明了4个引用,却没有赋值 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); } } /* Output: WaterSource() valve1 = null valve2 = null valve3 = null valve4 = null i = 0 f = 0.0 source = Constructed */ ``` **【可以看到,类变量如果是引用类型,没有初始化的默认是 null,如果是 int L诶性 则为0,float 类型为 0.0】** > **编译器不会Wie每个引用创建一个默认对象**,这会导致不必要的开销。**初始化引用有四种方法:** > 1. **当对象被定义时**,这意味着它们总是在调用构造函数之前初始化。 > 2. 在类的**构造函数**中。 > 3. 在实际使用对象之前。这通常被称为 **延迟初始化**。在对象创建开销大而且不需要每次都创建对象的情况下,它可以减少开销。 > 4. **使用实例初始化。** > ```java // 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 composition 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("构造函数执行了"); s3 = "Joy"; toy = 3.14f; castille = new Soap(); } // Instance initialization: { System.out.println("代码块中给i赋值的代码执行了"); 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); } } /** 输出 代码块中给i赋值的代码执行了 构造函数执行了 Soap() s1 = Happy s2 = Happy s3 = Joy s4 = Joy i = 47 toy = 3.14 castille = Constructed */ ``` **【可以看到代码块的执行优先级比构造函数要高】** ### 继承语法 > 继承是所有面向对象语言的一个组成部分。 > > > > 使用 `extends` 关键字来标识继承。**如果没有显示的使用关键字指定继承某类,则隐式继承 Java 标准根类对象 Object。** > > **当继承时,子类将获得父类中的所有字段和方法。** ```java // Detergent.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 { // 改变父类方法的行为 @Override public void scrub() { append(" 子类Detergent.scrub()"); super.scrub(); // Call base-class version } // 子类新加方法 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 args:"+ Arrays.toString(args) ); Cleanser.main(args); } } /** 输出 Cleanser 父类dilute() 父类apply() 子类Detergent.scrub() 父类scrub() 子类foam() Testing base class args:[] Cleanser 父类dilute() 父类apply() 父类scrub() */ ``` #### 初始化基类 > 子类与父类有相同的接口,可能还有更多的方法和字段。 > > 当你创建子类的对象时,它包含父类的子对象,也就是会先初始化父类。从外部看,父类的子对象被包裹在子类的对象中。 【怎么验证这个观点?】 **为了保证正确初始化基类的子对象,Java在构造函数中插入了对父类构造函数的调用。** ```java // Cartoon.java class Art { Art() { System.out.println("父类无参构造"); } } class Drawing extends Art { Drawing() { System.out.println("子类无参构造"); } } public class Cartoon extends Drawing{ public Cartoon() { System.out.println("二级子类 无参构造"); } public static void main(String[] args) { Cartoon cartoon = new Cartoon(); } } /** 输出 父类无参构造 子类无参构造 二级子类 无参构造 */ ``` ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200226114738.png) 构造函数的调用从父-子 自顶向下进行,如果你没有编写无参构造函数,编译器会自动帮你生成。 当我删除了 Cartoon 类的构造函数之后,可以看到还是存在 init 构造函数 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200226115115.png) #### 带参数的构造函数 > 构造函数可以传入参数,如果有带参构造则编译器不会生成默认的无参构造。 > > 如果没有无参构造,则必须使用或者必须调用带参数的构造函数,必须使用 super 关键字和适当的参数显示地编写对基类带参构造函数的调用: ```java // Chess.java class Game { Game(int i) { System.out.println("Game constructor 顶层父类带参构造函数"); } } class BoardGame extends Game { BoardGame(int i) { super(i); System.out.println("BoardGame construc 一级子类带参构造函数,先调用父类的带参构造函数"); } } public class Chess extends BoardGame{ Chess() { super(11); System.out.println("Chess 二级子类的无参构造函数,但是要先调用父类的带参构造函数"); } public static void main(String[] args) { new Chess(); } } /** 输出 Game constructor 顶层父类带参构造函数 BoardGame construc 一级子类带参构造函数,先调用父类的带参构造函数 Chess 二级子类的无参构造函数,但是要先调用父类的带参构造函数 */ ``` ### 委托 > Java 不直接支持的第三种重用关系称为委托。 这介于继承和组合之间。 > > 你将一个成员对象放在正在构建中的类中(比如组合) 但同时又在心累中公开来自成员对象的所有方法(比如继承) > > 例如:宇宙飞船需要一个控制模块: ```java // SapceShipControls.java public class SapceShipControls { 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() {} } ``` 建造宇宙飞船的一种方法是继承: ```java // DerivedSpaceShip.java 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,控制模块中的所有方法都暴露在宇宙飞船中, 而不能用飞船直接继承控制模块。 > > 委托解决了这个问题: ```java // SpaceShipDelegation.java public class SpaceShipDelegation { private String name; // 创建一个控制模块的实例对象 private SpaceShipControls controls = new SpaceShipControls(); public SpaceShipDelegation(String name) { this.name = name; } // 委托的方法: 在委托类中调用控制模块的方法,相当于做了2次封装 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` 对象上,因此接口与继承的接口相同。 > > 但是你对委托有更多的控制,因为你可以选择只在成员对象中提供 control 对象方法的子集。 > > 虽然 Java 语言不支持委托,但是开发工具常常支持,例如上面的例子是用 JetBarins IDEA 自动生成的。 这个我还真不知道,惊了,确实有。这个以前都没用到过。 ![](https://xuyanxin-blog-bucket.oss-cn-beijing.aliyuncs.com/blog/20200226122932.png) ### 结合组合与集成 > 组合与继承经常一起使用。 ```java // PlaceSetting.java 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 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); } } /** 输出 Custom constructor Utensil constructor Spoon constructor Utensil constructor Fork constructor Utensil constructor Knife constructor Plate constructor DinnerPlate constructor PlaceSetting constructor */ ``` 可以看到子类对象被构建之前会先构建其父类对象。 #### 保证适当的清理 #### 名称隐藏 ### 组合与继承的选择 ### protected ### 向上转型 #### 再论组合和继承 ### final关键字 #### final 数据 许多编程语言都有某种方法告诉编译器有一块数据是恒定不变的。恒定是有用的,如: 1. 一个永不改变的编译时常量。 2. 一个在运行时初始化就不会改变的值。 对于编译时常量这种情况,**编译器可以把常量带入计算中**;也就是说,可以在编译时计算,减少了一些运行时的负担。 在 Java 中,这类常量必须是基本类型,而且用关键字 final 修饰。你必须在定义常量的时候进行赋值。 **一个被 static 和 final 同时修饰的属性只会占用一段不能改变的存储空间。** 当用 final 修饰引用类型时,一旦引用被初始化指向了某个对象,它就不能指向其他对象。但是对象本身是可以修改的。 **这一限制同样适用于数组,数组也是对象。** #### 空白 final #### final 参数 在参数列表中,将参数声明为 final 意味着 在方法中不能改变参数指向的对象或基本变量。 ```java 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(); // it's ok } void f(final int i) { // i++; i is final, cant change } int g(final int i) { return i + 1; } } ``` 方法 f() 和 g() 展示了 final 基本类型参数的使用情况。你只能读取而不能修改参数。**这个特性主要用于传递数据给匿名内部类。** #### final 方法 使用 final 方法的原因有两个,**第一个原因是给方法上锁,防止子类通过 覆写改变方法的行为**。这是出于继承的考虑,确保方法的行为不会因继承而改变。 过去建议使用final 方法的第二个原因是效率,在早期 Java 实现中,如果将一个方法指明为 final ,就是同意编译器将对该方法的调用转为 内联调用。当编译器遇到 final 方法的调用时,就会很小心地跳过普通的插入代码,以执行方法的调用机制(将参数压栈,跳至方法代码处执行,然后跳回并清理栈中参数。最终处理返回值)而方法内实际代码的副本替代方法调用,这消除了方法调用的开销。 但是如果一个方法很大,代码膨胀,你也许就看不到方法内联带来的性能提升。同时在最近的 Java版本中,虚拟机可以探测到这些情况,并优化去掉这些反而降低效率的内联调用方法。 **所以只应该在禁止子类override 父类方法时使用final 关键字修饰方法。** #### final 和 private 类中所有 private 方法都隐式地指定为 final。 因为子类不能访问父类 private 方法,所以不能覆写它。 可以给私有方法增加 final 修饰,但是这并不能给方法带来额外的含义。 #### final 类 当说一个类是 final 的时候,就以为着该类不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类。 final 类的属性可以根据个人选择是否为 final类型。 这同样适用于不管类是否为 final 的内部 final 类型。然而由于 final 类禁止继承,类中所有方法都被隐式的指定为 final,所以没法覆写它们。你可以在 final 类中的方法前加上 final 修饰符,但不会增加任何意义。【final 类中的方法是隐式 final的。】 #### final 忠告 ### 类初始化和加载 在许多传统语言中,程序在启动时一次性全部加载。接着初始化,然后程序开始运行。必须仔细控制这些语言的初始化过程,以确保 statics 初始化的顺序不会造成麻烦。在 C++中,如果一个 static 期望使用另一个 static,而另一个 static 还没有被初始化,就会出现问题。 Java 中不存在这样的问题,因为**它采用了一种不同的方式加载**。因为 Java 中万物皆对象,所以加载活动就容易的多。 **记住每个类的编译代码都存在于它自己的独立的文件中(.class字节码文件),该文件只有在使用程序代码时才会被加载。** 一般可以说“类的代码在首次使用时加载”,这通常是指创建类的第一个对象,或者是访问了类的 静态属性或方法。 构造器也是一个静态方法,尽管它的 static 关键字是隐式的。 **因此准确的说,一个类当它任意一个静态成员被访问时,就会被加载。** #### 继承和初始化 了解可包括继承在内的整个初始化过程是有帮助的,这样可以对发生的一切有全局性的把握。考虑下面的例子: ```java // reuse/Beetle.java // The full process of initialization class Insect { private int i = 9; protected int j; Insect() { System.out.println("i = " + i + ", j = " + j); j = 39; } private static int x1 = printInit("static Insect.x1 initialized"); static int printInit(String s) { System.out.println(s); return 47; } } public class Beetle extends Insect { private int k = printInit("Beetle.k.initialized"); public Beetle() { System.out.println("k = " + k); System.out.println("j = " + j); } private static int x2 = printInit("static Beetle.x2 initialized"); public static void main(String[] args) { System.out.println("Beetle constructor"); Beetle b = new Beetle(); } } /** output static Insect.x1 initialized static Beetle.x2 initialized Beetle constructor i = 9, j = 0 Beetle.k initialized k = 47 j = 39 */ ``` **当执行 java Beetle 这个命令时,首先试图访问 Beetle 类的 main() 方法**,一个静态方法,加载器启动并找出 Bettle 类的编译代码(在名为 Bettle.class 中),在加载过程中,编译器注意到有一个基类,于是继续加载基类。不论是否创建了基类的对象,基类都会被加载。(可以尝试把创建基类的代码注释掉来证明这一点) 如果基类还存在自身的基类,就继续向上追溯并加载,以此类推,从最顶层的基类开始加载。接下来,根基类(这里的 Insect)的 **static 初始化开始执行**,接着是派生类的static 初始,以此类推。这点很重要,因为派生类中的 **static** 初始化很可能依赖基类成员是否被正确地初始化。 至此,必要的类都加载完毕,可以创建对象了。 **首先对象中的所有基本类型变量都设置为默认值,对象引用被设置为 null ,这是通过将对象内存设为二进制零来一举生成的**。接着会调用基类的构造器。本例中是自动调用的,但是你也可以使用 super 调用指定的基类构造器(在 Beetle 构造器中的第一部操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。**当基类构造器完成后**,**实例变量按文本顺序初始化**,最终,构造器的剩余部分被执行。 【类内执行顺序: 静态初始化—>类变量初始化—>构造函数】 ,派生类 —> 子类,自顶向下初始化】 ### 本章小结