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

<font size="3">html

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

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

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

<br />github


<br />web

譯者:李秋豪 江家偉api

審校:安全

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

<br />app

本次課程的目標

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

在此次課程後,你應該可以定義ADT的接口,並可以寫出對應的實現類。

<br />

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

<br />

接口

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

<br />

子類型

回憶一下,咱們以前說過類型就是值的集合。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

<br />

例子: 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

<br />

例子: 泛型 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 如今指向: --> 這一行不能被編譯

<br />

泛型接口的實現

假設如今咱們要實現上面的 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 是如何依靠每個類型都要求實現的操做來實現它本身的,由於它沒辦法依賴於特定類型的操做。

<br />

爲何要使用接口?

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

這段代碼是(選中全部正確答案):

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

它應該位於:

  • [ ] 接口

  • [x] 實現類

  • [ ] 都有

Interface + implementation 2

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

這段代碼是(選中全部正確答案):

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

它應該位於:

  • [ ] 接口
  • [x] 實現類
  • [ ] 都有

Interface + implementation 3

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

這段代碼是(選中全部正確答案):

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

它應該位於:

  • [ ] 接口
  • [x] 實現類
  • [ ] 都有

Interface + implementation 4

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

這段代碼是(選中全部正確答案):

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

它應該位於:

  • [x] 接口
  • [ ] 實現類
  • [ ] 都有

Interface + implementation 5

public boolean isZero()

這段代碼是(選中全部正確答案):

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

它應該位於:

  • [ ] 接口
  • [ ] 實現類
  • [x] 都有

Interface + implementation 6

return numer == 0;

這段代碼是(選中全部正確答案):

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

它應該位於:

  • [ ] 接口
  • [x] 實現類
  • [ ] 都有

<br />

枚舉

有時候一個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);

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

  • [x] 使用字符串字面量的方案不會快速報錯,由於用戶可能拼寫錯誤的學期,而不會獲得這樣的靜態錯誤信息:startRegistrationFor("FAll", 2023)
  • [ ] The named string constant approach isn’t safe from bugs, because the name can be reassigned: FALL = "Spring"
  • [x] 命名的字符串常量方案不會快速報錯,由於用戶可能直接用不正確的字符串字面量來調用,可是卻不會獲得靜態錯誤: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).

<br />

抽象數據類型在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/私有域

<br />

總結

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

這可以使咱們的代碼:

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

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

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

</font>

相關文章
相關標籤/搜索