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
的表示暴露的安全性进行说明,由于整个类型的不变性依赖于全部的成员变量的不变性。