7.1 组合语法
1)组合即 将对象引用置于新类中
2)每一个非基本类型的对象都有一个toString()方法,而且当编译器需要一个String而你却只有一个对象时,该方法便会被调用。
3)初始化一个类中的对象引用有如下四种方式:
1.在定义对象的地方初始化,,意味着总能在调用构造器之前被初始化
2.在类的构造器中
3.就在正要使用这些对象之前,这种叫惰性初始化,这种可以减少额外的负担
4.使用实例初始化
class Soap { private String s; Soap() { print("Soap()"); //2.在类的构造器中初始化 s = "Constructed"; } 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()"); //2.在类的构造器中初始化: s3 = "Joy"; toy = 3.14f; castille = new Soap(); } // 4.实例初始化: { i = 47; } public String toString() { if(s4 == null) // 3.惰性初始化(Delayed initialization): 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
7.2继承语法
1)当创建一个类时,总是在继承,因此,除非已明确指出要从其他类中继承,否则就是在隐式地从Java的标准根类Object进行继承。
2)继承关键字extends
:继承会自动得到基类中所有的域(什么是域)和方法。
3)调用另外一个类的main函数的方式与调用另外一个类的普通静态函数相同,即类名.main(args);
,args可以是主调用类从命令行获得的参数,也可以是其他任意的String数组。
4)可以为每个类都创建一个main方法。这种在每个类中都设置一个main方法的技术可使每个类的单元测试都变得简单易行。而且在完成单元测试之后,也无需删除main(),可以留待下次测试。
5)即使一个类只具有包访问权限,其public main()仍然是可以访问的。(还没证实?)
6)为了继承,一般的规则是将所有的数据成员都指定为private,将所有的方法指定为public(protected方法也可以借助导出类来访问,后面说到)。
7)Java用 super 关键字表示超类(父类)。表达式super.fun();
可以调用父类中的函数(此处是调用函数fun())。
7.2.1 初始化基类
1)当创建了一个导出类的对象时,该对象包含了一个基类的子对象,该子对象被包装在导出类对象内部。
2)基类子对象的初始化:在构造器中调用基类构造器来执行初始化。在执行基类构造器之前,定义处初始化、实例初始化等均会被执行。
示例代码:
class Art { private String art = " test art.\n"; private String artS; { artS = " ART"; } Art() { print("Art constructor"+art+artS); } } class Drawing extends Art { private String draw = " test drawing.\n"; private String drawS; { drawS = " DRAW"; } Drawing() { print("Drawing constructor"+draw+drawS); } } public class Cartoon extends Drawing { public Cartoon() { print("Cartoon constructor"); } public static void main(String[] args) { Cartoon x = new Cartoon(); } } /* Output: Art constructor test art. ART Drawing constructor test drawing. DRAW Cartoon constructor
可以看出,构建过程是从基类“向外”扩散的,所以基类在导出类构造器可以访问它之前,就已经完成初始化了。当然,默认构造器也会逐层调度基类的构造器。
7.2.2 带参数的构造器
编译器可以自动调用默认的构造函数,是因为它们没有任何参数。
但是如果没有默认的基类构造函数,或者想调用一个带参数的基类构造函数,必须使用关键字super显示地编写调用基类构造函数的语句,并且配以适当的参数列表。
如果基类没有默认构造器(无参构造器),导出类不显式的调用基类的带参构造器,则编译器会报错。
7.3 代理
代理是第三种复用代码的关系,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; } // Delegated methods: 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 turboBoost() { controls.turboBoost(); } public void up(int velocity) { controls.up(velocity); } public static void main(String[] args) { SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector"); protector.forward(100); } }
反例代码:
public class SpaceShip extends SpaceShipControls { private String name; public SpaceShip(String name) { this.name = name; } @Override public String toString() { return name; } public static void main(String[] args) { SpaceShip ship = new SpaceShip("NSEA Protector"); ship.foward(100); } }
SpaceShip并非真正的SpaceSbipControls类型,即便你可以“告诉”SpaceShip向前运动(forward())。更准确地讲,SpaceShip包含SpaceShipControls,与此同时,SpaceShipControls的所有方法在SpaceShip中都暴露了出来(不懂哪里暴露了)。上面的例子就可以解决。
7.4 结合使用组合和继承
同时使用组合和继承,并配以必要的构造器初始化,可以创建更加复杂的类。
7.4.1 确保正确清理
try{ //...... }finally{ x.cleanup(); }
上述代码中的finally子句表示的是“无论发生什么事,一定要为x调用cleanup()。”
在清理方法(dispose())中,必须注意对基类清理方法和成员对象清理方法的调用顺序,以防某个子对象依赖于另外一个子对象的情形发生。
- 一般,采用与C++编译器在其析构函数上所施加的形式:首先,执行类的所有特定的清理工作,其顺序同生成顺序相反(通常这就要求基类元素仍旧存活);然后调用基类的清理方法。
- 注意:除了内存以外,不能依赖垃圾回收器去做任何事。如果需要进行清理,最好编写自己的清理方法,但是不要使用finalize()。
7.4.2 名称屏蔽
-
如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽器在基类中的任何一个版本(这一点与C++不同)。因此,无论是在该层或者它的基类中对方法进行定义,重载机制都可以正常工作。
-
如果你只是想覆写某个方法,但是害怕不留心重载了该方法(而并非覆写了该方法)时,可以选择添加
@Override
注解(Java SE5新增)。
-
在一个方法前添加了
@Override
注解,该方法便只能是覆写父类的某个方法,若是不留心写成了重载,编译器便会报错。这样@Override
注解便可以防止你在不想重载时而意外地进行了重载。
7.8 final 关键字
7.8.1 数据
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
- 对于基本类型,final 使数值不变;
- 对于引用类型(包含数组),final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
final int x = 1; x = 2; // cannot assign value to final variable ‘x‘ final A y = new A(); y.a = 1;
空白final:是指被声明为final但又未给定初值的域。但空白final必须在构造器中用表达式赋值。例子如下:
class Poppet { private int i; Poppet(int ii) { i = ii; } } public class BlankFinal { private final int i = 0; // Initialized final private final int j; // Blank final private final Poppet p; // Blank final reference // Blank finals MUST be initialized in the constructor: public BlankFinal() { j = 1; // Initialize blank final p = new Poppet(1); // Initialize blank final reference } public BlankFinal(int x) { j = x; // Initialize blank final p = new Poppet(x); // Initialize blank final reference } public static void main(String[] args) { new BlankFinal(); new BlankFinal(47); } }
总之,必须在域的定义外或每个构造器中用表达式对fianl进行赋值。
7.8.2 方法
使用final方法的原因有两个:
- 把方法锁定,以防任何继承类修改它的含义。这是出于设计的考虑:确保在继承类中使方法行为保持不变,并且不会被覆盖。
- 效率:类似于C++的inline机制,早期的虚拟机需要,现在不需要了,故现在不需要使用final方法进行优化了。
综上,仅当你想显式地阻止覆盖该方法时,才使该方法成为final的。
声明方法不能被子类覆盖。
private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是覆盖基类方法,而是重载了。
final方法和private方法类似,区别在于,private只能在类内访问,类外访问不到,final方法可以在类外被访问,但不可以重写,可以使用该方法的功能但是不可以改变其功能
7.8.3 类
final置于类的定义之前表示该类不允许被继承。这样做的原因如下:
- 出于某种考虑,你对该类的设计永不需要做任何变动;
- 出于安全的考虑,你不希望它有子类。