問(wèn)題引出
最近接觸到一個(gè)關(guān)于排序的問(wèn)題:模擬奧運(yùn)會(huì)統(tǒng)計(jì)對(duì)每個(gè)國(guó)家獲取的獎(jiǎng)牌數(shù)進(jìn)行排序,排序規(guī)則:排序順序依次為金牌炸庞,銀牌呕乎,銅牌的數(shù)量,若金牌的數(shù)量相等則比較銀牌惩猫,銀牌的數(shù)量相等芝硬,則比較銅牌,若三種獎(jiǎng)牌數(shù)量都一樣帆锋,則根據(jù)國(guó)家的英文字符排序吵取,最終輸出排序后的國(guó)家名稱:
? ? ? ??其實(shí)也不是一個(gè)很復(fù)雜的問(wèn)題禽额;第一時(shí)間想到j(luò)ava8提供的流操作锯厢,直使用sorted方法直接排序,可以同時(shí)支持多個(gè)字段的排序脯倒。代碼如下:
public static void sortedCountry(List<CountryInfo> countryInfos) {
countryInfos.parallelStream().sorted(Comparator
.comparingInt(CountryInfo::getGold)
.thenComparingInt(CountryInfo::getSilver)
.thenComparingInt(CountryInfo::getBronze)
.thenComparing((a, b) -> b.getCountry().compareTo(a.getCountry()))
.reversed())
.forEach(e -> System.out.print(e.getCountry() + ", "));
}
private static class CountryInfo {
private String country;
private int gold;
private int silver;
private int bronze;
public CountryInfo(String country, int gold, int silver, int bronze) {
this.country = country;
this.gold = gold;
this.silver = silver;
this.bronze = bronze;
}
public String getCountry() {
return country;
}
public int getGold() {
return gold;
}
public int getSilver() {
return silver;
}
public int getBronze() {
return bronze;
}
}
代碼中sortedCountry是核心方法实辑,分析該段代碼的話,看起來(lái)并沒(méi)有什么問(wèn)題藻丢。但是運(yùn)行的時(shí)候出問(wèn)題了剪撬,如下:
預(yù)期結(jié)果應(yīng)該是china,japan悠反,uk残黑,aust,us斋否。
最后發(fā)現(xiàn)梨水,代碼改成這樣就行了。
public static void sortedCountry(List<CountryInfo> countryInfos) {
List<CountryInfo> collect = countryInfos.parallelStream().sorted(Comparator
.comparingInt(CountryInfo::getGold)
.thenComparingInt(CountryInfo::getSilver)
.thenComparingInt(CountryInfo::getBronze)
.thenComparing((a, b) -> b.getCountry().compareTo(a.getCountry()))
.reversed())
.collect(Collectors.toList());
for (CountryInfo countryInfo : collect) {
System.out.print(countryInfo.getCountry() + ", ");
}
}
運(yùn)行結(jié)果如下:
兩段代碼對(duì)比一下茵臭,可以發(fā)現(xiàn)疫诽,唯一不一樣的地方在于對(duì)最終集合遍歷操作,第一段代碼在流中直接執(zhí)行遍歷旦委,第二段代碼使用for循環(huán)奇徒,所以問(wèn)題應(yīng)該在并行流操作時(shí)出現(xiàn)的問(wèn)題。parallelStream工作原理采用forkjoin的思想缨硝,也就是采用jdk內(nèi)部的forkjoin框架摩钙,等于開(kāi)啟多個(gè)線程對(duì)集合操作,第二段代碼運(yùn)行時(shí)查辩,由于使用到流操作的執(zhí)行結(jié)果胖笛,即collect對(duì)象奕短,在進(jìn)行for循環(huán)時(shí)候會(huì)用到,會(huì)一直阻塞匀钧,直到流被執(zhí)行完成翎碑,才會(huì)開(kāi)始遍歷,所以結(jié)果是正確的之斯。
分析原因
為什么會(huì)產(chǎn)生這樣的結(jié)果:《java8實(shí)戰(zhàn)》中這樣描述:
流操作:無(wú)狀態(tài)和有狀態(tài)
你已經(jīng)看到了很多的流操作日杈。乍一看流操作簡(jiǎn)直是靈丹妙藥,而且只要在從集合生成流的
時(shí)候把Stream換成parallelStream就可以實(shí)現(xiàn)并行佑刷。
當(dāng)然莉擒,對(duì)于許多應(yīng)用來(lái)說(shuō)確實(shí)是這樣,就像前面的那些例子瘫絮。你可以把一張菜單變成流涨冀,
用filter選出某一類的菜肴,然后對(duì)得到的流做map來(lái)對(duì)卡路里求和麦萤,最后reduce得到菜單
的總熱量鹿鳖。這個(gè)流計(jì)算甚至可以并行進(jìn)行。但這些操作的特性并不相同壮莹。它們需要操作的內(nèi)部
狀態(tài)還是有些問(wèn)題的翅帜。
諸如map或filter等操作會(huì)從輸入流中獲取每一個(gè)元素,并在輸出流中得到0或1個(gè)結(jié)果命满。
這些操作一般都是無(wú)狀態(tài)的:它們沒(méi)有內(nèi)部狀態(tài)(假設(shè)用戶提供的Lambda或方法引用沒(méi)有內(nèi)
部可變狀態(tài))涝滴。
但諸如reduce、sum胶台、max等操作需要內(nèi)部狀態(tài)來(lái)累積結(jié)果歼疮。在上面的情況下,內(nèi)部狀態(tài)
很小诈唬。在我們的例子里就是一個(gè)int或double韩脏。不管流中有多少元素要處理,內(nèi)部狀態(tài)都是有界的讯榕。
相反骤素,諸如sort或distinct等操作一開(kāi)始都和filter和map差不多——都是接受一個(gè)
流,再生成一個(gè)流(中間操作)愚屁,但有一個(gè)關(guān)鍵的區(qū)別济竹。從流中排序和刪除重復(fù)項(xiàng)時(shí)都需要知
道先前的歷史。例如霎槐,排序要求所有元素都放入緩沖區(qū)后才能給輸出流加入一個(gè)項(xiàng)目送浊,這一操
作的存儲(chǔ)要求是無(wú)界的。要是流比較大或是無(wú)限的丘跌,就可能會(huì)有問(wèn)題(把質(zhì)數(shù)流倒序會(huì)做什么
呢袭景?它應(yīng)當(dāng)返回最大的質(zhì)數(shù)唁桩,但數(shù)學(xué)告訴我們它不存在)。我們把這些操作叫作有狀態(tài)操作耸棒。
所以第一段代碼輸出錯(cuò)誤結(jié)果的原因是foreach操作讀取了半成品的結(jié)果荒澡。想要得到正確的結(jié)果,可以使用第二段代碼与殃,也可以parallelStream改為stream单山,讓流串行執(zhí)行也是可以的,如下:
public static void sortedCountry(List<CountryInfo> countryInfos) {
countryInfos.stream().sorted(Comparator
.comparingInt(CountryInfo::getGold)
.thenComparingInt(CountryInfo::getSilver)
.thenComparingInt(CountryInfo::getBronze)
.thenComparing((a, b) -> b.getCountry().compareTo(a.getCountry()))
.reversed())
.forEach(e -> System.out.print(e.getCountry() + ", "));
}
總結(jié)(個(gè)人理解)
書(shū)中對(duì)有狀態(tài)和無(wú)狀態(tài)流描述的比較抽象幅疼,其實(shí)可以結(jié)合當(dāng)前的例子來(lái)理解:有狀態(tài)的流米奸,前邊使用的sort操作,它排序元素的時(shí)候比如說(shuō)A爽篷,B悴晰,C,最壞的情況比較三次逐工,即A和B比較铡溪,B和C比較,A和C比較钻弄,可以得到最終的排序結(jié)果佃却;無(wú)狀態(tài)的流者吁,比如說(shuō)sum窘俺,如果要求三個(gè)元素的和,A复凳,B瘤泪,C,需要兩次相加就可以得到最終結(jié)果育八。其他操作也可以采用這種方式分析对途。
下圖是《java8實(shí)戰(zhàn)》的總結(jié):