MIT_6.031之接口与枚举

接口(Interface)

定义

  • 接口是 Java 中一种抽象数据类型,它定义了一组方法(不包含方法体),用于描述对象应该具有的行为。通过接口,我们可以将实现与抽象接口分离。

接口的特点

  • 方法声明:接口中包含方法的声明,但不包含实现(方法体)。
  • 实现类:类通过implements关键字实现接口,并提供方法的具体实现。
  • 契约:接口为使用者提供一个“契约”,使用者只需理解接口的规定,而不必依赖具体的实现。

接口示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** Represents an immutable set of elements of type E. */
public interface Set<E> {
/** Make an empty set */
public Set();

/** @return true if this set contains e as a member */
public boolean contains(E e);

/** @return a set which is the union of this and that */
public ArraySet<E> union(Set<E> that);
}

/** Implementation of Set<E>. */
public class ArraySet<E> implements Set<E> {
/** Make an empty set */
public ArraySet() { ... }

/** @return a set which is the union of this and that */
public ArraySet<E> union(Set<E> that) { ... }

/** Add e to this set */
public void add(E e) { ... }
}

注意事项

  • 构造方法:接口不能有构造方法(示例中的 public Set() 是错误的)。
  • 实现依赖:接口中的方法返回类型不应依赖于特定的实现类(示例中的 ArraySet 会导致依赖性问题)。
  • 不可变性:接口定义为不可变的数据类型,但实现类(如 ArraySet)可能包含可变方法(如 add),这与接口的定义相矛盾。

子类型(Subtyping)

  • 子类型是父类型的一个子集。例如,在 Java 中,ArrayListLinkedList 都是 List 接口的子类型。它们都遵循 List 的规范,但在内部实现和性能特性上有所不同。
  • 规格说明:在定义子类型时,必须遵循一些规则,这些规则有助于确保类型的安全性和一致性:
    • 前置条件(Preconditions): 子类型的方法的前置条件不能比父类型的更严格。换句话说,子类型不能要求额外的参数或条件,这样会限制父类型的使用。例如,如果父类型方法要求输入一个正整数,子类型也不能要求输入一个正整数。
    • 后置条件(Postconditions): 子类型的方法的后置条件不能比父类型的更宽松。即使子类型实现了父类型的方法,子类型仍然必须满足父类型所定义的输出条件。例如,若父类型的方法返回一个对象,子类型的方法不能返回 null,除非父类型方法的文档中允许返回 null。

示例

考虑以下代码示例,其中使用了 MyString 接口和 FastMyString 子类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface MyString {
// 接口方法的声明
String getValue();

// 静态工厂方法,创建 MyString 实例
public static MyString valueOf(boolean b) {
return new FastMyString(b);
}
}

public class FastMyString implements MyString {
private String value;

public FastMyString(boolean b) {
// 实现构造函数
this.value = String.valueOf(b); // 将 boolean 转换为 String
}

@Override
public String getValue() {
return value;
}
}

在上述代码中:

  1. 接口定义MyString 接口定义了一个方法 getValue(),并提供了一个静态工厂方法 valueOf() 来创建 MyString 实例。

  2. 子类型实现FastMyString 类实现了 MyString 接口,并定义了其特有的构造函数。注意,构造函数根据 boolean 参数来生成 String 类型的值。

破坏抽象层次的例子

1
MyString s = new FastMyString(true); // 破坏了抽象层次

在这个例子中,直接使用子类型 FastMyString 创建对象可能会破坏抽象层次,因为使用者可能会依赖具体的实现,而不是依赖于更抽象的 MyString 接口。这种做法会使得系统的可维护性和可扩展性降低,因为如果更改了 FastMyString 的实现,可能会影响到所有使用 FastMyString 的代码。

解决方案

为了解决这个问题,我们可以使用 静态工厂方法,如下面示例中所示:

  • 静态工厂方法:允许在接口中定义静态方法来创建子类型的实例。通过静态工厂方法,使用者只需要依赖于接口,而不需要直接与具体的实现交互,这保持了抽象层次并提高了代码的灵活性。

示例

1
2
3
4
5
public interface MyString {
public static MyString valueOf(boolean b) {
return new FastMyString(true);
}
}

泛型与接口

泛型接口

