《Java编程思想》第7章 复用类

书中源代码:https://github.com/yangxian1229/ThinkingInJava_SourceCode使用类而不破坏现有程序代码。
第一种:只需在新的类中产生现有类的对象。由于新的类是由现有类的对象所组合。该方法只是复用了现有程序代码的功能,而非它的形式。
第二种:按照现有的类的类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。这种方式称为继承,而且编译器可以完成其中大部分工作。

7.1 组合语法

如果想要初始化引用,可以在代码中的下列位置进行:
1.在定义对象的地方。这意味着它们总是能够在构造器被调用之前被初始化。
2.在类的构造器中。
3.就在正要使用这些对象之前,这种方式称为惰性初始化。在生成对象不值得及不必要每次都生成对象的情况下,这种方式可以减少额外的负担。
4.使用实例初始化。

7.2 继承语法

当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从Java标准根类Object进行继承。
extends关键字。

//Inheritance syntax & properties
package ch7;

import static net.mindview.util.Print.*;

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()");}
	public String toString(){ return s;}
	public static void main(String[] args){
		Cleanser x = new Cleanser();
		x.dilute();
		x.apply();
		x.scrub();
		print(x);
	}
}


public class Detergent extends Cleanser{
	// Change a method:
	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();
		print(x);
		print("Testing base class:");
		Cleanser.main(args);
	}
}/* Output:
Cleanser dilute() apply() Detergent.scrub() scrub() foam()
Testing base class:
Cleanser dilute() apply() scrub()
*///:~

CleanserDetergent均含有main()方法。可以为每一个类都创建一个main()方法。
为了继承,一般的规则是将所有的数据成员都指定为private,将所有的方法都指定为public
Cleanser的接口中有一组方法:append(),dilute(),apply(),scrub()和toString()。由于Detergent是由关键字extendsCleanser导出的,所以它可以在其接口中自动获得这些方法,尽管并不能看到这些方法在Detergent中显式定义。
正如我们在scrub()中所见,使用基类中定义的方法及对它进行修改是可行的。Java用super关键字表示超类的意思,当前类就是从超类继承而来的。表达式super.scrub()将调用基类版本的scrub()。
在继承的过程中,并不一定非得使用基类的方法。也可以在导出类中添加新方法,其添加方式与在类中添加任意方法一样,即对其加以定义即可。foam()方法为一例。

7.2.1 初始化基类

当创建一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部。Java会自动在导出类的构造器中插入对基类构造器的调用。例如:

package ch7;

import static net.mindview.util.Print.print;

class Art{
	public Art() { print("Art constructor");}
}
class Drawing extends Art{
	public Drawing() { print("Drawing constructor");}
}

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

如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须用关键字super显式地编写调用基类构造器的语句,并且配以适当的参数列表。

package ch7;

import static net.mindview.util.Print.print;

class Game{
	Game(int i){ print("Game constructor");}
}
class BoardGame extends Game{
	BoardGame(int i) {
		super(i);
		print("BoardGame constructor");
	}
}


public class E06 extends BoardGame{
	E06(){
		super(11);
		print("E06 constructor");
	}
	public static void main(String[] args) {
		new E06();
	}
}

调用基类构造器必须是你在导出类构造器中要做的第一件事。

7.3 代理

第三种关系成为代理,Java并没有提供对它的直接支持。这是继承与组合之间的中庸之道。因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。

7.4 结合使用组合和继承

7.4.1 确保正确清理

可将清理动作置于finally子句之中。
在清理方法(dispose())中,需要注意对基类清理方法和成员对象清理方法的调用顺序。执行类的所有特定的清理动作,其顺序同生成顺序想法。
因为不能保证垃圾回收器会工作,所以除了内存以外,不能依赖垃圾回收器去做任何事情。如果需要进行清理,最好是编写你自己的清理方法,但不要使用finalize()。

7.4.2 名称屏蔽

