总结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
继承类的构造函数(意外地棘手)
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页。