<font size="3">javascript
本文內容來自MIT_6.031_sp18: Software Construction課程的Readings部分,採用CC BY-SA 4.0協議。html
因爲咱們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,因而打算作一些翻譯工做,本身學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習題,可是沒有標準答案,所給出的答案均爲譯者所寫,有錯誤的地方還請指出。java
<br />python
<br />git
譯者:李秋豪程序員
審校:web
V1.0 Fri Mar 23 17:20:24 CST 2018算法
<br />spring
譯者注:mutability還能夠翻譯爲「易變性」(that can change; likely to change),「易」彷佛也能突出使用應該謹慎,但後來我仍是以爲「可變」更準確,因此就譯爲「可變性」。shell
<br />
譯者注:麻省理工理工是先教的Python,Java並無做爲課程。因此這裏提供了一組從Python到Java過渡資料。Java基礎好的朋友這節能夠跳過。
From Ken Lambert’s tutorial From Python to Java, read the first 8 pages under Defining Classes:
- Class Structure
- Visibility Modifiers
- Instance Variables and Constructors
- Defining Other Constructors
- Instance Methods
- Method Overloading
- Class (
static
) Variables and Methods- Symbolic Constants (
final
Variables)Optional: if you want to see more examples, read these Java Tutorials pages:
Classes and objects
class Tortoise: def __init__(self): self.position = 0 def forward(self): self.position += 1 pokey = Tortoise() pokey.forward() print(pokey.position)
若是咱們將 Tortoise
轉換爲Java,應該怎麼進行聲明?
public class Tortoise
Under construction
在Python中,咱們經過聲明 __init__
函數來初始化新的對象。
在Java中相似的聲明應該怎麼寫?
public Tortoise()
咱們應該怎麼索引到一個新的 Tortoise
對象?
Tortoise t = new Tortoise()
Methodical
咱們在一個 Tortoise
對象中聲明一個 forward
方法:
public void forward() { // self.position += 1 (Python) }
如下哪一行代碼能夠達到代碼中註釋行的目的:
[x] position += 1;
[ ] self.position += 1;
[x] this.position += 1;
[ ] Tortoise.position += 1;
On your mark
在Python中,咱們經過 self.position = 0
初始化 Tortoise
對象中 position
爲0.
使用一行代碼將 position
初始化:
public class Tortoise { private int position = 0; // (1) static int position = 0; // (2) public Tortoise() { int position = 0; // (3) int self.position = 0; // (4) int this.position = 0; // (5) int Tortoise.position = 0; // (6) } // ... }
[x] 1
[ ] 2
[ ] 3
[ ] 4
[ ] 5
[ ] 6
或者用幾行初始化 position
:
public class Tortoise { private int position; // (1) static int position; // (2) public Tortoise() { self.position = 0; // (3) this.position = 0; // (4) Tortoise.position = 0; // (5) } // ... }
[x] 1
[ ] 2
[ ] 3
[x] 4
[ ] 5
Get set
如今咱們再聲明另外一個方法 Tortoise
:
public void jump(int position) { // set this Tortoise's position to the input value }
如下哪一行能夠將代碼中註釋部分實現?
[ ] position = position;
[ ] position = this.position;
[x] this.position = position;
[ ] this.position = this.position;
Static vs. instance
假設咱們想到記錄和 Tortoise
類及對象有關的信息,下面哪個聲明是合理的?
記錄有多少個對象已經被建立了:
int numberOfTortoisesInWorld;
static int numberOfTortoisesInWorld;
記錄tortoise對象中shell的顏色:
Color shell;
static Color shell;
對象的母親和父親:
Tortoise mother, father;
static Tortoise mother, father;
<br />
回憶以前咱們討論過的「用快照圖理解值與對象」(譯者注:「Java基礎」),有一些對象的內容是不變的(immutable):一旦它們被建立,它們老是表示相同的值。另外一些對象是可變的(mutable):它們有改變內部值對應的方法。
String
就是不變對象的一個例子,一個String
對象老是表示相同的字符串。而StringBuilder
則是可變的,它有對應的方法來刪除、插入、替換字符串內部的字符,等等。
由於 String
是不變的,一旦被建立,一個 String
對象老是有同樣的值。爲了在一個 String
對象字符串後加上另外一個字符串,你必須建立一個新的 String
對象:
String s = "a"; s = s.concat("b"); // s+="b" and s=s+"b" also mean the same thing
與此相對, StringBuilder
對象是可變的。這個類有對應的方法來改變對象,而不是返回一個新的對象:
StringBuilder sb = new StringBuilder("a"); sb.append("b");
因此這有什麼關係呢?在上面這兩個例子中,咱們最終都讓s
和sb
索引到了"ab"
。當對象的索引只有一個時,它們兩確實沒什麼去唄。可是當有別的索引指向同一個對象時,它們的行爲會大不相同。例如,當另外一個變量t
指向s
對應的對象,tb
指向sb
對應的對象,這個時候對t
和tb
作更改就會致使不一樣的結果:
String t = s; t = t + "c"; StringBuilder tb = sb; tb.append("c");
能夠看到,改變t
並無對s
產生影響,可是改變tb
確實影響到了sb
——這可能會讓編程者驚訝一下(若是他沒有注意的話)。這也是下面咱們會重點討論的問題。
既然咱們已經有了不變的 String
類,爲何還要使用可變的 StringBuilder
類呢?一個常見的使用環境就是當你要同時建立大量的字符串,例如:
String s = ""; for (int i = 0; i < n; ++i) { s = s + i; }
若是使用不變的字符串,這會發生不少「暫時拷貝」——第一個字符「0」實際上就被拷貝了n次,第二個字符被拷貝了n-1次,等等。總的來講,它會花費O(N^2)的時間來作拷貝,即便最終咱們的字符串只有n個字符。
StringBuilder
的設計就是爲了最小化這樣的拷貝,它使用了簡單可是聰明的內部結構避免了作任何拷貝(除非到了極限狀況)。若是你使用StringBuilder
,能夠在最後用 toString()
方法獲得一個String
的結果:
StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; ++i) { sb.append(String.valueOf(i)); } String s = sb.toString();
優化性能是咱們使用可變對象的緣由之一。另外一個緣由是爲了分享:程序中的兩個地方的代碼能夠經過共享一個數據結構進行交流。
Follow me
一個 terrarium
的使用者能夠更改紅色的 Turtle
對象嗎?
[ ] 不能,由於到 terrarium
的索引是不變的
[x] 不能,由於 Turtle
對象是不變的
[ ] 能夠,由於從列表的0下標處到 Turtle
的索引是可變的。
[ ] 能夠,由於 Turtle
對象是可變的
一個 george
的使用者能夠更改藍色的 Gecko
對象嗎?
[ ] 不能,由於到george
的索引是不變的
[x] 不能,由於 Gecko
對象是不變的
[ ] 能夠,由於從列表的1下標處到 Gecko
的索引是可變的。
[ ] 能夠,由於 Gecko
對象是可變的
一個 petStore
的使用者可使得另外一個 terrarium
的使用者沒法訪問藍色的 Gecko
對象嗎?選出最好的答案
[ ] 不能,由於到 terrarium
的索引是不變的
[ ] 不能,由於 Gecko
對象是不變的
[ ] 能夠,由於到 petStore
的索引是可變的
[ ] 能夠,由於 PetStore
對象是可變的
[x] 能夠,由於 List
對象是可變的
[ ] 能夠,由於從列表的1下標處到 Gecko
的索引是可變的。
<br />
可變的類型看起來比不可變類型強大的多。若是你在「數據類型商場」購物,爲何要選擇「無聊的」不可變類型而放棄強大的可變類型呢?例如 StringBuilder
應該能夠作任何 String
能夠作的事情,加上 set()
和 append()
這些功能。
答案是**使用不可變類型要比可變類型安全的多,同時也會讓代碼更易懂、更具有可改動性。**可變性會使得別人很難知道你的代碼在幹嘛,也更難制定開發規定(例如規格說明)。這裏舉出了兩個例子:
下面這個方法將列表中的整數相加求和:
/** @return the sum of the numbers in the list */ public static int sum(List<Integer> list) { int sum = 0; for (int x : list) sum += x; return sum; }
假設如今咱們要建立另一個方法,這個方法將列表中數的絕對值相加,根據DRY原則(Don’t Repeat Yourself),實現者寫了一個利用 sum()
的方法:
/** @return the sum of the absolute values of the numbers in the list */ public static int sumAbsolute(List<Integer> list) { // let's reuse sum(), because DRY, so first we take absolute values for (int i = 0; i < list.size(); ++i) list.set(i, Math.abs(list.get(i))); return sum(list); }
注意到這個方法直接改變了數組 —— 這對實現者來講很合理,由於利用一個已經存在的列表會更有效率。若是這個列表有幾百萬個元素,那麼你節省內存的同時也節省了大量時間。因此實現者的理由很充分:DRY與性能。
可是使用者可能會對結果很驚奇,例如:
// meanwhile, somewhere else in the code... public static void main(String[] args) { // ... List<Integer> myData = Arrays.asList(-5, -3, -2); System.out.println(sumAbsolute(myData)); System.out.println(sum(myData)); }
Risky #1
上面的代碼會打印出哪兩個數?
10
10
讓咱們想一想這個問題的關鍵點:
sumAbsolute()
的實現者,由於他可能違背了規格說明。可是,**傳入可變對象真的(可能)會致使隱祕的bug。**只要有一個程序員不當心將這個傳入的列表更改了(例如爲了複用或性能),程序就可能會出錯,並且bug很難追查。main()
的時候,你會對 sum()
和 sumAbsolute()
作出哪些假設?對於讀者來講,他能清晰的知道 myData
會被更改嗎?咱們剛剛看到了傳入可變對象可能會致使問題。那麼返回一個可變對象呢?
Date
是一個Java內置的類, 同時 Date
也正好是一個可變類型。假設咱們寫了一個判斷春天的第一天的方法:
/** @return the first day of spring this year */ public static Date startOfSpring() { return askGroundhog(); }
這裏咱們使用了有名的土撥鼠算法 (Harold Ramis, Bill Murray, et al. Groundhog Day, 1993).
如今使用者用這個方法來計劃他們的派對開始時間:
// somewhere else in the code... public static void partyPlanning() { Date partyDate = startOfSpring(); // ... }
這段代碼工做的很好。不過過了一段時間,startOfSpring()
的實現者發現「土撥鼠」被問的不耐煩了,因而打算重寫startOfSpring()
,使得「土撥鼠」最多被問一次,而後緩存下此次的答案,之後直接從緩存讀取:
/** @return the first day of spring this year */ public static Date startOfSpring() { if (groundhogAnswer == null) groundhogAnswer = askGroundhog(); return groundhogAnswer; } private static Date groundhogAnswer = null;
(思考:這裏緩存使用了private static
修飾符,你認爲它是全局變量嗎?)
另外,有一個使用者以爲startOfSpring()
返回的日期太冷了,因此他把日期延後了一個月:
// somewhere else in the code... public static void partyPlanning() { // let's have a party one month after spring starts! Date partyDate = startOfSpring(); partyDate.setMonth(partyDate.getMonth() + 1); // ... uh-oh. what just happened? }
(思考:這裏還有另一個隱祕的bug——partyDate.getMonth() + 1
,你知道爲何嗎?)
這兩個改動發生後,你以爲程序會出現什麼問題?更糟糕的是,誰會先發現這個bug呢?是這個 startOfSpring()
,仍是 partyPlanning()
? 或是在另外一個地方使用 startOfSpring()
的無辜者?
Risky #2
咱們不知道Date
具體是怎麼存儲月份的,因此這裏用抽象的值 ...march...
和 ...april...
表示,Date
中有一個mounth
索引到這些值上。
如下哪個快照圖表現了上文中的bug?
[ ]
[ ]
[ ]
[x]
[ ]
Understanding risky example #2
partyPlanning
在不知不覺中修改了春天的起始位置,由於 partyDate
和 groundhogAnswer
指向了同一個可變Date
對象 。
更糟糕的是,這個bug可能不會在這裏的 partyPlanning()
或 startOfSpring()
中出現。而是在另一個調用 startOfSpring()
的地方出現,獲得一個錯誤的值而後繼續進行運算。
上文中的緩存 groundhogAnswer
是全局變量嗎?
[ ] 是全局變量,這是合理的
[ ] 是全局變量,這是不合理的
[x] 不是全局變量
A second bug
上文中的代碼在加上1月的時候存在另外一個bug,請閱讀 Java API documentation for Date.getMonth
和 setMonth
.
對於 partyDate.getMonth()
,它的哪個返回值會致使bug的發生?
11
NoSuchMonthException
上面關於 Date.setMonth
文檔中說: month: the month value between 0-11
.那麼當這個bug觸發的時候可能會發生什麼?
[x] 這個方法不會作任何事情
[x] 這個方法會按照咱們本來的想法運行
[x] 這個方法會使得 Date
對象不可用,並報告一個錯誤的值
[ ] 這個方法會拋出一個已檢查異常
[x] 這個方法會拋出一個未檢查異常
[x] 這個方法會將時間設置爲9/9/99
[x] 這個方法會使得其餘的 Date
對象也不可用
[x] 這個方法永遠不會返回
SuchTerribleSpecificationsException
在關於 Date
的文檔中,有一句話是這樣說的,「傳入方法的參數並不必定要落在指定的區域內,例如傳入1月32號意味着2月1號」。
這看起來像是前置條件...但它不是的!
下面哪個選項表現了Date
這個特性是不合理的?
<br />
關鍵點:
在上面舉出的兩個例子( List<Integer>
和 Date
)中,若是咱們採用不可變對象,這些問題就迎刃而解了——這些bug在設計上就不可能發生。
事實上,你絕對不該該使用Date
!而是使用 包 java.time
: LocalDateTime
, Instant
, 等等這些類,它們規格說明都保證了對象是不可變的。
這個例子也說明了使用可變對象可能會致使性能上的損失。由於爲了在不修改規格說明和接口的前提下避開這個bug,咱們必須讓startOfSpring()
返回一個複製品:
return new Date(groundhogAnswer.getTime());
這樣的模式稱爲防護性複製 ,咱們在後面講抽象數據類型的時候會講解更多關於防護性複製的東西。這樣的方法意味着 partyPlanning()
能夠自由的操控startOfSpring()
的返回值而不影響其中的緩存。可是防護性複製會強制要求 startOfSpring()
爲每個使用者複製相同數據——即便99%的內容使用者都不會更改,這會很浪費空間和時間。相反,若是咱們使用不可變類型,不一樣的地方用不一樣的對象來表示,相同的地方都索引到內存中同一個對象,這樣會讓程序節省空間和複製的時間。因此說,合理利用不變性對象(譯者注:大可能是有多個變量索引的時候)的性能比使用可變性對象的性能更好。
<br />
事實上,若是你只在一個方法內使用可變類型並且該類型的對象只有一個索引,這時並不會有什麼風險。而上面的例子告訴咱們,若是一個可變對象有多個變量索引到它——這也被稱做「別名」,這時就會有產生bug的風險。
Aliasing 1
如下代碼的輸出是什麼?
List<String> a = new ArrayList<>(); a.add("cat"); List<String> b = a; b.add("dog"); System.out.println(a); System.out.println(b);
[ ] ["cat"]
`["cat", "dog"]`
[x] ["cat", "dog"]
`["cat", "dog"]`
[ ] ["cat"]
`["cat"]`
[ ] ["dog"]
`["dog"]`
如今試着使用快照圖將上面的兩個例子過一遍,這裏只列出一個輪廓:
List
例子中,一個相同的列表被list
(在 sum
和 sumAbsolute
中)和myData
(在main
中)同時索引。一個程序員(sumAbsolute
的)認爲更改這個列表是ok的;另外一個程序員(main
)但願列表保持原樣。因爲別名的使用,main
的程序員獲得了一個錯誤的結果。Date
的例子中,有兩個變量 groundhogAnswer
和 partyDate
索引到同一個Date
對象。這兩個別名出如今程序的不一樣地方,因此不一樣的程序員很難知作別人會對這個Date
對象作哪些改變。先在紙上畫出快照圖,可是你真正的目標應該是在腦海中構建一個快照圖,這樣之後你在看代碼的時候也能將其「視覺化」。
<br />
從上面的分析來看,咱們必須使用以前提到過的格式對那些會更改參數對象的方法寫上特定的規格說明。
下面是一個會更改參數對象的方法:
static void sort(List<String> lst) - requires: nothing - effects: puts lst in sorted order, i.e. lst[i] ≤ lst[j] for all 0 ≤ i < j < lst.size()
而這個是一個不會更改參數對象的方法:
static List<String> toLowerCase(List<String> lst) - requires: nothing - effects: returns a new list t where t[i] = lst[i].toLowerCase()
若是在effects內沒有顯式強調輸入參數會被更改,在本門課程中咱們會認爲方法不會修改輸入參數。事實上,這也是一個編程界的一個約定俗成的規則。
<br />
接下來咱們會看看另外一個可變對象——迭代器 。迭代器會嘗試遍歷一個聚合類型的對象,並逐個返回其中的元素。當你在Java中使用for (... : ...)
這樣的遍歷元素的循環時,其實就隱式的使用了迭代器。例如:
List<String> lst = ...; for (String str : lst) { System.out.println(str); }
會被編譯器理解爲下面這樣:
List<String> lst = ...; Iterator<String> iter = lst.iterator(); while (iter.hasNext()) { String str = iter.next(); System.out.println(str); }
一個迭代器有兩種方法:
next()
返回聚合類型對象的下一個元素hasNext()
測試迭代器是否已經遍歷到聚合類型對象的結尾注意到next()
是一個會修改迭代器的方法(mutator method),它不只會返回一個元素,並且會改變內部狀態,使得下一次使用它的時候會返回下一個元素。
感興趣的話,你能夠讀讀Java API中關於迭代器的定義 .
MyIterator
爲了更好的理解迭代器是如何工做的,這裏有一個ArrayList<String>
迭代器的簡單實現:
/** * A MyIterator is a mutable object that iterates over * the elements of an ArrayList<String>, from first to last. * This is just an example to show how an iterator works. * In practice, you should use the ArrayList's own iterator * object, returned by its iterator() method. */ public class MyIterator { private final ArrayList<String> list; private int index; // list[index] is the next element that will be returned // by next() // index == list.size() means no more elements to return /** * Make an iterator. * @param list list to iterate over */ public MyIterator(ArrayList<String> list) { this.list = list; this.index = 0; } /** * Test whether the iterator has more elements to return. * @return true if next() will return another element, * false if all elements have been returned */ public boolean hasNext() { return index < list.size(); } /** * Get the next element of the list. * Requires: hasNext() returns true. * Modifies: this iterator to advance it to the element * following the returned element. * @return next element of the list */ public String next() { final String element = list.get(index); ++index; return element; } }
MyIterator
使用到了許多Java的特性,例如構造體,static和final變量等等,你應該確保本身已經理解了這些特性。參考: From Python to Java 或 Classes and Objects in the Java Tutorials
上圖畫出了 MyIterator
初始狀態的快照圖。
注意到咱們將list
的索引用雙箭頭表示,以此表示這是一個不能更改的final索引。可是list索引的 ArrayList
自己是一個可變對象——內部的元素能夠被改變——將list
聲明爲final並不能阻止這種改變。
那麼爲何要使用迭代器呢?由於不一樣的聚合類型其內部實現的數據結構都不相同(例如鏈接鏈表、哈希表、映射等等),而迭代器的思想就是提供一個訪問元素的通用中間件。經過使用迭代器,使用者只須要用一種通用的格式就能夠遍歷訪問聚合類的元素,而實現者能夠自由的更改內部實現方法。大多數現代語言(Python、C#、Ruby)都使用了迭代器。這是一種有效的設計模式 (一種被普遍測試過的解決方案)。咱們在後面的課程中會看到不少其餘的設計模式。
MyIterator.next signature
迭代器的實現中使用到了實例方法(instance methods),實例方法是在一個實例化對象上進行操做的,它被調用時會傳入一個隱式的參數this
(就像Python中的self
同樣),經過這個this
該方法能夠訪問對象的數據(fields)。
咱們首先看看 MyIterator
中的 next
方法:
public class MyIterator { private final ArrayList<String> list; private int index; ... /** * Get the next element of the list. * Requires: hasNext() returns true. * Modifies: this iterator to advance it to the element * following the returned element. * @return next element of the list */ public String next() { final String element = list.get(index); ++index; return element; } }
next
的輸入是什麼類型?
[ ] void
– 沒有輸入
[ ] ArrayList
[x] MyIterator
[ ] String
[ ] boolean
[ ] int
next
的輸出是什麼類型?
[ ] void
– 沒有輸出
[ ] ArrayList
[ ] MyIterator
[x] String
[ ] boolean
[ ] int
MyIterator.next precondition
next
有前置條件 requires: hasNext() returns true.
next
的哪個輸入被這個前置條件所限制?
[ ] 都沒有被限制
[x] this
[ ] hasNext
[ ] element
當前置條件不知足時,實現的代碼能夠去作任何事。具體到咱們的實現中,若是前置條件不知足,代碼會有什麼行爲?
[ ] 返回 null
[ ] 返回列表中其餘的元素
[ ] 拋出一個已檢查異常
[x] 拋出一個非檢查異常
MyIterator.next postcondition
next
的一個後置條件是 @return next element of the list
.
next
的哪個輸出被這個後置條件所限制?
[ ] 都沒有被限制
[ ] this
[ ] hasNext
[x] 返回值
next
的另一個後置條件是 modifies: this iterator to advance it to the element following the returned element.
什麼會被這個後置條件所限制?
[ ] 都沒有被限制
[x] this
[ ] hasNext
[ ] 返回值
<br />
如今讓咱們試着將迭代器用於一個簡單的任務。假設咱們有一個MIT的課程代號列表,例如["6.031", "8.03", "9.00"]
,咱們想要設計一個 dropCourse6
方法,它會將列表中全部以「6.」開頭的代號刪除。根據以前所說的,咱們先寫出以下規格說明:
/** * Drop all subjects that are from Course 6. * Modifies subjects list by removing subjects that start with "6." * * @param subjects list of MIT subject numbers */ public static void dropCourse6(ArrayList<String> subjects)
注意到 dropCourse6
顯式的強調了它會對參數 subjects
作修改。
接下來,根據測試優先編程的原則,咱們對輸入空間進行分區,並寫出瞭如下測試用例:
// Testing strategy: // subjects.size: 0, 1, n // contents: no 6.xx, one 6.xx, all 6.xx // position: 6.xx at start, 6.xx in middle, 6.xx at end // Test cases: // [] => [] // ["8.03"] => ["8.03"] // ["14.03", "9.00", "21L.005"] => ["14.03", "9.00", "21L.005"] // ["2.001", "6.01", "18.03"] => ["2.001", "18.03"] // ["6.045", "6.031", "6.813"] => []
最後,咱們實現dropCourse6
方法:
public static void dropCourse6(ArrayList<String> subjects) { MyIterator iter = new MyIterator(subjects); while (iter.hasNext()) { String subject = iter.next(); if (subject.startsWith("6.")) { subjects.remove(subject); } } }
可是當咱們測試的時候,最後一個例子報錯了:
// dropCourse6(["6.045", "6.031", "6.813"]) // expected [], actual ["6.031"]
dropCourse6
彷佛沒有將列表中的元素清空,爲何?爲了追查bug是在哪發生的,咱們建議你畫出一個快照圖,並逐步模擬程序的運行。
Draw a snapshot diagram
如今畫出一個初始(代碼未執行)快照圖。你須要參考上面MyIterator
類和 dropCourse6()
方法的代碼實現。
在你的初始快照圖中有哪些標籤?
[ ] iter
[ ] index
[x] list
[x] subjects
[ ] subject
[x] ArrayList
[ ] List
[ ] MyIterator
[x] String
[ ] dropCourse6
如今執行第一條語句 MyIterator iter = new MyIterator(subjects);
,你的快照圖中又有哪些標籤?
[x] iter
[x] index
[x] list
[x] subjects
[ ] subject
[x] ArrayList
[ ] List
[x] MyIterator
[x] String
[ ] dropCourse6
Entering the loop
如今執行接下來的語句String subject = iter.next()
.,你的快照圖中添加了什麼東西?
[ ] 一個從 subject
到ArrayList 0
下標的箭頭
[ ] 一個從 subject
到ArrayList 1
下標的箭頭
[ ] 一個從index
到 0
的箭頭
[x] 一個從index
到 1
的箭頭
這個時候subject.startsWith("6.")
返回是什麼?
[x] 真,由於 subject
索引到了字符串 "6.045"
[ ] 真,由於 subject
索引到了字符串 "6.031"
[ ] 真,由於 subject
索引到了字符串 "6.813"
[ ] 假,由於 subject
索引到了其餘字符串
Remove an item
如今畫出在 subjects.remove(subject)
語句執行後的快照圖。
如今ArrayList subjects
是什麼樣子?
[ ] 下標0對應 "6.045"
[x] 下標0對應 "6.031"
[ ] 下標0對應 "6.813"
[ ] 沒有下標0
[ ] 下標1對應 "6.045"
[ ] 下標1對應 "6.031"
[x] 下標1對應 "6.813"
[ ] 沒有下標1
[ ] 下標2對應 "6.045"
[ ] 下標2對應 "6.031"
[ ] 下標2對應 "6.813"
[x] 沒有下標2
Next iteration of the loop
如今進行下一次循環,執行語句 iter.hasNext()
和String subject = iter.next()
,此時 subject.startsWith("6.")
的返回是什麼?
subject
索引到了字符串 "6.045"
subject
索引到了字符串 "6.031"
subject
索引到了字符串 "6.813"
subject
索引到了其餘字符串在這個測試用例中,哪個ArrayList中的元素永遠不會被 MyIterator.next()
返回?
[ ] "6.045"
[x] "6.031"
[ ] "6.813"
若是你想要解釋這個bug是如何發生的,如下哪一些聲明會出如今你的報告裏?
[x] list
和 subjects
是一對別名,它們都指向同一個 ArrayList
對象.
[x] 一個列表在程序的兩個地方被使用別名,當一個別名修改列表時,另外一個別名處不會被告知。
[ ] 代碼沒有檢查列表中奇數下標的元素。
[x] MyIterator
在迭代的時候是假設迭代對象不會發生更改的。
其實,這並非咱們設計的 MyIterator
帶來的bug。Java內置的 ArrayList
迭代器也會有這樣的問題,在使用for
遍歷循環這樣的語法糖是也會出現bug,只是表現形式不同,例如:
for (String subject : subjects) { if (subject.startsWith("6.")) { subjects.remove(subject); } }
這段代碼會拋出一個 ConcurrentModificationException
異常,由於這個迭代器檢測到了你在對迭代對象進行修改(你以爲它是怎麼檢測到的?)。
那麼應該怎修改這個問題呢?一個方法就是使用迭代器的 remove()
方法(而不是直接操做迭代對象),這樣迭代器就能自動調整迭代索引了:
Iterator iter = subjects.iterator(); while (iter.hasNext()) { String subject = iter.next(); if (subject.startsWith("6.")) { iter.remove(); } }
事實上,這樣作也會更有效率,由於 iter.remove()
知道要刪除的元素的位置,而 subjects.remove()
對整個聚合類進行一次搜索定位。
可是這並無徹底解決問題,**若是有另外一個迭代器並行對同一個列表進行迭代呢?**它們之間不會互相告知修改!
Pick a snapshot diagram
如下哪個快照圖描述了上面所述並行bug的發生?
[ ]
[ ]
[x]
[ ]
[ ]
<br />
這也是使用可變數據結構的一個基本問題。一個可變對象有多個索引(對於對象來講稱做「別名」)意味着在你程序的不一樣位置(可能分佈很廣)都依賴着這個對象保持不變。
爲了將這種限制放到規格說明中,規格不能只在一個地方出現,例如在使用者的類和實現者的類中都要有。如今程序正常運行依賴着每個索引可變對象的人遵照相應制約。
做爲這種非本地制約「契約」,想一想Java中的聚合類型,它們的文檔都清楚的寫出來使用者和實現者應該遵照的制約。試着找到它對使用者的制約——你不能在迭代一個聚合類時修改其自己。另外,這是哪一層類的責任?Iterator
? List
? Collection
? 你能找出來嗎?
同時,這樣的全局特性也會使得代碼更難讀懂,而且正確性也更難保證。但咱們不得不使用它——爲了性能或者方便——可是咱們也會爲安全性付出巨大的代價。
可變對象還會使得使用者和實現者之間的契約更加複雜,這減小了實現者和使用者改變代碼的自由度。這裏舉出了一個例子。
下面這個方法在MIT的數據庫中查找並返回用戶的9位數ID:
/** * @param username username of person to look up * @return the 9-digit MIT identifier for username. * @throws NoSuchUserException if nobody with username is in MIT's database */ public static char[] getMitId(String username) throws NoSuchUserException { // ... look up username in MIT's database and return the 9-digit ID }
假設有一個使用者:
char[] id = getMitId("bitdiddle"); System.out.println(id);
如今使用者和實現者都打算作一些改變: 使用者以爲要照顧用戶的隱私,因此他只輸出後四位ID:
char[] id = getMitId("bitdiddle"); for (int i = 0; i < 5; ++i) { id[i] = '*'; } System.out.println(id);
而實現者擔憂查找的性能,因此它引入了一個緩存記錄已經被查找過的用戶:
private static Map<String, char[]> cache = new HashMap<String, char[]>(); public static char[] getMitId(String username) throws NoSuchUserException { // see if it's in the cache already if (cache.containsKey(username)) { return cache.get(username); } // ... look up username in MIT's database ... // store it in the cache for future lookups cache.put(username, id); return id; }
這兩個改變致使了一個隱祕的bug。如上圖所示,當使用者查找 "bitdiddle"
並獲得一個字符數組後,實現者也緩存的是這個數組,他們兩個實際上索引的是同一個數組(別名)。這意味着用戶用來保護隱私的代碼會修改掉實現者的緩存,因此將來調用 getMitId("bitdiddle")
並不會返回一個九位數,例如 「928432033」 ,而是修改後的 「*****2033」。
**共享可變對象會增長契約的複雜度,**想一想,若是這個錯誤被交到了「軟件工程法庭」審判,哪個人會爲此承擔責任呢?是修改返回值的使用者?仍是沒有保存好返回值的實現者?
下面是一種寫規格說明的方法:
public static char[] getMitId(String username) throws NoSuchUserException - requires: nothing - effects: returns an array containing the 9-digit MIT identifier of username, or throws NoSuchUserException if nobody with username is in MIT’s database. Caller may never modify the returned array.
這是一個下下策這樣的制約要求使用者在程序中的全部位置都遵循不修改返回值的規定!而且這是很難保證的。
下面是另外一種寫規格說明的方法:
public static char[] getMitId(String username) throws NoSuchUserException - requires: nothing - effects: returns a new array containing the 9-digit MIT identifier of username, or throws NoSuchUserException if nobody with username is in MIT’s database.
這也沒有徹底解決問題. 雖然這個規格說明強調了返回的是一個新的數組,可是誰又知道實現者在緩存中不是也索引的這個新數組呢?若是是這樣,那麼用戶對這個新數組作的更改也會影響到將來的使用。This spec at least says that the array has to be fresh. But does it keep the implementer from holding an alias to that new array? Does it keep the implementer from changing that array or reusing it in the future for something else?
下面是一個好的多的規格說明:
public static String getMitId(String username) throws NoSuchUserException - requires: nothing - effects: returns the 9-digit MIT identifier of username, or throws NoSuchUserException if nobody with username is in MIT’s database.
經過使用不可變類型String,咱們能夠保證使用者和實現者的代碼不會互相影響。同時這也不依賴用戶認真閱讀遵照規格說明。不只如此,這樣的方法也給了實現者引入緩存的自由。
給出如下代碼:
public class Zoo { private List<String> animals; public Zoo(List<String> animals) { this.animals = animals; } public List<String> getAnimals() { return this.animals; } }
Aliasing 2
下面的輸出會是什麼?
List<String> a = new ArrayList<>(); a.addAll(Arrays.asList("lion", "tiger", "bear")); Zoo zoo = new Zoo(a); a.add("zebra"); System.out.println(a); System.out.println(zoo.getAnimals());
[x] ["lion", "tiger", "bear", "zebra"]
`["lion", "tiger", "bear", "zebra"]`
[ ] ["lion", "tiger", "bear", "zebra"]
`["zebra", "lion", "tiger", "bear", "zebra"]`
[ ] ["lion", "tiger", "bear"]
`["lion", "tiger", "bear", "zebra"]`
[ ] ["lion", "tiger", "bear", "zebra"]
`["lion", "tiger", "bear"]`
Aliasing 3
接着上面的問題,下面的輸出會是什麼?
List<String> b = zoo.getAnimals(); b.add("flamingo"); System.out.println(a);
[ ] ["lion", "tiger", "bear"]
[ ] ["lion", "tiger", "bear", "zebra"]
[x] ["lion", "tiger", "bear", "zebra", "flamingo"]
[ ] ["lion", "tiger", "bear", "flamingo"]
<br />
既然不可變類型避開了許多危險,咱們就列出幾個Java API中經常使用的不可變類型:
全部的原始類型及其包裝都是不可變的。例如使用BigInteger
和 BigDecimal
進行大整數運算。
不要使用可變類型 Date
,而是使用 java.time
中的不可變類型。
Java中常見的聚合類 — List
, Set
, Map
— 都是可變的:ArrayList
, HashMap
等等。可是 Collections
類中提供了能夠得到不可修改版本(unmodifiable views)的方法:
你能夠將這些不可修改版本當作是對list/set/map作了一下包裝。若是一個使用者索引的是包裝以後的對象,那麼 add
, remove
, put
這些修改就會觸發 UnsupportedOperationException
異常。
當咱們要向程序另外一部分傳入可變對象前,能夠先用上述方法將其包裝。要注意的是,這僅僅是一層包裝,若是你不當心讓別人或本身使用了底層可變對象的索引,這些看起來不可變對象仍是會發生變化!
Collections
也提供了獲取不可變空聚合類型對象的方法,例如Collections.emptyList
給出如下代碼:
List<String> arraylist = new ArrayList<>(); arraylist.add("hello"); List<String> unmodlist = Collections.unmodifiableList(arraylist); // unmodlist should now always be [ "hello" ]
Unmodifiable
會出現什麼類型的錯誤?
unmodlist.add("goodbye"); System.out.println(unmodlist);
動態錯誤
Unmodifiable?
輸出是什麼?
arraylist.add("goodbye"); System.out.println(unmodlist);
[ 「hello」 「goodbye」 ]
Immutability
如下哪些選項是正確的?
[ ] 若是一個類的全部索引都被final修飾,它就是不可變的
[x] 若是一個類的全部實例化數據都不會改變,它就是不可變的
[x] 不可變類型的數據能夠被安全的共享
[ ] 經過使用防護性複製,咱們可讓對象變成不可變的
[ ] 不可變性使得咱們能夠關注於全局而非局部代碼
<br />
在這篇閱讀中,咱們看到了利用可變性帶來的性能優點和方便,可是它也會產生不少風險,使得代碼必須考慮全局的行爲,極大的增長了規格說明設計的複雜性和代碼編寫、測試的難度。
確保你已經理解了不可變對象(例如String
)和不可變索引(例如 final
變量)的區別。畫快照圖可以幫助你理解這些概念:其中對象用圓圈表示,若是是不可變對象,圓圈有兩層;索引用一個箭頭表示,若是索引是不可變的,用雙箭頭表示。
本文最重要的一個設計原則就是不變性 :儘可能使用不可變類型和不可變索引。接下來咱們仍是將本文的知識點和咱們的三個目標聯繫起來:
</font>