為什么要有練習題?
所謂學而不思則罔,思而不學則殆,在系列第一篇就表明我認為寫博客,既是分享,也是自己的鞏固,我深信"紙上得來終覺淺,絕知此事要躬行"的道理,因此之后的幾篇博客都會在知識點后面附上幾道練習題,不會單獨開一篇來說練習題。
大部分題練習題來自於Richard Warburton的《Java8 函數式編程》,練習題的難度不會很難,大部分都十分的基礎(可能偶爾會有一兩道進階題),並且我在后面也會附上可供參考的思路與代碼,但是我認為想要學習的人(包括我)應當首先對問題沉下心來思考,然后再查看參考答案,當然如果看博客的你沒有這么多時間我的建議是記錄下來,閑暇時光在腦袋里想一想也是好的,我認為魯迅的說的時間就像海綿,擠擠就有了是十分有道理的,其實大家都沒有自己想象的那么忙。
基礎題
Question 1 (常用流操作)
- 編寫一個求和函數,計算流中所有數的和。例int addUp(Stream
numbers) - 編寫一個函數,參數為藝術家集合,返回一個字符串集合,其中包含了藝術家的姓名與國籍。(藝術家類名為Artist,里面有獲得姓名與國籍的get方法getName()與getNationality())
- 編寫一個函數,參數為專輯集合,返回一個由 最多包含3首歌曲的專輯 組成的集合。(專輯類名為Album,里面包含了一個返回本專輯所有歌曲的集合的方法getTrackList())
請耐心思考一會,看看能不能自己寫出來,下面給一些參考的思路,具體的方法確實是有很多種的。
Question 1 的參考思路
- 編寫一個求和函數,計算流中所有數的和。
這道練習題要求計算流中所有的數的和,參數為流,返回他們的和,這其實就是要求我們重復實現類庫中的sum()方法,那系列第二篇提到的reduce操作里面已經提到了,類庫中的sum,max,min等一系列操作都是用reduce完成的,因此在這里我們也可以用reduce進行操作。參考代碼如下
public static int addUp(Stream<Integer> numbers){
reutrn numbres.reduce(0,(x,y) -> x+y);
}
- 編寫一個函數,參數為藝術家集合,返回一個字符串集合,其中包含了藝術家的姓名與國籍。
這道題參數為藝術家集合,返回值為字符串,那么顯然這是涉及到映射map的操作,但是要將一個藝術家元素映射成兩個字符串(分別是這個藝術家的姓名與這個藝術家的國籍),那么就要涉及到flatMap了,flatMap方法可以用Stream替換值,然后在將多個Stream鏈接成一個Stream,對於本題來說,使用藝術家姓名與藝術家國籍的流替換掉藝術家,再將這些流鏈接到一起即可。
更具體一點的說法,假設有2個藝術家,分別是來自中國的張三與來自美國的jack,那么直接使用map映射返回的結果是2個流,格式大約為這樣:[張三,中國],[jack,美國],使用flatMap就可以將這兩個流合並,於是格式就變成了[張三,中國,jack,美國],符合題目中的返回一個字符串集合。
參考代碼如下
public static List<String> getArtistNamesAndNations(List<Artist> artists){
return artists.stream()
.flatMap(artist -> Stream.of(artist.getName(),artist.getNationality()))
.collect(toList());
}
注:這里最后一行要說明一下,事實上標准的寫法應該是collect(Collectors.toList()),但是因為這個方法十分常用,所以在類開頭導入了靜態方法
import static java.util.stream.Collectors.toList
這是來自於java5的特性,可以方便靜態屬性與方法的調用。
- 編寫一個函數,參數為專輯集合,返回一個由 最多包含3首歌曲的專輯 組成的集合。(專輯類名為Album,里面包含了一個返回本專輯所有歌曲的集合的方法getTrackList())
這道題很明顯參數為專輯,返回值也是專輯,中間多了一個條件,因此使用過濾器即可。參考代碼如下
public static List<Album> getAlbumsWithMostThreeTracks(List<Album> albums){
return albums.stream()
.filter(album -> album.getTrackList().size <= 3)
.collect(toList());
}
Question 2 (關於迭代)
- 修改下面的代碼,將外部迭代轉換成內部迭代
- 為了保證不理解錯誤,說明一下,藝術家有的是個人,有的是團體或者樂隊,getMembers()方法是獲得該藝術家團體的所有人數,如果是個人藝術家,人數自然就是一個人。
- 並且注意下面這段代碼的最后一行有效行,members使用了count()方法來計數,因此該方法的返回值是一個stream流,想要獲得數量要用count()方法。
int totalMembers = 0;
for(Artist artist: artists){
Stream<Artist> members = artist.getMembers();
totalMembers += members.count();
}
附:getMembers方法
public Stream<Artist> getMembers() {
return members.stream();
}
Question 2 的參考思路
不拘泥於具體實現方式,首先了解清楚這段代碼想做什么,有幾步。這段代碼想做的就是統計出所有藝術家的人數,因為有的藝術家是團體,有的藝術家是個人,個人的藝術家就算一個人,團體的話就統計出該團體的所有人數。那么相應的步驟就顯而易見了。
第一步統計出每個藝術家個人或者團體的人數,第二步將他們求和。對於流操作第一步是一個惰性求值,首先將藝術家映射成他們的團體人數,第二步是一個及早求值,對這些人數進行求和。
參考代碼如下
public static int countAllArtistMembers(List<Aritst> artists){
return artists.stream()
.map(artist -> artist.getMembers().count())
.reduce(0,Integer::sum);
}
當然目前還沒有介紹一些屬於高階流的例如收集器等,事實上下面這段使用數值流的代碼看起來更舒服一些,閱讀性也更強
public static int countAllArtistMembers(List<Aritst> artists){
return artists.stream()
.mapToInt(artist -> artist.getMembers().count())//統計人數,轉換成數值流
.sum();//對人數求和
Question 3 (判斷求值方式)
-依據以下兩個方法的方法簽名,判斷下面兩個方法是屬於惰性求值還是及早求值
1.boolean anyMatch(Predicate<? super T> predicate);
2.Stream
注:第一題的Predicate是java8的函數接口,看不明白並沒有關系,后面會進行介紹,題目與之也沒有什么聯系。
Question 3 的參考思路
之前系列博客上一篇將惰性求值與及早求值的區別已經介紹過,這兩者最大的區別的就是惰性求值返回的值是Stream,而及早求值是直接返回具體的數據,那么結果就顯而易見了
第一個方法返回了具體的boolean數值,第二個方法返回的則是Steam流,因此第一個方法屬於及早求值,第二個方法屬於惰性求值。
Question 4 (字符串練習題兩道)
-
計算一個字符串中小寫字母的個數
注:可以參考Java8 string中新引入的chars方法 -
在一個字符串集合中,找出包含最多小寫字母的字符串。
對於還沒有熟悉流操作的同學來說,第一反應思路是for循環,if語句之類的,其實這很正常,那么不妨試着先寫出你第一反應程序的偽代碼,接着再使用流操作對它進行重構,接着再想一想重構的過程中,提取出真正的幾步需求,也就是我要干什么,總共有幾步,分解成流操作可以怎么樣,是過濾器?還是收集器?又還是映射?還是比較器?,慢慢的這樣思考,相信以后工作中碰到相應的問題第一反應就會是流操作啦:)
Question 4 的參考思路
- 計算一個字符串中小寫字母的個數
首先引入一段java8 String類的chars方法的api
/**
* Returns a stream of {@code int} zero-extending the {@code char} values
* from this sequence. Any char which maps to a <a
* href="{@docRoot}/java/lang/Character.html#unicode">surrogate code
* point</a> is passed through uninterpreted.
*
* <p>If the sequence is mutated while the stream is being read, the
* result is undefined.
*
* @return an IntStream of char values from this sequence
* @since 1.8
*/
public default IntStream chars()
簡單點說呢,這個方法返回一個由string里字符組成的數值流,數值流,當然也是屬於流的一種了。
那么這個問題的思路是這樣的,第一步獲得所有字母的流,這點通過chars方法已經得到了,第二步選出所有的小寫字母,這點的話使用過濾器filter也可以輕松做到(也就是第一反應的if),第三部統計個數,這個就不用說了吧,count()方法搞定。
下面是參考代碼
public class Question4 {
public static int countStringLowercaseLetters(String string){
return string.chars()//獲得字母流
.filter(Character::isLowerCase)//篩選出所有小寫字母
.count;//統計數量
}
}
這里注意到參考代碼里給了一個類,那是因為后面一道題要用到這個方法,為了減少代碼量,這里給一個類,方便到時候引用。
-
在一個字符串集合中,找出包含最多小寫字母的字符串。
同樣的理清思路,第一步統計出每一個字符串小寫字母的數量(這一步上一題已經做完了)並將他們映射成數值流,第二步選出這些數值中最大的。
這里依舊給兩個參考代碼,一個是使用了Comparator比較器,返回的是optional對象,使用get()方法獲得里面的值,另一個是直接映射成數值流(個人推薦第二種啦) -
使用比較器
public static Optional<String> mostLowcaseLetters(List<String> strings){
return strings.stream()
.max(Comparator.comparing(Question4::countStringLowercaseLetters));
}
- 使用數值流
public static int mostLowcaseLetters(List<String> strings){
return strings.stream()
.mapToInt(Question4::countStringLowercaseLetters))
.max();
}
思考題
- 試着只用reduce和Lambda表達式寫出實現Stream上的filter操作的代碼
- 試着只用reduce和Lambda表達式寫出實現Stream上的map操作的代碼
注:使用collect收集器會更容易一些,不過目前本系列博客還沒有介紹。當然只是用reduce與lambda也是完全可以做到的,可以試着看一看java8 filter與map的源碼。
思考題答案放在下一篇博客里,當然實現的方式有很多,這里所有的答案都只是一個參考,僅提供一種思路。
本篇小結
本篇是屬於上一篇的一個延伸,由幾道練習題講解流的一些實際操作與思考過程,在這里還是要重復說一下上面的重構建議,可能大家(包括我一開始)剛開始想當然就是for與if之類,確實很容易這樣,我的建議就是試着用流操作去重構自己的原先代碼,同時從具體的實現細節中脫離出來,去關注究竟需要做什么,這樣你會發現解決一個問題並沒有想象的那么難。