清單的轉化
將集合轉換成一個新的集合就和遍歷它一樣簡單。假設我們要將清單中的名字轉換成全大寫的。我們看下都有哪些實現方式。
Java中的字串是不可變的,所以它沒辦法改變。我們可以產生新的字串,用來替換清單中原有的元素。然而這樣做的話,原來列表就沒了;還有一個問題,原來的列表可能也是不可變的,比如Arrays.asList()生成的,所以修改原來的列表這招不行。還有一個缺點就是這樣做很難並行操作。
產生一個新的全大寫的清單是個不錯的選擇。
乍聽起來這個建議弱爆了;性能是我們都很關注的一個問題。令人吃驚的是,函數式程式設計通常比命令式的效能還要高,我們在153頁的效能問題中會講到。
我們先開始用這個集合產生一個大寫字母的新集合吧。
複製代碼代碼如下:
final List<String> uppercaseNames = new ArrayList<String>();
for(String name : friends) {
uppercaseNames.add(name.toUpperCase());
}
在命令式的程式碼中,我們先建立一個空列表,然後把大寫的名字填進去,在遍歷原來列表的過程中,每次插入一個。為了改進成函數式的版本,我們第一步可以考慮採用19頁遍歷列表中提到的那個內部迭代器forEach來取代for循環,如下例所示的。
複製代碼代碼如下:
final List<String> uppercaseNames = new ArrayList<String>();
friends.forEach(name -> uppercaseNames.add(name.toUpperCase()));
System.out.println(uppercaseNames);
我們用了內部迭代器,但還得新建一個列表,然後再把元素插入裡面。我們還可以進一步改進。
使用lambda表達式
一個新引入的Stream介面裡面,有個map方法,它可以幫助我們遠離可變性,並使程式碼看起來更簡潔。 Steam有點像是集合的迭代器,同時它也提供了流函數(fluent functions)的功能。使用這個介面的方法,我們可以把一系列呼叫給組合起來,使程式碼讀起來就像描述問題的順序一樣,可讀性更強。
Steam的map方法可以用來將輸入序列轉換成一個輸出的序列――這和我們要做的工作非常吻合。
複製代碼代碼如下:
friends.stream()
.map(name -> name.toUpperCase())
.forEach(name -> System.out.print(name + " "));
System.out.println();
JDK8中的所有集合都支援這個stream方法,它把集合封裝成一個Steam實例。 map方法對Stream中的每個元素都呼叫了指定的lambda表達式或程式碼區塊。 map方法跟forEach方法很不一樣, forEach只是簡單的對集合中的元素執行了一下指定的函數。而map方法把lambda表達式的運行結果收齊起來,回傳一個結果集。最後我們用forEach方法印了所有的元素。
新集合中的名字全都是大寫的了:
複製代碼代碼如下:
BRIAN NATE NEAL RAJU SARA SCOTT
map方法很適合把一個輸入集合轉換成一個新的輸出集合。這個方法確保了輸入輸出序列的元素的數量是相同的。然而輸入元素和輸出元素的類型可以是不一樣的。在這個例子中,我們輸入和輸出的都是字串的集合。我們可以傳給map方法一段程式碼,讓它傳回比如說名字中包含字元的個數。這樣的話,輸入的還是字串的序列,而輸出的卻是數字序列了,就像下面這樣。
複製代碼代碼如下:
friends.stream()
.map(name -> name.length())
.forEach(count -> System.out.print(count + " "));
結果是每個名字中字母的個數:
複製代碼代碼如下:
5 4 4 4 4 5
使用了lambda表達式的之後版本,避免了明確的修改操作;這樣的程式碼非常簡潔。這樣寫不再需要初始化空的集合以及那個垃圾變數了;這個變數乖乖的躲到了底層實作裡面了。
使用方法引用
我們也可以使用方法引用讓它變得更簡潔一些。在需要傳入函數式介面的實作的地方,Java編譯器可以接受lambda表達式或方法引用。有了這個特性,用String::toUpperCase就可以取代掉name -> name.toUpperCase()了,就像這樣:
複製代碼代碼如下:
friends.stream()
.map(String::toUpperCase)
.forEach(name -> System.out.println(name));
當參數傳入到這個產生的方法――函數式介面的抽象方法的實作――裡面的時候,Java會去呼叫這個String參數的toUpperCase方法。這個參數引用在這裡就隱藏起來了。像前面這種簡單的場景,我們可以用方法引用來替換掉lambda表達式;更多的內容看一下26頁的什麼時候應該使用方法引用。
複製代碼代碼如下:
小夥伴發問了:
什麼時候應該使用方法引用?
當使用Java程式設計的時候,通常我們用lambda表達式的時候要比方法引用多得多。但這並不代表方法引用不重要或沒啥用處。當lambda表達式非常簡短的時候,它是一個很好的替代方案,它直接呼叫了實例方法或靜態方法。也就是說,如果lambda表達式只是傳遞了一下參數給方法呼叫的話,我們應該改用方法引用。
像這樣的lambda表達式,有點像Tom Smykowski在電影上班一條蟲中講的那樣,它的工作就是"從客戶那把需求拿給軟體工程師"。因為這個,我把這種重構成方法引用的模式叫做上班一條蟲模式。
除了簡潔外,使用方法引用,方法名字本身的意義和作用可以更好的體現。
使用方法引用背後,編譯器就起到了很關鍵的作用。方法所引用的目標物件和參數都會從這個產生的方法傳進來的參數推導出來。這使得你可以用方法引用寫出比使用lambda表達式更簡潔的程式碼。不過,如果參數在傳遞給方法之前或是呼叫結果在傳回之後要被修改的話,這種便利的寫法我們就用不了了。
在前面這個例子中,方法引用是引用了一個實例方法。方法引用還可以引用一個靜態方法以及接受傳參的方法。後面我們會看到這樣的例子。
lambda表達式能幫助我們遍歷集合,並且進行集合的轉換。就像下面我們即將看到的,它也能幫助我們快速的從集合中選取一個元素。