组合和继承是复用类的两种主要方式。
7.1 组合语法
组合只需将对象引用置于新类中即可。这些引用的初始化方式有四种:
1) 在定义对象的地方进行初始化,即在创建新类对象前就被初始化;
2) 在新类的构造器中初始化;
3) 在使用这些引用之前再初始化,这被称之为惰性初始化,推荐这一方式;
4) 使用新类对象进行初始化。
7.2 继承语法
当创建一个类时,总是在继承,不是明确从其他类继承就是隐式地继承Java标准根类Object。
1) 子类继承父类的所有成员变量和成员方法;
2) 子类可以添加父类所不具备的新方法;
3) 子类可以重写与父类同名的方法。
7.2.1 初始化基类
当创建子类对象时,该对象会包含一个父类的子对象。针对该父类子对象的初始化,Java会在子类的构造器中首先自动调用父类的构造器,来对父类子对象进行初始化。
public class Cartoon extends Drawing {
public Cartoon(){
System.out.println("Cartoon constructor");
}
public static void main(String[] args){
Cartoon x = new Cartoon();
}
}
class Art{
Art(){
System.out.println("Art constructor");
}
}
class Drawing extends Art{
Drawing(){
System.out.println("Drawing constructor");
}
}
在上述代码中,当创建Cartoon对象时,在Cartoon的构造器中,会首先调用Art类、Drawing类的构造器。
以上是无参构造器的情况。针对有参的的情况,在父类没有默认的无参构造器或者想调用父类的有参构造器的情况下,必须在子类的构造器中第一句显式地使用super关键字调用父类相应的构造器。
public class Chess extends BoardGame {
Chess(){
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args){
Chess x = new Chess();
}
}
class Game{
Game(int i){
System.out.println("Game constructor");
}
}
class BoardGame extends Game{
BoardGame(int i){
super(i);
System.out.println("BoardGame constructor");
}
}
在父类没有默认的无参构造器的情况下,必须在子类的构造器的第一行明确调用父类的有参构造器。
7.4 代理
代理也是复用类的一种方式,只是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 SpaceShipDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShipDelegation(String name){
this.name = name;
}
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 up(int velocity){
controls.up(velocity);
}
public void turboBoost(){
controls.turboBoost();
}
public static void main(String[] args){
SpaceShipDelegation protector = new SpaceShipDelegation("NESA Protector");
protector.forward(100);
}
}
7.4 结合使用组合和继承
可以同时使用组合和继承。
7.4.1 确保正确清理
子对象的清理顺序与其初始化顺序最好正好相反,以防出现一个子对象依赖于另一个子对象的情形发生。
public class CADSystem extends Shape{
private Circle c;
private Triangle t;
private Line[] lines = new Line[3];
public CADSystem(int i){
super(i + 1);
for(int j = 0; j < lines.length; j++){
lines[j] = new Line(j, j * j);
}
c = new Circle(1);
t = new Triangle(1);
System.out.println("Combined constructor");
}
/**
* 清理顺序与初始化顺序相反,以防出现某个子对象依赖于另一个子对象情形发生
*/
public void dispose(){
System.out.println("CADSystem.dispose()");
t.dispose();
c.dispose();
for(int i = lines.length - 1; i >= 0; i--){
lines[i].dispose();
}
super.dispose();
}
public static void main(String[] args){
CADSystem x = new CADSystem(47);
try{
}finally {
x.dispose();
}
}
}
class Shape{
Shape(int i){
System.out.println("Shape constructor");
}
void dispose(){
System.out.println("Shape dispose");
}
}
class Circle extends Shape{
Circle(int i){
super(i);
System.out.println("Drawing Circle");
}
void dispose(){
System.out.println("Erasing Circle");
super.dispose();
}
}
class Triangle extends Shape{
Triangle(int i){
super(i);
System.out.println("Drawing triangle");
}
void dispose(){
System.out.println("Erasing Triangle");
super.dispose();
}
}
class Line extends Shape{
private int start, end;
Line(int start, int end){
super(start);
this.start = start;
this.end = end;
System.out.println("Drawing Line: " + start + ", " + end);
}
void dispose(){
System.out.println("Erasing Line: " + start + ", " + end);
}
}
7.4.2 名称屏蔽
方法重载的不同版本可以分别位于父类和子类。
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());
}
}
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)");
}
}
7.5 在继承和组合之间选择
组合和继承都允许在新类中放置子对象,只是前者是显式放置,后者是隐式放置。
组合技术通常用于在新类中使用现有类的功能而非现有类的接口。即,在新类中嵌入某个对象,让其实现所需功能,但新类的用户看到的只是新类的接口,而非嵌入对象的接口,此时该嵌入对象可以设定为private。如果该嵌入对象自身隐藏了具体的实现,也可以将该嵌入对象设置 为public。
继承是使用某个现有类,并开发它的一个特殊版本。
7.6 protected关键字
protected关键字表示只有本类以及其衍生的子类能访问的成员,包括成员变量和成员方法,但是一般成员变量都声明为private。
7.7 向上转型
子类对象的类型既是子类类型,也是父类类型,也就是子类对象同时也是父类对象。在引用上就表现为,子类引用可以转变为父类引用,这称之为向上转型(upcasting)。
注意与多态相联系。
7.7.1 为什么称之为向上转型
在继承图上,父类在上,子类在下,有箭头从子类指向父类,表示继承。
7.7.2 再论组合与继承
多用组合,慎用继承。
判断使用组合还是继承的标准是,是否需要从子类向父类进行向上转型。
7.8 final关键字
final关键字表示只能初始化一次,一旦被初始化后,就不能被再次赋值。其可以作用在数据(成员变量)、方法(成员方法)和类上。
7.8.1 final 数据
这种情况主要有两种应用场景:
1) 永不改变的编译时常量,可以是基本数据类型,也可以是引用类型;
2) 一旦被初始化后,程序员不希望被改变。
一个既是static又是final的数据(成员变量)只占据一段不能改变的存储空间,这种数据在类被加载进入内存时,一旦初始化后(基本数据类型赋值,引用类型创建对象),无论利用该类创建多少对象,这种数据都不变(基本数据类型值不变,引用数据类型地址不变,但是其相应的属性值可以改变)。
空白final
被声明为final但又没有给定初值的成员变量被称为空白final。它具有较大的灵活性,可以在使用时再初始化。
注意,空白final必须被初始化,否则编译报错。
final参数
Java允许在方法的参数列表中将参数声明为final,表示在方法内,无法更改参数引用指向的对象或者对应的基本数据类型的值。
final方法
当一个方法被final修饰时,则继承类无法重写该方法。
实际上,当方法声明为private时,就已经隐式地被声明为final了,显然子类不能继承该方法,所以也就无所谓重写的问题了,但是仍然可以在子类中声明同名的方法,但是这两个方法不存在重写关系。
public class FinalOverridingIllusion {
public static void main(String[] args){
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
//向上转型
OverridingPrivate op = op2;
//显然无法调用f()和g()
//op.f();
//op.g();
WithFinals wf = op2;
//同上
//wf.f();
//wf.g();
}
}
class WithFinals{
private final void f(){
System.out.println("WithFinals.f()");
}
private void g(){
System.out.println("WithFinals.g()");
}
}
class OverridingPrivate extends WithFinals{
private final void f(){
System.out.println("OverridingPrivate.f()");
}
private void g(){
System.out.println("OverridingPrivate.g()");
}
}
class OverridingPrivate2 extends OverridingPrivate{
public final void f(){
System.out.println("OverridingPrivate2.f()");
}
public void g(){
System.out.println("OverridingPrivate2.g()");
}
}
7.8.3 final类
被final修饰的类无法被继承,即没有子类。
7.8.4 有关final的忠告
7.9 初始化及类的加载
一般来说,类的代码只有在初次使用时才加载。初次使用是指使用该类创建第一个对象,或者是第一次使用该类访问器static成员变量或者成员方法。
7.9.1 继承与初始化
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();
Beetle b2 = new Beetle();
}
}
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;
}
}
在这个例子中,在Beetle上运行Java时,第一件事就是访问Beetle.main()方法,于是加载器开始启动并找出Beetle.class,在对其进行加载的过程中,发现其由父类Insect,继而寻找并加载Insect.class,如果还有父类,则继续寻找并加载相应父类的.class文件。
此后是初始化的过程,先对父类的static成员变量进行初始化,然后再是子类的static成员变量进行初始化,这样做是因为子类的static成员变量可能会依赖于父类的static成员变量能否被正确初始化(在这个例子中就是如此)。
至此,类加载完毕,开始创建对象。与前面相同,先执行父类的构造器,再执行子类的构造器。