接口和枚举
接口
avainterface是一种用于表达抽象数据类型的有用语言机制。Java中的接口是方法签名的列表,但没有方法主体。如果一个类在其子句中声明了接口,则实现一个接口implements,并为该接口的所有方法提供方法主体。因此,在Java中定义抽象数据类型的一种方法是作为接口,并将其实现作为实现该接口的类。
这种方法的一个优点是,该接口仅指定客户端的合同。客户程序员需要阅读该接口以了解ADT。客户端无法在ADT的代表上创建无意的依赖项,因为实例变量根本不能放在接口中。实施完全分开,完全分开,实现得井井有条。
另一个优点是,由于实现接口的不同类,抽象数据类型的多个不同表示形式可以共存于同一程序中。当抽象数据类型仅作为一个类表示而没有接口时,则很难具有多个表示形式。在Abstract Data Types的MyString示例中,MyString只有一个类。我们为探索了两种不同的表示形式MyString,但是在同一程序中不能同时具有ADT的两种表示形式。
Java的静态类型检查使编译器在实现ADT合同时遇到许多错误。例如,忽略必需的方法之一或为方法指定错误的返回类型是编译时错误。不幸的是,编译器没有检查我们的代码是否符合文档注释中所写方法的规范。
类型
回想一下,类型是一组值。JavaList类型由接口定义。如果我们考虑所有可能的List值,那么它们都不是List对象:我们无法创建接口的实例。取而代之的是,这些值是所有ArrayList对象或LinkedList一个或多个实现的其他类的对象List。甲亚型仅仅是的一个子集的超类型:ArrayList和LinkedList是的亚型List。
“ B是A的子类型”表示“每个B都是A”。在规范方面:“每个B都满足A的规范。”
这意味着,如果B的规范至少与A的规范一样强,则B只是A的子类型。当我们声明一个实现接口的类时,Java编译器会自动执行此要求的一部分:例如,它确保A中的每个方法都以兼容的类型签名出现在B中。如果没有实现A中声明的所有方法,则B类无法实现接口A。
但是编译器无法检查我们是否还没有以其他方式削弱规范:在方法的某些输入上增强先决条件,削弱后置条件,削弱接口抽象类型向客户端发布的保证。如果您在Java中声明一个子类型(实现接口是我们当前的重点),那么您必须确保该子类型的规范至少与父类型的规范一样强。
实例1:MyString
让我们重温一下MyString。使用接口而不是ADT的类,我们可以支持多种实现:
/** MyString represents an immutable sequence of characters. */
public interface MyString {
// We'll skip this creator operation for now
// /** @param b a boolean value
// * @return string representation of b, either "true" or "false" */
// public static MyString valueOf(boolean b) { ... }
/** @return number of characters in this string */
public int length();
/** @param i character position (requires 0 <= i < string length)
* @return character at position i */
public char charAt(int i);
/** Get the substring between start (inclusive) and end (exclusive).
* @param start starting index
* @param end ending index. Requires 0 <= start <= end <= string length.
* @return string consisting of charAt(start)...charAt(end-1) */
public MyString substring(int start, int end);
}
我们将跳过静态valueOf方法,并在一分钟后返回它。相反,让我们继续使用与Java中ADT概念工具箱不同的技术:构造函数。
这是我们的第一个实现
public class SimpleMyString implements MyString {
private char[] a;
/** Create a string representation of b, either "true" or "false".
* @param b a boolean value */
public SimpleMyString(boolean b) {
a = b ? new char[] { 't', 'r', 'u', 'e' }
: new char[] { 'f', 'a', 'l', 's', 'e' };
}
// private constructor, used internally by producer operations
private SimpleMyString(char[] a) {
this.a = a;
}
@Override public int length() { return a.length; }
@Override public char charAt(int i) { return a[i]; }
@Override public MyString substring(int start, int end) {
char[] subArray = new char[end - start];
System.arraycopy(this.a, start, subArray, 0, end - start);
return new SimpleMyString(subArray);
}
}
这是优化的实现:
public class FastMyString implements MyString {
private char[] a;
private int start;
private int end;
/** Create a string representation of b, either "true" or "false".
* @param b a boolean value */
public FastMyString(boolean b) {
a = b ? new char[] { 't', 'r', 'u', 'e' }
: new char[] { 'f', 'a', 'l', 's', 'e' };
start = 0;
end = a.length;
}
// private constructor, used internally by producer operations.
private FastMyString(char[] a, int start, int end) {
this.a = a;
this.start = start;
this.end = end;
}
@Override public int length() { return end - start; }
@Override public char charAt(int i) { return a[start + i]; }
@Override public MyString substring(int start, int end) {
return new FastMyString(this.a, this.start + start, this.end + end);
}
}
-
这些类比较的的实现MyString在抽象数据类型。请注意,以前在静态valueOf方法中出现的代码现在如何在构造函数中出现,并稍作更改以引用的rep
this。 -
另请注意的使用@Override。此注释告知编译器该方法必须与我们要实现的接口中的方法之一具有相同的签名。但是由于编译器已经检查了我们是否已经实现了所有接口方法,因此@Override此处的主要价值在于代码的读者:它告诉我们在接口中寻找该方法的规格。重复规范不会是DRY,但是什么也不说会使代码更难理解。
-
并注意,我们为生产者添加了一个私有的构造函数,substring(…)以用于创建类的新实例。它的参数是rep字段。我们以前不必编写此特殊的构造函数,因为默认情况下,当我们不声明任何其他构造函数时,Java将提供一个空的构造函数。添加采用的构造函数boolean
b意味着我们必须显式声明另一个构造函数以供生产者操作使用。
客户将如何使用此ADT?这是一个例子:
MyString s = new FastMyString(true);
System.out.println("The first character is: " + s.charAt(0));
该代码看起来与我们编写的使用Java集合类的代码非常相似:
List<String> s = new ArrayList<String>();
...
不幸的是,这种模式打破了我们为在抽象类型及其具体表示之间建立而努力的抽象障碍。客户必须知道具体表示形式类的名称。因为Java中的接口不能包含构造函数,所以它们必须直接调用具体类的构造函数之一。该构造函数的规范不会在接口中的任何位置出现,因此不能静态保证不同的实现甚至会提供相同的构造函数。
幸运的是,(从Java 8开始)接口被允许包含静态方法,因此我们可以在接口中将创建者操作实现valueOf为静态工厂方法MyString:
public interface MyString {
/** @param b a boolean value
* @return string representation of b, either "true" or "false" */
public static MyString valueOf(boolean b) {
return new FastMyString(true);
}
// ...
现在,客户端可以使用ADT而不会打破抽象障碍:
MyString s = MyString.valueOf(true);
System.out.println("The first character is: " + s.charAt(0));
实例2:通用集
Java的集合类提供了分离接口和实现的想法的一个很好的例子。
让我们以Java集合库中的ADT之一为例Set。 Set是某种其他类型的有限元素集的ADT E。这是Set接口的简化版本:
/** A mutable set.
* @param <E> type of elements in the set */
public interface Set<E> {
Set是泛型类型的一个示例:一种类型,其规范取决于稍后要填充的占位符类型。而不是写单独的规范和实现为Set,Set,等等,我们设计并实现了一个Set。
我们可以从创建者开始,将Java接口与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它是静态方法。它需要自己的泛型类型参数,与E我们在实例方法规范中使用的参数分开。)
// 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用特定类型替换的非通用实现,也可以编写一个保留占位符的通用实现。
通用接口,非通用实现。 让我们Set为特定类型实现E。
在“抽象函数和CharSet表示式不变式”中,我们研究了,它表示一组字符。的例如代码CharSet包括通用Set接口和各实施方式的CharSet1/ 2/3宣告:
public class CharSet implements Set<Character>
当接口提及占位符类型时E,CharSet实现将替换E为Character。例如:
通过所使用的表示CharSet1/ 2/3不适合用于表示套任意类型的元素。的String代表,例如,不能代表一个Set没有认真工作,以确定新的代表不变,并抽象函数来处理多位数。
通用接口,通用实现。 我们也可以实现通用Set接口,而无需选择类型E。在这种情况下,我们会将代码写成与客户将选择的实际类型无关的代码E。Java就是HashSet这样做的Set。其声明如下:
通用实现只能依赖接口规范中包含的占位符类型的详细信息。我们将在以后的阅读中看到如何HashSet依赖Java中每种类型都必须实现的方法—并且仅依赖于那些方法,因为它不能依赖以任何特定类型声明的方法。
为什么要有接口
接口在实际的Java代码中普遍使用。并非每个类都与一个接口相关联,但是有很多充分的理由将接口引入图片。
- 编译器和人类的文档。接口不仅可以帮助编译器捕获ADT实现的错误,而且对于人类而言,比起具体实现的代码而言,它更有用。这样的实现将ADT级别的类型和规范散布在实现细节中。
- 允许性能折衷。ADT的不同实现方式可以提供具有非常不同的性能特征的方法。不同的应用程序在不同的选择下可能会更好地工作,但是我们希望以独立于表示的方式对这些应用程序进行编码。从正确性的角度来看,应该可以通过简单的本地化代码更改来添加任何新的密钥ADT实现。
- **规范故意欠缺的方法。**有限集的ADT在转换为列表时可能未指定元素顺序。某些实现可能使用较慢的方法实现,这些实现设法将集合表示保持在某种排序顺序中,从而允许快速转换为排序列表。其他实现可能通过不费心地支持转换为已排序列表来使许多方法更快。
- 一类的多重观点。Java类可以实现多个接口。例如,显示下拉列表的用户界面窗口小部件自然既可以作为窗口小部件又可以作为列表来查看。该小部件的类可以实现两个接口。换句话说,我们不会因为选择不同的数据结构而多次实现ADT。我们可能会进行多种实现,因为除其他有用的观点外,许多不同种类的对象也可能被视为ADT的特殊情况。
- 越来越不值得信赖的实现。多次实现一个接口的另一个原因可能是,您很容易构建一个您认为正确的简单实现,而您可以更努力地构建一个更可能包含错误的高级版本。您可以根据被错误咬的严重程度来选择应用程序的实现。
枚举
有时,ADT具有一小组有限的不可变值,例如:
- 一年中的月份:一月,二月…
- 星期几:星期一,星期二……
- 指南针点:北,南,东,西
- 线图中的线帽:对接,圆形,正方形
这样的类型可以用作更复杂类型的一部分(如DateTime或Latitude),也可以用作改变方法行为的参数(如drawLine)。
当值的集合很小且有限时,将所有值定义为命名常量(称为枚举)是有意义的。Java具有enum使之方便的结构:
public enum Month { JANUARY, FEBRUARY, MARCH, ..., DECEMBER };
这enum定义了一种新类型的名称,Month在相同的方式,class并interface确定新的类型名称。它还定义了一组命名值,因为它们实际上是public static final常量,所以我们用大写形式编写它们。因此,您现在可以编写:
Month thisMonth = MARCH;
这个想法称为枚举,因为您明确列出了集合的元素,而Java为其分配了数字作为它们的默认rep值。
在枚举的最简单用例中,您唯一需要执行的操作是测试值之间的相等性:
if (day.equals(SATURDAY) || day.equals(SUNDAY)) {
System.out.println("It's the weekend");
}
您可能还会看到类似这样的代码,它使用==而不是equals():
if (day == SATURDAY || day == SUNDAY) {
System.out.println("It's the weekend");
}
如果将日期表示为a String,则此代码将非常不安全,因为它将测试两个表达式是否在相同的内存位置引用了同一对象,这对于两个任意字符串而言可能是正确的,也可能不是"Saturday"。这就是为什么我们总是equals()用于比较对象的原因。但是,使用枚举类型的优点之一是,有永远只能一个对象代表每个枚举值存储器,并没有办法为客户创造更多的(没有构造函数!)所以是一样好equals()了枚举。
从这种意义上讲,使用枚举感觉就像您在使用原始int常量。Java甚至支持在switch语句中使用它们(否则,它们仅允许原始整数类型,而不允许对象):
switch (direction) {
case NORTH: return "polar bears";
case SOUTH: return "penguins";
case EAST: return "elephants";
case WEST: return "llamas";
}
但是与int值不同,枚举具有更多的静态检查:
Month firstMonth = MONDAY; // static error: MONDAY has type DayOfWeek, not type Month
enum声明可以包含一个罐子的所有常规字段和方法class。因此,您可以为ADT定义其他操作,也可以定义自己的代表。这是一个具有代表,观察者和生产者的示例:
public enum Month {
// the values of the enumeration. Written as calls to the private constructor below.
JANUARY(31),
FEBRUARY(28),
MARCH(31),
APRIL(30),
MAY(31),
JUNE(30),
JULY(31),
AUGUST(31),
SEPTEMBER(30),
OCTOBER(31),
NOVEMBER(30),
DECEMBER(31);
// rep
private final int daysInMonth;
// enums also have an automatic, invisible rep field:
// private final int ordinal;
// which takes on values 0, 1, ... for each value in the enumeration.
// rep invariant:
// daysInMonth is the number of days in this month in a non-leap year
// abstraction function:
// AF(ordinal,daysInMonth) = the (ordinal+1)th month of the Gregorian calendar
// safety from rep exposure:
// all fields are private, final, and have immutable types
// Make a Month value. Not visible to clients, only used to
// initialize the constants above.
private Month(int daysInMonth) {
this.daysInMonth = daysInMonth;
}
/**
* @param isLeapYear true iff the year under consideration is a leap year
* @return number of days in this month in a normal year (if !isLeapYear)
* or leap year (if isLeapYear)
*/
public int getDaysInMonth(boolean isLeapYear) {
if (this == FEBRUARY && isLeapYear) {
return daysInMonth+1;
} else {
return daysInMonth;
}
}
/**
* @return first month of the semester after this month
*/
public Month nextSemester() {
switch (this) {
case JANUARY:
return FEBRUARY;
case FEBRUARY: // cases with no break or return
case MARCH: // fall through to the next case
case APRIL:
case MAY:
return JUNE;
case JUNE:
case JULY:
case AUGUST:
return SEPTEMBER;
case SEPTEMBER:
case OCTOBER:
case NOVEMBER:
case DECEMBER:
return JANUARY;
default:
throw new RuntimeException("can't get here");
}
}
}
所有enum类型还具有一些自动提供的操作,这些操作由定义Enum:
- ordinal()是该值在枚举中的索引,因此 JANUARY.ordinal()返回0。
- compareTo() 根据其序号比较两个值。
- name()以字符串形式JANUARY.name()返回值常量的名称,例如return “JANUARY”。
- toString()具有与相同的行为name()。
用Java实现ADT概念
概括
Java接口帮助我们将抽象数据类型的概念形式化为必须由一种类型支持的一组操作。
这有助于使我们的代码…
-
安全的错误。 ADT由其操作定义,而接口正是通过其操作来定义的。客户端使用接口类型时,静态检查可确保它们仅使用接口定义的方法。如果实现类公开了其他方法(或更糟的是具有可见的表示形式),则客户端不会偶然看到或依赖它们。当我们有多种数据类型的实现时,接口可以对方法签名进行静态检查。
-
容易明白。 客户和维护人员确切地知道在哪里可以找到ADT的规范。由于该接口不包含实例字段或实例方法的实现,因此将实现的细节保留在规范之外更为容易。
-
准备好进行更改。 通过添加实现接口的类,我们可以轻松地添加类型的新实现。如果我们避免使用构造函数支持静态工厂方法,则客户端只会看到该接口。这意味着我们可以切换客户端使用的实现类,而根本无需更改其代码。
Java枚举允许使用一小组有限的不可变值定义ADT。与特殊整数值或特殊字符串的老式替代方法相比,枚举有助于编写以下代码:
-
安全的错误。 静态检查可确保客户端不能使用有限集之外的值,也不能混淆两个不同的枚举类型。
-
容易明白。 命名常量不如整数常量那么神奇,并且命名类型比int或更好地说明自己String。
-
准备好进行更改。 枚举在这里没有特别的优势。