十、函數式程式設計
文章目錄
- 十、函數式程式設計
-
- 1、Lambda表達式
-
- 1.1 FunctionalInterface
- 2、方法引用
-
- 2.1 構造方法引用
- 3、使用Stream
-
- 3.1 建立Stream
-
- Stream.of()
- 基于數組或Collection
- 基于Supplier
- 其他方法
- 基本類型
- 3.2 使用map
- 3.3 使用filter
- 3.4 使用reduce
- 3.5 輸出集合
-
- 輸出為List
- 輸出為數組
- 輸出為Map
- 分組輸出
函數式程式設計的一個特點就是,允許把函數本身作為參數傳入另一個函數,還允許傳回一個函數。
從 Java8 開始支援函數式程式設計。
1、Lambda表達式
Java的方法分為執行個體方法,例如
Integer
定義的
equals()
方法:
public final class Integer {
boolean equals(Object o) {
...
}
}
以及靜态方法,例如
Integer
定義的
parseInt()
方法:
public final class Integer {
public static int parseInt(String s) {
...
}
}
上面的方法,本質上都相當于過程式語言的函數。隻不過Java的執行個體方法隐含的傳入了一個
this
變量。
函數式程式設計是把函數作為基本元算單元,函數可以作為變量,可以接受函數,還可以傳回函數。
在Java程式中,我們經常遇到一些但方法接口,即一個接口隻定義一個方法:
- Comparator
- Runnable
- Callable
以
Comparator
為例,從 Java8 開始,我們可以使用 Lambda表達式替換單方法接口:
@Test
public void m0() {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
System.out.println(String.join(", ", array));
}
和
JavaScript
的箭頭函數一樣,在方法中隻有一句時,可以省略
{}
:
在Lambda 表達式中,它隻需要寫出方法定義,參數為(s1, s2),參數的類型可以省略,因為編譯器可以自動推斷出
String
類型。
傳回值的類型也是由編譯器自動推斷。
1.1 FunctionalInterface
隻定義了單方法的接口稱之為
FunctionalInterface
,用注解
@FunctionalInterface
标記。例如,
Callable
接口:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
在接口
Comparator
中:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
...
}
...
}
雖然
Comparator
接口有很多方法,但隻有一個抽象方法
int compare(T o1, T o2)
,其他方法都是
default
和
static
方法。
boolean equals(object obj)
是
Object
定義的方法,不算在接口方法内。
2、方法引用
使用 Lambda表達式,我們可以不必編寫
FunctionalInterface
接口的實作類,進而簡化了代碼。
當然,除了 Lambda 表達式,還可以直接傳入方法引用,例如:
public class LambdaTest {
@Test
public void m1() {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, LambdaTest::cmp);
System.out.println(String.join(", ", array));
}
static int cmp(String s1, String s2) {
return s1.compareTo(s2);
}
}
上述代碼在
Arrays.sort()
中傳入了靜态方法
cmp
的引用,用
LambdaTest::cmp
表示。
方法引用,就是說某個方法簽名和接口一樣,就可以直接傳入方法引用。
ps:方法簽名隻看參數類型和傳回類型,不看方法名稱,也不看類的繼承關系。
2.1 構造方法引用
如果要把一個
String
數組轉化為
Person
數組,在以前,我們可能會:
@Test
public void m2() {
Stream<String> stream = Stream.of("Bob", "Alice", "Tim");
List<Person> list = new ArrayList<>();
stream.forEach(s -> {
list.add(new Person(s));
});
System.out.println(list);
}
現在我們可以引用
Person
的構造方法來實作
String
到
Person
的轉化:
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
System.out.println(persons);
}
}
class Person {
String name;
public Person(String name) {
this.name = name;
}
public String toString() {
return "Person:" + this.name;
}
}
3、使用Stream
從 Java8 開始,引入了一種全新的流式API:Stream API。位于
java.util.stream
包中。
這個
stream
和
java.io
中的
InputStram
和
OutputStream
不一樣,它代表的是任意Java對象的序列。
java.io | java.util.stream | |
---|---|---|
存儲 | 順序讀寫的 或 | 順序輸出的任意Java對象執行個體 |
用途 | 序列化至檔案或網絡 | 記憶體計算/業務邏輯 |
java.util.stream
和
List
也不一樣,
List
存儲的每個元素都已經在記憶體中存儲,而
stream
輸出的對象可能并沒有存儲在記憶體中,而是實時計算得到的,且是惰性計算的。
簡單來說,
List
就是一個個實實在在的元素,這些元素也已經存儲在記憶體中,使用者可以用它來操作其中的元素(例如,周遊、排序等)。而
stream
可能就根本沒有配置設定記憶體。下面,看一個例子:
如果想用
List
表示全體的自然數,這是不可能的,因為自然數是無窮的,但記憶體是有限的。
如果我們使用
stram
就可以做到,如下:
上面的 createNaturalStram() 方法沒有實作。
也可以對
Stream
計算,例如,對每個自然數做一個平方:
Stream<BigInteger> naturals = createNaturalStream(); // 全體自然數
Stream<BigInteger> streamNxN = naturals.map(n -> n.multiply(n)); // 全體自然數的平方
上面的
streamNxN
也有無限多個元素,如果要列印它,可以用
limit()
方法截取前100個元素,最後用
forEach()
處理每個元素。
Stream<BigInteger> naturals = createNaturalStream();
naturals.map(n -> n.multiply(n)) // 1, 4, 9, 16, 25...
.limit(100)
.forEach(System.out::println);
惰性計算的特點:一個
Stream
轉換為另一個時,實際上隻存儲了轉化規則,并不會有任何的計算。例如,上面的例子中,隻有在調用
Stream
确實需要輸出元素時,才會進行計算。
forEach
3.1 建立Stream
Stream.of()
使用
Stream.of()
建立雖然沒有實質性用途,但在測試時很友善。
@Test
public void m1() {
Stream<String> stream = Stream.of("A", "B", "C", "D");
// forEach()方法相當于内部循環調用,
// 可傳入符合Consumer接口的void accept(T t)的方法引用:
stream.forEach(System.out::println);
}
基于數組或Collection
可以基于一個數組或者
Collection
建立
Stream
,這樣
Stream
在輸出時的元素也就是數組或
Collection
的元素。
@Test
public void m1() {
//數組變成 `Stream` 使用 `Arrays.stream()` 方法
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
//對于 `Collection`(List/Set/Queue等),直接調用 `stream()` 方法即可。
Stream<String> stream2 = List.of("X", "Y", "Z").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
}
上述建立的方法都是把一個現有的序列變成
Stream
,它的元素都是固定的。
Stream
基于Supplier
建立
Stream
還可以通過
Stream.generate()
方法,它需要傳入的是一個
Supplier
對象:
這時
Stream
儲存的不是具體的元素,而是一種規則,在需要産生一個元素時,
Stream
自己回去調用
Supplier.get()
方法。
例子,通過
Stream
不斷的産生自然數:
public class StreamTest {
@Test
public void m1() {
Stream<Integer> natural = Stream.generate(new NaturalSupplier());
natural.limit(10).forEach(System.out::println);
}
}
class NaturalSupplier implements Supplier<Integer> {
int n = 0;
@Override
public Integer get() {
return ++n;
}
}
即使
int
的範圍有限,但如果用
List
存儲,也會占用巨大的記憶體,而使用
Stream
時,因為隻儲存計算規則,是以幾乎不占用空間。
在調用
forEach()
或者
count()
進行最終求值前,一定要把
Stream
的無限序列變成有限序列,否則會因為不能完成這個計算進入死循環。
其他方法
建立
Stream
的第三種方法是通過一些API提供的接口,直接獲得
Stream
。
例如,
Files
類的
lines()
方法把一個檔案變成
Stream
,每個元素代表檔案的一行内容:
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
...
}
在需要對文本檔案按行周遊時,該方法十分有用。
正規表達式的
Pattern
對象有一個
splitAsStream()
方法,可以直接把一個長字元串分割成
Stream
序列而不是數組:
Pattern p = Pattern.compile("\\s+");
Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");
s.forEach(System.out::println);
基本類型
因為Java的泛型不支援基本類型,是以不能使用
Stream<int>
這種類型。為了友善,Java标準庫提供了
IntStream
、
LongStream
、
DoubleStream
這3種使用基本類型的
Stream
,它們的使用方法和範型
Stream
沒有大的差別。
3.2 使用map
Java中的
map
、
filter
、
reduce
類似于
JavaScript
中高階函數的用法。
類似的用法,可以寫出下面的例子:
@Test
public void m2() {
List.of(1, 2, 3, 4)
.stream()
.map(n -> n * n) //求平方
.forEach(System.out::println); // 列印
}
3.3 使用filter
@Test
public void m2() {
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(n -> n % 2 != 0)
.forEach(System.out::println);
}
3.4 使用reduce
map()
和
filter()
都是
Stream
的轉換方法,而
Stream.reduce()
則是
Stream
的一個聚合方法。
@Test
public void m2() {
int n = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.reduce(0, (x, y) -> x + y);
System.out.println(n);
}
上面的代碼如果去掉初始值,會傳回一個:
Optional<Integer>
這是因為Optional<Integer> opt = stream.reduce((acc, n) -> acc + n); if (opt.isPresent) { System.out.println(opt.get()); }
的元素有可能是0個,這樣就沒法調用
Stream
方法,是以傳回
reduce()
對象,需要怕斷結果是否存在。
Optional
對的操作分為兩類:
Stream
- 轉換操作:把一個
轉化為 另一個
Stream
,例如
Stream
和
map()
,
filter()
- 聚合操作:會對
的每個元素進行計算,得到一個确定的結果,例如
Stream
,這類操作會觸發計算。
reduce()
3.5 輸出集合
輸出為List
因為需要把
Stream
的元素儲存到集合,而集合儲存的都是确定的 Java對象,是以把
Stream
變成
List
是一個聚合操作。
@Test
public void m2() {
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> lists =
stream.filter(s -> s != null && !s.trim().isEmpty()).collect(Collectors.toList());
System.out.println(lists);
}
把
Stream
的每個元素收集到
List
的方法是調用
collect()
并傳入
Collectors.toList()
對象,它實際上是一個
Collector
執行個體,通過類似
reduce()
的操作,把每個元素添加到一個收集器中(實際上是
ArrayList
)。
類似的,
collect(Collectors.toSet())
可以把
Stream
的每個元素收集到
Set
中。
輸出為數組
@Test
public void m2() {
Stream<String> stream = Stream.of("Apple", "Pear", "Orange");
String[] array = stream.toArray(String[]::new);
System.out.println(String.join(", ", array));
}
傳入的“構造方法”是,它的簽名實際上是
String[]::new
定義的
IntFunction
,即傳入
String[] apply(int)
參數,獲得
int
數組的傳回值
String[]
輸出為Map
對于
Stream
的元素輸出到
Map
,需要分别把元素映射為key和value:
@Test
public void m2() {
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
// 把元素s映射為key:
s -> s.substring(0, s.indexOf(':')),
// 把元素s映射為value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
分組輸出
Stream
還有一個強大的功能就是可以按組輸出。
@Test
public void m2() {
Stream<String> stream =
Stream.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, List<String>> groups = stream
.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
System.out.println(groups);
}
//輸出結果
{A=[Apple, Avocado, Apricots], B=[Banana, Blackberry], C=[Coconut, Cherry]}
在上面使用到的
Collectors.groupinigBy()
方法,需要提供兩個函數:
- 第一個是分組的key,
表示隻要首字母相同的s -> s.substring(0, 1)
分到一組。String
- 第二個是分組的value,這裡直接輸出為
。List