软件构造第一篇博客(“可变形与不可变性”)
回憶之前我們討論過的“用快照圖理解值與對象”(譯者注:“Java基礎(chǔ)”),有一些對象的內(nèi)容是不變的(immutable):一旦它們被創(chuàng)建,它們總是表示相同的值。另一些對象是可變的(mutable):它們有改變內(nèi)部值對應(yīng)的方法。
String?就是不變對象的一個例子,一個String?對象總是表示相同的字符串。而StringBuilder?則是可變的,它有對應(yīng)的方法來刪除、插入、替換字符串內(nèi)部的字符,等等。
因?yàn)?String?是不變的,一旦被創(chuàng)建,一個?String?對象總是有一樣的值。為了在一個?String?對象字符串后加上另一個字符串,你必須創(chuàng)建一個新的?String?對象:
String s = "a"; s = s.concat("b"); // s+="b" and s=s+"b" also mean the same thing與此相對,?StringBuilder?對象是可變的。這個類有對應(yīng)的方法來改變對象,而不是返回一個新的對象:
StringBuilder sb = new StringBuilder("a"); sb.append("b");所以這有什么關(guān)系呢?在上面這兩個例子中,我們最終都讓s和sb索引到了"ab"?。當(dāng)對象的索引只有一個時,它們兩確實(shí)沒什么去唄。但是當(dāng)有別的索引指向同一個對象時,它們的行為會大不相同。例如,當(dāng)另一個變量t指向s對應(yīng)的對象,tb指向sb對應(yīng)的對象,這個時候?qū)和tb做更改就會導(dǎo)致不同的結(jié)果:
String t = s; t = t + "c";StringBuilder tb = sb; tb.append("c");可以看到,改變t并沒有對s產(chǎn)生影響,但是改變tb確實(shí)影響到了sb?——這可能會讓編程者驚訝一下(如果他沒有注意的話)。這也是下面我們會重點(diǎn)討論的問題。
既然我們已經(jīng)有了不變的?String?類,為什么還要使用可變的?StringBuilder?類呢?一個常見的使用環(huán)境就是當(dāng)你要同時創(chuàng)建大量的字符串,例如:
String s = ""; for (int i = 0; i < n; ++i) {s = s + i; }如果使用不變的字符串,這會發(fā)生很多“暫時拷貝”——第一個字符“0”實(shí)際上就被拷貝了n次,第二個字符被拷貝了n-1次,等等。總的來說,它會花費(fèi)O(N^2)的時間來做拷貝,即使最終我們的字符串只有n個字符。
StringBuilder?的設(shè)計就是為了最小化這樣的拷貝,它使用了簡單但是聰明的內(nèi)部結(jié)構(gòu)避免了做任何拷貝(除非到了極限情況)。如果你使用StringBuilder?,可以在最后用?toString()?方法得到一個String的結(jié)果:
StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; ++i) {sb.append(String.valueOf(i)); } String s = sb.toString();優(yōu)化性能是我們使用可變對象的原因之一。另一個原因是為了分享:程序中的兩個地方的代碼可以通過共享一個數(shù)據(jù)結(jié)構(gòu)進(jìn)行交流。
閱讀小練習(xí)
Follow me
一個?terrarium?的使用者可以更改紅色的?Turtle?對象嗎?
-
[ ] 不能,因?yàn)榈?terrarium?的索引是不變的
-
[x] 不能,因?yàn)?Turtle?對象是不變的
-
[ ] 可以,因?yàn)閺牧斜淼?下標(biāo)處到?Turtle?的索引是可變的。
-
[ ] 可以,因?yàn)?Turtle?對象是可變的
一個?george?的使用者可以更改藍(lán)色的?Gecko?對象嗎?
-
[ ] 不能,因?yàn)榈絞eorge?的索引是不變的
-
[x] 不能,因?yàn)?Gecko?對象是不變的
-
[ ] 可以,因?yàn)閺牧斜淼?下標(biāo)處到?Gecko?的索引是可變的。
-
[ ] 可以,因?yàn)?Gecko?對象是可變的
一個?petStore?的使用者可以使得另一個?terrarium?的使用者無法訪問藍(lán)色的?Gecko?對象嗎?選出最好的答案
-
[ ] 不能,因?yàn)榈?terrarium?的索引是不變的
-
[ ] 不能,因?yàn)?Gecko?對象是不變的
-
[ ] 可以,因?yàn)榈?petStore?的索引是可變的
-
[ ] 可以,因?yàn)?PetStore?對象是可變的
-
[x] 可以,因?yàn)?List?對象是可變的
-
[ ] 可以,因?yàn)閺牧斜淼?下標(biāo)處到?Gecko?的索引是可變的。
可變性帶來的風(fēng)險
可變的類型看起來比不可變類型強(qiáng)大的多。如果你在“數(shù)據(jù)類型商場”購物,為什么要選擇“無聊的”不可變類型而放棄強(qiáng)大的可變類型呢?例如?StringBuilder?應(yīng)該可以做任何?String?可以做的事情,加上?set()?和?append()?這些功能。
答案是使用不可變類型要比可變類型安全的多,同時也會讓代碼更易懂、更具備可改動性。可變性會使得別人很難知道你的代碼在干嗎,也更難制定開發(fā)規(guī)定(例如規(guī)格說明)。這里舉出了兩個例子:
#1: 傳入可變對象
下面這個方法將列表中的整數(shù)相加求和:
/** @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; }假設(shè)現(xiàn)在我們要創(chuàng)建另外一個方法,這個方法將列表中數(shù)的絕對值相加,根據(jù)DRY原則(Don’t Repeat Yourself),實(shí)現(xiàn)者寫了一個利用?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 valuesfor (int i = 0; i < list.size(); ++i)list.set(i, Math.abs(list.get(i)));return sum(list); }注意到這個方法直接改變了數(shù)組?—— 這對實(shí)現(xiàn)者來說很合理,因?yàn)槔靡粋€已經(jīng)存在的列表會更有效率。如果這個列表有幾百萬個元素,那么你節(jié)省內(nèi)存的同時也節(jié)省了大量時間。所以實(shí)現(xiàn)者的理由很充分:DRY與性能。
但是使用者可能會對結(jié)果很驚奇,例如:
// 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)); }閱讀小練習(xí)
Risky #1
上面的代碼會打印出哪兩個數(shù)?
10
10
讓我們想想這個問題的關(guān)鍵點(diǎn):
- 遠(yuǎn)離bug?在這個例子中,很容易就會把指責(zé)轉(zhuǎn)向?sum-Absolute()?的實(shí)現(xiàn)者,因?yàn)樗赡苓`背了規(guī)格說明。但是,傳入可變對象真的(可能)會導(dǎo)致隱秘的bug。只要有一個程序員不小心將這個傳入的列表更改了(例如為了復(fù)用或性能),程序就可能會出錯,而且bug很難追查。
- 易懂嗎?當(dāng)閱讀?main()的時候,你會對?sum()?和?sum-Absolute()做出哪些假設(shè)?對于讀者來說,他能清晰的知道?myData?會被更改嗎?
#2: 返回可變對象
我們剛剛看到了傳入可變對象可能會導(dǎo)致問題。那么返回一個可變對象呢?
Date是一個Java內(nèi)置的類, 同時?Date也正好是一個可變類型。假設(shè)我們寫了一個判斷春天的第一天的方法:
/** @return the first day of spring this year */ public static Date startOfSpring() {return askGroundhog(); }這里我們使用了有名的土撥鼠算法 (Harold Ramis, Bill Murray, et al.?Groundhog Day, 1993).
現(xiàn)在使用者用這個方法來計劃他們的派對開始時間:
// somewhere else in the code... public static void partyPlanning() {Date partyDate = startOfSpring();// ... }這段代碼工作的很好。不過過了一段時間,startOfSpring()的實(shí)現(xiàn)者發(fā)現(xiàn)“土撥鼠”被問的不耐煩了,于是打算重寫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修飾符,你認(rèn)為它是全局變量嗎?)
另外,有一個使用者覺得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,你知道為什么嗎?)
這兩個改動發(fā)生后,你覺得程序會出現(xiàn)什么問題?更糟糕的是,誰會先發(fā)現(xiàn)這個bug呢?是這個?startOfSpring()?,還是?partyPlanning()?? 或是在另一個地方使用?startOfSpring()的無辜者?
Risky #2
我們不知道Date具體是怎么存儲月份的,所以這里用抽象的值?...march...?和?...april...?表示,Date中有一個mounth索引到這些值上。
以下哪一個快照圖表現(xiàn)了上文中的bug?
-
[ ]?
-
[ ]?
-
[ ]?
-
[x]?
-
[ ]?
Understanding risky example #2
partyPlanning?在不知不覺中修改了春天的起始位置,因?yàn)?partyDate?和?groundhogAnswer?指向了同一個可變Date?對象 。
更糟糕的是,這個bug可能不會在這里的?partyPlanning()?或?startOfSpring()?中出現(xiàn)。而是在另外一個調(diào)用?startOfSpring()的地方出現(xiàn),得到一個錯誤的值然后繼續(xù)進(jìn)行運(yùn)算。
上文中的緩存?groundhogAnswer?是全局變量嗎?
-
[ ] 是全局變量,這是合理的
-
[ ] 是全局變量,這是不合理的
-
[x] 不是全局變量
A second bug
上文中的代碼在加上1月的時候存在另一個bug,請閱讀?Java API documentation for?Date.getMonth?和?setMonth.
對于?partyDate.getMonth()?,它的哪一個返回值會導(dǎo)致bug的發(fā)生?
11
NoSuchMonthException
上面關(guān)于?Date.setMonth?文檔中說:?month: the month value between 0-11.那么當(dāng)這個bug觸發(fā)的時候可能會發(fā)生什么?
-
[x] 這個方法不會做任何事情
-
[x] 這個方法會按照我們原本的想法運(yùn)行
-
[x] 這個方法會使得?Date?對象不可用,并報告一個錯誤的值
-
[ ] 這個方法會拋出一個已檢查異常
-
[x] 這個方法會拋出一個未檢查異常
-
[x] 這個方法會將時間設(shè)置為9/9/99
-
[x] 這個方法會使得其他的?Date?對象也不可用
-
[x] 這個方法永遠(yuǎn)不會返回
SuchTerribleSpecificationsException
在關(guān)于?Date?的文檔中,有一句話是這樣說的,“傳入方法的參數(shù)并不一定要落在指定的區(qū)域內(nèi),例如傳入1月32號意味著2月1號”。
這看起來像是前置條件...但它不是的!
下面哪一個選項(xiàng)表現(xiàn)了Date這個特性是不合理的?
- [ ] 不要寫重復(fù)的代碼 (DRY)
- [x] 快速失敗/報錯
- [ ] 土撥鼠算法
- [ ] 使用異常報告特殊結(jié)果
- [ ] 使用前置條件限制使用者
?
關(guān)鍵點(diǎn):
- 遠(yuǎn)離bug??沒有,我們產(chǎn)生了一個隱晦的bug。
- 可改動??很顯然,這里的可改動指的是我們可以改動一部分代碼而不用擔(dān)心其他代碼的改動,而不是可變對象本身的可改動性。在上面的例子中,我們在程序的兩個地方做了改變,結(jié)果導(dǎo)致了一個隱晦的bug。
在上面舉出的兩個例子(?List<Integer>?和?Date?)中,如果我們采用不可變對象,這些問題就迎刃而解了——這些bug在設(shè)計上就不可能發(fā)生。
事實(shí)上,你絕對不應(yīng)該使用Date?!而是使用 包?java.time:?LocalDateTime,?Instant, 等等這些類,它們規(guī)格說明都保證了對象是不可變的。
這個例子也說明了使用可變對象可能會導(dǎo)致性能上的損失。因?yàn)闉榱嗽诓恍薷囊?guī)格說明和接口的前提下避開這個bug,我們必須讓startOfSpring()?返回一個復(fù)制品:
return new Date(groundhogAnswer.getTime());這樣的模式稱為防御性復(fù)制?,我們在后面講抽象數(shù)據(jù)類型的時候會講解更多關(guān)于防御性復(fù)制的東西。這樣的方法意味著?partyPlanning()?可以自由的操控startOfSpring()的返回值而不影響其中的緩存。但是防御性復(fù)制會強(qiáng)制要求?startOfSpring()?為每一個使用者復(fù)制相同數(shù)據(jù)——即使99%的內(nèi)容使用者都不會更改,這會很浪費(fèi)空間和時間。相反,如果我們使用不可變類型,不同的地方用不同的對象來表示,相同的地方都索引到內(nèi)存中同一個對象,這樣會讓程序節(jié)省空間和復(fù)制的時間。所以說,合理利用不變性對象(譯者注:大多是有多個變量索引的時候)的性能比使用可變性對象的性能更好。
別名會讓可變類型存在風(fēng)險
事實(shí)上,如果你只在一個方法內(nèi)使用可變類型而且該類型的對象只有一個索引,這時并不會有什么風(fēng)險。而上面的例子告訴我們,如果一個可變對象有多個變量索引到它——這也被稱作“別名”,這時就會有產(chǎn)生bug的風(fēng)險。
閱讀小練習(xí)
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"]`
現(xiàn)在試著使用快照圖將上面的兩個例子過一遍,這里只列出一個輪廓:
- 在?List?例子中,一個相同的列表被list(在?sum?和?sumAbsolute中)和myData(在main中)同時索引。一個程序員(sumAbsolute的)認(rèn)為更改這個列表是ok的;另一個程序員(main)希望列表保持原樣。由于別名的使用,main的程序員得到了一個錯誤的結(jié)果。
- 而在Date的例子中,有兩個變量?groundhogAnswer?和?partyDate索引到同一個Date對象。這兩個別名出現(xiàn)在程序的不同地方,所以不同的程序員很難知道別人會對這個Date對象做哪些改變。
先在紙上畫出快照圖,但是你真正的目標(biāo)應(yīng)該是在腦海中構(gòu)建一個快照圖,這樣以后你在看代碼的時候也能將其“視覺化”。
?
更改參數(shù)對象的(mutating)方法的規(guī)格說明
從上面的分析來看,我們必須使用之前提到過的格式對那些會更改參數(shù)對象的方法寫上特定的規(guī)格說明。
下面是一個會更改參數(shù)對象的方法:
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()而這個是一個不會更改參數(shù)對象的方法:
static List<String> toLowerCase(List<String> lst) - requires:nothing - effects:returns a new list t where t[i] = lst[i].toLowerCase()如果在effects內(nèi)沒有顯式強(qiáng)調(diào)輸入?yún)?shù)會被更改,在本門課程中我們會認(rèn)為方法不會修改輸入?yún)?shù)。事實(shí)上,這也是一個編程界的一個約定俗成的規(guī)則。
?
對列表和數(shù)組進(jìn)行迭代
接下來我們會看看另一個可變對象——迭代器?。迭代器會嘗試遍歷一個聚合類型的對象,并逐個返回其中的元素。當(dāng)你在Java中使用for (... : ...)?這樣的遍歷元素的循環(huán)時,其實(shí)就隱式的使用了迭代器。例如:
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()?測試迭代器是否已經(jīng)遍歷到聚合類型對象的結(jié)尾
注意到next()?是一個會修改迭代器的方法(mutator?method),它不僅會返回一個元素,而且會改變內(nèi)部狀態(tài),使得下一次使用它的時候會返回下一個元素。
感興趣的話,你可以讀讀Java API中關(guān)于迭代器的定義?.
MyIterator
為了更好的理解迭代器是如何工作的,這里有一個ArrayList<String>迭代器的簡單實(shí)現(xiàn):
/*** 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的特性,例如構(gòu)造體,static和final變量等等,你應(yīng)該確保自己已經(jīng)理解了這些特性。參考:?From Python to Java?或?Classes and Objects?in the Java Tutorials
上圖畫出了?MyIterator?初始狀態(tài)的快照圖。
注意到我們將list的索引用雙箭頭表示,以此表示這是一個不能更改的final索引。但是list索引的?ArrayList?本身是一個可變對象——內(nèi)部的元素可以被改變——將list聲明為final并不能阻止這種改變。
那么為什么要使用迭代器呢?因?yàn)椴煌木酆项愋推鋬?nèi)部實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)都不相同(例如連接鏈表、哈希表、映射等等),而迭代器的思想就是提供一個訪問元素的通用中間件。通過使用迭代器,使用者只需要用一種通用的格式就可以遍歷訪問聚合類的元素,而實(shí)現(xiàn)者可以自由的更改內(nèi)部實(shí)現(xiàn)方法。大多數(shù)現(xiàn)代語言(Python、C#、Ruby)都使用了迭代器。這是一種有效的設(shè)計模式?(一種被廣泛測試過的解決方案)。我們在后面的課程中會看到很多其他的設(shè)計模式。
閱讀小練習(xí)
MyIterator.next signature
迭代器的實(shí)現(xiàn)中使用到了實(shí)例方法(instance methods),實(shí)例方法是在一個實(shí)例化對象上進(jìn)行操作的,它被調(diào)用時會傳入一個隱式的參數(shù)this?(就像Python中的self一樣),通過這個this該方法可以訪問對象的數(shù)據(jù)(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
當(dāng)前置條件不滿足時,實(shí)現(xiàn)的代碼可以去做任何事。具體到我們的實(shí)現(xiàn)中,如果前置條件不滿足,代碼會有什么行為?
-
[ ] 返回?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
-
[ ] 返回值
可變性對迭代器的損害
現(xiàn)在讓我們試著將迭代器用于一個簡單的任務(wù)。假設(shè)我們有一個MIT的課程代號列表,例如["6.031", "8.03", "9.00"]?,我們想要設(shè)計一個?dropCourse6?方法,它會將列表中所有以“6.”開頭的代號刪除。根據(jù)之前所說的,我們先寫出如下規(guī)格說明:
/*** 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?顯式的強(qiáng)調(diào)了它會對參數(shù)?subjects?做修改。
接下來,根據(jù)測試優(yōu)先編程的原則,我們對輸入空間進(jìn)行分區(qū),并寫出了以下測試用例:
// 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"] => []最后,我們實(shí)現(xiàn)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);}} }但是當(dāng)我們測試的時候,最后一個例子報錯了:
// dropCourse6(["6.045", "6.031", "6.813"]) // expected [], actual ["6.031"]dropCourse6?似乎沒有將列表中的元素清空,為什么?為了追查bug是在哪發(fā)生的,我們建議你畫出一個快照圖,并逐步模擬程序的運(yùn)行。
閱讀小練習(xí)
Draw a snapshot diagram
現(xiàn)在畫出一個初始(代碼未執(zhí)行)快照圖。你需要參考上面MyIterator?類和?dropCourse6()?方法的代碼實(shí)現(xiàn)。
在你的初始快照圖中有哪些標(biāo)簽?
-
[ ]?iter
-
[ ]?index
-
[x]?list
-
[x]?subjects
-
[ ]?subject
-
[x]?ArrayList
-
[ ]?List
-
[ ]?MyIterator
-
[x]?String
-
[ ]?dropCourse6
現(xiàn)在執(zhí)行第一條語句?MyIterator iter = new MyIterator(subjects);?,你的快照圖中又有哪些標(biāo)簽?
-
[x]?iter
-
[x]?index
-
[x]?list
-
[x]?subjects
-
[ ]?subject
-
[x]?ArrayList
-
[ ]?List
-
[x]?MyIterator
-
[x]?String
-
[ ]?dropCourse6
Entering the loop
現(xiàn)在執(zhí)行接下來的語句String subject = iter.next().,你的快照圖中添加了什么東西?
-
[ ] 一個從?subject?到ArrayList?0?下標(biāo)的箭頭
-
[ ] 一個從?subject?到ArrayList?1?下標(biāo)的箭頭
-
[ ] 一個從index?到?0?的箭頭
-
[x] 一個從index?到?1?的箭頭
這個時候subject.startsWith("6.")?返回是什么?
-
[x] 真,因?yàn)?subject?索引到了字符串?"6.045"
-
[ ] 真,因?yàn)?subject?索引到了字符串?"6.031"
-
[ ] 真,因?yàn)?subject?索引到了字符串?"6.813"
-
[ ] 假,因?yàn)?subject?索引到了其他字符串
Remove an item
現(xiàn)在畫出在?subjects.remove(subject)語句執(zhí)行后的快照圖。
現(xiàn)在ArrayList?subjects?是什么樣子?
-
[ ] 下標(biāo)0對應(yīng)?"6.045"
-
[x] 下標(biāo)0對應(yīng)?"6.031"
-
[ ] 下標(biāo)0對應(yīng)?"6.813"
-
[ ] 沒有下標(biāo)0
-
[ ] 下標(biāo)1對應(yīng)?"6.045"
-
[ ] 下標(biāo)1對應(yīng)?"6.031"
-
[x] 下標(biāo)1對應(yīng)?"6.813"
-
[ ] 沒有下標(biāo)1
-
[ ] 下標(biāo)2對應(yīng)?"6.045"
-
[ ] 下標(biāo)2對應(yīng)?"6.031"
-
[ ] 下標(biāo)2對應(yīng)?"6.813"
-
[x] 沒有下標(biāo)2
Next iteration of the loop
現(xiàn)在進(jìn)行下一次循環(huán),執(zhí)行語句?iter.hasNext()?和String subject = iter.next()?,此時?subject.startsWith("6.")?的返回是什么?
- [ ] 真,因?yàn)?subject?索引到了字符串?"6.045"
- [ ] 真,因?yàn)?subject?索引到了字符串?"6.031"
- [x] 真,因?yàn)?subject?索引到了字符串?"6.813"
- [ ] 假,因?yàn)?subject?索引到了其他字符串
在這個測試用例中,哪一個ArrayList中的元素永遠(yuǎn)不會被?MyIterator.next()?返回?
-
[ ]?"6.045"
-
[x]?"6.031"
-
[ ]?"6.813"
如果你想要解釋這個bug是如何發(fā)生的,以下哪一些聲明會出現(xiàn)在你的報告里?
-
[x]?list?和?subjects?是一對別名,它們都指向同一個?ArrayList?對象.
-
[x] 一個列表在程序的兩個地方被使用別名,當(dāng)一個別名修改列表時,另一個別名處不會被告知。
-
[ ] 代碼沒有檢查列表中奇數(shù)下標(biāo)的元素。
-
[x]?MyIterator?在迭代的時候是假設(shè)迭代對象不會發(fā)生更改的。
其實(shí),這并不是我們設(shè)計的?MyIterator帶來的bug。Java內(nèi)置的?ArrayList?迭代器也會有這樣的問題,在使用for遍歷循環(huán)這樣的語法糖是也會出現(xiàn)bug,只是表現(xiàn)形式不一樣,例如:
for (String subject : subjects) {if (subject.startsWith("6.")) {subjects.remove(subject);} }這段代碼會拋出一個?Concurrent-Modification-Exception異常,因?yàn)檫@個迭代器檢測到了你在對迭代對象進(jìn)行修改(你覺得它是怎么檢測到的?)。
那么應(yīng)該怎修改這個問題呢?一個方法就是使用迭代器的?remove()?方法(而不是直接操作迭代對象),這樣迭代器就能自動調(diào)整迭代索引了:
Iterator iter = subjects.iterator(); while (iter.hasNext()) {String subject = iter.next();if (subject.startsWith("6.")) {iter.remove();} }事實(shí)上,這樣做也會更有效率,因?yàn)?iter.remove()?知道要刪除的元素的位置,而?subjects.remove()?對整個聚合類進(jìn)行一次搜索定位。
但是這并沒有完全解決問題,如果有另一個迭代器并行對同一個列表進(jìn)行迭代呢?它們之間不會互相告知修改!
閱讀小練習(xí)
Pick a snapshot diagram
以下哪一個快照圖描述了上面所述并行bug的發(fā)生?
-
[ ]?
-
[ ]?
-
[x]?
-
[ ]?
-
[ ]?
?
變化與契約(contract)
可變對象會使得契約(例如規(guī)格說明)變得復(fù)雜
這也是使用可變數(shù)據(jù)結(jié)構(gòu)的一個基本問題。一個可變對象有多個索引(對于對象來說稱作“別名”)意味著在你程序的不同位置(可能分布很廣)都依賴著這個對象保持不變。
為了將這種限制放到規(guī)格說明中,規(guī)格不能只在一個地方出現(xiàn),例如在使用者的類和實(shí)現(xiàn)者的類中都要有。現(xiàn)在程序正常運(yùn)行依賴著每一個索引可變對象的人遵守相應(yīng)制約。
作為這種非本地制約“契約”,想想Java中的聚合類型,它們的文檔都清楚的寫出來使用者和實(shí)現(xiàn)者應(yīng)該遵守的制約。試著找到它對使用者的制約——你不能在迭代一個聚合類時修改其本身。另外,這是哪一層類的責(zé)任?Iterator??List??Collection? 你能找出來嗎?
同時,這樣的全局特性也會使得代碼更難讀懂,并且正確性也更難保證。但我們不得不使用它——為了性能或者方便——但是我們也會為安全性付出巨大的代價。
可變對象降低了代碼的可改動性
可變對象還會使得使用者和實(shí)現(xiàn)者之間的契約更加復(fù)雜,這減少了實(shí)現(xiàn)者和使用者改變代碼的自由度。這里舉出了一個例子。
下面這個方法在MIT的數(shù)據(jù)庫中查找并返回用戶的9位數(shù)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 }假設(shè)有一個使用者:
char[] id = getMitId("bitdiddle"); System.out.println(id);現(xiàn)在使用者和實(shí)現(xiàn)者都打算做一些改變:?使用者覺得要照顧用戶的隱私,所以他只輸出后四位ID:
char[] id = getMitId("bitdiddle"); for (int i = 0; i < 5; ++i) {id[i] = '*'; } System.out.println(id);而實(shí)現(xiàn)者擔(dān)心查找的性能,所以它引入了一個緩存記錄已經(jīng)被查找過的用戶:
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 alreadyif (cache.containsKey(username)) {return cache.get(username);}// ... look up username in MIT's database ...// store it in the cache for future lookupscache.put(username, id);return id; }這兩個改變導(dǎo)致了一個隱秘的bug。如上圖所示,當(dāng)使用者查找?"bitdiddle"?并得到一個字符數(shù)組后,實(shí)現(xiàn)者也緩存的是這個數(shù)組,他們兩個實(shí)際上索引的是同一個數(shù)組(別名)。這意味著用戶用來保護(hù)隱私的代碼會修改掉實(shí)現(xiàn)者的緩存,所以未來調(diào)用?getMitId("bitdiddle")?并不會返回一個九位數(shù),例如 “928432033” ,而是修改后的 “*****2033”。
共享可變對象會增加契約的復(fù)雜度,想想,如果這個錯誤被交到了“軟件工程法庭”審判,哪一個人會為此承擔(dān)責(zé)任呢?是修改返回值的使用者?還是沒有保存好返回值的實(shí)現(xiàn)者?
下面是一種寫規(guī)格說明的方法:
public static char[] getMitId(String username) throws NoSuchUserException - requires:nothing - effects:returns an array containing the 9-digit MIT identifier of username, or throws NoSuchUser-Exception if nobody with username is in MIT’s database. Caller may never modify the returned array.這是一個下下策這樣的制約要求使用者在程序中的所有位置都遵循不修改返回值的規(guī)定!并且這是很難保證的。
下面是另一種寫規(guī)格說明的方法:
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 NoSuchUser-Exception if nobody with username is in MIT’s database.這也沒有完全解決問題. 雖然這個規(guī)格說明強(qiáng)調(diào)了返回的是一個新的數(shù)組,但是誰又知道實(shí)現(xiàn)者在緩存中不是也索引的這個新數(shù)組呢?如果是這樣,那么用戶對這個新數(shù)組做的更改也會影響到未來的使用。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?
下面是一個好的多的規(guī)格說明:
public static String getMitId(String username) throws NoSuchUserException - requires:nothing - effects:returns the 9-digit MIT identifier of username, or throws NoSuchUser-Exception if nobody with username is in MIT’s database.通過使用不可變類型String,我們可以保證使用者和實(shí)現(xiàn)者的代碼不會互相影響。同時這也不依賴用戶認(rèn)真閱讀遵守規(guī)格說明。不僅如此,這樣的方法也給了實(shí)現(xiàn)者引入緩存的自由。
閱讀小練習(xí)
給出以下代碼:
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"]
有用的不可變類型
既然不可變類型避開了許多危險,我們就列出幾個Java API中常用的不可變類型:
-
所有的原始類型及其包裝都是不可變的。例如使用BigInteger和?BigDecimal?進(jìn)行大整數(shù)運(yùn)算。
-
不要使用可變類型?Date?,而是使用?java.time?中的不可變類型。
-
Java中常見的聚合類 —?List,?Set,?Map?— 都是可變的:ArrayList,?HashMap等等。但是?Collections?類中提供了可以獲得不可修改版本(unmodifiable views)的方法:
- Collections.unmodifiableList
- Collections.unmodifiableSet
- Collections.unmodifiableMap
你可以將這些不可修改版本當(dāng)做是對list/set/map做了一下包裝。如果一個使用者索引的是包裝之后的對象,那么?add,?remove,?put這些修改就會觸發(fā)?Unsupported-Operation-Exception異常。
當(dāng)我們要向程序另一部分傳入可變對象前,可以先用上述方法將其包裝。要注意的是,這僅僅是一層包裝,如果你不小心讓別人或自己使用了底層可變對象的索引,這些看起來不可變對象還是會發(fā)生變化!
-
Collections?也提供了獲取不可變空聚合類型對象的方法,例如Collections.emptyList
閱讀小練習(xí)
給出以下代碼:
List<String> arraylist = new ArrayList<>(); arraylist.add("hello"); List<String> unmodlist = Collections.unmodifiableList(arraylist); // unmodlist should now always be [ "hello" ]Unmodifiable
會出現(xiàn)什么類型的錯誤?
unmodlist.add("goodbye"); System.out.println(unmodlist);動態(tài)錯誤
Unmodifiable?
輸出是什么?
arraylist.add("goodbye"); System.out.println(unmodlist);[ “hello” “goodbye” ]
Immutability
以下哪些選項(xiàng)是正確的?
-
[ ] 如果一個類的所有索引都被final修飾,它就是不可變的
-
[x] 如果一個類的所有實(shí)例化數(shù)據(jù)都不會改變,它就是不可變的
-
[x] 不可變類型的數(shù)據(jù)可以被安全的共享
-
[ ] 通過使用防御性復(fù)制,我們可以讓對象變成不可變的
-
[ ] 不可變性使得我們可以關(guān)注于全局而非局部代碼
?
總結(jié)
在這篇閱讀中,我們看到了利用可變性帶來的性能優(yōu)勢和方便,但是它也會產(chǎn)生很多風(fēng)險,使得代碼必須考慮全局的行為,極大的增加了規(guī)格說明設(shè)計的復(fù)雜性和代碼編寫、測試的難度。
確保你已經(jīng)理解了不可變對象(例如String)和不可變索引(例如?final?變量)的區(qū)別。畫快照圖能夠幫助你理解這些概念:其中對象用圓圈表示,如果是不可變對象,圓圈有兩層;索引用一個箭頭表示,如果索引是不可變的,用雙箭頭表示。
本文最重要的一個設(shè)計原則就是不變性?:盡量使用不可變類型和不可變索引。接下來我們還是將本文的知識點(diǎn)和我們的三個目標(biāo)聯(lián)系起來:
- 遠(yuǎn)離bug.不可變對象不會因?yàn)閯e名的使用導(dǎo)致bug,而不可變索引永遠(yuǎn)指向同一個對象,也會減少bug的發(fā)生。
- 易于理解. 因?yàn)椴豢勺儗ο蠛退饕偸且馕吨蛔兊臇|西,所以它們對于讀者來說會更易懂——不用一邊讀代碼一邊考慮這個時候?qū)ο蠡蛩饕l(fā)生了哪些改動。
- 可改動性. 如果一個對象或者索引不會在運(yùn)行時發(fā)生改變,那么依賴于這些對象的代碼就不用在其他代碼更改后進(jìn)行審查。
?
參考:HIT-李秋豪,MIT
總結(jié)
以上是生活随笔為你收集整理的软件构造第一篇博客(“可变形与不可变性”)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 持久_Java持久锁总结 -解
- 下一篇: spring cloud gateway