Java编程思想笔记四:复用类


复用类就是指在不复制代码的前提下,通过某种手段创建新类来复用代码。作者本章介绍了两种手段:组合和继承,此外,还介绍了一种介于组合和继承之间的方法:代理。
本文内容概览

一、组合语法

class WaterSource {
    private String s;
    WaterSource() {
        System.out.println("WaterSource()");
        s = "Constructed";
    }
}

private class SprinklerSystem {
    private String value;
    private WaterSource source = new WaterSource();
}

二、继承语法

2.1.语法

class Cleaner {
    private String s = "Cleaner";
    public void append(String a) { s += a; }
    public void scrub() {
        append("scrub()");
    }
}

public class Detergent extends Cleaner {
    public void scrub() {
        append("Detergent.scrub()");
        super.scrub();
    }
}
  1. 按照惯例,基类中的方法都定义为 private,方法都定义为 public;
  2. 基类的方法要想被导出类使用,必须定义为 public 的;
  3. 如果基类和导出类的方法名称重复时,导出类想显式使用基类中的方法,使用 super 关键字;

2.2.初始化基类

继承并不只是复制基类的接口,当创建一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象和你用基类直接创建的对象是一样的,二者的区别就在于直接创建的基类对象来自外部,而基类的子对象被包装在导出类对象的内部。

导出类&基类初始化顺序:

  1. 导出类内部调用基类构造器,初始化基类子对象;
  2. 导出类调用自己的构造器,初始化导出类;

如果是默认构造器,编译器会为我们完成第 1、2 步,如果是带参数的构造器,我们必须要用 super 显示调用基类的构造器,否则编译器会报错。

2.3.清理

我们在《Java编程思想笔记二:初始化与清理》 里提过,在 Java 中的对象可以依靠垃圾回收器在必要的时候释放其内存,但我们并不知道垃圾回收器何时工作。因此,当我们想要在某些时候清理一些东西,就必须手动来完成。按照常规,这个方法一般放在 finally 子句中,以预防异常出现。

清理方法中,需要注意基类清理方法和成员对象清理方法的调用顺序:

  1. 先清理执行类中的所有对象,对象的清理顺序和生成顺序正好相反(后构造的先清理);
  2. 调用基类的清理方法。

2.4.名称屏蔽

我们在使用继承时,导出类和基类经常会有形同名称、相同入参的方法,这经常会让人疑惑。如果想避免这种情况出现,可以在继承类中定义方法时用 @Override 注解:

class Lisa extends Homer {
    @Override
    void doh(Milhouse m) {
        System.out.println("doh(Milhouse m)");
    }
}

如果基类 Homer 中也存在 doh(Milhouse m) 方法,编译时就会报错,这样可以有效避免上述疑惑,这一点我们在项目开发中基本都会用到。

2.5.向上转型

将导出类引用转换为基类引用的动作我们称为向上转型。之所以称为向上转型,是以传统的类继承图的绘制方法为基础的:

继承

由导出类(Wind)转型成基类(Instrument),在继承图中是由下而上移动的,因此称为向上转型。向上转型总是安全的。

到底该用组合还是继承,一个最清晰的判断就是是否需要向上转型,如果需要则要使用继承。

三、代理语法

代理关系是介于继承和组合之间的中庸之道。代理指将一个成员对象置于所要构造的类中(就像组合),但与此同时,在新类中暴露了该成员对象的所有方法(就像继承)。例如,太空船需要一个控制模块:

public class SpaceShipControls {
    void up(int velocity) {}
    void down(int velocity) {}
}

制造太空船的一种方式是使用继承:

public class SpaceShip extends SpaceShipControls {
    private String name;
    public SpaceShip (String name) { this.name = name; }
}

但是这其实并不符合我们的预期,其实太空舱和控制模块是包含关系,而不是 is-a (继承)的关系,而且我们在 SpaceShipControls 类中并不希望把 SpaceShip 类中的所有方法都暴露出去。代理可以解决此难题:

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 void down(int velocity) {
        controls.down(velocity);
    }
}

