麻省理工18年春軟件構造課程閱讀12“接口與枚舉”


本文內容來自MIT_6.031_sp18: Software Construction課程的Readings部分,采用CC BY-SA 4.0協議。

由於我們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,於是打算做一些翻譯工作,自己學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有的練習題沒有標准答案,所給出的“正確答案”為譯者所寫,有錯誤的地方還請指出。

(更新:從第10章開始只翻譯正確答案)




譯者:李秋豪 江家偉

審校:

V1.0 Sun Apr 8 13:29:19 CST 2018


本次課程的目標

本次課程的主題是接口:將抽象數據類型中的實現與抽象接口分離開,並在Java中運用interface強制這種分離。

在這次課程后,你應該能夠定義ADT的接口,並能夠寫出對應的實現類。


譯者注:本次閱讀少部分說法基於Javase8及以后的版本。參考:Java 8 Interface Changes – static method, default method


接口

Java中的interface(接口)是一種表示抽象數據類型的好方法。接口中是一連串的方法標識,但是沒有方法體(定義)。如果想要寫一個類來實現接口,我們必須給類加上implements關鍵字,並且在類內部提供接口中方法的定義。所以接口+實現類也是Java中定義抽象數據類型的一種方法。

這種做法的一個優點就是接口只為使用者提供“契約”(contract),而使用者只需要讀懂這個接口即可使用該ADT,他也不需要依賴ADT特定的實現/表示,因為實例化的變量不能放在接口中(具體實現被分離在另外的類中)。

接口的另一個優點就是它允許了一種抽象類型能夠有多種實現/表示,即一個接口可以有多個實現類(譯者注:一個類也可以同時實現多個接口)。而當一個類型只用一個類來實現時,我們很難改變它的內部表示。例如之前閱讀中的 MyString 這個例子,我們對 MyString 實現了兩種表示方法,但是這兩個類就不能同時存在於一個程序中。

Java的靜態檢查會發現沒有實現接口的錯誤,例如,如果程序員忘記實現接口中的某一個方法或者返回了一個錯誤的類型,編譯器就會在編譯期報錯。不幸的是,編譯器不會去檢查我們的方法是否遵循了接口中的文檔注釋。

關於定義接口的細節,請參考 Java Tutorials section on interfaces.

閱讀小練習

Java interfaces

思考下面這個Java接口和實現類,它們嘗試實現一個不可變的集合類型:

    /** Represents an immutable set of elements of type E. */
    public interface Set<E> {
        /** make an empty set */
A       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 */
B       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) { ... }
    }

下面關於 Set<E>ArraySet<E>的說法哪一個是正確的?

A 標號處有問題,因為接口不能有構造方法。--> True

The line labeled B is a problem because Set mentions ArraySet, but ArraySet also mentions Set, which is circular. --> False

B 標號處有問題,因為它沒有實現“表示獨立”。--> True

ArraySet 並沒有正確實現 Set,因為它缺失了 contains() 方法。--> True

ArraySet doesn’t correctly implement Set because it includes a method that Set doesn’t have. --> False

ArraySet 並沒有正確實現 Set,因為 ArraySet 是可變的,但是 Set 是不可變的。--> True


子類型

回憶一下,我們之前說過類型就是值的集合。Java中的 List 類型是通過接口定義的,如果我們想一下List所有的可能值,它們都不是List對象:我們不能通過接口實例化對象——這些值都是 ArrayList 對象, 或 LinkedList 對象,或者是其他List實現類的對象。我們說,一個子類型就是父類型的子集,正如 ArrayListLinkedListList的子類型一樣。

“B是A的子類型”就意味着“每一個B都是A”,換句話說,“每一個B都滿足了A的規格說明”。

這也意味着B的規格說明至少強於A的規格說明。當我們聲明一個接口的實現類時,編譯器會嘗試做這樣的檢查:它會檢查類是否全部實現了接口中規定的函數,並且檢查這些函數的標識是否對的上。

但是編譯器不會檢查我們是否通過其他形式弱化了規格說明:例如強化了某個方法輸入的前置條件,或弱化了接口對於用戶的保證(后置條件)。如果你在Java中定義了一個子類型——我們這里是實現接口——你必須要確保子類型的規格說明至少要比父類型強。

閱讀小練習

Immutable shapes

