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
4
public 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
19
public class VengefulSLList<Item> extends SLList<Item> {
SLList<Item> deletedItems;

public VengefulSLList() {
deletedItems = new SLList<Item>();
}

@Override
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
2
public class Human {...}
public class TA extends Human {...}

  由\(extends\)语句,\(TA\)会继承\(Human\)的属性与行为。

  如果我们运行下面的代码:

1
TA Christine = new TA();

  那么首先,一个\(Human\)要先被创建,这样\(Human\)才能把自己的特性传递给\(TA\),如果没有创建\(Human\)就创建\(TA\)显然是错误的。

  因此,在创建一个子类时,我们可以直接先用\(super\)关键词,调用主类的构造方法:

1
2
3
4
public VengefulSLList() {
super();
deletedItems = new SLList<Item>();
}

  或者,如果我们不这么做,\(Java\)会自动调用主类的无参数(\(non-argument\))构造方法。

  但有时候,如果不直接调用父类的构造方法,\(Java\)的自动调用的方法可能不是我们想要的。例如我们有一个包含一个参数的单参数构造方法,\(Java\)自动调用的无参数构造方法会让我们传入的参数无效!这时,我们就必须直接调用我们需要的构造方式:

1
2
3
4
public 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
10
public 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
2
VengefulSLList<Integer> vsl = new VengefulSLList<Integer>(9);
SLList<Integer> sl = vsl;

  程序的前两行没有问题,因为\(VengefulSLList\)\(is-a\)\(SLList\),我们可以把\(VengefulSLList\)放入\(SLList\)的内存盒子中。

1
2
sl.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
6
Poodle 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 Dog
1
2
Poodle largerPoodle = maxDog(frank, frankJr); 
//does not compile! RHS has compile-time type Dog

  该行代码也会编译错误,因为maxDog的编译时类型为\(Dog\)

6.类型铸造\(casting\)

  在\(Java\)中,我们可以告诉编译器某个表达式有特定的编译时类型,这样可以实现类型的“转换”。

  例如,我们可以对上面的代码进行如下更改:

1
2
Poodle largerPoodle = (Poodle) maxDog(frank, frankJr); 
// compiles! Right hand side has compile-time type Poodle after casting

  但是,对两个毫不相干的类进行类型铸造,会导致ClassCastException的错误。

7.高阶函数

  在\(Java\)中,我们可以利用接口实现高阶函数。

  先在接口中定义一个函数:

1
2
3
public interface IntUnaryFunction {
int apply(int x);
}

  然后我们写一个类来实现接口中指定的函数:

1
2
3
4
5
6
public class TenX implements IntUnaryFunction {
/* Returns ten times the argument. */
public int apply(int x) {
return 10 * x;
}
}

  然后,我们就可以写出高阶函数的表达式了:

1
2
3
public 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.