1.extends
前面我们学习的继承是从interface继承,使用implements关键字来定义与接口的层次关系,现在我们希望可以继承class,使用extends关键字定义类的层次关系。
比如现在有一个class RotatingSLList,是继承class SLList
我们可以在class声明中设置这种继承关系,使用extends关键字如下:
public class RotatingSLList- extends SLList
即定义了"is-a"的关系
该RotatingSLList除了能使用SLList的全部method之外,其额外作用是将元素右旋(元素之间顺序不变):
比如[5 10 15 20],以20为枢轴,右旋其左边所有元素后变成[20 5 10 15]实现方法:
public void rotateRight() {
Item x = removeLast();
addFirst(x);
}
通过使用extends关键字,子类继承父类的所有成员。“成员”包括:
- 所有实例和静态变量
- 所有方法method
- 所有嵌套类
注意构造函数不是继承的,子类不能直接访问父类的私有成员(private)。
除此之外,子类还可以自定义一些其他的method,变量等等,也可以Override属于父类的method
super关键字
通过super关键字,可以让子类使用父类中的方法,使用 super. 访问,比如某子类使用并Override SLList中的removeLast()方法:
@Override
public Item removeLast() {
Item x = super.removeLast();
deletedItems.addLast(x);
return x;
}
构造函数非继承
正如我们前面提到的,子类继承父类,其中包括实例和静态变量,方法和嵌套类的所有成员,但不包括构造函数。当我们给子类写构造函数初始化时,需要先考虑父类的构造函数,一个形象的例子是,假设我们有两个类:
public class Human {...}
public class TA extends Human {...}
如果我们运行下面的构造函数:
TA(){
somebody = new TA();
}
那么首先必须创造一个人类。然后该人类才可以被赋予 TA 的品质。如果不先创建人类,就构建 TA 则毫无意义。
因此,子类在构造函数初始化之前需要先调用父类的构造函数,使用super():
TA() {
super();
somebody = new TA();
}
假设你在子类构造函数初始化的时候并没有加super(),Java会隐式地帮你自动先调用父类的构造函数,但是它只会调用无参数的版本,比如:
public VengefulSLList(Item x) {
deletedItems = new SLList- ();
}
尽管子类的构造函数是含参数版本,由于没有调用super(x),Java仍会自动调用父类的无参数版的构造函数,要想正确调用父类含参的构造函数:
public VengefulSLList(Item x) {
super(x);
deletedItems = new SLList- ();
}
请注意:
Java不允许使用 super.super,请查看this link
Java 中的每个class都是 Object class或extends Object class的后代。也就是说每个类都继承了Object class:
Class Object is the root of the class hierarchy. Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.
public class A extends Object extends B(){......}
因此在任何类中都可以调用Object class中存在的一些method:
.equals(Object obj), .hashCode(), and toString()
see more detaile
但是interface 并没有 extends Object class
see more detail
2.封装
(本段是精校Google翻译)
封装是面向对象编程的基本原则之一,也是程序员用来抵御最大敌人--复杂性 的方法之一。管理复杂性是我们在编写大型程序时必须面临的主要挑战之一。
对抗复杂性的一些工具主要包括层次抽象(抽象障碍!)和一个被称为“设计变革”的概念。围绕这个想法,程序应该构建成模块化的、可交互的部分,即在不破坏系统的情况下与外界进行交换。此外,隐藏其他人不需要的信息是管理大型系统时的另一种基本方法。
封装的根源在于“向外部隐藏信息”的概念。以细胞来解释封装,细胞的内部结构可能极其复杂,由染色体、线粒体、核糖体等组成,但它却完全封装在一个模块中——抽象掉了内部的复杂性。
在计算机科学术语中,一个模块可以定义为一系列方法,它们作为一个整体协同工作以执行一个任务或一组相关任务。这可能类似于表示List的class。现在,如果模块的实现细节在内部被隐藏,并且与其交互的唯一方法是通过接口文档,那么该模块则被称为封装。
3.Implementation Inheritance如何打破封装
假设我们有一个封装好的Dog interface,包含:
- bark()
- barkMany()
其函数实现如下图所示,现在有一个子类VerboseDog,通过implementation继承Dog,并Override barkMany()函数:
现在调用VerboseDog的barkMany(),步骤是:
- VerboseDog d = new VerboseDog(); complier类型(静态类型)与runtime类型(动态类型)均是VerboseDog;
- 考虑Dynamic selection(出现Override均考虑Dynamic Selection),由于此时的runtime类型是VerboseDog,调用子类Override的BarkMany(3)
- -->调用bark(),此时的runtime类型是VerboseDog,准备调用子类的Override bark(),由于子类中没有bark()方法,使用继承父类的bark()方法
- 打印三次 bark
假设管理者改变了Dog类内部的函数实现,但是在外部看来,其函数作用与原来仍相同:
在这种情况下,仍然按照之前的步骤调用子类的barkMany():
- VerboseDog d = new VerboseDog(); complier类型(静态类型)与runtime类型(动态类型)均是VerboseDog;
- 考虑Dynamic selection,由于此时的runtime类型是VerboseDog,调用子类Override的BarkMany(3)
- -->调用bark(),此时的runtime类型是VerboseDog,准备调用子类的Override bark(),由于子类中没有bark()方法,使用继承父类的bark()方法
- 问题的关键来了,此时父类的bark()是
public void bark() {
barkMany(1);
}
也就是调用父类的bark()--->调用barkMany(1),由于此时的runtime type是VerboseDog,barkMany(1)实际是要调用子类的Override barkMany(),其中1作为参数传入子类的barkMany()
@Override
public void barkMany(int N) {
System.out.println("As a dog, I say: ");
for (int i = 0; i < N; i += 1) {
bark();
}
}
然后子类的barkMany()调用父类的bark(),父类的bark()又再次调用子类的barkMany()......
从而造成无限循环!