如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本。
使用与基类完全相同的特征签名及返回类型类覆盖具有相同名称的方法,是一件及其平常的事情。
@Override注解,它并不是关键字,但是可以把它当作关键字使用。当你想要覆写某个方法时,可以选择添加这个注解,在你不留心重载而非覆写了该方法时,编译器就会生成一条错误信息。
例子和说明:http://zpointer.iteye.com/blog/1066992

7.5 在组合与继承之间选择

组合和继承都允许在新的类中放置子对象,组合是显式地这样做,而继承是隐式地做。
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。为取得此效果,需要在新类中嵌入一个现有类的private对象。
在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其初始化。“is-a"的关系是用继承来表达的,而"has-a"的关系是用组合来表达的。如:汽车有轮子:组合;汽车是交通工具:继承。

7.6 protected关键字

7.7 向上转型

将现有类引用转换为基类引用的动作,称为向上转型
在这里插入图片描述
导出类是基类的一个超集。它可能比基类含有更多的方法,但它必须至少具备基类中所含有的方法。在向上转型的过程中,类接口中唯一可能发生的事情是丢失方法,而不是获取它们。
应当慎用这一技术。到底是该组合还是用继承,一个最清晰的判断办法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必须的;但是如果不需要,则应当好好考虑自己是否需要继承。

7.8 final关键字

根据上下文环境,Java的关键字final的含义存在着细微的区别,但通常它指的是“这是无法改变的”。
可能使用到final的三种情况:数据、方法和类。

7.8.1 final数据

一个既是static又是final的域只占据一段不能改变的存储空间。根据惯例,既是static又是final的域(即编译期常量)将用大写表示,并使用下划线分隔各个单词。
对于一个引用,final意味着无法将该引用指向另一个新的对象。
Java允许生成“空白final”,所谓空白final是指被声明为final但又未给定初值的域。必须在域的定义处或者每个构造器中用表达式对final进行赋值,这正是final域在使用前总是被初始化的原因所在。
Java允许参数列表中声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象。

7.8.2 final方法

final方法一方任何继承类修改它的含义。
类中所有的private方法都隐式地指定为是final的。
“覆盖”只有在某方法是基类的接口的一部分时才会出现。如果某个方法为private,他就不是基类的接口的一部分。如果现有类有一个相同名称的类出现,此时你并没有覆盖原方法,仅是生成了一个新的方法。

7.8.3 final类

将某个类整体定义为final时,就表明你不打算继承该类。由于final类禁止继承,所以final类中所有的方法都隐式的指定为final的,因为无法覆盖它们。
final class S{...}

7.9 初始化及类的加载

一般来说,可以说“类的代码在初次使用时才加载”。这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载。

7.9.1 继承与初始化

package ch7;

import static net.mindview.util.Print.print;

class Insect{
	private int i=9;
	protected int j;
	Insect() {
		print("i="+i+" ,j="+j);
		j = 39;
	}
	static int printInit(String s){
		print(s);
		return 47;
	}
	private static int x1 = printInit("static Insect.x1 initialized");
}

public class Beetle extends Insect{
	private int k = printInit("Beetle.k initialized");
	public Beetle(){
		print("k="+k);
		print("j="+j);
	}
	private static int x2 =printInit("static Beetle.x2 initialized");
	public static void main(String[] args) {
		print("Beetle constructor");
		Beetle beetle = new Beetle();
	}
}/* Output:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i=9 ,j=0
Beetle.k initialized
k=47
j=39
*///:~

7.10 总结

继承和组合都能从现有类型生成新类型。组合一般是将现有类型作为新类型底层实现的一部分来加以复用,而继承复用的是接口。
在使用继承时,由于导出类具有基类接口,因此它可以向上转型至基类。
在开始一个设计时,一般应优先考虑组合(或者可能是代理),只在确实必要时才使用继承。

猜你喜欢

转载自blog.csdn.net/lanzijingshizi/article/details/84131794