1. Intro and iterfaces
1.1 The Problem
回想我们之前写过的两个类SLList
和AList
,如果你仔细观看它们的文档的话,会发现他们非常相似,事实上,他们支持的所有方法全都一样。
下面我们来设计一个类WordUtils
,它包含了一些可以处理一系列字符串的方法,其中的一个方法是找到SLList中最长的字符串
,下面是这个方法的实现:
public static String longest(SLList<String> list) {
int maxDex = 0;
for (int i = 0; i < list.size(); i += 1) {
String longestString = list.get(maxDex);
String thisString = list.get(i);
if (thisString.length() > longestString.length()) {
maxDex = i;
}
}
return list.get(maxDex);
}
那么,这个方法如何能让AList
使用呢?我们只需要改一下函数签名:
public static String longest(AList<String> list)
当然,在实现的过程中,我们发现大量的代码重复,并且我们要做修改时,必须更改两个版本,要增加时,同样也要增加两个几乎完全一样的版本。
1.2 Hypernyms, Hyponyms, and Interface Inheritance
我们可以这样表示几个类之间的关系:
其中类List61B
是一个接口(Interface)
,它是一种抽象,规定了一个链表必须具备哪些功能,但没有提供任何实现。下面是接口List61B:
public interface List61B<Item> {
public void addFirst(Item x);
public void add Last(Item y);
public Item getFirst();
public Item getLast();
public Item removeLast();
public Item get(int i);
public void insert(Item x, int position);
public int size();
}
然后,我们的子类需要实现接口:
public class AList<Item> implements List61B<Item>{...}
而且可以重写接口的方法:
@Override
public void addFirst(Item x) {
insert(x, 0);
}
关于接口的细节将不再记录,这里主要讨论与数据结构的设计相关的内容。
2. Extends
我们用inplements
关键字来定义接口与类之间的上下级关系,同样,用extends
可以定义类之间的上下级关系。
假定我们要建立一个类RotatingSLList
,除了和SLList
相同的方法,它还具备一个rotateRight
方法将链表的最后一项移动到第一项。
一种方法是将SLList
的所有方法复制一份,再新增rotateRight
。显然,这增加了代码的重复率,会导致很多问题。这时,我们就可以使用extends
关键字:
public class RotatingSLList<Item> extends SLList<Item>
于是,我们就可以在RotatingSLList
类中增加新的方法:
public void rotateRight() {
Item x = removeLast();
addFirst(x);
}
现在,类之间的关系变成了下面这样:
为了介绍super
关键字,我们不得不再引入一个类——VengefulSLList
,它同样继承自SLList
,增加的功能是保存remove
方法删除掉的元素。在这里直接给出它的实现:
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();
}
}
在重写的removeLast
方法中,为了调用父类的该方法,使用super
关键字。
3. Type Checking and Casting
初学Java的时候类型检查
和类型强制转换
是件很烦的事情,所以在这里总结一下。
3.1 Type Checking
我们先来看一段代码,看的时候思考两件事情:
- 这一行导致编译出错了吗?
- 哪个方法使用了它的动态类型?
首先我们知道一个初始化或赋值表达式的左边相当于一个盒子,在栈中拥有自己的内存区域;等式右边则储存大量数据,实际上是在堆中开辟了一块空间。编译时期是以静态类型为准,运行时是以动态类型为准。接下来让我们一行一行的分析代码:
VengefulSLList<Integer> vsl = new VengefulSLList<Integer>(9);
SLList<Integer> sl = vsl;
上面这两行成功编译,因为sl
是vsl
的父类,父类的“盒子”可以容纳子类在堆中的数据。
sl.addLast(50);
sl.removeLast();
这两行也可以成功编译,addLast
正常运行,因为vsl
并没有重写此方法。然而,sl
在调用remove
方法时,实际上调用的是重写的方法,也就是说此时sl的运行时类型变为它的动态类型VengefulSLList
。
sl.printLostItems();
这行会报编译错误,记住编译时变量是静态类型
,编译器发现sl的静态类型SLList
中并没有printLostItems
方法,所以报错。
VengefulSLList<Integer> vsl2 = sl;
这行同样会报编译错误,此时以静态类型为主,左边的静态类型是VengefulSLList
,右边的静态类型是SLList
,子类类型的“盒子”不能容纳父类类型。
在初始化表达式中,我们使用new
操作符时,分析也是如此。
SLList<Integer> sl = new VengefulSLList<Integer>();
编译时看静态类型,左边是父类类型,右边是子类类型,所以正常编译,只不过运行时,sl转换成动态类型VengefulSLList
。
VengefulSLList<Integer> vsl = new SLList<Integer>();
父类类型为子类类型赋值,编译报错。
下面考虑一个综合性的问题:
public static Dog maxDog(Dog d1, Dog d2) { ... }
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
最后一步maxDog
返回父类类型,却用子类类型Poodle
去接收,所以会报编译错误。
总结一下:
- Java中父类类型的变量的动态类型可以是子类类型,而子类类型变量的动态类型只能是子类类型本身。
- 子类类型可以赋值给父类类型,而父类类型不能赋值给子类类型(除非强制转换,下面讲)。
- 编译时期看静态类型,运行时期看动态类型。
3.2 Casting
编译时期,可以使用强制类型转换
来使父类类型复制给子类类型。
Poodle largerPoodle = (Poodle) maxDog(frank, frankJr);
// compiles! Right hand side has compile-time type Poodle after casting
不过,强制类型转换很强大,也很危险,比如说下面的例子:
Poodle frank = new Poodle("Frank", 5);
Malamute frankSr = new Malamute("Frank Sr.", 100);
Poodle largerPoodle = (Poodle) maxDog(frank, frankSr); // runtime exception!
上面的代码编译正常,可是最后一行运行后应该返回Malamute
类型,却使用Poodle
类型接收,会抛出一个ClassCastException
异常。
4. Subtype Polymorphism
下面讨论一个问题,假设我们要写一个通用的求最大值函数max()
,用于求不同类型的对象之间的最大值,我们知道Java的比较符局限性很大,比如不能用于字符串的比较等,那么我们该如何实现呢?
public interface OurComparable {
// return -1 if this < o.
// return 0 if this equals o.
// return 1 if this > o.
public int compareTo(Object o);
}
我们定义了一个接口,如果有需要进行比较操作的类,就实现它。比如我们可以定义一个dog
类:
public class Dog implements OurComparable {
private String name;
private int size;
public Dog(String n, int s) {
name = n;
size = s;
}
public void bark() {
System.out.println(name + " says: bark");
}
public int compareTo(Object o) {
Dog uddaDog = (Dog) o;
if (this.size < uddaDog.size) {
return -1;
} else if (this.size == uddaDog.size) {
return 0;
}
return 1;
}
}
在这里,求dog之间的最大值在于比较他们的size,可能另一个求cat类的最大值在于比较cat的length。通用的max函数实现的难处在于不同对象之间如何比较
,这也是我们引入OurComparable
接口的原因。也是在这个接口的基础上,我们才可以写出具有一般性质的max
方法:
public static OurComparable max(OurComparable[] items) {
int maxDex = 0;
for (int i = 0; i < items.length; i += 1) {
int cmp = items[i].compareTo(items[maxDex]);
if (cmp > 0) {
maxDex = i;
}
}
return items[maxDex];
}
max
方法规定传入参数的类型必须有一个compareTo
方法,它将借助这个方法来找到一堆对象中的最大值。
在dog
类中,判断谁最”大“的标准就是谁的size最大,所以它的compareTo
方法是这样的:
public int compareTo(Object o) {
Dog uddaDog = (Dog) o;
return this.size - uddaDog.size;
}
在这里,我又深深的感受到了接口的强大之处,我也从未见识过这么精妙的设计。不过,这仍不是完美的,首先用object
作为参数就有一些缺陷,而且我们再思考,一些基本数据类型来调用max
方法呢?我们难不成要改动它们内部的方法?
事实上Java中已经定义了一个Comparable
接口,它和我们定义的接口十分相似:
它避免了Object作为参数类型,这时dog的实现变成了这样:
public class Dog implements Comparable<Dog> {
...
public int compareTo(Dog uddaDog) {
return this.size - uddaDog.size;
}
}
总结一下,Java中接口提供给了我们一个回调
的功能,有时候,一个函数需要另一个函数的帮助才能完成自己的任务(就像max需要compareTo),回调函数实际上就是在max定义后再定义的辅助函数。
很多语言中都有这个思想,比如说比较函数sort()
,最开始语言设计者要设计一个可以排序任何类型的函数(跟max同一个想法),不过不同类型的比较方法不同,而这些比较方法恰恰是使用者在自定义一种类型之后才会考虑的,所以sort()
函数就会提供一个回调函数,让使用者将自己的辅助函数写入其中,从而使sort()函数奏效。
比如说JavaScript
中的sort(),如果需要降序,则可以在它的回调函数中写上比较规则:
var arr = [4,3,6,5,7,2,1];
arr.sort(function(a,b){
return b-a;
});
再比如C++STL
中的sort()函数,我们可以为它添加函数或仿函数来进行降序排序,下面的compare
实际上就是STL sort()的回调函数。
bool compare(int x,int y)
{
return x<y?true:false;
}
int main()
{
int arr[5] = {3,2,5,8,4};
sort(arr,arr+5,compare);
return 0;
所以,不同语言都具备回调这一特性,只不过Java是用接口实现的而已。