上面的代理方式实现了和继承一样的效果,SpaceShipDelegation 类拥有了 SpaceShipControls 类的所有方法。但是,代理更加灵活,比如我们不想把 down() 方法暴露出去,代理类就可以不定义 public void down(int velocity),但是继承做不到这一点。

四、final 关键字

final 关键字主要有三种使用情况:数据、方法和类。

4.1.final 数据

final 修饰基本类型和引用的区别

final 修饰某个成员时,这个成员如果是基本类型,可以实现下面两种效果:

  1. 一个永远不变的编译时常量;
  2. 一个在运行时被初始化的值,而你并不希望它被改变。

另外,一个既是 static 又是 final 的域,只占据一段不能改变的存储空间。

final 也可以修饰对象引用,final 会使引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它指向另一个对象,但是对象本身的内容是可以被修改的。这一限制同样适用于数组,它也是对象。下面的示例展示了上述三种情况:

class Value {   // 包访问权限
    int i;
    public Value(int i) { this.i = i; }
}

public class FinalData {
    private final int valueOne = 1;
    private static final int VALUE_TWO = 2;
    public static final int VALUE_THREE = 3;

    private final Value v4 = new Value(4);
    private static final Value VAL_5 = new Value(5);

    private final int[] a = { 1, 2, 3 };

    public static void main(String[] args) {
        FinalData fd1 = new FinalData();
        // fd1.valueOne++;         -- 错误:编译器常量,不可修改
        // fd1.VALUE_TWO++;
        // fd1.VALUE_THREE++;

        // fd1.v4 = new Value(44); -- 错误:final引用,不可修改引用指向的对象
        // fd1.VAL_5 = new Value(55);
        // fd1.a = new int[5];

        for(int i=0; i<fd1.a.length; i++)
            fd1.a[i]++;          // -- 正确:final引用,不可修改引用指向的对象,但可以修改内容
    } 
}

空白 final

Java 允许生成空白 final,即被声明为 final 但又未给定初始值的域。但是必须在使用前进行初始化:

class class BlankFinal {
    private final int i = 0;
    private final int j;    // 空白 final

    public BlankFinal() {
        j = 1;              // 空白 final 在使用前要进行初始化
    }
}

所以,无论是否是空白 final,必须在域的定义处或者构造器中要对 final 进行赋值!

final 参数

Java 允许在参数列表中以声明的方式将参数指明为 final,在方法中该参数为只读参数,无法被修改:

public class FinalArguments {
    private Integer age;
    public Integer add(final Integer year) {
        // year += age;       -- 错误:year 为只读参数,不可修改
        return year + age;
    } 
}

4.2. final 方法

指明为 final 的方法在继承体系中,不会被导出类覆盖。

类中所有 private 方法都隐式指定为 final 的,由于导出类无法取用 private 方法,所以也就无法覆盖它。

给 private 方法添加 final 修饰词,不会增加任何额外意义。

导出类为什么不能覆盖基类的 private 方法?

class WithFinals {
    private final void f() { print("WithFinal.f()"); }
}

class OverridingPrivate extends WithFinals {
    private final void f() { print("OverridingPrivate.f()"); }
}

基类的 private 方法 f() 并不是基类接口的一部分,只是隐藏于内部的一块儿代码。导出类的 public/protected 方法 f() 是一个全新的方法,仅仅是和基类的 private 方法同名罢了,没有任何关系,也不会发生覆盖。

4.3. final 类

当类为 final 时,不允许继承该类。

final 类中的所有方法都隐式指定为 final 的,因为无法覆盖它们。在 final 类中可以给方法添加 fianl 修饰词,但不会添加任何意义。

final 类的域可以根据个人意愿设置是否为 final。

五、继承的初始化顺序

  1. 在其他任何事情发生之前,将分配给对象的存储空间初始化成二进制的零;
  2. 调用基类构造器;
  3. 按照声明顺序调用成员的初始化方法;
  4. 调用导出类构造器主体。

后面还会记录更多的学习内容,同步分享到公众号 @红薯的Java私房菜 欢迎关注。
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红薯的Java私房菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值