Thinking in Java 第七章——复用类(1)
Thinking in Java 第七章——复用类(2)
Thinking in Java 第七章——复用类(3)
前言:
复用代码是Java众多引人注目的功能之一。但是想要成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,他还需要能够做更多的事情。
有两种达到这一目的的方法:
1. 在新的类中产生现有类的对象。由于新的类是由现有类的对象所组成,多以这种方法称为组合;
2. 按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的工作形式并在其中添加新代码。这种神奇的方式称之为继承。
一、组合语法
使用组合技术,只需要将对象引用置于新类中即可。
例如:
假设你需要某个对象,它具有多个String对象,几个基本数据类型,以及另一个类的对象。对于非基本类型对象,必须将其引用置于新的类中,但是可以直接定义基本类型数据。
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = "Constructed";
}
public String toString() { return s; }
}
public class SprinklerSystem {
private String valve1, valve2, valve3, valve4;
private WaterSource source = new WaterSource();
private int i;
private float f;
public String toString() {
return
"valve1 = " + valve1 + " " +
"valve2 = " + valve2 + " " +
"valve3 = " + valve3 + " " +
"valve4 = " + valve4 + "\n" +
"i = " + i + " " + "f = " + f + " " +
"source = " + source;
}
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
*///:~
在上述定义的两个类中,有一个方法很特殊:toString()
,每一个非基本类型的对象都有一个该方法,并且当编译器需要一个String
而你却只有一个对象时,该方法就会被调用!
例如在上述代码中System.out.println(sprinklers);
,参数需要的是一个String
类型的对象,但是我们传入的是SprinklerSystem
类型的,其实在底层是调用了SprinklerSystem。toString()
。
我们知道的是,类中域为基本类型时,编译器可以自动将其初始化为零;对象引用会被初始化为null
,但是编译器并不是为每一个引用都创建默认的对象,如果想初始化这些引用,可以在代码中的如下位置进行:
- 在定义对象的地方。这意味着它们总是能够在构造器被调用之前被初始化。
- 在类的构造器中。
- 就在正要使用这些对象之前,这种方式被称为惰性初始化。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担。
- 使用实例初始化。
如下所示:
import static net.mindview.util.Print.*;
class Soap {
private String s;
Soap() {
print("Soap()");
s = "Constructed"; //2 类的构造器中
}
public String toString() { return s; }
}
public class Bath {
private String //1 定义初始化
s1 = "Happy",
s2 = "Happy",
s3, s4;
private Soap castille;
private int i;
private float toy;
public Bath() {
print("Inside Bath()");
s3 = "Joy";
toy = 3.14f;
castille = new Soap();
}
//4 使用实例初始化:
{ i = 47; }
public String toString() {
if(s4 == null) //5 惰性初始化:
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();
print(b);
}
} /* Output:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
*///:~
二、继承语法
当创建一个类时,总是在继承,因此,除非已经明确的指出新类继承于其他的某个类,否则就是在隐式的从Java的标准根类Object
进行继承。
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()
*///:~
上述程序也示范了Java的许多特性。
- 在
Cleanser
的append()
方法中,我们使用“+=”
操作符将几个String
类型的对象连接成s,此操作符是被Java设计者重载用于处理String
对象的操作符之一。(另外一个是“+”
)。 Cleanser
和Detergent
均含有main()
方法。可以为每个类都创建一个main()
方法。这种在每一个类中都创建一个main()
方法的技术可以试每个类的单元测试都变得简便易行。而且在完成单元测试之后也不需要删除,可以将其留待下次测试。- 既是一个程序中含有多个类,也只有命令行所调用的那个类的
main()
方法会被调用。 - 为了继承,一般的规则是将所有的数据成员都制定陈
private
,将所有的方法都指定为public
。当然在特殊情况下必须做出调整。 - Java用
super
关键字表示超类的意思,当前类就是从超类继承而来的。 - 在继承的过程中,并不一定需要使用基类的方法,也可以在导出类中添加新的方法,例如
Detergent
类中的foam()
方法
2.1 初始化基类
现在涉及到基类和导出类这两个概念,从外部看,导出类是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。但是继承并不只是复制基类的接口。当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象时一样的。二者的区别在于:导出类来自于外部,而基类的子对象被包装在导出类对象内部。
基类对象的初始化也是至关重要的,而且也仅有一种方法来保证这一点:在构造器中调用基类构造器来初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。Java会自动在导出类的构造器中插入对基类构造器的调用。 如下:
import static net.mindview.util.Print.*;
class Art {
Art() { print("Art constructor"); }
}
class Drawing extends Art {
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
*///:~
构建过程是从基类向外扩散的,所以基类在导出类构造器可以访问它之前,就已经初始化完成了。即使不为Cartoon()
创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类构造器。
但是:在基类构造器有参数时,必须使用super关键字显示的编写调用基类构造器的语句,并且配以合适的参数列表。
例如:
import static net.mindview.util.Print.*;
class Game {
Game(int i) {
print("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i); //显示调用!!!!!!
print("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);//显示调用!!!!
print("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
} /* Output:
Game constructor
BoardGame constructor
Chess constructor
*///:~