软件构造复习五
接口
Java中的interface(接口)是一种表示抽象数据类型的好方法。接口中是一连串的方法标识,但是没有方法体(定义)。如果想要写一个类来实现接口,我们必须给类加上implements关键字,并且在类内部提供接口中方法的定义。所以接口+实现类也是Java中定义抽象数据类型的一种方法。
这种做法的一个优点就是接口只为使用者提供“契约”(contract),而使用者只需要读懂这个接口即可使用该ADT,他也不需要依赖ADT特定的实现/表示,因为实例化的变量不能放在接口中(具体实现被分离在另外的类中)。
接口的另一个优点就是它允许了一种抽象类型能够有多种实现/表示,即一个接口可以有多个实现类而当一个类型只用一个类来实现时,我们很难改变它的内部表示。例如之前阅读中的 MyString 这个例子,我们对 MyString 实现了两种表示方法,但是这两个类就不能同时存在于一个程序中。
Java的静态检查会发现没有实现接口的错误,例如,如果程序员忘记实现接口中的某一个方法或者返回了一个错误的类型,编译器就会在编译期报错。不幸的是,编译器不会去检查我们的方法是否遵循了接口中的文档注释。
子类型
回忆一下,我们之前说过类型就是值的集合。Java中的 List 类型是通过接口定义的,如果我们想一下List所有的可能值,它们都不是List对象:我们不能通过接口实例化对象——这些值都是 ArrayList 对象, 或 LinkedList 对象,或者是其他List实现类的对象。我们说,一个子类型就是父类型的子集,正如 ArrayList 和 LinkedList是List的子类型一样。
“B是A的子类型”就意味着“每一个B都是A”,换句话说,“每一个B都满足了A的规格说明”。
这也意味着B的规格说明至少强于A的规格说明。当我们声明一个接口的实现类时,编译器会尝试做这样的检查:它会检查类是否全部实现了接口中规定的函数,并且检查这些函数的标识是否对的上。
但是编译器不会检查我们是否通过其他形式弱化了规格说明:例如强化了某个方法输入的前置条件,或弱化了接口对于用户的保证(后置条件)。如果你在Java中定义了一个子类型——我们这里是实现接口——你必须要确保子类型的规格说明至少要比父类型强。
泛型 Set
Java中的聚合类型为“将接口和实现分离”提供了很好的例子。
现在我们来思考一下java聚合类型中的Set 。Set是一个用来表示有着有限元素E的集合。这里是Set的一个简化的接口:
/** A mutable set.
* @param <E> type of elements in the set */
public interface Set<E> {
Set 是一个泛型类型(generic type):这种类型的规格说明中用一个占位符(以后会被作为参数输入)表示具体类型,而不是分开为不同类型例如 Set, Set, 进行说明。我们只需要设计实现一个 Set.
现在我们分别实现/声明这个ADT的各个操作,从创建者开始:
// example creator operation
/** Make an empty set.
* @param <E> type of elements in the set
* @return a new set instance, initially empty */
public static <E> Set<E> make() { ... }
这里的make是作为一个静态工厂方法实现的。使用者会像这样调用它:Set strings = Set.make(); ,而编译器也会知道新的Set会是一个包含String对象元素的集合。(注意我们将写在函数标识前面,因为make是一个静态方法,而是它的泛型类型)。
// example observer operations
/** Get size of the set.
* @return the number of elements in this set */
public int size();
/** Test for membership.
* @param e an element
* @return true iff this set contains e */
public boolean contains(E e);
接下来我们声明两个观察者。注意到规格说明中的提示,这里不应该提到具体某一个实现的细节或者它们的标识,而规格说明也应该适用于所有SetADT的实现。
// example mutator operations
/** Modifies this set by adding e to the set.
* @param e element to add */
public void add(E e);
/** Modifies this set by removing e, if found.
* If e is not found in the set, has no effect.
* @param e element to remove */
public void remove(E e);
对于改造者的要求也和观察者一样,我们依然要在接口抽象的层次书写规格说明。
泛型接口的实现
假设现在我们要实现上面的 Set 接口。我们既可以使用一个非泛型的实现(用一个特定的类型替代E),也可以使用一个泛型实现(保留类型占位符)。
首先我们来看看泛型接口的非泛型实现:
在 抽象函数 & 表示不变量 我们实现了 CharSet类型,它被用来表示字符的集合。其中 CharSet1/2/3 这三种实现类都是 Set接口 的子类型,它们的声明如下:
public class CharSet implements Set<Character>
当在Set声明中提到 E时,Charset的实现将类型占位符E替换为了Character :
public interface Set<E> {
// ...
/**
* Test for membership.
* @param e an element
* @return true iff this set contains e
*/
public boolean contains(E e);
/**
* Modifies this set by adding e to the set.
* @param e element to add
*/
public void add(E e);
// ...
}
public class CharSet1 implements Set<Character> {
private String s = "";
// ...
@Override
public boolean contains(Character e) {
checkRep();
return s.indexOf(e) != -1;
}
@Override
public void add(Character e) {
if (!contains(e)) s += e;
checkRep();
}
// ...
}
CharSet1/2/3 的实现方法不适用于任意类型的元素,例如,由于它使用的是String成员, Set 这种集合就无法直接表示。
我们也可以在实现 Set 接口的时候不对E选择一个特定的类型。在这种情况下,我们会让使用者决定E到底是什么。
一个泛型实现只能依靠接口规格说明中对类型占位符的要求,我们会在以后的阅读中看到 HashSet 是如何依靠每一个类型都要求实现的操作来实现它自己的,因为它没办法依赖于特定类型的操作。
上一篇: java的正则表达式