Java 之路 (七) -- 复用类(组合、继承、代理、向上转型、final、再谈初始化和类的加载、方法覆盖)


更新内容:方法覆盖

本以为覆盖的主体在多态一章,结果写完之后才发现实际上本书对覆盖的讲解几乎没有。。。特此补充。


学习内容

  • 两种代码复用机制 - 组合 & 继承
    • 以及中庸之道 - 代理
  • 组合 和 继承
    • 如何选择使用
    • 如何结合使用
  • 向上转型
  • final 关键字
  • 方法覆盖

1. 组合

1.1 组合的概念

​ 在新的类中产生现有类的对象,由于新的类是由现有类的对象所组成,所以这种方法成为组合。

1.1.1 组合的写法及样例

只需要在当前类中声明另一个类的对象作为其成员变量即可,如下:

class B {
    //...
}
public class A {
    private B b;
    //...
    //使用 b 时,需要进行初始化 比如 b = new B();
}

下面给出具体例子:

class Game {
    private String name;
    Game(String name){
        this.name = name;
    }
    void play(){
        System.out.println("play game : " + name);
    }
}

public class Person {
    private Game game;//持有对象的引用
    void doSomething(){
        game = new Game("超级玛丽");
        game.play();//调用包含对象 Game 的方法
    }
    public static void main(String[] args){
        Person person = new person();
        person.doSomething();
    }
}

//输出结果为 : play game : 超级玛丽

1.2 组合的特性

