Java 编程思想笔记:Learn 5

继承和组合的使用时机

到底是该用组合还是继承,一个最清晰的办法就是判断是否需要从新类向基类进行向上转型。如果必须向上转型,则继承是必要的。

什么时候,一定会用到向上转型呢?当然是子类在定义时需要调用父类方法的时候。

另外,子类与父类的关系是 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 呢?它可以防止其他人覆盖该方法,可以有效地关闭动态绑定,告诉编译器不需要对其进行动态绑定。

多态的缺陷

  1. 私有方法无法重载
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()");
    }
}
  1. 只有普通的方法调用可以是多态的

只有普通的方法调用可以是多态的,如果直接访问某个属性,那么这个访问将在编译期间就进行解析

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 构造器和多态

  1. 调用基类构造器,这个步骤会反复地不断递归下去。首先是构造这种层次的根,然后是下一层导出类,直到最低层的导出类。
  2. 按照声明顺序调用成员的初始化方法
  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 构造器内部的多态方法的行为

  1. 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。

  2. 如前所述的那样调用基类构造器。此时,调用被覆盖后的 draw() 方法(要在调用 RoundGlyph 构造器之前调用),
    由于步骤1的缘故,我们此时会发现 radius 的值为 0

  3. 按照声明的顺序调用成员的初始化方法

  4. 调用导出类的构造器主体

编写构造器时有一条有效的准则:用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其它方法。在构造器内

唯一能够安全调用的那些方法是基类中的 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();
    }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容