CS61B sp2018笔记 | Inheritance, Implements

1. Intro and iterfaces

1.1 The Problem

  回想我们之前写过的两个类SLListAList,如果你仔细观看它们的文档的话,会发现他们非常相似,事实上,他们支持的所有方法全都一样。
  下面我们来设计一个类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

  我们先来看一段代码,看的时候思考两件事情:

  • 这一行导致编译出错了吗?
  • 哪个方法使用了它的动态类型?
    Java类型检查

  首先我们知道一个初始化或赋值表达式的左边相当于一个盒子,在栈中拥有自己的内存区域;等式右边则储存大量数据,实际上是在堆中开辟了一块空间。编译时期是以静态类型为准,运行时是以动态类型为准。接下来让我们一行一行的分析代码:

VengefulSLList<Integer> vsl = new VengefulSLList<Integer>(9);
SLList<Integer> sl = vsl;

  上面这两行成功编译,因为slvsl的父类,父类的“盒子”可以容纳子类在堆中的数据。

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接口,它和我们定义的接口十分相似:
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是用接口实现的而已。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

隐秀_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值