1.2 什麼是函數式編程
每個人對函數式編程的理解不盡相同。但其核心是:在思考問題時,使用不可變值和函 數,函數對一個值進行處理,映射成另一個值。
第二章 Lambda表達式
2.2 辨別Lambda表達式
Runnable noArguments = () -> System.out.println("Hello World”);
ActionListener oneArgument = event -> System.out.println("button clicked”);
Runnable multiStatement = () -> {
System.out.print("Hello");
System.out.println(" World");
};
BinaryOperator<Long> add = (x, y) -> x + y;
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
上述例子還隱含了另外一層意思:Lambda 表達式的類型依賴於上下文環境,是由編譯器 推斷出來的。
2.3 引用值,而不是變量
final String name = getUserName(); button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("hi " + name);
}
});
Java 8雖然放鬆了這一限制,可以引用非final變量,但是該變量在既成事實上必須是 final
//這裏編譯不會通過
String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));
這種行爲也解釋了爲什麼 Lambda 表達式也被稱爲閉包。未賦值的變量與周邊環境隔離起 來,進而被綁定到一個特定的值
總而言之, lambda 引用的是值 而非變量
2.4 函數接口
使用只有一個方法的接口來表示某特定方法並反覆使用,是很早就有的習慣。使用 Swing 編寫過用戶界面的人對這種方式都不陌生,這裏無需再標新立 異,Lambda 表達式也使用同樣的技巧,並將這種接口稱爲函數接口
2.5 類型推斷
javac 根據 Lambda 表達式上下文信息 就能推斷出參數的正確類型。程序依然要經過類型檢查來保證運行的安全性,但不用再顯 式聲明類型罷了。這就是所謂的類型推斷。
BinaryOperator<Long> add = (x, y) -> x + y;
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
2.6 要點回顧
- Lambda 表達式是一個匿名方法,將行爲像數據一樣進行傳遞。
- Lambda 表達式的常見結構:BinaryOperator add = (x, y) → x + y。
- 函數接口指僅具有單個抽象方法的接口,用來表示Lambda表達式的類型。
3.1 從外部迭代到內部迭代
3.2 實現機制
1、惰性求值法
如下,不會出現打印結果
allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName());
return artist.isFrom("London");
}
);
2、及早求值法
如下,會打印結果
long count = allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName());
return artist.isFrom("London");
}).count();
使用這些操作的理 想方式就是形成一個惰性求值的鏈,最後用一個及早求值的操作返回想要的結果,這正是 它的合理之處。計數的示例也是這樣運行的
3.3 常用的流操作
3.3.1 collect(toList())
List<String> collected = Stream.of("a", "b", "c") .collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);
這個例子也展示了本節中所有示例代碼的通用格式。首先由列表生成一個 Stream ,然後 進行一些 Stream 上的操作,繼而是 collect 操作,由 Stream 生成列表,最後使用斷言 判斷結果是否和預期一致。
3.3.2 map
使用普通的方式將數組中的數據改成大寫
List<String> collected = new ArrayList<>();
for (String string : asList("a", "b", "hello")) {
String uppercaseString = string.toUpperCase();
collected.add(uppercaseString);
}
assertEquals(asList("A", "B", "HELLO"), collected);
使用stream.map
List<String> collected = Stream.of("a", "b", "hello").map(string -> string.toUpperCase()).collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);
看源碼map的傳參是function,正好適用將一個值變爲另外一個值的場景
3.3.3 filter
filter的傳參是Predicate接口,傳入一個值,返回一個boolean判斷,適用於篩選的場景
3.3.4 flatmap
flatMap 方法可用 Stream 替換值,然後將多個 Stream 連接成一個 Stream
List list = Stream.of(Arrays.asList("a,b,c"),Arrays.asList("d,e,f")).flatMap(strings -> {
return strings.stream().map(string-> string.toUpperCase());
}).collect(Collectors.toList());
System.out.println(list);
flatMap 方法的相關函數接口和 map 方法的一樣,都是 Function 接口,只是方法的返回值 限定爲 Stream 類型罷了。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
3.3.5 max和min
List<Track> tracks = asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
.min(Comparator.comparing(track -> track.getLength()))
.get();
assertEquals(tracks.get(1), shortestTrack);
這裏的stream調用 max 和 min 會獲得一個 Optional對象,調用get纔會獲得具體的值
comparing的源碼,最終返回的是一個Comparator函數
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
3.3.7 reduce
使用 reduce 求和
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element);
assertEquals(6, count);
3.4 重構遺留代碼
/**
* 唱片篩選(參考函數式編程 3。4 重構遺留代碼)
*/
public class AlbumScreen {
/**
* 專輯
*/
static class Album{
public Album(String albumName, List<Track> trackList) {
this.albumName = albumName;
this.trackList = trackList;
}
//專輯名稱
private String albumName;
//曲目列表
private List<Track> trackList;
public String getAlbumName() {
return albumName;
}
public void setAlbumName(String albumName) {
this.albumName = albumName;
}
public List<Track> getTrackList() {
return trackList;
}
public void setTrackList(List<Track> trackList) {
this.trackList = trackList;
}
}
/**
* 曲目
*/
static class Track{
public Track(String trackName,Long seconds) {
this.trackName = trackName;
this.seconds = seconds;
}
//曲目名稱
private String trackName;
//時長 秒
private Long seconds;
public Long getSeconds() {
return seconds;
}
public void setSeconds(Long seconds) {
this.seconds = seconds;
}
public String getTrackName() {
return trackName;
}
public void setTrackName(String trackName) {
this.trackName = trackName;
}
}
/**
* 篩選時長大於60秒以上的曲目
* @param albumList
* @return
*/
public static Set<String> screen(List<Album> albumList){
Set<String> trackNames = new HashSet<>();
for(Album album : albumList) {
for (Track track : album.getTrackList()) {
if (track.seconds > 60) {
String name = track.getTrackName();
trackNames.add(name);
}
}
}
return trackNames;
}
/**
* 篩選時長大於60秒以上的曲目(lambda版)
* @param albumList
* @return
*/
public static Set<String> screenLambda(List<Album> albumList){
return albumList.stream().flatMap(tracks -> tracks.trackList.stream()) //flatmap 將多個流合併成一個
.filter(track -> track.seconds>60) //filter 進行篩選
.map(track -> track.trackName) //map 通過一個值獲取另外一個值,這裏根據曲目對象獲取名稱
.collect(Collectors.toSet()); //創建 set
}
public static void main(String[] args){
List<Track> trackList1 =
Stream.of(new Track("燃燒我的卡路里",180L)
,new Track("我已經愛上你",59L)
,new Track("好漢歌",100L))
.collect(Collectors.toList());
List<Track> trackList2 =
Stream.of(new Track("一百萬個可能",90L)
,new Track("答案",30L)
,new Track("一個人去巴黎",120L))
.collect(Collectors.toList());
List<Album> albumList =
Arrays.asList(new Album("火箭隊",trackList1),new Album("銀河隊",trackList2));
System.out.println(screen(albumList));
System.out.println(screenLambda(albumList));
}
}
3.8 要點回顧
內部迭代將更多控制權交給了集合類。
和Iterator類似,Stream是一種內部迭代方式。
將Lambda表達式和Stream上的方法結合起來,可以完成很多常見的集合操作。
4 類庫
4.1 在代碼中使用lambda表達式
使用 isDebugEnabled 方法降低日誌性能開銷
Logger logger = new Logger(); if (logger.isDebugEnabled()) {
logger.debug("Look at this: " + expensiveOperation());
}
//使用lambda表達式簡化日誌
Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());
public void debug(Supplier<String> message) { if (isDebugEnabled()) {
debug(message.get());
}
}
4.2 基本類型
由於裝箱類型是對象,因此在內存中存在額外開銷。比如,整型在內存中佔用 4 字節,整型對象卻要佔用 16 字節。這一情況在數組上更加嚴重,整型數組中的每個元素 只佔用基本類型的內存,而整型對象數組中,每個元素都是內存中的一個指針,指向 Java 堆中的某個對象。在最壞的情況下,同樣大小的數組,Integer[] 要比 int[] 多佔用 6 倍 內存。
爲了減小這些性能開銷,Stream 類的某些方法對基本類型和裝箱類型做了區分。圖 4-1 所 示的高階函數mapToLong和其他類似函數即爲該方面的一個嘗試
//Stream 中的源碼
LongStream mapToLong(ToLongFunction<? super T> mapper);
//LongStream 中的源碼
<U> Stream<U> mapToObj(LongFunction<? extends U> mapper);
4.3 重載解析
Lambda 表達式作爲參數時,其類型由它的目標類型推導得出,推導過程遵循 如下規則:
如果只有一個可能的目標類型,由相應函數接口裏的參數類型推導得出;
如果有多個可能的目標類型,由最具體的類型推導得出;
如果有多個可能的目標類型且最具體的類型不明確,則需人爲指定類型。
4.4 @FunctionalInterface
該註釋會強制 javac 檢查一個接口是否符合函數接口的標準。如果該註釋添加給一個枚舉 類型、類或另一個註釋,或者接口包含不止一個抽象方法,javac 就會報錯。重構代碼時, 使用它能很容易發現問題。
4.6 默認方法
因爲接口的改造,接口方法的增加,會導致用舊的jdk編譯的類有不兼容的問題,所以採用了default關鍵字,接口提供一個默認的實現方法
默認方法示例:forEach 實現方式
default void forEach(Consumer<? super T> action) {
for(Tt:this){
action.accept(t);
}
}
和類不同,接口沒有成員變量,因此默認方法只能通過調用子類的方法來修改子類本身, 避免了對子類的實現做出各種假設。
4.7 多重繼承
public interface Jukebox {
public default String rock() { return "... all over the world!";
}
}
public interface Carriage {
public default String rock() { return "... from side to side";
}
}
public class MusicalCarriage
implements Carriage, Jukebox {
@Override
public String rock() {
return Carriage.super.rock();
}
}
javac 並不明確應該繼承哪個接口中的方法,因此編譯器會報錯:class Musical Carriage inherits unrelated defaults for rock() from types Carriage and Jukebox。當然,在類 中實現 rock 方法就能解決這個問題
三定律
如果對默認方法的工作原理,特別是在多重繼承下的行爲還沒有把握,如下三條簡單的定 律可以幫助大家。
- 類勝於接口。如果在繼承鏈中有方法體或抽象的方法聲明,那麼就可以忽略接口中定義 的方法。
- 子類勝於父類。如果一個接口繼承了另一個接口,且兩個接口都定義了一個默認方法, 那麼子類中定義的方法勝出。
- 沒有規則三。如果上面兩條規則不適用,子類要麼需要實現該方法,要麼將該方法聲明 爲抽象方法。
其中第一條規則是爲了讓代碼向後兼容。
4.9 接口的靜態方法
Stream 是個接口, Stream.of是接口的靜態方法。這也是Java 8中添加的一個新的語言特性,旨在幫助編寫 類庫的開發人員,但對於日常應用程序的開發人員也同樣適用。
4.10 Optional
Optional 是爲核心類庫新設計的一個數據類型,用來替換 null 值。
使用 Optional 對象有兩個目的:首先,Optional 對象鼓勵程序員適時檢查 變量是否爲空,以避免代碼缺陷;其次,它將一個類的 API 中可能爲空的值文檔化,這比 閱讀實現代碼要簡單很多。
//創建某個值的 Optional 對象
Optional<String> a = Optional.of("a");
assertEquals("a", a.get());
//創建一個空的 Optional 對象,並檢查其是否有值
Optional emptyOptional = Optional.empty();
Optional alsoEmpty = Optional.ofNullable(null);
assertFalse(emptyOptional.isPresent());
//使用 orElse 和 orElseGet 方法
assertEquals("b", emptyOptional.orElse("b"));
assertEquals("c", emptyOptional.orElseGet(() -> "c"));
4.11 要點回顧
使用爲基本類型定製的Lambda表達式和Stream,如IntStream可以顯著提升系統性能。
默認方法是指接口中定義的包含方法體的方法,方法名有default關鍵字做前綴。
在一個值可能爲空的建模情況下,使用Optional對象能替代使用null值。