优点:

  1. 耦合度低,包含对象的内部细节改变不会影响当前对象的使用

  2. 可以动态绑定,当前对象可以在运行时动态的绑定所包含的对象(也就是后面提到的 使用实例初始化

缺点:

  1. 容易产生过多对象

1.3 组合 - 引用的初始化

通过在以下 4 种位置进行初始化:

  1. 在定义对象的地方进行初始化

    这也为这总是能够在构造器被调用之前被初始化

  2. 在类的构造器中初始化

  3. 惰性初始化 - 就在正要使用这些对象之前进行初始化。

    不必要生成对象时,这种方式可以减少额外的负担

  4. 使用实例初始化


2. 继承

2.1 继承的概念

​ 按照现有类的类型来创建新类,无需改变现有类的形式,采用现有类的形式向其中添加新代码,这种方法叫继承。

​ 简单来说,继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

继承的重点不在于“为新的类提供方法”,而是表现新类和基类的之间的关系–”新类是现有类的一种类型“。

2.1.1 类的继承语法

通过关键字 extends 申明一个类是从另一个类继承而来的,形式如下:

class 父类{
    ...
}
class 子类 extends 父类{
    ...
}

2.1.2 继承示例

class Animal { 
    private String name;  
    private int id; 
    public Animal(String myName, int myid) { 
        name = myName; 
        id = myid;
    } 
    public void eat(){ 
        System.out.println(name+"正在吃"); 
    }
    public void sleep(){
        System.out.println(name+"正在睡");
    }
    public void introduction() { 
        System.out.println("大家好!我是"         + id + "号" + name + "."); 
    } 
}

//子类 Mouse 继承 Animal
public class Mouse extends Animal { 
    public Mouse(String myName, int myid) { 
        super(myName, myid); 
    } 
    public static void main(String[] args){
        Mouse mouse = new Mouse("jerry"1);
        mouse.sleep();//sleep 方法继承自父类 Animal 的 sleep 方法
    }
}

//结果为 : jerry正在睡

2.2 继承的特性

  1. 子类拥有父类非 private 的属性、方法
  2. 子类可以拥有自己的属性和方法,即对父类进行扩展
  3. 子类可以用自己的方式实现父类的方法(方法覆盖)
  4. 单继承,即一个子类只能继承一个父类
  5. 多重继承,即一个子类继承一个父类,而这个父类还可以继承它的父类
  6. 提高了类之间的耦合性 – 继承的缺点
  7. 破坏封装,为了复用代码可能使子类具有不该具有的功能 – 结成的缺点

2.3 继承 - 基类的初始化

​ 对于基类和导出类(或者说父类和子类),从外部看,导出类就像是一个与基类具有相同接口的新类,附加上一些额外的方法和属性。但是!继承不只是复制基类的接口。当创建了一个导出类的对象时,该对象隐含了一个基类的对象。

隐含的基类对象和我们使用基类直接创建的对象是一样的,二者区别在于,基类的子对象是被包装在导出类对象的内部的,而后者来自于外部。

因此,在创建导出类对象时,需要考虑基类对象的正确初始化。

2.3.1 初始化过程

只有一种方式能保证对基类对象进行初始化

  • 在导出类的构造器种调用基类构造器来执行初始化。

    Java 会自动在导出类的构造器种插入对基类构造器(默认/无参构造)的调用

初始化过程总结如下:

  • 从基类向外”扩散”

    所有基类在导出类构造器可以访问它之前,就已经完成了初始化

  • 带参数的构造器,使用关键字 super 显式编写。

示例如下:

class Game{
    Game(int i){
        System.out.println("Game constructor with param i");
    }
    Game(){
        System.out.println("Game constructor");
    }
}
public class Chess extends Game{
    Chess(int i){
        super(i);
        System.out.println("Chess constructor with param i");
    }
    Chess(){
        System.out.println("Chess constructor");
    }

    public static void main(String[] args){
        Chess chess1 = new Chess();//输出 (1)
        Chess chess2 = new Chess(11);//输出 (2)
    }
}
// (1)
/* 
Game constructor
Chess constructor
*/

// (2)
/*
Game constructor with param i
Chess constructor with param i
*/

2.4 初始化与类的加载

主要是涉及到 static 以及 继承时的问题

Java 中每个类的编译代码存在于它自己的独立文件中。该文件只在创建类的第一个对象之时被加载,但是访问其中 static 域或者 static 方法时,也会发生加载。

构造器也是 static 方法,尽管没有显式写出 static 关键字。因此可以更准确地说,类是在其任何 static 成员被访问时被加载。

所有 static 对象和 static 代码段都会在加载时依照程序中的顺序而一次初始化。当然 static 成员只会被加载一次。

2.4.1 继承与初始化

本节重点强调一下包括继承在内的初始化全过程。

加载顺序:

  • 从 X.main() 入口开始,加载器开始启动并找出 X 类的编译代码(在X.class 文件中)。
  • 对它加载的过程中,如果它有基类(由关键词 extends 得知),就会去加载这个基类(编译器强制要求)。
  • 如果该基类上层还有基类,那么就加载上层的基类。如此类推。
  • 接下来,执行 根基类的 static 初始化,然后是它的导出类。以此类推,直到全部导出类都加载完毕

对象创建:

  • 相关类加载完毕之后,开始创建对象。首先,对象中所有基本类型被设为默认值,对象引用设为 null
  • 然后,调用基类的构造器(编译器自动调用或者开发者显式 super 调用)
  • 基类构造器完成之后,当前导出类的实例变量按照次序进行初始化。
  • 最后执行当前导出类的构造器。

小结:

类的加载顺序: 类内部静态块的执行先于类内部静态属性的初始化,会发生在类被第一次加载初始化的时候;类内部属性的初始化先于构造函数会发生在一个类的对象被实例化的时候。

类的初始化顺序:父类 static > 子类 static(其中类内部静态块 > 类静态属性) >父类 非 static > 子类非 static(其中类内部属性 > 类构造函数)

2.5 方法覆盖

原书中,对方法覆盖并没有明确的定义,就只是提到用 @Override 注解防止本意是覆盖方法而不留心进行了重载的情况,所以这里补充一下方法覆盖的相关内容。

方法覆盖是父类和子类之间的一种多态,子类必须拥有父类方法的实现。

作用:

解决子类继承父类之后,父类的方法不满足子类的具体特征,此时在子类中重新定义该方法,并重写方法体。

规则:

  • 覆盖方法必须和父类中被覆盖方法具有相同的方法名称、输入参数和返回值类型
  • 覆盖方法不能使用比父类中被覆盖方法更严格的访问权限
  • 覆盖方法不能比父类中被覆盖方法抛出更多的异常
  • 不能覆盖父类 static 方法

多态与方法覆盖

多态是方法覆盖的基础,而方法覆盖又是一种多态的表现形式。(另一种是重载)

多态的内容在下一章,此处先把结论放出来。

  • 多态也就是动态绑定,运行时根据对象的类型进行绑定并调用相应方法,这为覆盖提供了前提。
  • 同时,为了让多态表现出“允许不同类的对象对同一消息做出响应”,我们需要让子类覆盖父类的方法,否则无论是子类还是父类对象,最后调用的都是父类的方法。

方法重载与方法覆盖:

二者都是多态的表现形式,但是二者并没什么直接的关联

  • 重载是针对同一个类中的重名方法而言 – 每个方法的参数表不同
  • 覆盖是针对父类和子类两个类来说的,子类重新定义父类中的方法 – 覆盖的方法名字、类型和参数表都相同。

3. 代理

Java 并没有提供对代理的直接支持

3.1 代理的概念

​ 代理是继承和组合之间的中庸之道,将一个成员对象置于所要构造的类中(就像组合),但与此同时在新类暴露该成员对象的所有方法(就像继承)。

​ 换句话说,就是将基类对象作为代理类的成员,而代理类有对应于基类的所有方法,这些方法内部使用基类对象成员调用基类的方法

3.2 代理的示例

CarControls.java

public class CarControls{
    void up(int velocity){
        //...
    }
    void down(int velocity){
        //...
    }
    void left(int velocity){
        //...
    }
    void right(int velocity){
        //...
    }
}

Car.java

public class Car{
    private String name;
    private CarControls controls = new CarControls();
    void up(int velocity){
        controls.up()
    }
    void down(int velocity){
        controls.down()
    }
    void left(int velocity){
        controls.left()
    }
    void right(int velocity){
        controls.right()
    }
}

以上代码,Car 包含了 CarControls,与此同时 CarControls 的所有方法暴漏在 Car 中,也就是说现在我们可以直接对 Car 进行操作(比如让它 up()),而 Car 会在内部通过 CarControls 对这个操作进行实现。


4. 组合 & 继承

4.1 结合使用

emmm..例子看书吧。

4.1.1 确保正确清理

前面几章也提到过,在 Java 中,我们依赖垃圾回收器在必要时释放内存,但是这个我们并不知道它在什么时候被调用或者它是否被调用。

因此在某些情况下,我们需要执行一些必须的清理活动时,就必须显式地编写一个特殊方法来进行处理,并确保这个方法能够执行。

除了内存以外,不依赖垃圾回收器做任何事。如果需要清理非内存的东西,编写自己的清理方法,注意不要使用 finalize()。

通常我们会在 try-catch-finally 中的 finally 子句中进行清理工作。

4.1.2 名称屏蔽

如果 Java 的基类拥有某个已被多次重载的方法,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本(与 C++ 不同)。

因此,无论是在导出类或者它的基类中对方法进行定义,重载机制都可以正常工作。

换个说法,就是子类可以重载父类的方法

重载的定义是:同一类中,方法名相同,参数列表不同的一组方法

嗯?好像哪里不对?这不是说了是”同一类”么?子类和父类是不同的类啊。

别急别急,此处可以理解为,子类继承了父类的方法,那么子类就包含了父类中的方法,此时在子类添加一个同名不同参的方法,那么不就满足重载了么。

4.2 不同的使用场景

首先,组合和继承都允许在新的类中放置子对象,组合是显式地这么做,而继承是隐式的。那么二者有什么区别?适用于什么场景呢?

组合

  • 通常用于在新类中使用现有类的功能而非它的接口

    即在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的知识为新类定义的接口,而非所嵌入对象的接口。此种情况下,嵌入的对象权限声明为 private。

  • 表示一种 has - a 的关系

继承

  • 通常用于 在现有类的基础上,开发一个它的特殊版本
  • 表示一种 is - a 的关系

    (子类) is a (父类) – cat is an animal.


5. 向上转型

​ 前面我们提到了,继承的重点在于突出 “新类是现有类的一种类型” 这一关系,由于继承确保基类中所有的方法在导出类中同样有效,所以能够向基类发送的所有信息同样也可以向导出类发送。这一说法就很有趣了,我们是不是有就可以将一个 导出类对象的引用 作为 基类对象的引用 来使用呢?

​ 事实上,当然是可以的。Java 中将 导出类转型成基类 的动作 称为向上转型

5.1 为什么称之为向上转型

历史原因使然

  • 传统的类继承图的绘制方法是:将根置于页面的顶端,然后逐渐向下。于是将导出类转型成基类时,继承图上是上向移动的,因此一般称为向上转型。

向上转型的安全性

  • 向上转型是安全的,因为这个过程中唯一可能发生的问题是丢失方法,而不是获取没有的方法

向下转型:有向上转型,也有与之相对的 向下转型,不过此处没涉及到,就留到之后再说了。

5.2 再论组合与继承

尽量多使用组合,尽量少使用继承

一个清晰的判断方法:是否需要从新类向基类进行转型

  • 如果需要,那么继承是必要的。
  • 如果不需要,则慎重考虑是否使用继承。

6. final 关键字

通常 final 指的是”这是无法改变的“,不想做改变可能处于两个理由:设计或效率。

final 可能有三种使用情况:数据、方法 和 类。

6.1 final 数据

一句话概括:final 使数据恒定不变。

  1. 对于 基本数据类型:使 数值恒定不变,即常量

  2. 对于 对象引用:使 引用恒定不变

    注意!

    引用恒定不变指的是 无法再把它改为指向另一个对象。

    但是!对象本身仍可以被修改

编译期常量

  • 带有恒定初始值的 static final 基本数据类型
  • 占据一段不能改变的存储空间

6.1.1 空白 final

Java 允许生成 空白final,空白final 指的是 被声明为 final 但是为给定初值的域。但是无论如何,空白final 使用前必须被初始化(必须在域的定义处或者每个构造器中用表达式对 final 进行赋值)

下面用一个例子展示 空白final 的灵活性:

public class Person{
    private final String type; // 空白 final
    public Person(String type){
        type = type
    }
    public static void main(String[] args){
        Person person = new Person("white");
        // Person person = new Person("black");
        //...
    }
}

上述代码,通过传入的参数,进行 final 字段的初始化。

6.1.2 final 参数

Java 允许在参数列表中将参数指明为 final

  • 对于对象引用:无法在方法中更改参数引用所指向的对象。
  • 对于基本类型:可以读参数,但不能修改参数

这一特性主要用来向匿名内部类传递数据,具体在原书第 10 章。

6.2 final 方法

final 方法的原因:

  1. [设计] 把方法锁定,保证继承中方法的行为保持不变,并且不会被覆盖。

  2. [效率] 将方法指明为 final,编译器会将该方法的所有调用转为内联调用

    1. JVM 会根据情况优化内联

    我们应当让编译器和 JVM 去处理效率问题,只有在明确禁止覆盖时,才将方法设置为 final

关于 private:

  • 所有 private 方法都隐式指定为 final
  • private 方法无法被覆盖。

6.3 final 类

final 类之后 该类无法被继承。

通常处于某种考虑下,使得该类不允许被继承,对该类的设计不再做任何改动,或者处于安全考虑,不希望它有子类。


总结

这一章主体介绍的是类的复用,附带着介绍了 向上转型以及 final 关键字。核心还是在于面向对象编程的复用思想,具体实现是次要的。

向上转型不仅仅这一章介绍的这么一点,它对下一章中的多态至关重要,此处也只是抛砖引玉。

果然我还是不知道应该总结什么。

就这样吧,共勉。

猜你喜欢

转载自blog.csdn.net/whdAlive/article/details/81507847
今日推荐