1
2
3
4
5
6
7
8
9
/** A mutable set.
* @param <E> type of elements in the set */
public interface Set<E> {
/** Creates an empty set */
public static <E> Set<E> make() { ... }
public void add(E e);
public void remove(E e);
public boolean contains(E e);
}

示例实现

  1. 非泛型实现
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CharSet1 implements Set<Character> {
private String s = "";

@Override
public boolean contains(Character e) {
return s.indexOf(e) != -1;
}

@Override
public void add(Character e) {
if (!contains(e)) s += e;
}
}
  1. 泛型实现
1
2
3
public class HashSet<E> implements Set<E> {
// ...
}

接口的优势

  • 文档化:接口为编译器和开发者提供重要文档。
  • 性能权衡:允许实现者根据性能需求选择不同的实现方案。
  • 多种视角:一个类可以实现多个接口,提供多种行为。
  • 实现选择权:使用者可以根据具体需求选择简单可靠的实现或性能优越但不稳定的实现。

枚举

枚举(Enum)在Java中是一种特殊的类,用于定义一组有限且不可变的常量。这种特性使得枚举在需要定义一小组相关常量时非常有用,如月份、星期几、方位等。以下是对Java枚举的详细解析,包括其特点、用法、优势和示例。

定义枚举

枚举通过enum关键字定义,并且可以包含构造函数、字段、方法等。例如,以下是定义一个月份枚举的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public enum Month {
JANUARY(31),
FEBRUARY(28),
MARCH(31),
APRIL(30),
MAY(31),
JUNE(30),
JULY(31),
AUGUST(31),
SEPTEMBER(30),
OCTOBER(31),
NOVEMBER(30),
DECEMBER(31);

private final int daysInMonth;

private Month(int daysInMonth) {
this.daysInMonth = daysInMonth;
}

public int getDaysInMonth(boolean isLeapYear) {
if (this == FEBRUARY && isLeapYear) {
return daysInMonth + 1;
}
return daysInMonth;
}
}

枚举的使用

枚举通常用于比较值、控制流(如switch语句)和保证值的唯一性。下面是一个使用枚举的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY
}

public class EnumTest {
Day day;

public EnumTest(Day day) {
this.day = day;
}

public void tellItLikeItIs() {
switch (day) {
case MONDAY:
System.out.println("Mondays are bad.");
break;
case FRIDAY:
System.out.println("Fridays are better.");
break;
case SATURDAY: case SUNDAY:
System.out.println("Weekends are best.");
break;
default:
System.out.println("Midweek days are so-so.");
break;
}
}

public static void main(String[] args) {
EnumTest firstDay = new EnumTest(Day.MONDAY);
firstDay.tellItLikeItIs();
EnumTest thirdDay = new EnumTest(Day.WEDNESDAY);
thirdDay.tellItLikeItIs();
EnumTest fifthDay = new EnumTest(Day.FRIDAY);
fifthDay.tellItLikeItIs();
EnumTest sixthDay = new EnumTest(Day.SATURDAY);
sixthDay.tellItLikeItIs();
EnumTest seventhDay = new EnumTest(Day.SUNDAY);
seventhDay.tellItLikeItIs();
}
}

输出

1
2
3
4
5
Mondays are bad.
Midweek days are so-so.
Fridays are better.
Weekends are best.
Weekends are best.

枚举的好处

  1. 唯一性与不可变性:每个枚举常量都是唯一且不可变的,这确保了不会创建多个相同的对象。

  2. 类型安全:编译时静态检查确保使用者只能使用定义的枚举常量,避免了错误的类型使用。

    1
    Month firstMonth = MONDAY; // 静态错误: MONDAY 的类型是 Day,而不是 Month
  3. 更清晰的代码:使用枚举代替魔法数字或字符串常量,可以使代码更具可读性和可维护性。

  4. 丰富的内置功能:Java枚举自动提供一些有用的方法,例如ordinal()(返回枚举值的序号)、name()(返回枚举值的字符串形式)和compareTo()(比较两个枚举值)。

  5. switch语句中的使用:Java枚举可以直接在switch语句中使用,这使得基于枚举的控制流更简洁。

内置操作

  • ordinal(): 返回枚举常量的索引值,从0开始。例如,JANUARY.ordinal()返回0。
  • compareTo(): 基于枚举常量的索引值来比较两个枚举常量。
  • name(): 返回当前枚举常量的名称字符串,功能与toString()相同。