3-1節研究了「數據類型」及其特性 ; 3-2節研究了方法和操做的「規約」及其特性;在本節中,咱們將數據和操做複合起來,構成ADT,學習ADT的核心特徵,以及如何設計「好的」ADT。java
【ADT的基本概念】git
ADT是由操做定義的,與其內部如何實現無關!程序員
【ADT的四種類型】編程
【設計一個好的ADT】數組
設計好的ADT,靠「經驗法則」,提供一組操做,設計其行爲規約 spec安全
【測試ADT】模塊化
【一個例子:字符串的不一樣表示】函數
讓咱們先來看看一個表示獨立的例子,而後考慮爲何頗有用,下面的MyString抽象類型是咱們舉出的例子。下面是規格說明:post
1 /** MyString represents an immutable sequence of characters. */ 2 public class MyString { 3 4 //////////////////// Example of a creator operation /////////////// 5 /** @param b a boolean value 6 * @return string representation of b, either "true" or "false" */ 7 public static MyString valueOf(boolean b) { ... } 8 9 //////////////////// Examples of observer operations /////////////// 10 /** @return number of characters in this string */ 11 public int length() { ... } 12 13 /** @param i character position (requires 0 <= i < string length) 14 * @return character at position i */ 15 public char charAt(int i) { ... } 16 17 //////////////////// Example of a producer operation /////////////// 18 /** Get the substring between start (inclusive) and end (exclusive). 19 * @param start starting index 20 * @param end ending index. Requires 0 <= start <= end <= string length. 21 * @return string consisting of charAt(start)...charAt(end-1) */ 22 public MyString substring(int start, int end) { ... } 23 }
使用者只須要/只能知道類型的公共方法和規格說明。下面是如何聲明內部表示的方法,做爲類中的一個實例變量:性能
private char[] a;
使用這種表達方法,咱們對操做的實現多是這樣的:
1 public static MyString valueOf(boolean b) { 2 MyString s = new MyString(); 3 s.a = b ? new char[] { 't', 'r', 'u', 'e' } 4 : new char[] { 'f', 'a', 'l', 's', 'e' }; 5 return s; 6 } 7 8 public int length() { 9 return a.length; 10 } 11 12 public char charAt(int i) { 13 return a[i]; 14 } 15 16 public MyString substring(int start, int end) { 17 MyString that = new MyString(); 18 that.a = new char[end - start]; 19 System.arraycopy(this.a, start, that.a, 0, end - start); 20 return that; 21 }
執行下列的代碼
MyString s = MyString.valueOf(true); MyString t = s.substring(1,3);
咱們用快照圖展現了在使用者進行 subString 操做後的數據狀態:
這種實現有一個性能上的問題,由於這個數據類型是不可變的,那麼 substring 實際上沒有必要真正去複製子字符串到⼀個新的數組中。它能夠僅僅指向原來的 MyString 字符數組,而且記錄當前的起始位置和終⽌位置。
爲了優化,咱們能夠將這個類的內部表示改成:
private char[] a; private int start; private int end;
有了這個新的表示,操做如今能夠這樣實現:
1 public static MyString valueOf(boolean b) { 2 MyString s = new MyString(); 3 s.a = b ? new char[] { 't', 'r', 'u', 'e' } 4 : new char[] { 'f', 'a', 'l', 's', 'e' }; 5 s.start = 0; 6 s.end = s.a.length; 7 return s; 8 } 9 10 public int length() { 11 return end - start; 12 } 13 14 public char charAt(int i) { 15 return a[start + i]; 16 } 17 18 public MyString substring(int start, int end) { 19 MyString that = new MyString(); 20 that.a = this.a; 21 that.start = this.start + start; 22 that.end = this.start + end; 23 return that; 24 }
如今運行上面的調用代碼,可用快照圖從新進行 substring 操做後的數據狀態:
因爲 MyString 的使用者僅依賴於其公共方法和規格說明,而不依賴其私有的存儲,所以咱們能夠在不檢查和更改全部客戶端代碼的狀況下進行更改。 這就是表示獨立性的力量。
一個好的抽象數據類型的最重要的屬性是它保持不變量。一旦一個不變類型的對象被建立,它老是表明一個不變的值。當一個ADT可以確保它內部的不變量恆定不變(不受使用者/外部影響),咱們就說這個ADT保護/保留本身的不變量。
【一個栗子:表示泄露】
1 /** 2 * This immutable data type represents a tweet from Twitter. 3 */ 4 public class Tweet { 5 6 public String author; 7 public String text; 8 public Date timestamp; 9 10 /** 11 * Make a Tweet. 12 * @param author Twitter user who wrote the tweet 13 * @param text text of the tweet 14 * @param timestamp date/time when the tweet was sent 15 */ 16 public Tweet(String author, String text, Date timestamp) { 17 this.author = author; 18 this.text = text; 19 this.timestamp = timestamp; 20 } 21 }
咱們如何保證這些Tweet對象是不可變的,(即一旦建立了Tweet,其author,message和 date 永遠不會改變)
對不可變性的第一個威脅來自使用者能夠直接訪問Tweet內部數據的事實,例如執行以下的引用操做:
1 Tweet t = new Tweet("justinbieber", 2 "Thanks to all those beliebers out there inspiring me every day", 3 new Date()); 4 t.author = "rbmllr";
這是一個表示泄露(Rep exposure)的簡單例子,這意味着類外的代碼能夠直接修改表示。像這樣的表示暴露不只威脅到不變量,並且威脅到表示獨立性。若是咱們改變類內部數據的1表示方式,使用者也會相應的受到影響。
幸運的是,java給咱們提供了處理表示暴露的方法:
1 public class Tweet { 2 private final String author; 3 private final String text; 4 private final Date timestamp; 5 6 public Tweet(String author, String text, Date timestamp) { 7 this.author = author; 8 this.text = text; 9 this.timestamp = timestamp; 10 } 11 12 /** @return Twitter user who wrote the tweet */ 13 public String getAuthor() { 14 return author; 15 } 16 17 /** @return text of the tweet */ 18 public String getText() { 19 return text; 20 } 21 22 /** @return date/time when the tweet was sent */ 23 public Date getTimestamp() { 24 return timestamp; 25 } 26 }
在private和public關鍵字代表哪些字段和方法可訪問時,只在類內部仍是能夠從類外部訪問。所述final關鍵字還保證該變量的索引不會被更改,對於不可變的類型來講,就是確保了變量的值不可變。
但這不能解決所有的問題:表示仍然會泄露!考慮這個徹底合理的客戶端代碼,它使用Tweet:
1 /** @return a tweet that retweets t, one hour later*/ 2 public static Tweet retweetLater(Tweet t) { 3 Date d = t.getTimestamp(); 4 d.setHours(d.getHours()+1); 5 return new Tweet("rbmllr", t.getText(), d); 6 }
retweetLater 但願接受一個Tweet對象而後修改Date後返回一個新的Tweet對象。
這裏有什麼問題?其中的 getTimestamp 調用返回一個同樣的 Date 對象,它會被t、t.timestamp 和 d 同時索引
。所以,當日期對象被突變,d.gsetHours( ) 被調用時
,t 也會影響日期,如快照圖所示。
這樣,Tweet的不變性就被破壞,Tweet將本身內部對於可變對象的索引「泄露」了出來,所以整個對象都變成可變的了,使用者在使用時也容易形成隱藏的bug。
咱們能夠經過使用防護性拷貝來修補這種風險:製做可變對象的副本以免泄漏對錶明的引用。代碼以下:
public Date getTimestamp() { return new Date(timestamp.getTime()); }
可變類型一般具備一個專門用來複制的構造函數,它容許建立一個複製現有實例值的新實例。在這種狀況下,Date
的複製構造函數就接受了一個timestamp值,而後產生一個新的對象。
複製可變對象的另外一種方法是clone()
,某些類型但不是所有類型支持該方法。然而clone()在
Java中的工做方式存在問題,更多可參考 Effective Java , item 11
如今咱們已經經過防護性複製解決了 timestamp 返回值的問題。但咱們尚未完成任務!還有表示泄露。考慮這個很是合理的客戶端代碼:
1 /** @return a list of 24 inspiring tweets, one per hour today */ 2 public static List<Tweet> tweetEveryHourToday () { 3 List<Tweet> list = new ArrayList<Tweet>(); 4 Date date = new Date(); 5 for (int i = 0; i < 24; i++) { 6 date.setHours(i); 7 list.add(new Tweet("rbmllr", "keep it up! you can do it", date)); 8 } 9 return list; 10 }
此代碼旨在建立24個Tweet對象,爲每一個小時建立一條推文。但請注意,Tweet的構造函數保存傳入的引用,所以全部24個Tweet對象最終都以同一時間結束,如此快照圖所示。
可是,Tweet的不變性再次被打破了,由於每⼀個Tweet建立時對Date對象的索引都是⼀樣的。因此咱們應該對建立者也進⾏防護性編程:
1 public Tweet(String author, String text, Date timestamp) { 2 this.author = author; 3 this.text = text; 4 this.timestamp = new Date(timestamp.getTime()); 5 }
一般來講,要特別注意ADT操做中的參數和返回值。若是它們之中有可變類型的對象,確保你的代碼沒有直接使⽤索引或者直接返回索引。
你可能反對說這看起來很浪費。爲何要製做全部這些日期的副本?爲何咱們不能經過像這樣仔細書寫的規範來解決這個問題?
/** * Make a Tweet. * @param author Twitter user who wrote the tweet * @param text text of the tweet * @param timestamp date/time when the tweet was sent. Caller must never * mutate this Date object again! */ public Tweet(String author, String text, Date timestamp) {
這種方法通常只在特不得已的時候使用——例如,當可變對象太大而沒法有效地複製時。可是,由此引起的潛在bug也將不少。除非無可奈何,不然不要把但願寄託於客戶端上,ADT有責任保證本身的不變量,並避免表示泄露。
最好的辦法就是使用immutable的類型,完全避免表示泄露,例如 java.time.ZonedDateTime
而不是 java.util.Date
。
【AF與RI】
AF : R → A
RI : R → boolean
public class CharSet { private String s; // Rep invariant: // s contains no repeated characters // Abstraction function: // AF(s) = {s[i] | 0 <= i < s.length()} ... }
public class CharSet { private String s; // Rep invariant: // s[0] <= s[1] <= ... <= s[s.length()-1] // Abstraction function: // AF(s) = {s[i] | 0 <= i < s.length()} ... }
public class CharSet { private String s; // Rep invariant: // s.length() is even // s[0] <= s[1] <= ... <= s[s.length()-1] // Abstraction function: // AF(s) = union of {s[2i],...,s[2i+1]} for 0 <= i < s.length()/2 ... }
【用註釋寫AF和RI】
Tweet
類的例子,它將表示不變量和抽象函數以及表示暴露的安全性註釋了出來:1 // Immutable type representing a tweet. 2 public class Tweet { 3 4 private final String author; 5 private final String text; 6 private final Date timestamp; 7 8 // Rep invariant: 9 // author is a Twitter username (a nonempty string of letters, digits, underscores) 10 // text.length <= 140 11 // Abstraction function: 12 // AF(author, text, timestamp) = a tweet posted by author, with content text, 13 // at time timestamp 14 // Safety from rep exposure: 15 // All fields are private; 16 // author and text are Strings, so are guaranteed immutable; 17 // timestamp is a mutable Date, so Tweet() constructor and getTimestamp() 18 // make defensive copies to avoid sharing the rep's Date object with clients. 19 20 // Operations (specs and method bodies omitted to save space) 21 public Tweet(String author, String text, Date timestamp) { ... } 22 public String getAuthor() { ... } 23 public String getText() { ... } 24 public Date getTimestamp() { ... } 25 }
注意到咱們並無對 timestamp
的表示不變量進行要求(除了以前說過的默認 timestamp!=null
)。可是咱們依然須要對timestamp
的表示暴露的安全性進行說明,由於整個類型的不變性依賴於全部的成員變量的不變性。