4.2.扩展
\(4.2\)扩展
1.扩展的引入
假设我们想实现\(RotatingSLList\),它具有和\(SLList\)完全相同的方法,但增添了一个方法rotateRight
,这个方法将链表的最后一个元素添加到链表前端。
为了继承\(SLList\)的方法,我们可以使用\(extend\)关键词: 1
public class RotatingSLList<Item> extends SLList<Item>
这里,\(RotatingSLList\)和\(SLList\)间也是\(is-a\)关系,\(extends\)关键词允许我们保留\(SLList\)的原始功能,同时允许我们进行修改并添加其他的功能(\(functionality\))。
使用\(extends\)语句后,我们便可以借助\(SLList\)中的方法来实现rotateRight
:
1
2
3
4public void rotateRight() {
Item x = removeLast();
addFirst(x);
}
在\(extend\)关键词下,子类继承了父类的大部分成员:
- 所有的实例和静态变量
- 所有的方法
- 所有的嵌套类
当我们要覆盖所继承的方法时,使用@Override
即可。例如下面\(VengefulSLList\)的实现:它的removeLast
方法在删去末尾元素后会将其添加到前端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class VengefulSLList<Item> extends SLList<Item> {
SLList<Item> deletedItems;
public VengefulSLList() {
deletedItems = new SLList<Item>();
}
public Item removeLast() {
Item x = super.removeLast();
deletedItems.addLast(x);
return x;
}
/** Prints deleted items. */
public void printLostItems() {
deletedItems.print();
}
}
2.关于构造方法的继承
虽然子类会继承父类的大部分成员,但是子类无法继承父类的构造方法(\(constructor\))。同时,\(Java\)要求所有的构造函数必须从对一个主类(\(superclass\))的构造方式的调用开始。
以下面的两个类为例: 1
2public class Human {...}
public class TA extends Human {...}
由\(extends\)语句,\(TA\)会继承\(Human\)的属性与行为。
如果我们运行下面的代码: 1
TA Christine = new TA();
那么首先,一个\(Human\)要先被创建,这样\(Human\)才能把自己的特性传递给\(TA\),如果没有创建\(Human\)就创建\(TA\)显然是错误的。
因此,在创建一个子类时,我们可以直接先用\(super\)关键词,调用主类的构造方法:
1
2
3
4public VengefulSLList() {
super();
deletedItems = new SLList<Item>();
}
或者,如果我们不这么做,\(Java\)会自动调用主类的无参数(\(non-argument\))构造方法。
但有时候,如果不直接调用父类的构造方法,\(Java\)的自动调用的方法可能不是我们想要的。例如我们有一个包含一个参数的单参数构造方法,\(Java\)自动调用的无参数构造方法会让我们传入的参数无效!这时,我们就必须直接调用我们需要的构造方式:
1
2
3
4public VengefulSLList(Item x) {
super(x);
deletedItems = new SLList<Item>();
}
3.对象类
\(Java\)中的所有类都是对象类(\(object\;class\))的子类(\(descendant\)),或者对象类的\(extends\)。不管一个类有没有显性的\(extends\)关键词,它都隐性地继承了对象类。以前面为例:
- \(VengefulSLList\)是\(SLList\)的显式\(extends\)。
- \(SLList\)隐性继承了对象类。
对象类提供了很多所有对象都应遵循的操作,如.equal(Object obj)
、.hashCode()
等。
4.封装
我们可以通过阶层抽象(抽象屏障)来减小程序的复杂性(\(complexity\)),以及“为准备修改而编程”的概念。这是围绕着程序应该被建造为模块化的、可内部改变(\(interchangeable\))的片段,它们可以互相交换、并且不会破坏原有的系统。同时,把他人不需要的信息隐藏起来也是减小程序复杂性的重要手段。
封装的根源即存在于将信息对外隐藏的概念。在计算机科学中,一个模块可以被定义为一系列方法,它们作为一个整体工作来解决问题。当这个模块的实现细节都被隐藏起来、只能通过接口与其进行交互,这样的模块就是已封装的(\(encapsulated\))。
在理想状态下,用户不应观察到它们所使用的数据结构的内部工作原理。而\(Java\)的\(private\)关键词就很好地建立了抽象屏障。
5.类型错误、编译时类型
我们考虑下面的程序: 1
2
3
4
5
6
7
8
9
10public static void main(String[] args) {
VengefulSLList<Integer> vsl = new VengefulSLList<Integer>(9);
SLList<Integer> sl = vsl;
sl.addLast(50);
sl.removeLast();
sl.printLostItems();
VengefulSLList<Integer> vsl2 = sl;
}
1 | VengefulSLList<Integer> vsl = new VengefulSLList<Integer>(9); |
程序的前两行没有问题,因为\(VengefulSLList\)\(is-a\)\(SLList\),我们可以把\(VengefulSLList\)放入\(SLList\)的内存盒子中。 1
2sl.addLast(50);
sl.removeLast();
这两行也没有问题,根据前面的讲解,第一个addLast
会调用\(SLList\)的方法,而第二个removeLast
会调用\(VengefulSLList\)的方法。
1 | sl.printLostItems(); |
但是这一行程序无法编译,因为\(SLList\)及其主类都没有printLostItems
这一方法,即使\(sl\)的运行类型是\(VengefulSLList\)依然不行。
1
VengefulSLList<Integer> vsl2 = sl;
同样地,这一行程序也会编译错误。因为\(SLList\)并不\(is-a\;\)\(VengefulSLList\)。
这两例说明,编译器只支持编译时(\(compile-time\))的类型所包含的方法。
除了表达式,对方法的调用也会有编译时类型,即该方法声明时的类型。例如下面的程序:
1
2
3
4
5
6Poodle frank = new Poodle("Frank", 5);
Poodle frankJr = new Poodle("Frank Jr.", 15);
Dog largerDog = maxDog(frank, frankJr);
Poodle largerPoodle = maxDog(frank, frankJr);
//does not compile! RHS has compile-time type Dog1
2Poodle largerPoodle = maxDog(frank, frankJr);
//does not compile! RHS has compile-time type Dog
该行代码也会编译错误,因为maxDog
的编译时类型为\(Dog\)。
6.类型铸造\(casting\)
在\(Java\)中,我们可以告诉编译器某个表达式有特定的编译时类型,这样可以实现类型的“转换”。
例如,我们可以对上面的代码进行如下更改: 1
2Poodle largerPoodle = (Poodle) maxDog(frank, frankJr);
// compiles! Right hand side has compile-time type Poodle after casting
但是,对两个毫不相干的类进行类型铸造,会导致ClassCastException
的错误。
7.高阶函数
在\(Java\)中,我们可以利用接口实现高阶函数。
先在接口中定义一个函数: 1
2
3public interface IntUnaryFunction {
int apply(int x);
}
然后我们写一个类来实现接口中指定的函数: 1
2
3
4
5
6public class TenX implements IntUnaryFunction {
/* Returns ten times the argument. */
public int apply(int x) {
return 10 * x;
}
}
然后,我们就可以写出高阶函数的表达式了: 1
2
3public static int do_twice(IntUnaryFunction f, int x) {
return f.apply(f.apply(x));
}
在调用时,我们创造一个新的\(tenX\)实例,然后传入参数\(x\)即可: 1
System.out.println(do_twice(new TenX(), 2));
函数式接口允许我们以优雅的方式传递函数,我们可以通过接口定义函数的签名与行为,然后通过实现接口的类来提供具体的函数。
8.summar(copy by origin text)
VengefulSLList extends SLList means VengefulSLList "is-an" SLList, and inherits all of SLList's members:
- Variables, methods nested classes
- Not constructors Subclass constructors must invoke superclass constructor first. The super keyword can be used to invoke overridden superclass methods and constructors.
Invocation of overridden methods follows two simple rules:
- Compiler plays it safe and only allows us to do things according to the static type.
- For overridden methods (not overloaded methods), the actual method invoked is based on the dynamic type of the invoking expression
- Can use casting to overrule compiler type checking.