<font size="3">html
本文内容来自MIT_6.031_sp18: Software Construction课程的Readings部分,采用CC BY-SA 4.0协议。java
因为咱们学校(哈工大)大二软件构造课程的大部分素材取自此,也是推荐的阅读材料之一,因而打算作一些翻译工做,本身学习的同时也能帮到一些懒得看英文的朋友。另外,该课程的阅读资料中有许多练习题,可是没有标准答案,所给出的“正确答案”均为译者所写,有错误的地方还请指出。程序员
(更新:从第10章开始,只提供正确答案,再也不翻译错误答案)web
<br />数据库
<br />编程
审校:李秋豪数组
V1.0 Thu Mar 29 00:41:23 CST 2018安全
<br />数据结构
在这篇阅读中,咱们将会讲解一个重要的概念——抽象数据类型,它会帮助咱们将数据结构的使用和数据结构的具体实现分开。
抽象数据类型解决了一个很危险的问题:使用者可能对类型的内部表示作假设。咱们在后面会探讨为何这种假设是危险的,以及如何避免它。咱们也会讨论操做符的分类和如何设计好的抽象数据类型。
<br />
阅读如下代码并回答问题:
class Wallet { private int amount; public void loanTo(Wallet that) { // put all of this wallet's money into that wallet /*A*/ that.amount += this.amount; /*B*/ amount = 0; } public static void main(String[] args) { /*C*/ Wallet w = new Wallet(); /*D*/ w.amount = 100; /*E*/ w.loanTo(w); } } class Person { private Wallet w; public int getNetWorth() { /*F*/ return w.amount; } public boolean isBroke() { /*G*/ return Wallet.amount == 0; } }
假设程序在运行 /*A*/
语句后当即中止,上图列出了此时的内部状态,请问各个数字所标出的方框内应该填上什么?
1 -> w
2 -> that
3 -> loanTo
4 -> 200
Access control A
关于语句 /*A*/
,如下哪个说法是正确的?
that.amount += this.amount;
[x] 在Java中容许对this.amount
的索引
[x] 在Java中容许对 that.amount
的索引
Access control B
关于语句 /*B*/
,如下哪个说法是正确的?
amount = 0;
amount
的索引Access control C
关于语句 /*C*/
,如下哪个说法是正确的?
Wallet w = new Wallet();
Wallet()
构造函数的调用Access control D
关于语句 /*D*/
,如下哪个说法是正确的?
w.amount = 100;
w.amount
的访问Access control E
关于语句 /*E*/
,如下哪个说法是正确的?
w.loanTo(w);
loanTo()
的调用w
指向的Wallet
对象的金额将会是0Access control F
关于语句 /*F*/
,如下哪个说法是正确的?
return w.amount;
[x] 这里关于 w.amount
的索引不会被容许,由于 amount
是在另外一个类中的私有区域
[x] 这个非法访问会被静态捕捉
Access control G
关于语句 /*G*/
,如下哪个说法是正确的?
return Wallet.amount == 0;
Wallet.amount
的索引不会被容许,由于 amount
是一个私有地址Wallet.amount
的索引不会被容许,由于 amount
是一个实例变量<br />
抽象数据类型是软件工程中一个广泛原则的实例,从它衍生出不少意思相近的名词。这里列出了几个可以表达其中思想的词:
做为一个软件工程师,你应该知道这些名词,由于你会在之后的工做中常常遇到它们。这些思想的本质目的都是为了实现咱们这门课的三个目标:远离bug、易于理解、可改动。
事实上,咱们在以前的课程中已经碰到过这些思想,特别是在设计方法和规格说明的时候:
从今天的课程开始,咱们将跳出对方法的抽象,看看对数据的抽象。可是在咱们描述数据抽象时方法也会扮演很重要的角色。
在早期的编程语言中,用户只能本身定义方法,而全部的类型都是规定好的(例如整型、布尔型、字符串等等)。而现代编程语言容许用户本身定义类型对数据进行抽象,这是软件开发中的一个巨大进步。
对数据进行抽象的核心思想就是类型是经过其对应的操做来区分的:一个整型就是你能对它进行加法和乘法的东西;一个布尔型就是你能对它进行取反的东西;一个字符串就是你能对它进行连接或者取子字符串的东西,等等。在必定意义上,用户在之前的编程语言上彷佛已经可以定义本身的类型了,例如定义一个名叫Date的结构体,里面用int表示天数和年份。可是真正使得抽象类型变得新颖不一样的是对操做的强调:用户不用管这个类型里面的数据是怎么保存表示的,就好像是程序员不用管编译器是怎么存储整数同样。起做用的只是类型对应的操做。
和不少现代语言同样,在Java中内置类型和用户定义类型之间的关系很模糊。例如在 java.lang
中的类 Integer
和 Boolean
就是内置的——Java标准中规定它们必须存在,可是它们的定义又是和用户定义类型的方式同样的。另外,Java中还保留了原始类型,它们不是类和对象,例如 int
和 boolean
,用户没法对它们进行继承。
Abstract Data Types
思考抽象数据类型 Bool
,它有以下操做:
true : Bool false : Bool
and : Bool × Bool → Bool or : Bool × Bool → Bool not : Bool → Bool
头两个操做构建了这个类型对应的两个值,后三个操做对应逻辑操做 和、或、取非。
如下哪些选项能够是 Bool
具体的实现方法(而且知足上面的操做符)?
int
值,5表明true,8表明falseString
对象的索引,"false"
表明true, "true"
表明falseint
值,大于1的质数表明true,其他的表明false<br />
对于类型,无论是内置的仍是用户定义的,均可以被分为可改变 和 不可变两种。其中可改变类型的对象可以被改变:它们提供了改变对象内容的操做,这样的操做执行后能够改变其余对该对象操做的返回值。因此 Date
就是可改变的,由于你能够经过调用setMonth
操做改变 getMonth
操做的返回值。但 String
就是不可改变的,由于它的操做符都是建立一个新的 String
对象而不是改变现有的这个。有时候一个类型会提供两种形式,一种是可改变的一种是不可改变的。例如 StringBuilder
就是一种可改变的字符串类型。
而抽象类型的操做符大体分类:
String
类里面的 concat
方法就是一个生产者,它接受两个字符串而后据此产生一个新的字符串。List
的 size
方法,它返回一个 int
。List
的 add
方法,它会在列表中添加一个元素。咱们能够将这种区别用映射来表示:
其中T表明抽象类型自己;t表明其余的类型;+
表明这个参数可能出现一次或屡次;*
表明这个参数可能出现零次或屡次。例如, String.concat()
这个接受两个参数的生产者:
有些观察者不会接受其余类型的参数,例如:
而有些则会接受不少参数:
构造者一般都是用构造函数实现的,例如 new ArrayList()
,可是有的构造体是静态方法(类方法),例如 Arrays.asList()
和 String.valueOf
,这样的静态方法也称为工厂方法。
改造者一般没有返回值(void
)。一个没有返回值的方法必定有反作用 ,由于否则这个方法就没有任何意义了。可是不是全部的改造者都没有返回值。例如Set.add()
会返回一个布尔值用来提示这个集合是否被改变了。在Java图形库接口中,Component.add()
会将它本身这个对象返回,所以add()
能够被连续链式调用。
int 是Java中的原始整数类型,它是不可变类型,没有改造者。
0
, 1
, 2
, …+
, -
, *
, /
==
, !=
, <
, >
List 是Java中的列表类型,它是可更改类型。另外,List
也是一个接口,因此对于它的实现能够有不少类,例如 ArrayList
和 LinkedList
.
ArrayList
和 LinkedList
的构造函数, Collections.singletonList
Collections.unmodifiableList
size
, get
add
, remove
, addAll
, Collections.sort
String 是Java中的字符串类型,它是不可变类型。
String
构造函数, valueOf
静态方法(工厂方法)concat
, substring
, toUpperCase
length
, charAt
这个分类告诉了咱们一些有用的术语,但它不是完美的。例如对于复杂的数据类型,有些操做可能既是生产者也是改造者。
Operations
下面都是咱们从Java库中选取的几个抽象数据类型的操做,试着经过阅读文档将这些操做分类。
提示:注意类型自己是否是参数或者返回值,同时记住实例方法(没有static
关键词的)有一个隐式的参数。
creator
producer
mutator
producer
observer
observer
mutator
<br />
这一节的重要思想就是抽象类型是经过它的操做定义的.
对于类型T来讲,它的操做集合和规格说明彻底定义和构造了它的特性。例如,当咱们谈到List
类型时,咱们并无特指一个数组或者连接链表,而是一系列模糊的值——哪些对象能够是List
类型——知足该类型的规格说明和操做规定,例如 get()
, size()
, 等等。
上一段说到的“模糊的值”是指咱们不能去检查数据具体是在类型中怎么存储的,而是要经过特定的操做去处理。例如上图中画出的,经过规格说明这道“防火墙”,咱们将类型中具体的实现和这些实现共享的私有数据封装起来,而用户只能看到和使用接口上的操做。
<br />
设计一个抽象类型包括选择合适的操做以及它们对应的行为,这里列出了几个重要的规则。
设计少许,简单,能够组合实现强大功能的操做而非设计不少复杂的操做。
每一个操做都应该有一个被明肯定义的目的,而且应该设计为对不一样的数据结构有一致的行为,而不是针对某些特殊状况。例如,或许咱们不该该为List
类型添加一个sum
操做。由于这虽然可能对想要操做一个整数列表的用户有帮助,可是若是用户想要操做一个字符串列表呢?或者一个嵌套的列表? 全部这些特殊状况都将会使得sum
成为一个难以理解和使用的操做。
操做集合应该充分地考虑到用户的需求,也就是说,用户能够用这个操做集合作他们可能想作的计算。一个较好测试方法是检查抽象类型的每一个属性是否都能被操做集提取出来。例如,若是没有get
操做,咱们就不能提取列表中的元素。抽象类型的基本信息的提取也不该该特别困难。例如,size
方法对于List
并非必须的,由于咱们能够用get
增序遍历整个列表,直到get
执行失败,可是这既不高效,也不方便。
抽象类型能够是通用的:例如,列表、集合,或者图。或者它能够是适用于特定领域的:一个街道的地图,一个员工数据库,一个电话簿等等。可是一个抽象类型不能兼有上述两者的特性。被设计用来表明一个纸牌序列的Deck
类型不该该有一个通用的add
方法来向类型实例中添加任意对象,好比整型和字符串类型。反过来讲,对于像dealCards
这样的只对特定领域(译者注:纸牌游戏)有效的方法,把它加入List
这样的通用类型中也是没有意义的。
<br />
特别地,一个好的抽象数据类型应该是表示独立的。这意味着它的使用和它的内部表示(实际的数据结构和实现)无关,因此内部表示的改变将对外部的代码没有影响。例如,List
就是表示独立的——它的使用与它是用数组仍是链接链表实现无关。
若是一个操做彻底在规格说明中定义了前置条件和后置条件,使用者就知道他应该依赖什么,而你也能够安全的对内部实现进行更改(遵循规格说明)。
让咱们先来看看一个表示独立的例子,而后想一想它为何颇有用。下面的 MyString
抽象类型是咱们举出的例子,虽然它远远没有Java中的String
操做多,规格说明也有些不一样,可是仍是有解释力的。下面是规格说明:
/** MyString represents an immutable sequence of characters. */ public class MyString { //////////////////// Example of a creator operation /////////////// /** @param b a boolean value * @return string representation of b, either "true" or "false" */ public static MyString valueOf(boolean b) { ... } //////////////////// Examples of observer operations /////////////// /** @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) { ... } //////////////////// Example of a producer operation /////////////// /** 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) { ... } }
使用者只须要/只能知道这个类型的公共方法和规格说明。
如今让咱们看一个MyString
简单的表示方法,仅仅使用一个字符数组,并且它的大小恰好是字符串的长度,没有多余的空间:
private char[] a;
若是使用这种表示方法,咱们对操做的实现可能就是这样的:
public static MyString valueOf(boolean b) { MyString s = new MyString(); s.a = b ? new char[] { 't', 'r', 'u', 'e' } : new char[] { 'f', 'a', 'l', 's', 'e' }; return s; } public int length() { return a.length; } public char charAt(int i) { return a[i]; } public MyString substring(int start, int end) { MyString that = new MyString(); that.a = new char[end - start]; System.arraycopy(this.a, start, that.a, 0, end - start); return that; }
这里想一个问题:为何 charAt
和 substring
不去检查参量在合法的范围内?你认为这种类型的对象对于非法的输入会有什么反应?
下面的快照图展现了在使用者进行substring
操做后的数据状态:
MyString s = MyString.valueOf(true); MyString t = s.substring(1,3);
这种实现有一个性能上的问题,由于这个数据类型是不可变的,那么 substring
实际上没有必要真正去复制子字符串到一个新的数组中。它能够仅仅指向原来的 MyString
字符数组,而且记录当前的起始位置和终止位置。
为了实现这种优化,咱们能够将内部表示改成:
private char[] a; private int start; private int end;
经过这种新的表示方法,咱们能够这样实现操做:
public static MyString valueOf(boolean b) { MyString s = new MyString(); s.a = b ? new char[] { 't', 'r', 'u', 'e' } : new char[] { 'f', 'a', 'l', 's', 'e' }; s.start = 0; s.end = s.a.length; return s; } public int length() { return end - start; } public char charAt(int i) { return a[start + i]; } public MyString substring(int start, int end) { MyString that = new MyString(); that.a = this.a; that.start = this.start + start; that.end = this.start + end; return that; }
如今进行substring
操做后的数据状态:
MyString s = MyString.valueOf(true); MyString t = s.substring(1,3);
由于 MyString
的使用者只使用到了它的公共方法和规格说明(没有使用私有的存储表示),咱们能够“私底下”完成这种优化而不用担忧影响使用者的代码。这就是表示独立的力量。
Representation 1
思考下面这个抽象类型:
/** * Represents a family that lives in a household together. * A family always has at least one person in it. * Families are mutable. */ class Family { // the people in the family, sorted from oldest to youngest, with no duplicates. public List<Person> people; /** * @return a list containing all the members of the family, with no duplicates. */ public List<Person> getMembers() { return people; } }
下面是一个使用者的代码:
void client1(Family f) { // get youngest person in the family Person baby = f.people.get(f.people.size()-1); ... }
假设全部的代码都能顺利运行( Family
和 client1
)并经过测试。
如今 Family
的数据表示从 List
变为了 Set
:
/** * Represents a family that lives in a household together. * A family always has at least one person in it. * Families are mutable. */ class Family { // the people in the family public Set<Person> people; /** * @return a list containing all the members of the family, with no duplicates. */ public List<Person> getMembers() { return new ArrayList<>(people); } }
如下哪个选项是在 Family
更改后对 client1
的影响?
client1
依赖于 Family
的数据表示, 而且这种依赖会致使静态错误。Representation 2
原始版本:
/** * Represents a family that lives in a * household together. A family always * has at least one person in it. * Families are mutable. */ class Family { // the people in the family, // sorted from oldest to youngest, // with no duplicates. public List<Person> people; /** @return a list containing all * the members of the family, * with no duplicates. */ public List<Person> getMembers() { return people; } }
新版本:
/** * Represents a family that lives in a * household together. A family always * has at least one person in it. * Families are mutable. */ class Family { // the people in the family public Set<Person> people; /** * @return a list containing all * the members of the family, * with no duplicates. */ public List<Person> getMembers() { return new ArrayList<>(people); } }
使用者 client2
的代码:
void client2(Family f) { // get size of the family int familySize = f.people.size(); ... }
如下哪个选项是新版本对 client2
的影响?
client2
依赖于 Family
的表示,这种依赖不会被捕捉错误可是会(幸运地)获得正确答案。Representation 3
原始版本:
/** * Represents a family that lives in a * household together. A family always * has at least one person in it. * Families are mutable. */ class Family { // the people in the family, // sorted from oldest to youngest, // with no duplicates. public List<Person> people; /** @return a list containing all * the members of the family, * with no duplicates. */ public List<Person> getMembers() { return people; } }
新版本:
/** * Represents a family that lives in a * household together. A family always * has at least one person in it. * Families are mutable. */ class Family { // the people in the family public Set<Person> people; /** * @return a list containing all * the members of the family, * with no duplicates. */ public List<Person> getMembers() { return new ArrayList<>(people); } }
使用者 client3
的代码:
void client3(Family f) { // get any person in the family Person anybody = f.getMembers().get(0); ... }
如下哪个选项是新版本对 client3
的影响?
client3
独立于 Family
的数据表示, 因此它依然能正确的工做Representation 4
对于上面的Family
数据类型,对每行/段判断他是规格说明(specification)仍是数据表示(representation)仍是具体实现(implementation)?
/** * Represents a family that lives in a household together. * A family always has at least one person in it. * Families are mutable. */
--> 规格说明
public class Family {
--> 规格说明
// the people in the family, sorted from oldest to youngest, with no duplicates.
--> 数据表示
private List<Person> people;
--> 数据表示
/** * @return a list containing all the members of the family, with no duplicates. */
--> 规格说明
public List<Person> getMembers() {
--> 规格说明
return people;
--> 具体实现
<br />
让咱们总结一下咱们在这篇文章中讨论过的主要思想以及使用JAVA语言特性实现它们的具体方法,这些思想对于使用任何语言编程通常都是适用的。重点在于有不少种方式来实现,很重要的一点是:既要对大概念(好比构造操做:creator operation)有较好的理解,也要理解它们不一样的实现方式。
ADT concept | Ways to do it in Java | Examples |
---|---|---|
Abstract data type | Class | String |
Interface + class(es) | List and ArrayList |
|
Enum | DayOfWeek |
|
Creator operation | Constructor | ArrayList() |
Static (factory) method | Collections.singletonList() , Arrays.asList() |
|
Constant | BigInteger.ZERO |
|
Observer operation | Instance method | List.get() |
Instance method | Collections.max() |
|
Producer operation | Instance method | String.trim() |
Static method | Collections.unmodifiableList() |
|
Mutator operation | Instance method | List.add() |
Static method | Collections.copy() |
|
Representation | private fields |
这个表中有三项咱们尚未在以前的阅读中讲过:
List
和 ArrayList
这些例子,而且咱们将会在之后的阅读中讨论接口。enum
)定义一个抽象数据类型。枚举对于有固定取值集合的ADTs(例如一周中有周1、周二等等)来讲,是很理想的类型。咱们将会在之后的阅读中讨论枚举。<br />
当咱们测试一个抽象数据类型的时候,咱们分别测试它的各个操做。而这些测试不可避免的要互相交互:咱们只能经过观察者来判断其余的操做的测试是否成功,而测试观察者的惟一方法是建立对象而后使用观察者。
下面是咱们测试 MyString
类型时对输入空间的一种可能划分方案:
// testing strategy for each operation of MyString: // // valueOf(): // true, false // length(): // string len = 0, 1, n // string = produced by valueOf(), produced by substring() // charAt(): // string len = 1, n // i = 0, middle, len-1 // string = produced by valueOf(), produced by substring() // substring(): // string len = 0, 1, n // start = 0, middle, len // end = 0, middle, len // end-start = 0, n // string = produced by valueOf(), produced by substring()
如今咱们试着用测试用例覆盖每个分区。注意到 assertEquals
并不能直接应用于 MyString
对象,由于咱们没有在 MyString
上定义判断相等的操做,因此咱们只能使用以前定义的 valueOf
, length
, charAt
, 以及 substring
,例如:
@Test public void testValueOfTrue() { MyString s = MyString.valueOf(true); assertEquals(4, s.length()); assertEquals('t', s.charAt(0)); assertEquals('r', s.charAt(1)); assertEquals('u', s.charAt(2)); assertEquals('e', s.charAt(3)); } @Test public void testValueOfFalse() { MyString s = MyString.valueOf(false); assertEquals(5, s.length()); assertEquals('f', s.charAt(0)); assertEquals('a', s.charAt(1)); assertEquals('l', s.charAt(2)); assertEquals('s', s.charAt(3)); assertEquals('e', s.charAt(4)); } @Test public void testEndSubstring() { MyString s = MyString.valueOf(true).substring(2, 4); assertEquals(2, s.length()); assertEquals('u', s.charAt(0)); assertEquals('e', s.charAt(1)); } @Test public void testMiddleSubstring() { MyString s = MyString.valueOf(false).substring(1, 2); assertEquals(1, s.length()); assertEquals('a', s.charAt(0)); } @Test public void testSubstringIsWholeString() { MyString s = MyString.valueOf(false).substring(0, 5); assertEquals(5, s.length()); assertEquals('f', s.charAt(0)); assertEquals('a', s.charAt(1)); assertEquals('l', s.charAt(2)); assertEquals('s', s.charAt(3)); assertEquals('e', s.charAt(4)); } @Test public void testSubstringOfEmptySubstring() { MyString s = MyString.valueOf(false).substring(1, 1).substring(0, 0); assertEquals(0, s.length()); }
Partition covering
哪个测试覆盖了分区“charAt()
以及字符串长度=1”?
testMiddleSubstring
哪个测试覆盖了分区“子字符串的子字符串”?
testSubstringOfEmptySubstring
哪个测试覆盖了分区“valueOf(true)
”?
[x] testValueOfTrue
[x] testEndSubstring
Unit testing an ADT
testValueOfTrue
测试的是哪个“单元”?
valueOf
操做length
操做charAt
操做<br />
T将本次阅读的内容和咱们的三个目标联系起来:
</font>