复用代码,即使用已经开发并调试好的类。组合和继承是两种实现方法。
组合语法:
在新类中创建现有类的对象。该方法只是复用了现有代码的功能,而非它的形式。
组合的例子随处可见,这里不举例说明。但书中特意强调了toString方法。
每一个非基本类型的对象都有一个toString方法,因为每一个类都是继承Object类而来的,而Object类中包含这个方法。具体需要注意的是,当你要将一个对象和字符串连接的时候,编译器会自动调用toString方法,当Object类中的toString方法不能满足要求时,则需要重写这个方法。
继承语法:
继承是所有OOP语言不可缺少的组成部分。当创建一个类时,总是在继承,因为若没有明确指出要从其他类中继承,就默认从Java的标准根类Object进行继承。
为了继承,一般的规则是将所有的数据成员指定为private,所有的方法指定为public。虽然在特殊的情况下必须做出调整,但是这的确是一个很有用的规则。
当继承类中有对基类中定义的方法修改时,欲调用基类的方法,必须加上super关键字,否则程序将产生递归。当然,继承类同样可以定义属于自己的方法。
初始化基类:
当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与直接创建的基类对象时一样的。二者的却别在于后者来自于外部,而前者来自于导出类对象的内部。
1)无参构造器的初始化
class Art {
Art() {
System.out.println("Art");
}
}
class Drawing extends Art () {
Drawing() {
System.out.println("Drawing");
}
}
public class Cartoon extends Drawing {
public Cartoon() {
System.out.println("Cartoon");
}
}
/*
Output:
Art
Drawing
Cartoon
*/
2)有参构造器的初始化
对于无参构造器,当然默认的构造器也是无参的,编译器可以轻松地调用而不需要考虑传递参数的问题。但是想调用带参数的基类构造器,就必须用super显示地编写调用基类构造器的语句,而且调用基类构造器必须是你在导出类构造器中要做的第一件事,否则编译器将报错。
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 {
public Chess() {
super(11);
System.out.println("Chess Constructor");
// super(11); // 报错
}
public static void main(String[] args) {
new Chess();
}
}
/*
output:
Game Constructor
BoardGame Constructor
Chess Constructor
*/
代理:
代理是继承与组合之间的中庸之道,但是Java并没有提供对它的直接支持。代理可控制需要哪些被代理类中的方法,而组合和继承则拥有了所有方法。
代理的具体过程,先创建被代理类的对象引用,然后创建与被代理类中方法同名的方法,并通过这个对象引用来调用被代理类的方法,这里有点像重写(注意重写是建立在继承的基础上的)。
class SpaceShipControls {
void up(int velocity) {}
void down(int velocity) {}
}
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();//创建被代理类的对象
public SpaceShipDelegation(String name){
this.name = name;
}
public void up(int velocity){//选择需要代理的方法,注意名字需一样
controls.up(velocity);//通过对象引用,调用被代理类的方法。实现代理
}
public static void main (String[] args) {
SpaceShipDelegation protector = new SpaceShipDelegation("lalala");
protector.up(100);
}
}
确保正确清理:
Java中没有析构函数的概念。虽然Java中有垃圾回收机制,但是你永远不知道它什么时候才会被调用。因此,如果想要某个类清理一些东西,就必须显示地编写一个特殊方法来实现。清理的首要任务是,将这一清理动作置于finally子句之中,以防异常的出现。finally子句表示无论发生什么事,一定要执行这个动作。清理动作的顺序和生成顺序相反,因为可能存在子对象依赖于另一个子对象的情况。
名称屏蔽:
如果导出类中有对基类的方法进行重载,不会对名称进行屏蔽,即所有重载方法都是可用的。@override注解重写基类方法,避免方法名称写错。
向上转型:
导出类转型为基类,在继承图上是向上的,因此得名。由于向上转型是从一个较专用类型向较通用类型转换,所以总是安全的,而且在向上转型的过程中,类接口中唯一发生的事情是丢失方法,而不是获取他们,所以在安全的考虑上是可以接受的,编译器也是允许的。
组合与继承之间选择:
尽管面向对象的过程中,一直强调继承的概念,但并不是尽可能的使用它。相反,应当慎用这一技术。一个清晰的方法是问问自己到底是否需要从新类向基类向上转型。如果需要,那就用继承吧,如果不需要,那应当好好考虑下了。所以向上转型是判断组合和继承选择的重要依据。
final数据:
对于基本类型,final使数值恒定不变,而用于对象引用,final引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它指向另一个对象。但是对象本身是可以修改的。既是static又是final的域将用大写表示,并使用下划线分隔各个单词。
有一点需要注意的是,空白final是指指定了final但又没有赋初值的域。无论什么情况,编译器都要确保空白final在被使用前都必须要被初始化。一般情况下,空白final是在其他类的构造器中初始化,即某个类的final域可以根据创建不同的对象具有不同的初始值,而且还能保持其恒定不变的特性。空白final大大提高了灵活性。
class Poppet {
private int i;
Poppet(int ii) {
i = ii;
}
}
public class BlankFinal {
private final int i = 0;
private final int j;
private final Poppet p;
public BlankFinal() {
j = 1;
p = new Poppet(1);
}
public BlankFinal(int x) {
j = x;
p = new Poppet(x);
}
public static void main(String[] args) {
/*通过调用不同的构造器,创建不同的对象,使空白final域P有不同的初始值*/
new BlankFinal();
new BlankFinal(1);
}
}
final参数:
Java允许在参数列表中以声明的方式将参数指明为final,这意味着你不能在方法中更改参数引用所指向的对象。
class Gizmo {
public void spin() {
}
}
public class FinalArguments {
void with(final Gizmo g) {
//g = new Gizmo(); 不能更改
}
void without(Gizmo g) {
g = new Gizmo();
g.spin();
}
//void f(final int i){i++} 不能更改i的值
int f(final int i){ return i + 1; } //这里并没有更改i的值
public static void main (String[] args){
FinalArguments bf = new FinalArguments();
bf.with(null);
bf.without(null);
}
}
final方法:
使用final方法的原因有两个,第一个原因是效率,这是使用初衷。但是这仅仅是在代码块不是很大的情况下才能显示出其作用,后来Java找到了其他的方式进行提高效率,所以在final方法的使用上不再考虑效率问题。第二个原因是为了防止在继承中对方法进行覆盖。(注意:final方法是可以被重载的!重载和覆盖不一样!)此外,定义为private访问权限的方法,其隐式指定为final。
特别注意,覆盖只有在某方法是基类接口中的一部分时才会出现,基类中的private方法不是接口的一部分,所以如果在基类的导出类中定义一个和基类中private方法同名的public或者protect方法,不是覆盖,而是定义了一个新的方法,切记!
final类:
当定义某个类为final时,就表示你不想继承这个类,而且也决不允许别人继承这个类,或者说这个类完全没有被继承的必要,又或者出于安全的考虑。总之,它被限制了。由于final类无法被继承,所以类下的所有方法都是final的,无论是否指定为final。
对于使用final的忠告:
将一个方法或者一个类指定为final,大部分可能是明智的。但是你必须注意到,这些所谓的不能重载、不能被继承,都是你自己的想象,总有你意想不到的运用它的情况。所以,使用final请慎重!
初始化及类的加载:
类只有在创建类的第一个对象或者访问static域和static方法时才会发生加载。其实创建类的对象也是在访问static方法,因为创建时调用的构造器是隐式的static。所以,类的加载之处也是static初始化之处,而所有的static只会被初始化一次,按照定义的顺序初始化。
class Insect {
private int i = 9;
protected int j;
Insect() {
print("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 =
printInit("static Insect.x1 initialized");
static int printInit(String s) {
print(s);
return 47;
}
}
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 b = new Beetle();
}
}
/**
* static Insect.x1 initialized
* static Beetle.x2 initialized
* Beetle constructor
* i = 9, j = 0
* Beetle.k initialized
* k = 47
* j = 39
*/
总结:
在开始一个设计时,一般优先选择使用组合,或者可能是代理,只有确实必要时才使用继承,因为组合更具灵活性。
在设计一个系统时,目标应该是创建某些类,其中每个类都有具体的用途,而且既不会太大,也不会太小。太大则复杂难以复用,太小则可能不添加其他功能就无法使用。所以,太大的情况下,就适当的细分。在系统的设计阶段,必须意识到这是一种增量过程,是不断累积的过程,并不是一蹴而就的。