Java 的继承机制是一种复用类的技术,从原理上来说,是更好的使用了组合技术,因此要理解继承,首先需要了解类的组合技术是如何实现类的复用的。

使用组合技术复用类

假设现在的需求是要创建一个具有基本类型,String 类型以及一个其他非基本类型的对象。该如何处理呢?

对于基本类型的变量,在新类中成员变量处直接定义即可,但对于非基本类型变量,不仅需要在类中声明其引用,并且还需要手动初始化这个对象。

这里需要注意的是,编译器并不会默认将所有的引用都创建对象,因为这样的话在很多情况下会增加不必要的负担,因此,在合适的时机初始化合适的对象,可以通过以下几个位置做初始化操作:

  1. 在定义对象的地方,先于构造方法执行
  2. 在构造方法中。
  3. 在正要使用之前,这个被称为惰性初始化
  4. 使用实例初始化。
class Soap {
    private String s;
    Soap() {
        System.out.println("Soap()");
        s = "Constructed";
    }
    public String tiString(){
        return s;
    }
}

public class Bath {
    // s1 初始化先于构造函数
    private String s1 = "Happy", s2 = "Happy", s3, s4;
    private Soap soap;
    private int i;
    private float f;
    
    public Both() {
        System.out.println("inSide Both");
        s3 = "Joy";
        f = 3.14f;
        soap = new Soap();
    }
    
    {
        i = 88;
    }
    
    public String toString() {
        if(s4 == null){
            s4 = "Joy"
        }
        return "s1 = " + s1 +"\n" +
               "s2 = " + s2 +"\n" +
               "s3 = " + s3 +"\n" +
               "s4 = " + s4 +"\n" +
               "i = " + i +"\n" +
               "f = " + f +"\n" +
               "soap = " + soap;
    }
}

继承

Java 中的继承由 extend 关键字实现,组合的语法比较平实,而继承是一种特殊的语法。当一个类继承自另一个类时,那么这个类就可以拥有另一个类的域和方法

class Cleanser{
    private String s = "Cleanser";
    
    public void append(String a){
        s += a;
    }
    public void apply(){
        append("apply");
    }
    public void scrub(){
        append("scrub");
    }
    public String toString(){
        return s;
    }
    public static void main(String args){
        Cleanser c = new Cleanser();
        
        c.apply();
        System.out.println(c);
    }
}

public class Deter extends Cleanser{
    public void apply(){
        append("Deter.apply");
        super.scrub();
    }
    public void foam(){
        append("foam");
    }
    public static void main(String args){
        Deter d = new Deter();
        
        d.apply();
        d.scrub();
        d.foam();
        System.out.println(d);
        Cleanser.main(args);
    }
}

上面的代码中,展示了继承语法中的一些特性:

  1. 子类可以直接使用父类中公共的方法和成员变量(通常为了保护数据域,成员变量均为私有)
  2. 子类中可以覆盖父类中的方法,也就是子类重写了父类的方法,此时若还需要调用被覆盖的父类的方法,则需要用到 super 来指定是调用父类中的方法。
  3. 子类中可以自定义父类中没有的方法。
  4. 可以发现上面两个类中均有 main 方法,命令行中调用的哪个类就执行哪个类的 main 方法,例如:java Deter

继承语法的原理

接下来我们将通过创建子类对象来分析继承语法在我们看不到的地方做了什么样的操作。

可以先思考一下,如何理解使用子类创建的对象呢,首先这个对象中包含子类的所有信息,但是也包含父类的所有公共的信息。

下面来看一段代码,观察一下子类在创建对象初始化的时候,会不会用到父类相关的方法。

class Art{
    Art() {
        System.out.println("Art Construct");
    }
}

class Drawing extends Art {
    Drawing() {
        System.out.println("Drawing Construct");
    }
}

public class Cartoon extends Drawing {
    public Cartoon() {
        System.out.println("Cartoon construct");
    }
    public void static main(String args) {
        Cartoon c = new Cartoon();
    }
}
/*output:
Art Construct
Drawing Construct
Cartoon construct
*/

通过观察代码可以发现,在实例化Cartoon时,事实上是从最顶层的父类开始向下逐个实例化,也就是最终实例化了三个对象。编译器会默认在子类的构造方法中增加调用父类默认构造方法的代码。

因此,继承可以理解为编译器帮我们完成了类的特殊组合技术,即在子类中存在一个父类的对象,使得我们可以用子类对象调用父类的方法。而在开发者看来只不过是使用了一个关键字。

注意:虽然继承很接近组合技术,但是继承拥有其他更多的区别于组合的特性,例如父类的对象我们是不可见的,对于父类中的方法也做了相应的权限校验等。

那么,如果类中的构造方法是带参的,该如何操作呢?(使用super关键字显示调用)

见代码:

class Game {
    Game(int i){
        System.out.println("Game Construct");
    }
}

class BoardGame extends Game {
    BoardGame(int j){
        super(j);
        System.out.println("BoardGame Construct");
    }
}
public class Chess extends BoardGame{
    Chess(){
        super(99);
        System.out.println("Chess construct");
    }
    public static void main(String args) {
        Chess c = new Chess();
    }
}
/*output:
Game Construct
BoardGame Construct
Chess construc
*/