函數式編程
-
一個大型程序調用若干底層函數, 這些函數又可以調用其他函數
-
大任務被一層層拆解並執行
-
函數是面向過程的程序設計的基本單元
-
Java不支持單獨定義函數, 靜態方法視爲獨立的函數
-
函數式編程歸結爲面向過程的程序設計
-
計算機: CPU執行計算代碼, 條件判斷還有調整等指令代碼, 彙編是最貼近計算機的語言
-
計算: 數學意義上的計算, 越是抽象的計算, 離計算機硬件越近
-
編程語言約低級, 越解決計算機, 抽象程度低, 執行效率高. C語言
-
編程語言越高級, 越貼近計算, 抽象程度高, 執行效率低. Python
-
函數式編程是抽象程度很高的編程範式, 純粹的函數式編程語言編寫的函數沒有任何的變量.
-
因此, 任一函數, 只要輸入確定, 輸出就是一定確定的. 稱爲沒有副作用
-
允許函數本身作爲參數傳入另一個函數, 還可以返回一個函數
lambda基礎
- java的實例方法和靜態方法, 本質上都相當於過程式語言的函數. 例如C函數:
char* strcpy(char* dest, char* src)
- 只不過java的實例方法隱含地傳入了一個
this
變量 - 函數式編程把函數作爲基本運算單位, 可以接受函數, 可以返回函數.
- 支持函數式編程的編碼風格稱爲
Lambda
表達式
Lambda表達式
- 參數類型和返回值類型都是由編譯器自動推斷
FunctionalInterface
- 單方法接口稱之爲
FunctionalInterface
, 用註解@FunctionalInterface
標記
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
@FunctionalInterface
public interface Compare<T> {
int compare(T o1, T o2); // 唯一一個抽象方法, 其他都是`default`和`static`方法
boolean equals(Object obj); // 這是`Object`定義的方法
default Comparator<T> reversed() {
return Collections.reverseOrder();
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
// ...
}
// ...
}
方法引入
除了Lambda表達式, 可以直接傳入方法使用
public static void main(String[] args) {
String[] array = new String[] {"Apple", "Orange", "Banana", "Lemon"};
Arrays.sort(array, Main::cmp); // 直接傳入靜態方法
/**
* String compareTo(String o), 在實際方法調用的時候爲
* `public static int compareTo(this, String o)`
*/
Arrays.sort(array, String::compareTo); // 直接傳入靜態方法
System.out.println(String.join(", ", array));
}
static int cmp(String s1, String s2) {
return s1.compareTo(s2);
}
- 方法引用: 某個方法簽名和接口恰好一致, 就可以直接傳入方法引用
- 簽名一致: 方法參數一致, 返回類型相同, 兩個方法簽名一致
構造方法引用
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Bob", "Alice", "Tim");
// List<Person> persons = new ArrayList<>();
// for (String name: names) {
// persons.add(new Person(name));
// }
List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
System.out.println(persons);
}
static int cmp(String s1, String s2) {
return s1.compareTo(s2);
}
}
class Person {
String name;
public Person(String name) {
this.name = name;
}
public String toString() {
return "Person" + this.name;
}
}
- 構造方法的引用寫法:
類名::new
- 構造方法隱式的返回
this
總結
FunctionalInterface
允許傳入- 接口實現類
- Lambda表達式(只需要列出參數名, 其他由編譯器推斷類型)
- 符合方法簽名的靜態實例
- 符合方法簽名的實例方法 (實例類型被看作第一個參數類型)
- 符合方法簽名的構造方法 (實例方法被看作返回類型)
FunctionalInterface
不強制繼承關係, 不需要方法名稱相同, 只要求方法參數(類型和數量)與方法返回類型相同, 即認爲方法簽名相同
使用Stream
-
這個
Stream
代表任意Java對象序列 -
區別: java.io / java.util.stream
- 存儲: 順序讀寫的
byte
或char
/ 順序輸出的任意Java對象實例 - 用途: 序列化至文件或網絡 / 內存計算或者業務邏輯
- 存儲: 順序讀寫的
-
List
用來操作一組已存在的Java對象,Stream
輸出的元素可能沒有預先存儲再內存中, 而是實時計算出來的 -
區別: java.util.List / java.util.stream
- 元素: 已分配並存儲在內存中 / 可能未分配, 實時計算
- 用途: 操作一組存在的Java對象 / 惰性計算
-
總結:
- 可以"存儲"有限或者無限個元素, 可能全部存儲在內存, 也有根據需要實時計算出來的
- 一個
Stream
可以輕易轉化爲另一個Stream
, 不會修改原Stream
本身
-
惰性計算的特定: 一個
Stream
轉另一個Stream
時, 只存儲了轉換規則, 沒有任何計算 -
真正的計算通常發生在最後結果的獲取
創建Stream
Stream.of()
創建Stream
最簡單的方法, 直接使用Stream.of()
, 傳入可變參數即創建一個能確定輸出確定元素的Stream
Stream<String> stream = Stream.of("A", "B", "C", "D");
// forEach()相當於內部循環調用
// 可以傳入符合Consumer接口的 void accept(T t)方法引用
stream.forEach(System.out::println);
基於數組或Collection
Stream<String> stream1 = Arrays.stream(new String[] {"A", "B", "C"});
Stream<String> stream2 = List.of("X", "Y", "Z").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
-
數組使用
Arrays.stream()
方法 -
Collection
直接使用stream()
方法即可 -
上述方法都是現有序列變成stream, 元素是固定的
基於Supplier
- 使用
Stream.generate()
方法, 傳入一個Supplier
對象 Stream
會不斷調用Supplier.get()
方法來不斷產生下一個元素Stream
保存的是算法, 可以表示無限隊列
public class Main {
public static void main(String[] args) {
Stream<Integer> natual = Stream.generate(new NatualSupplier());
// 注意: 無限序列必須先編程有限序列再打印
natual.limit(20).forEach(System.out::println);
}
}
class NatualSupplier implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
- 無限循環直接調用
forEach()
或者count()
求值, 會進入死循環
其他方法
創建Stream
的第三種方法是通過一些API接口, 直接獲得Stream
// 文件讀取
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
// 每個元素, 表示一行內容
}
// 正則
Pattern p = Pattern.compile("\\s+");
Stream<String> s = p.splitAsStream("The quick brow fox jumps over the lazy dog");
s.forEach(System.out::println);
基本類型
- 泛型不支持基本類型, 無法進行
Stream<int>
, 會編譯錯誤. - 提供
IntStream
,LongStream
,DoubleStream
, 使用基本類型的Stream
- 使用方法和泛型
Stream
, 並無大礙. - 目的是: 爲了避免使用
Stream<Integer>
的運行效率低
IntStream is = Arrays.stream(new int[] {1, 2, 4});
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
使用map
- 將一個
Stream
轉換成另一個Stream
map
操作, 把一個Stream
的每一個元素一一對應到應用了目標函數的結果上- 參數接收一個
Function
對象, 並定義一個apply()
, 負責把一個T
類型轉換成R
類型 - 對任何Java對象都有效
public class Main {
public static void main(String[] args) {
List.of("Apple ", " pear", "ORANGE", "BanaNa ")
.stream()
.map(String::trim) // 去除空格
.map(String::toLowerCase) // 變成小寫
.forEach(System.out::println); // 打印
}
}
String[] array = new String[] { " 2019-12-31 ", "2020 - 01-09 ", "2020- 05 - 01 ", "2022 - 02 - 01",
" 2025-01 -01" };
// 請使用map把String[]轉換爲LocalDate並打印:
Arrays.stream(array)
.map(n -> n.replace(" ", ""))
.map(LocalDate::parse)
.forEach(System.out::println);
使用filter
Stream
的轉換方法, 對每個元素一一測試, 過濾不滿足條件的元素- 接受
Predicate
方法, 定義一個test()
方法, 判斷元素是否符合條件 - 可以應用與任何Java對象
public class Main {
public static void main(String[] args) {
Stream.generate(new LocalDateSupplier())
.limit(31)
.filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
.forEach(System.out::println);
}
}
class LocalDateSupplier implements Supplier<LocalDate> {
LocalDate start = LocalDate.of(2020, 1, 1);
int n = -1;
@Override
public LocalDate get() {
n++;
return start.plusDays(n);
}
}
使用reduce
- reduce是一個聚合方法, 可以把Stream的所有元素按照函數聚合成一個結果
- 傳入
BinaryOperator
接口, 定義了一個apply()
方法, 負責把上次累加的結果和本地的元素進行運算. 並返回累加的結果. - 如果去掉初始值, 並stream爲空, 會出現返回
Optional
對象的情況, 所以我們需要進一步進行判斷
public static void main(String[] args) {
// 按行讀取配置文件:
List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
Map<String, String> map = props.stream()
// 把k=v轉換爲Map[k]=v:
.map(kv -> {
String[] ss = kv.split("\\=", 2);
return Map.of(ss[0], ss[1]);
})
// 把所有聚合到一個Map
.reduce(new HashMap<String, String>(), (m, kv) -> {
m.putAll(kv);
return m;
});
// 打印結果
map.forEach((k, v) -> {
System.out.println(k + " = " + v);
});
}
輸出集合
Stream
操作分成兩種- 轉換操作: 並不會觸發任何計算
- 聚合操作: 真正的計算從這裏開始, 因爲要獲取具體元素
public class Main {
public static void main (String[] args) {
Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
Stream<Long> s4 = s3.limit(10);
Long s5 = s4.reduce((long) 0, (acc, n) -> acc + n);
System.out.println(s5);
}
}
class NatualSupplier implements Supplier<Long> {
long n = 0;
public Long get() {
System.out.println("調用get()");
n++;
return n;
}
}
-
s1 -> s3的過程不會有任何計算, 只保留計算過程
-
s4的聚合開始真正的獲取元素, 一直從s1開始計算
-
不進行s5操作的話, 不會調用
get()
方法
輸出爲List
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);
Stream
的每個元素收集到List
方法是調用collect()
方法, 並傳入Collectors.toList()
對象.- 使用
collect(Collectors.toSet())
可以把Stream
每個元素收集到Set
中
輸出爲數組
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
String[] array = list.stream().toArray(String[]::new);
- 傳入的"構造方法"是
String[]::new
輸出爲Map
- 需要指定兩個映射函數, 分別把元素映射爲key和value
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream.collect(Collectors.toMap(
s -> s.substring(0, s.indexOf(':')),
s -> s.substring(s.indexOf(':') + 1)
));
System.out.println(map);
分組輸出
List<String> list = List.of("Apple", "Banana", "Blackberry", "Cocount");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
System.out.println(groups);
- 使用
groupingBy()
進行分組輸出, 第一個參數, 表示key
, 第二個參數表示value
其他操作
排序
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted(String::compareToIgnoreCase)
.collect(Collectors.toList());
- 如果需要自定義排序, 傳入
Comparator
即可
去重
List<String> list = List.of("A", "A", "b", "B")
.stream()
.distinct()
.collect(Collectors.toList());
截取
List list = List.of("a", "b", "c", "d", "e")
.stream()
.skip(2) // 跳過多少個元素
.limit(3) // 截取3個
.collect(Collectors.toList());
System.out.println(list);
合併
Stream<String> s1 = List.of("a", "b", "c").stream();
Stream<String> s2 = List.of("d", "c").stream();
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList()));
flatMap
flatMap
把Stream
中的每個元素, 都變成Stream
, 然後合併成一個Stream
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
Stream<Integer> i = s.flatMap(list -> list.stream());
System.out.println(i.collect(Collectors.toList()));
並行
Stream<String> s = List.of("a", "b", "c", "d").stream();
String[] r = s.parallel() // 此處變爲並行處理stream, 提高處理效率
.sorted()
.toArray(String[]::new);
for (String i : r) {
System.out.println(i);
}
System.out.println(r);
其他聚合方法
- 其他的聚合方法:
count()
: 返回stream
中元素個數max(Comparator<? super T> cp)
: 找出最大元素
- 針對
IntStream
,LongStream
和DoubleStream
:sum()
: 求和操作average()
: 對所有元素求平均數
- 測試
Stream
的元素是否滿足條件boolean allMatch(Predicate<? super T>)
: 測試所有元素是否滿足測試條件boolean anyMatch(Predicate<? super T>
: 測試至少有一個元素滿足條件
forEach()
循環處理每一個元素
s.forEach(System.out::println);
困惑
String[]::new
到底是個什麼
- 相當於
size -> new String[size]