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