<font size="3">html
本文內容來自MIT_6.031_sp18: Software Construction課程的Readings部分,採用CC BY-SA 4.0協議。java
因爲咱們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,因而打算作一些翻譯工做,本身學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有的練習題沒有標準答案,所給出的「正確答案」爲譯者所寫,有錯誤的地方還請指出。git
(更新:從第10章開始只翻譯正確答案)程序員
<br />github
<br />web
審校:安全
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
實現類的對象。咱們說,一個子類型就是父類型的子集,正如 ArrayList
和 LinkedList
是List
的子類型同樣。
「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庫中的ArrayList
和LinkedList
「暴露」給了用戶,由於這兩個實如今 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聚合類型中的Set
。Set
是一個用來表示有着有限元素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);
接下來咱們聲明兩個觀察者。注意到規格說明中的提示,這裏不該該提到具體某一個實現的細節或者它們的標識,而規格說明也應該適用於全部Set
ADT的實現。
// 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代碼中,接口被用的很普遍(但也不是全部類都是接口的實現),這裏列出來了幾個使用接口的好處:
假設你有一個有理數的類型,它如今是以類來表示的:
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] 實現類
[ ] 都有
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;
這段代碼是(選中全部正確答案):
它應該位於:
<br />
有時候一個ADT的值域是一個很小的有限集,例如:
這樣的類型每每會被用來組成更復雜的類型(例如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)
FALL = "Spring"
startRegistrationFor("Autumn", 2023)
.startRegistrationFor(new Semester("Autumn"), 2023)
.startRegistrationFor(JANUARY, 2023)
.<br />
如今咱們完成了對「抽象數據類型」中「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中的結構可以幫助咱們形式化這種思想。
這可以使咱們的代碼:
Java的枚舉類型可以定義一種只有少部分不可變值的ADT。和之前使用特殊的整數或者字符串相比,枚舉類型可以幫助咱們的代碼:
</font>