七、复用类
1.组合语法
在新的类中产生现有类的对象。由于新的类是由现有类的对象所组成,所以这种方法叫做组合。
类中域为基本类型时能够自动被初始化为零。对象引用被初始化为null。
编译器不是简单地为每一个引用都创建默认对象,如果想初始化这些引用,可以在代码中的下列位置进行:
1.在定义对象的地方。这意味着它们总是能够在构造器被调用之前被初始化。
2.在类的构造器中。
3.就在正要使用这些对象之前,这种方式称为惰性初始化。
4.使用实例初始化。
2.继承语法
当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从Java的标准根类Object进行继承。
为了继承,一般的规则是将所有的数据成员都指定为private,将所有的方法都指定为public。
Java用super关键字表示超类的意思,当前类就是从超类继承来的。
①初始化基类
从外部看,导出类就像是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。但继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在导出类的内部。
对基类子对象的正确初始化也是至关重要的,而且仅有一种方法来保证这一点:构造器中调用基类构造器来执行初始化。Java会自动在导出类的构造器中插入对基类构造器的调用。
但是,如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须用关键字super显式地编写调用基类构造器的语句,并且配以适当的参数列表。
调用基类构造器必须是你在导出类构造器中要做的第一件事。
3.代理
第三种关系称为代理,Java并没有提供对它的直接支持。这是继承和组合的中庸之道,因为我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。例如,太空船需要一个控制模块:
public class SpaceShipControls {
void up(int velocity) {}
void down(int velocity) {}
void left(int velocity) {}
void right(int velocity) {}
}
构造太空船的一种方式是使用继承:
public class SpaceShip extends SpaceShipControls {
private String name;
public SpaceShip(String name) {
this.name = name;
}
public static void main(String[] args) {
SpaceShip protector = new SpaceShip("NASA");
protector.up();
}
}
然而,SpaceShip并非真正的SpaceShipControls类型。更准确地说,SpaceShip包含了SpaceShipControls,与此同时,SpaceShipControls的所有方法在SpaceShip中都暴露了出来。代理解决了此难题:
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls =
new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// Delegation methods
public void back(int velocity) {
controls.back(velocity);
} public void up(int velocity) {
controls.up(velocity);
}
... public static void main(String[] args) {
SpaceShipDelegation protector =
new SpaceShipDelegation("NASA");
protector.up();
}
}
4.结合使用组合和继承
名称屏蔽
如果Java的基类拥有某个已被多次重载的方法的名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作。
Java SE5新增加了@Override注解,它并不是关键字,但是可以把它当作关键字使用。当你想要覆写某个方法时,可以选择添加这个注解,在你不留心重载而并非覆写了该方法时,编译器就会生成一条错误消息。
5.在组合与继承之间选择
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。
在继承的时候,使用某个现有类,并开发一个它的特殊版本。通常,这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。
思考一下,用一个“交通工具”对象来构成一部“车子”是毫无意义的,因为“车子”并不包含“交通工具”,它仅是一种交通工具(is-a关系)。“is-a"(是一个)关系是用继承来表达的,而“has-a”(有一个)的关系是用组合来表达的。
6.protected关键字
protected,它指明“就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的。”
尽管可以创建protected域,但是最好的方式还是将域保持为private;你应当一直保留“更改底层实现”的权利。然后通过protected方法来控制类的继承者的访问权限。
7.向上转型
“向新的类提供方法”并不是继承技术中最重要的方面,其最重要的方面是用来表现新类和基类之间的关系。这种关系可以用“新类是现有类的一种类型”这句话加以概括。
由于继承可以确保基类中所有的方法导出类中也同样有效,所以能够向基类发送的所以信息同样也可以向导出类发送。将导出类引用转换为基类引用的动作,我们称之为向上转型。
由于向上转型是从一个较专用类型向较通用类型转换,所以总是很安全的。在向上转型的过程中,类接口唯一可能发生的事情是丢失方法。
到底是该用组合还是继承,一个最清晰的判断方法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的。
8.final关键字
①final数据
许多编程语言都有某种方法,来向编译器告知一块数据是恒定不变的。有时数据的恒定不变是很有用的,例如:
1.一个永不改变的编译时常量
2.一个在运行时被初始化的值,而你不希望它被改变。
在Java中,编译时常量必须时基本数据类型,并且以关键字final表示。在对这个常量进行定义的时候,必须对其进行赋值。
一个既是static又是final的域只占据一段不能改变的存储空间。根据惯例,既是static又是final的域将用大写表示,并使用下划线分隔各个单词。
对于对象引用,final使其引用恒定不变。然而对象本身却是可以改变的,Java并未提供使任何对象恒定不变的途径。
不能因为某数据是final的就认为在编译时可以知道它的值。
Java允许生成“空白final”,所谓空白final是指被声明为final但又未给定初值的域。无论什么情况,编译器都确保空白final在使用前都必须被初始化。但是,空白final在关键字final的使用上提供了更大的灵活性,为此,一个类中的final域就可以做到根据对象而有所不同,却又保持恒定不变的特性。
Java允许在参数列表中以声明的方式将参数指明为final。这意味着你无法在方法中更改参数引用所指向的对象。这一特性主要用来向匿名内部类传递数据。
②final方法
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。过去建议使用final方法的第二个原因是效率。在Java的早期实现中,如果将一个方法指明为final,就是同意编译器将针对该方法的所有调都转为内嵌调用。如今不需要用final来进行效率优化了。
类中的所有的private方法都隐式地指定为是final的。
“覆盖”只有在某方法是基类的接口的一部分时才会出现。即,必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private,它就不是基类接口的一部分。它仅是一些隐藏于类中的程序代码,只不过时具有相同的名称而已。但如果在导出类中以相同的名称生成一个public、protected或包访问权限方法的话,该方法就不会产生在基类中出现的“仅具有相同名称”的情况。此时你并没有覆盖该方法,仅是生成了一个新的方法。由于private方法无法触及而且能有效隐藏,所有除了把它所归属的类的组织结构的原因而存在外,其他任何事物都不需要考虑到它。
③final类
当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许别人这样做。
由于final类禁止继承,所以final类中所有的方法都隐式指定为是final的,因为无法覆盖它们。
9.初始化及类的加载
Java中所有事物都是对象。每个类的编译代码都存在于它自己的独立的文件中,该文件只在需要使用程序代码时才会被加载。一般来说,可以说:“类的代码在初次使用时才加载。”这通常是指加载发生于创建类的第一个对象之时,但是当访问static域或static方法时,也会发生加载。(构造器也是static方法。因此更准确地说,类是在任何static成员被访问时加载的。)
继承与初始化
了解包括继承在内的初始化全过程,以对所发生的一切有个全局的把握,是很有益的。看下例:
class Insect {
private int i = ;
protected int j;
Insect() {
System.out.println("i = "+i+", j = "+j);
j=;
}
private static int x1 = printInit("static Insect.x1 initialized");
static int printInit(String s) {
System.out.println(s);
return ;
}
} 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 constrictor");
Beetle b = new Beetle();
}
} /**Output
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constrictor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*/
在Java上运行Beetle时,第一件事就是试图访问Bettle.main()(一个static方法),于是加载器开始启动并找出Beetle类的编译代码(在名为Beetle.class的文件中)。在加载过程中,编译器注意到它有一个基类(由extends),于是它继续进行加载。不管是否打算创建一个该基类的对象,这都要发生。
如果该基类还有自身的基类,那么第二个基类就会被加载,如此类推。接下来,根基类中的static初始化(此例中是Insect)即会被执行,然后是下一个导出类,以此类推。这种方式很重要,因为导出类的static初始化可能会依赖于基类成员能否被正确初始化。
至此为止,必要的类都已加载完毕,对象就可以创建了。首先,对象中的所有基本类型都会被设为默认值、对象引用被设为null——这是通过将对象内存设为二进制零值而一举生成的。然后,基类的构造器会被调用。在基类构造器完成以后,实例变量按其次序被初始化。最后,构造器的其余部分将执行。
10.总结
继承和组合都能从现有类型生成新类型。组合一般是将现有类型作为新类型底层实现的一部分来加以复用,而继承复用的是接口。
尽管面向对象编程对继承极力强调,但在开始一个设计时,一般应优先选择使用组合(或者可能是代理),只在确实必要时才使用继承。