讓我們為矩形定義一個接口:

/** An immutable rectangle. */
public interface ImmutableRectangle {
    /** @return the width of this rectangle */
    public int getWidth();
    /** @return the height of this rectangle */
    public int getHeight();
}

而每一個正方形類型都是矩形類型:

/** An immutable square. */
public class ImmutableSquare {
    private final int side;
    /** Make a new side x side square. */
    public ImmutableSquare(int side) { this.side = side; }
    /** @return the width of this square */
    public int getWidth() { return side; }
    /** @return the height of this square */
    public int getHeight() { return side; }
}

ImmutableSquare.getWidth() 是否滿足了 ImmutableRectangle.getWidth()的規格說明? --> Yes

ImmutableSquare.getHeight() 是否滿足了 ImmutableRectangle.getHeight()的規格說明? -->Yes

ImmutableSquare 的規格說明是否滿足了(至少強於) ImmutableRectangle 的規格說明? --> Yes

Mutable shapes

/** A mutable rectangle. */
public interface MutableRectangle {
    // ... same methods as above ...
    /** Set this rectangle's dimensions to width x height. */
    public void setSize(int width, int height);
}

現在每一個正方形類型還是矩形類型嗎?

/** A mutable square. */
public class MutableSquare {
    private int side;
    // ... same constructor and methods as above ...
    // TODO implement setSize(..)
}

對於下面的每一個 MutableSquare.setSize(..) 實現,請判斷它是否合理:

/** Set this square's dimensions to width x height.
 *  Requires width = height. */
public void setSize(int width, int height) { ... }

--> No – stronger precondition

/** Set this square's dimensions to width x height.
 *  @throws BadSizeException if width != height */
public void setSize(int width, int height) throws BadSizeException { ... }

--> Specifications are incomparable

/** If width = height, set this square's dimensions to width x height.
 *  Otherwise, new dimensions are unspecified. */
public void setSize(int width, int height) { ... }

--> No – weaker postcondition

/** Set this square's dimensions to side x side. */
public void setSize(int side) { ... }

--> Specifications are incomparable


例子: 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 這個方法,用我們在“抽象數據類型”中學習到的知識去實現這個接口。

下面是我們的第一種實現類:

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);
    }
}
  • 與我們之前的實現相比,注意到之前的代碼中valueOf是靜態方法,但是在這里就不是了。而這里也使用了指向實例內部表示的this
  • 同時要注意到 @Override的使用,這個詞是通知編譯器這個方法必須和其父類中的某個方法的標識完全一樣(覆蓋)。但是由於實現接口時編譯器會自動檢查我們的實現方法是否遵循了接口中的方法標識,這里的 @Override 更多是一種文檔注釋,它告訴讀者這里的方法是為了實現某個接口,讀者應該去閱讀這個接口中的規格說明。同時,如果你沒有對實現類(子類型)的規格說明進行強化,這里就不需要再寫一遍規格說明了。(DRY原則)
  • 另外注意到我們添加了一個私有的構造方法,它是為 substring(..) 這樣的生產者服務的。它的參數是表示的域。我們之前並不需要寫出構造方法,因為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接口中不能包含構造方法,它們必須通過調用實現類的構造方法來獲取接口類型的對象,而接口中是不可能含有構造方法的規格說明的。另外,由於接口中沒有對構造方法進行說明,所以我們甚至無法保證不同的實現類會提供同樣的構造方法。

幸運的是,Java8以后允許為接口定義靜態方法,所以我們可以在接口MyString中通過靜態的工廠方法來實現創建者valueOf

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));

將實現完全英寸起來是一種“妥協”,因為有時候使用者會希望有對具體實現的選擇權利。這也是為什么Java庫中的ArrayListLinkedList“暴露”給了用戶,因為這兩個實現在 get()insert()這樣的操作中會有性能上的差別。

閱讀小練習

Code review

現在讓我們來審查以下 FastMyString實現,下面是對這個實現的一些批評,你認為哪一些是對的?

應該把抽象函數注釋出來 --> True

應該把表示不變量注釋出來 --> True

表示域應該使用關鍵詞 final 以便它們不能被重新改變索引 --> True

The private constructor should be public so clients can use it to construct their own arbitrary strings --> False

The charAt specification should not expose that the rep contains individual characters --> False

