继承和组合的使用时机
到底是该用组合还是继承,一个最清晰的办法就是判断是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的。
什么时候,一定会用到向上转型呢?当然是子类在定义时需要调用父类方法的时候。
另外,子类与父类的关系是 is-a 的关系。
final 数据
final 修饰常量时,表示这个常量是不改变的。一个既是 static 又是 final 的域只占据一段不能改变的存储空间。
final 修饰引用时,表示 final 使得引用恒定不变,一旦引用被初始化指向一个对象,就无法再把它改为指向另外一个对象。
然而,对象其自身却是可以被修改的。这一限制同样适用于数组,它也是对象。
public class A{
final int[] a0 = {1};
}
class TestA{
void test(){
A a = new A();
a.a0[0]++;
}
}
空白 final
Java 允许在声明变量但未赋值的情况下用final修饰,但是 final 修饰的变量必须在 constructor 中被赋值。
这样的做法,保证了灵活性。
class Poppet{
private int i;
Poppet(int x){
i = x;
}
}
public class BlankFinal{
private final int j;
private final Poppet poppet;
public BlankFinal(){
j = 0;
poppet = new Poppet(j);
}
public BlankFinal(int x){
j = x;
poppet = new Poppet(x);
}
}
final 修饰参数
final 也可以用来修饰参数,表示参数不可以被更改。这一特性主要是用来向匿名内部类中传递数据。
class Gizmo{
public void spin(){}
}
public class FinalArguments{
void with(final Gizmo g){
// g = new Gizmo() 这种写法是错误的,因为 g 是 final,这样的写法保证了 g 只具有只读属性。
}
}
final 方法
使用 final 方法有两个原因,第一个原因是把方法锁定,以防止任何继承类修改它的含义。
这是出于设计的考虑,想要确保继承在子类中方法不变,并且不会被覆盖。
第二个原因是,早期发现 final 修饰的方法会更快,现在已经不需要了。
final 和 private 关键字
类中所有的 private 方法都隐式指定为 final的。由于无法取用 private 方法,也就无法覆盖它。
final 类
当将某个类的整体定义为 final 时,表明该类不允许继承
7.9 初始化及类的加载
在 Beetle 上运行 Java 时,所发生的第一件事就是试图访问 Beetle.main() (一个 static 方法),
于是加载器开始启动并找出 Bettle 类的编译代码(在名为 Bettle.class 的文件之中)。在对它进行加载
的过程中,编译器注意到它有一个基类(由关键字 extends 得知的),于是它继续进行加载。
对象中所有的基本类型都会被设为默认值,对象引用被设为 null ——这是由将对象内存设为二进制零值而一举生成的。
然后,基类的构造器会被调用。在本例中,它是被自动调用的。但也可以用 super 来指定对基类构造器的调用。
在开始设计时,一般优先选择使用组合,只有必要时才选择继承。因为组合更具有灵活性。此外,通过对成员类型使用
继承技术的添加技巧,可以在运行时改变那些成员对象的类型和行为。
多态
将一个方法调用同一个主体关联起来被成为绑定,若程序执行前进行绑定,叫做前期绑定。
后期绑定,也叫动态绑定或运行时绑定,在运行时根据对象的类型进行绑定。Java 中除了 static 和 final 方法,其他所有的方法
都是后期绑定的。为什么要讲某个方法声明为 final 呢?它可以防止其他人覆盖该方法,可以有效地关闭动态绑定,告诉编译器不需要对其进行动态绑定。
多态的缺陷
- 私有方法无法重载
public class PrivateOverride {
private void f(){
System.out.println("private f()");
}
public static void main(String[] args){
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride {
public void f(){
System.out.println("public f()");
}
}
- 只有普通的方法调用可以是多态的
只有普通的方法调用可以是多态的,如果直接访问某个属性,那么这个访问将在编译期间就进行解析
class Super{
public int filed = 0;
public int getFiled(){
return filed;
}
}
class Sub extends Super{
public int filed = 1;
public int getFiled(){
return field;
}
public int getSuperField(){
return super.field;
}
}
public class FieldAccess{
public static void main(String[] args){
Super sup = new Sub();
// 对属性的访问是在编译期间就确定的
System.out.println("sup.field = " + sup.field +
", sup.getField() = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " +
sub.field + ", sub.getFiled() = " +
sub.getField() +
sub.getSuperField());
}
}
8.3 构造器和多态
- 调用基类构造器,这个步骤会反复地不断递归下去。首先是构造这种层次的根,然后是下一层导出类,直到最低层的导出类。
- 按照声明顺序调用成员的初始化方法
- 调用导出类构造器的主体
package com.zzjack.wxorder.javathought;
class Meal{
Meal(){
System.out.println("Meal()");
}
}
class Bread{
Bread(){
System.out.println("Bread()");
}
}
class Cheese{
Cheese(){
System.out.println("Cheese()");
}
}
class Lettuce{
Lettuce(){
System.out.println("Lettuce()");
}
}
class Lunch extends Meal{
Lunch(){
System.out.println("Lunch()");
}
}
class PortableLunch extends Lunch{
PortableLunch(){
System.out.println("PortableLunch");
}
}
public class Sandwich extends PortableLunch{
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich(){
System.out.println("Sanwich()");
}
public static void main(String[] args){
new Sandwich();
}
}
8.3.3 构造器内部的多态方法的行为
在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
如前所述的那样调用基类构造器。此时,调用被覆盖后的 draw() 方法(要在调用 RoundGlyph 构造器之前调用),
由于步骤1的缘故,我们此时会发现 radius 的值为 0按照声明的顺序调用成员的初始化方法
调用导出类的构造器主体
编写构造器时有一条有效的准则:用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其它方法。在构造器内
唯一能够安全调用的那些方法是基类中的 final 方法。final 方法不能被覆盖,因此也就不会出现上述令人惊讶的问题。
8.4 协变返回类型
在设计时,组合优先于继承。因为组合更加灵活,它可以动态选择类型。
class Actor{
public void act(){};
}
class HappyActor extends Actor{
public void act() {
System.out.println("HappyActor");
}
}
class SadActor extends Actor{
public void act(){
System.out.println("SadActor");
}
}
class Stage{
private Actor actor = new HappyActor();
public void change(){
actor = new SadActor();
}
public void performPlay(){
actor.act();
}
}
public class Transmogrify {
public static void main(String[] args){
Stage stage=new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
}
在 Stage 中,performPlay() 的的输出结果会随着 change() 而改变,
这样一来,我们在运行期间就获得了动态灵活性,这被称为是状态模式。
用继承来表达行为间的差异,并用字段表达状态上的变化。这是一条通用的编程准则,
这个例子中,通过继承得到了两个不同的类,用于表达 act() 方法上的差异,而 Stae
运用组合使自己的状态发生变化。
向上转型和向下转型
向上转型是安全的,因为子类一定具备了父类的接口。
但是,向下转型不一定安全。因为子类可以扩展自己独有的接口。
向下转型需要进行类型转换,并且是在进入运行期对其进行类型检查。
class Useful{
public void f() {}
public void g() {}
}
class MoreUseful extends Useful{
public void f() {}
public void g() {}
public void u() {}
public void v() {}
public void w() {}
}
public class RTTI {
public static void main(String[] args){
Useful[] x = {
new Useful(),
new MoreUseful(),
};
x[0].f();
x[1].g();
((MoreUseful) x[1]).w();
((MoreUseful) x[0]).w();
}
}