Java8函數式編程
為什么要用Java8
Java8在並行處理大型集合上有很大優勢。可以更好的利用多核處理器的優勢。Java8可以用Lambda表達式很簡便的寫出復雜的處理集合的邏輯。
函數式編程
函數式編程是一種編程范式,我們常見的編程范式有命令式編程(Imperative programming),函數式編程,邏輯式編程,常見的面向對象編程是也是一種命令式編程。
命令式編程是面向計算機硬件的抽象,有變量(對應着存儲單元),賦值語句(獲取,存儲指令),表達式(內存引用和算術運算)和控制語句(跳轉指令),一句話,命令式程序就是一個馮諾依曼機的指令序列。而函數式編程是面向數學的抽象,將計算描述為一種表達式求值,一句話,函數式程序就是一個表達式。
函數式編程中的函數這個術語不是指計算機中的函數(實際上是Subroutine),而是指數學中的函數,即自變量的映射。也就是說一個函數的值僅決定於函數參數的值,不依賴其他狀態。比如sqrt(x)函數計算x的平方根,只要x不變,不論什么時候調用,調用幾次,值都是不變的。在函數式語言中,函數作為一等公民,可以在任何地方定義,在函數內或函數外,可以作為函數的參數和返回值,可以對函數進行組合。
Java是一門面向對象的語言,對函數式編程的支持並不全面,如果想深入理解學習函數式編程可以學習Scala、Clojure,這兩種語言都是可以在jvm上運行的。
lambda表達式
Java8最大的改變就是加入了lambda表達式。什么是lambda表達式呢?
簡單來說,編程中提到的 lambda 表達式,通常是在需要一個函數,但是又不想費神去命名一個函數的場合下使用,也就是指匿名函數。
比如有些函數你只會使用一次,但是你卻給他定義一次,這個函數就成了污染函數,匿名函數就是用來干這個的。
舉個栗子:
faculties.sort(new Comparator<Faculty>() {
@Override
public int compare(Faculty o1, Faculty o2) {
return o1.getName().compareTo(o2.getName());
}
});
為了給這個專業列表(faculties)排序,你專門寫了一個匿名內部類,來實現比較大小的方法。這樣看起來又臃腫又復雜,如果用lambda表達式就很簡單了:
faculties.sort((f1, f2) -> f1.getName().compareTo(f2.getName()));
還可以用方法引用可以更簡單:
faculties.sort(Comparator.comparing(Faculty::getName));
下面有各種不同的方法來寫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表達式可以接受一個或者多個參數,lambda表達式可以轉換成一個接口不能轉化成類,轉化成接口要保證接口中只有一個可以復寫的方法。不然會報錯。
Streams
Stream可以讓我們在更高級別的抽象上寫集合的處理代碼,stream有一系列的方法來讓我們使用。
現在有一需求,需要篩選出來自倫敦的藝術家,通常我們會這么寫:
int count = 0;
for (Artist artist : allArtists) {
if (artist.isFrom("London")) {
count++;
}
}
用stream可以這么寫:
long count = allArtists.stream()
.filter(artist -> artist.isFrom("London"))
.count();
這樣看起來就清晰很多,而且效率也是差不多的,用stream不會循環兩遍的,stream的方法分為eager和lazy兩種,比如下面這段代碼:
allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName());
return artist.isFrom("London");
});
它不會打印任何東西,filter就是一個lazy的方法,那怎么區分lazy和eager方法呢?如果返回的還是一個stream那么就是一個lazy的方法,如果返回的不是stream就是eager方法。例如上面的count()方法返回的是int,那么就會被立刻執行。
collect(toList())
List<String> collected = Stream.of("a", "b", "c")
.collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);
collect(toList())把一個Stream轉換成List。
map
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase())
.collect(toList());
map可以把一種類型的stream數據轉換成另一種。
filter
List<String> beginningWithNumbers
= Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
filter可以把filter內返回為true的的數據過濾出來。
flatMap
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
看上面的圖更好理解一點,flatmap可以把stream的元素轉換成stream,然后再把這些stream合成一個新的stream,而map只能把stream中的元素從一種類型轉換成另一種類型。
max and 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();
在stream中找到最大值和最小值。
reduce
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element);
reduce會把第一個參數作為初始的acc,遍歷stream的元素作為element,然后每次計算的結果會作為下次的acc.上面的代碼可以拓展為:
BinaryOperator<Integer> accumulator = (acc, element) -> acc + element; int count = accumulator.apply(
accumulator.apply(
accumulator.apply(0, 1),
2), 3);
綜合
把上面的各種方法結合起來用一下:
Set<String> origins = album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.collect(toSet());
- 過濾出名字以The為開頭的藝術家
- 取出藝術家的國籍
- 得到一個藝術家的國籍的Set(去重)
再看一個例子:
public Set<String> findLongTracks(List<Album> albums) { return albums.stream()
.flatMap(album -> album.getTracks())
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.collect(toSet());
}
- 把所有專輯的曲目取出來合並成一個stream
- 過濾出大於60秒的曲目
- 把曲目的名字取出來的
- 轉換成一個Set
方法引用
artist -> artist.getName()
Artist::getName
(name, nationality) -> new Artist(name, nationality)
Artist::new
方法引用是一種更簡潔的寫法,而且比lambda表達式更容易理解。
對數據進行分類
一種分類方法是:partiioningBy
public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) {
return artists.collect(partitioningBy(artist -> artist.isSolo()));
}
有時候我們想根據某個值對數據進行分類就可以用partitionBy,方法的返回的boolean值作為key,滿足條件的值合並成list作為value。如果不想得到list還以在后面再加一個collector。
public Map<Boolean, Integer> bandsAndSolo(Stream<Artist> artists) {
return artists.collect(partitioningBy(artist -> artist.isSolo(), list -> list.size()));
}
這樣改一下就是把滿足條件的值的數量作為value。
另一種是:groupingBy
public Map<Boolean, List<String>> bandsAndSolo(Stream<String> artists) {
return artists.collect(partitioningBy(artist -> artist.isSolo(), artist.size()));
}
不同點在於partitioningBy只能把boolean作為key,而且永遠只有兩個entry,效率要高一點。而groupingBy可以把任意對象作為key。
總結
Java8加入的lambda表達式和stream庫,可以讓我們很方便的操作集合,聚合數據等等,熟練使用的話能很好提高編程效率,同時也提高了代碼的可讀性。當然它也有缺點,比如不方便debug,有一定的學習成本。