charAt 應該對於大於字符串長度的 i 有更好的處理 --> True


例子: 泛型 Set<E>

Java中的聚合類型為“將接口和實現分離”提供了很好的例子。

現在我們來思考一下java聚合類型中的SetSet是一個用來表示有着有限元素E的集合。這里是Set的一個簡化的接口:

/** A mutable set.
 *  @param <E> type of elements in the set */
public interface Set<E> {

Set 是一個泛型類型(generic type):這種類型的規格說明中用一個占位符(以后會被作為參數輸入)表示具體類型,而不是分開為不同類型例如 Set<String>, Set<Integer>, 進行說明。我們只需要設計實現一個 Set<E>.

現在我們分別實現/聲明這個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<String> strings = Set.make(); ,而編譯器也會知道新的Set會是一個包含String對象元素的集合。(注意我們將<E>寫在函數標識前面,因為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);

對於改造者的要求也和觀察者一樣,我們依然要在接口抽象的層次書寫規格說明。

閱讀參考:

閱讀小練習

Collection interfaces & implementations

假設下面的代碼都是逐次執行的,並且不能被編譯的代碼都會被注釋掉。

這里的代碼使用到了 Collections中的兩個方法,你可能需要閱讀一些參考。請為下面的問題回答出最合理的答案。

Set<String> set = new HashSet<String>();

set 現在指向: --> 一個HashSet對象

set = Collections.unmodifiableSet(set);

set 現在指向: --> 一個實現了Set接口的對象

set = Collections.singleton("glorp");

set 現在指向: --> 一個實現了Set接口的對象

set = new Set<String>();

set 現在指向: --> 這一行不能被編譯

List<String> list = set;

set 現在指向: --> 這一行不能被編譯


泛型接口的實現

假設現在我們要實現上面的 Set<E> 接口。我們既可以使用一個非泛型的實現(用一個特定的類型替代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<Integer> 這種集合就無法直接表示。

接着我們再來看看泛型接口的泛型實現:

我們也可以在實現 Set<E> 接口的時候不對E選擇一個特定的類型。在這種情況下,我們會讓使用者決定E到底是什么。例如,Java的 HashSet 就是這種實現,它的聲明像這樣:

public interface Set<E> {

    // ...
public class HashSet<E> implements Set<E> {

    // ...

一個泛型實現只能依靠接口規格說明中對類型占位符的要求,我們會在以后的閱讀中看到 HashSet 是如何依靠每一個類型都要求實現的操作來實現它自己的,因為它沒辦法依賴於特定類型的操作。


為什么要使用接口?

在Java代碼中,接口被用的很廣泛(但也不是所有類都是接口的實現),這里列出來了幾個使用接口的好處:

  • **接口對於編譯器和讀者來說都是重要的文檔:**接口不僅會幫助編譯器發現ADT實現過程中的錯誤,它也會幫助讀者更容易/快速的理解ADT的操作——因為接口將ADT抽象到了更高的層次,用戶不需要關心具體實現的各種方案。
  • **允許進行性能上的權衡:**接口使得ADT可以有不同的實現方案,而這些實現方案可能在不同環境下的性能或其他資源特性有很大差別。使用者可以根據自己的環境/需求選擇合適的實現方案。但是,在我們選擇特定的方案后,我們依舊要保持代碼的表示獨立性,即當ADT發生(內部)改變或更換實現方案后代碼依然能正常運行。
  • **通過未決定的規格說明給實現者以定義方法的自由:**例如,當把一個有限集合轉化為一個列表的時候,有一些實現可能是使用較慢的方法,但是它們確保這些元素在列表中是排好序的;而其他的實現可能是不管這些元素轉換后在列表中的排序,但是它們的速度更快。
  • **一個類具有多種“視角”:**在Java中,一個類可以同時實現多個接口,例如,一個能夠顯示列表的窗口部件就可能是一個同時實現了窗口和列表這兩個接口的類。這反映的是多種ADT特性同時存在的特殊情況。
  • **允許不同信任度的實現:**另一個多次實現一個接口的原因在於,你可以寫一個簡單但是非常可靠的實現,也可以寫一個很“炫”但是bug存在的幾率(穩定性)高一些的實現。而使用者可以根據實際情況選擇相應的方案。

閱讀小練習

假設你有一個有理數的類型,它現在是以類來表示的:

public class Rational {
    ...
}

現在你決定將 Rational 換成Java接口,同時定義了一個實現類IntFraction

public interface Rational {
    ...
}

public class IntFraction implements Rational {
    ...
}

對於下面之前 Rational 類中的代碼,請你判定它們對應的身份,以及應該出現在新的接口或者新的實現類中?

Interface + implementation 1

private int numerator;
private int denominator;

這段代碼是(選中所有正確答案):

  • 抽象函數
  • 創建者
  • 改造者
  • 觀察者
  • 生產者
  • (成員)表示
  • 表示不變量
  • 規格說明

它應該位於:

  • 接口

  • 實現類

  • 都有

Interface + implementation 2

//   denominator > 0
//   numerator/denominator is in reduced form

這段代碼是(選中所有正確答案):

  • 抽象函數
  • 創建者
  • 改造者
  • 觀察者
  • 生產者
  • (成員)表示
  • 表示不變量
  • 規格說明

它應該位於:

  • 接口
  • 實現類
  • 都有

Interface + implementation 3

//   AF(numerator, denominator) = numerator / denominator

這段代碼是(選中所有正確答案):

  • 抽象函數
  • 創建者
  • 改造者
  • 觀察者
  • 生產者
  • (成員)表示
  • 表示不變量
  • 規格說明

它應該位於:

  • 接口
  • 實現類
  • 都有

Interface + implementation 4

    /**
     * @param that another Rational
     * @return a Rational equal to (this / that)
     */

這段代碼是(選中所有正確答案):

  • 抽象函數
  • 創建者
  • 改造者
  • 觀察者
  • 生產者
  • (成員)表示
  • 表示不變量
  • 規格說明

它應該位於:

  • 接口
  • 實現類
  • 都有

Interface + implementation 5

    public boolean isZero()

這段代碼是(選中所有正確答案):

  • 抽象函數
  • 創建者
  • 改造者
  • 觀察者
  • 生產者
  • (成員)表示
  • 表示不變量
  • 規格說明

它應該位於:

  • 接口
  • 實現類
  • 都有

Interface + implementation 6

    return numer == 0;

這段代碼是(選中所有正確答案):

  • 抽象函數
  • 創建者
  • 改造者
  • 觀察者
  • 生產者
  • (成員)表示
  • 表示不變量
  • 規格說明

它應該位於:

  • 接口
  • 實現類
  • 都有

枚舉

有時候一個ADT的值域是一個很小的有限集,例如:

  • 一年中的月份: January, February, …
  • 一周中的天數: Monday, Tuesday, …
  • 方向: north, south, east, west
  • 畫線時的line caps : butt, round, square

這樣的類型往往會被用來組成更復雜的類型(例如DateTime或者Latitude),或者作為一個改某個方法的行為的參數使用(例如drawline)。

當值域很小且有限時,將所有的值定義為被命名的常量是有意義的,這被稱為枚舉(enumeration)。JAVA用enum使得枚舉變得方便:

public enum Month { JANUARY, FEBRUARY, MARCH, ..., DECEMBER };

這個enum定義類一種新的類型名,Month,這和使用class以及interface定義新類型名時是一樣的。它也定義了一個被命名的值的集合,由於這些值實際上是public static final,所以我們將這個集合中的每個值的每個字母都大寫。所以你可以這么寫:

Month thisMonth = MARCH;

這種思想被稱為枚舉,因為你顯式地列出了一個集合中的所有元素,並且JAVA為每個元素都分配了數字作為代表它們的值。

在枚舉類型最簡單的使用場景中,你需要的唯一操作是比較兩個值是否相等:

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");
}

如果使用String類型來表示天數,那么這個代碼是不安全的,因為==檢測兩邊的表達式是否引用的是同一個對象,對於任意的兩個字符串“Saturday”來說,這是不一定的。這也是為什么我們總是在比較兩個對象時使用equals()的原因。但是使用枚舉類型的好處之一就是:實際上只有一個對象來表示枚舉類型的每個取值,且用戶不可能創建更多的對象(沒有構造者方法!)所以對於枚舉類型來說,==equals()的效果是一樣的。

在這個意義上,使用枚舉就像使用原式的int常量一樣。JAVA甚至支持在switch語句中使用枚舉類型(switch在其他情況下只允許使用原式的整型,而不能是對象):

switch (direction) {
    case NORTH: return "polar bears";
    case SOUTH: return "penguins";
    case EAST:  return "elephants";
    case WEST:  return "llamas";
}

但是和int值不同的是,JAVA對枚舉類型有更多的靜態檢查:

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類型也都有一些內置的(automatically-provided)操作,這些操作在Enum中定義:

  • ordinal() 是某個值在枚舉類型中的索引值,因此 JANUARY.ordinal() 返回 0.
  • compareTo() 基於兩個值的索引值來比較兩個值.
  • name() 返回字符串形式表示的當前枚舉類型值,例如, JANUARY.name() 返回"JANUARY".
  • toString()name()是一樣的.

閱讀JAVA教程中的Enum Types (1頁)和 Nested Classes(1頁)

閱讀測試

Semester

考慮這三種可選的方式來命名你將要注冊的Semester:

  • 用一個字符串字面量:
startRegistrationFor("Fall", 2023);
  • 用一個命名的String類型常量:
public static final String FALL = "Fall";
...
startRegistrationFor(FALL, 2023);
  • 用一個枚舉類型的值:
public enum Semester { IAP, SPRING, SUMMER, FALL };
...
startRegistrationFor(FALL, 2023);

下列關於每個方案的優缺點敘述正確的是:

  • 使用字符串字面量的方案不會快速報錯,因為用戶可能拼寫錯誤的學期,而不會得到這樣的靜態錯誤信息:startRegistrationFor("FAll", 2023)
  • The named string constant approach isn’t safe from bugs, because the name can be reassigned: FALL = "Spring"
  • 命名的字符串常量方案不會快速報錯,因為用戶可能直接用不正確的字符串字面量來調用,但是卻不會得到靜態錯誤:startRegistrationFor("Autumn", 2023).
  • The enumeration approach isn’t safe from bugs, because the client can define new semesters without getting a static error: startRegistrationFor(new Semester("Autumn"), 2023).
  • The enumeration approach isn’t safe from bugs, because the client can substitute a different enumeration type without getting a static error: startRegistrationFor(JANUARY, 2023).

抽象數據類型在Java中的實現

現在我們完成了對“抽象數據類型”中“Java中ADT實現”的理解:

ADT 角度 Java實現 例子
抽象數據類型 String
接口 + 類 List and ArrayList
枚舉(Enum) DayOfWeek
創建者操作 構造方法 ArrayList()
靜態(工廠)方法 Collections.singletonList(), Arrays.asList()
常量 BigInteger.ZERO
觀察者操作 實例方法 List.get()
靜態方法 Collections.max()
生產者操作 實例方法 String.trim()
靜態方法 Collections.unmodifiableList()
改造者操作 實例方法 List.add()
靜態方法 Collections.copy()
(成員)表示 private/私有域

總結

抽象數據類型是由它支持的操作集合所定義的,而Java中的結構能夠幫助我們形式化這種思想。

這能夠使我們的代碼:

  • 遠離bug. 一個ADT是由它的操作集合定義的,而接口就是做了這件事情。當使用者使用接口類型時,靜態檢查能夠確保它們只使用了接口規定的方法。如果實現類寫出了/暴露了其他方法——或者更糟糕,暴露了內部表示——,使用者也不會依賴於這些操作。當我們實現一個接口時,編譯器會確保所有的方法標識都得到實現。
  • 易於理解. 使用者和維護者都知道在哪里尋找ADT的規格說明。因為接口沒有實例成員或者實例方法的函數體,所以它能更容易的將具體實現從規格說明中分離開。
  • 可改動. 我們可以輕松地為已有的接口添加新的實現類。如果我們認為靜態工廠方法比類構造方法更合適,使用者將只會看到這個接口。這意味着我們可以調整接口中工廠方法的實現類而不用改變使用者的代碼。

Java的枚舉類型能夠定義一種只有少部分不可變值的ADT。和以前使用特殊的整數或者字符串相比,枚舉類型能夠幫助我們的代碼:

  • 遠離bug. 靜態檢查能夠確保使用者沒有使用到規定集合外的值,或者是不同枚舉類型的值。
  • 易於理解. 將常量命名為枚舉類型名字而非幻數(或其他字面量)能夠更清晰的做自我注釋。
  • 可改動.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM