总结Java的继承(Java Silver8)

00. 首先

我是一位对Java的继承了解得不太好的作者。
虽然我有一种模糊的理解,但事实上并没有完全理解,是的。

在本文中,我想介绍一下我个人经历中那种以为自己“明白”却实际上是“不明白”的Java继承。如果这能对你们理解Java继承有所帮助,我就感到非常荣幸。

※ 注意
1.本文是作为学习Java Silver8考试的一部分而写的,因此可能没有涉及到考试范围之外的功能。
2.本文中不涉及接口(目前还没有计划涉及)。
3.尽管不断地出现了“不理解”的表达,但这纯粹是为了解决考试问题。

01. 简言之

即将到来

02. 究竟继承是什么来着?

继承是指”基于现有的类创建(定义)一个新类”的过程。在这个过程中,
– 继承自的类被称为超类或基类
– 继承到的类被称为子类或派生类
(本文中采用的表述是超类和子类)。

在下面的例子中,我们从哺乳动物类创建了一个猫类。

// スーパークラス
class Mammals { }

// サブクラス
class Cat extends Mammals { }

继承的方法就是这些了。很简单吧!

我明白这样的事情!但是,通过继承发生的事情,我(尤其是我)其实并不太清楚。

接下来的章节将会介绍我个人关注的四个方面来讲解继承:
1. 继承的基本规则
2. 继承类的类型转换
3. 继承与访问器
4. 继承类的构造函数

继承的基本规则

1.1 只有一个可以继承的超类

例如,像下面这样的声明将会导致编译错误。

// スーパークラス その1
class Mammals { }

// スーパークラス その2
class Reptiles { }

// サブクラス
class Cat extends Mammals, Reptiles { }

1.2 在具体类中必须重写抽象方法。

请看下面的代码。

抽象类Mammals有抽象方法eact、walk和具体方法sleep。
另外,Cat类继承了Mammals类,并且对walk和sleep方法进行了重写。

public abstract class Mammals {
  public abstract void eat();
  public abstract void walk();
  public void sleep() {}
}
public class Cat extends Mammals {
  public void walk() {}
  public void sleep() {}
}

嗯,关于上述的代码,如果编译的话会出错(如果使用Eclipse、VSCode等编辑器来查看,那肯定会变成鲜红色的,毫无疑问)。

理由很明显,因为具体类没有在抽象类内覆盖抽象方法。
在这种情况下,由于猫没有在哺乳动物中重载eat()方法,所以会产生编译错误。应该覆盖所有的抽象方法。

继承的类转换

2.1 对于没有继承关系的类进行转换会导致编译错误。

在下面的代码中,我们通过继承哺乳动物类来创建了猫和狗类。

class Mammals { }  // 哺乳類クラス
class Cat extends Mammals { }  // 猫クラス
class Dog extends Mammals { }  // 犬クラス

public class Main {
  public static void main(String[] args) {
    Cat catA = new Cat();
    Dog dogA = new Dog();

    // Cat型のインスタンスをMammals型にキャスト
    Mammals mammals = (Mammals) catA;  // OK
    // Cat型のインスタンスをDog型にキャスト
    Dog dogB = (Dog) catA;  // NG コンパイルエラー
  }
}

Java在编译时会检查是否可以进行强制类型转换(即是否进行了继承类之间的转换)。所以,它产生的是编译错误而不是运行错误!

无论是继承还是其他情况,无法进行类型转换的代码通常会导致编译错误。

3:继承和访问器

3.1 继承的类方法无法严格限制其公开范围

下面的代码将导致编译错误。
这是因为当Cat类通过继承Mammals类并覆盖eat方法时,将原有eat方法的访问范围设为更严格的访问级别。

class Mammals { 
  void eat() {}
} 
class Cat extends Mammals {
  private void eat() {}
}

请用中文将以下内容进行改写,只需要一种版本:
I went to the bookstore and bought a new novel.

关于访问器<参考1 p175中的部分内容更改引用>

在中国,通常使用以下访问器(访问修饰符)的方法:

// 厳しい     ←       公開範囲         →          緩い
   private   protected   /*(アクセッサなし)*/   public
アクセッサ説明private同一クラス内からのみ利用可能protected同一パッケージのクラス or 対象のクラスを継承したクラスから利用可能(アクセッサなし)同一パッケージのクラスからのみ利用可能publicどのクラスからでも利用可能

继承类的构造函数(意外地棘手)

4.1 无法从超类继承的东西。

无论继承,都不能继承的是,
私有字段、方法(包括构造函数)!

注意要按照构造函数的执行顺序。

在上面提到了“即使继承也无法继承构造函数”的说法,但需要注意的是,在子类的构造函数执行之前,父类的构造函数会先执行。

请看下面的代码。
有三个类 A、B、C,C 继承自 B,B 继承自 A。在每个类的构造函数中,会将类名输出到控制台(代码参考自引用2,并略有修改)。

class A {
  public A() { 
    System.out.print("A"); 
  }
}

class B extends A {
  public B() { 
    System.out.print("B"); 
  }
}

class C extends B {
  public C() { 
    System.out.print("C");
  }
}

public class Main {
  public static void main(String[] args) {
    new C();  // ABC
  }
}

当我们执行main()函数时,您是否能够了解发生了什么情况呢?
是的,A、B、C的构造函数都被执行了!

对不起,我夸张了一下3。

关于为什么会如此,我想引用参考资料3第429页来解释。

实际上,在Java中,有一个规则:“所有构造函数必须在开头调用内部实例部分(即父类)的构造函数。”

简而言之,考虑到刚才的代码的实际情况和执行顺序,

class A {
  public A() { 
    System.out.print("A");   //  3
  }
}

class B extends A {
  public B() { 
    super();  // Aのコンストラクタを呼び出す   2
    System.out.print("B");   //  4
  }
}

class C extends B {
  public C() { 
    super();  // Bのコンストラクタを呼び出す   1
    System.out.print("C");  // 5
  }
}

public class Main {
  public static void main(String[] args) {
    new C();  // C をインスタンス化   0
  }
}

当C()被调用时,会执行B(),在B()的内部调用A()…的方式,如果继承的父类又继承了另一个类,会先执行该继承类的构造函数。

另外,在子类的内部,如果没有调用超类的构造函数,编译器会自动添加super()56!

然而,只要明确调用超类的构造函数(如super()或super(str))或重载的构造函数(如this()或this(str)),编译器就不会自动添加super()。

在子类的构造函数中调用了父类的构造函数…
即使知道这一点,如果连继承了多个类的构造函数都被调用了,那还是会吃惊呢w

这种构造函数调用是不允许的。

「02. 注意构造函数的执行顺序」相关的是,还有一个陷阱。那就是,调用超类构造函数两次的代码将会导致编译错误。

首先,请看以下代码!(参考2页,源自第362-363页,仅做了部分格式更改)

public class Mammals {
  String name;
  int age;
  public Mammals(String name, int age) {
    this.name = name;
    this.age = age;
  }
}
class Cat extends Mammals {
  String color
  publip Cat(String color) {
    this.color = color;
  }
  public Cat(String name, int num, String color) {
    super(name, num);
    this(color);
  }
}
public class Main {
  public static void main(String[] args) {
    Cat cat1 = new Cat("white");
    Cat cat2 = new Cat("mike", 2, "Black");

   System.out.println(cat1.name + ", " + "" + "")

  }
}

来吧,如果编译这段代码,你认为会发生什么?
答案是,在猫类的第3行和第8行会产生编译错误!

为了思考这个原因,我们假设这段代码被执行,并跟踪生成的每个实例的处理过程!

②生成cat1实例的过程
由于只有一个参数,如Cat cat1 = new Cat(“white”),因此会执行public Cat(String color)的方法。
在此之前,由于Cat类继承自Mammals类,因此会执行Mammals类的构造函数。

  String color
  publip Cat(String color) {
    super();  // コンパイル時に自動追加
    this.color = color;
  }

然而,Mammals类中没有无参构造函数public Mammals()!在试图执行不存在的方法时,在写publip Cat(String color)的那一行会出现错误。

生成cat2实例的处理
这次给出了三个参数来生成实例。
因此会执行对应的以下构造函数!

  public Cat(String name, int num, String color) {
    super(name, num);
    this(color);
  }

首先,调用了父类的构造函数publip Cat(String color)。由于该函数存在,因此没有特殊问题。

然而,在下一行代码中,我们现在执行的是Cat类的构造函数publip Cat(String color)。
虽然我原以为会出现与步骤①相同的错误,但实际上却遇到了不同类型的编译错误。

原因是因为要调用超类构造函数两次。

在Java中,无法在调用父类构造函数之后调用重载的构造函数。

这次是publip Cat(String color)重载的构造函数。由于super();之后执行了它,所以会出现错误。

在实际考试中,构造函数内部

super();
this();

如果你看到这段代码,记住在 “this ();” 部分会导致编译错误!这样就好了!

05. 最后

非常感谢您读到最后。

06. 参考 -> 06. 参考

1.山本道子(2015) 在㈱翔泳社发行的《Java程序员Silver SE 7(第5刷发行)》
2.志贺澄人(2019) 在㈱インプレス发行的《彻底攻略Java SE 8 Silver问题集[1Z0-808]对应》
3.中山清乔/国本大吾(2018) 在㈱インプレス发行的《轻松理解Java入门 第2版》

通常情况下,编辑器和编译器会为我预先指引,所以即使我不明白也没有问题,但当需要靠自己去发现时,就不能再这样说了!的意思。

顺便说一下,抽象方法只能在抽象类中定义。如果在Cat类中不实现该方法,则给Cat类加上abstract修饰符是一个逃避办法。

我写得有点夸张。

参考3 第429页。

并不是说代码会自动出现。只是编译器只是按照super()所写的方式编译代码而已。

首先解释为什么要执行超类的构造函数。如果构造函数是一个方法,用于生成该类,则在生成子类之前,必须要准备好该基础类(即超类),这样理解吧。

由于Java编译器会检查即将执行的方法是否存在,所以会被视为编译错误。

参考2 第416页。

广告
将在 10 秒后关闭
bannerAds