函數式數據處理
Java 8 中新增的特性其目的是為了幫助開發人員寫出更好地代碼,其中關鍵的一部分就是對核心類庫的改進。流( Stream )和集合類庫便是核心類庫改進的內容。
1. 從外部迭代到內部迭代
對于一個集合迭代是我們常用的一種操作,通過迭代我們可以處理返回每一個操作。常用的就是 for
循環了。我們來看個例子:
import java.util.Arrays;
import java.util.List;
public class Test{
public static void main(String...s){
List<Integer> numbers = Arrays.asList(new Integer[]{1,2,3,4,5,6,7});
int counter = 0;
for(Integer integer : numbers){
if(integer > 5) counter++;
}
System.out.println(counter);
}
}
輸出: 2
這里我們統計數組 numbers
中大于 5 的元素的個數,我們通過 for
循環對 numbers
數組進行迭代,隨后對每一個元素進行比較。這個調用過程如下:
在這個過程中,編譯器首先會調用 List
的 iterator()
方法產生一個 Iterator
對象來控制迭代過程,這個過程我們稱之為 外部迭代。 在這個過程中會顯示調用 Iterator
對象的 hasNext()
和 next()
方法來完成迭代。
這樣的外部迭代有什么問題呢?
Tips: 對于循環中不同操作難以抽象。
比如我們前面的例子,假設我們要對大于 5 小于 5 和等于 5 的元素分別進行統計,那么我們所有的邏輯都要寫在里面,并且只有通過閱讀里面的邏輯代碼才能理解其意圖,這樣的代碼可閱讀性是不是和 Lambda 表達式的可閱讀性有著天壤之別呢?
Java 8 中提供了另一種通過 Stream 流來實現 內部迭代 的方法。我們先來看具體的例子:
import java.util.List;
import java.util.Arrays;
public class Test{
public static void main(String...s){
List<Integer> numbers = Lists.newArrayList(1,2,3,4,5,6,7);
long counter = numbers.stream().filter(e->e>5).count();
System.out.println(counter);
}
}
輸出: 2
在這個例子中,我們調用 stream()
方法獲取到 Stream 對象,然后調用該對象的 filter()
方法對元素進行過濾,最后通過 count()
方法來計算過濾后的 Stream 對象中包含多少個元素對象。
與外部迭代不同,內部迭代并不是返回控制對象 Iterator
, 而是返回內部迭代中的相應接口 Stream
。進而把對集合的復雜邏輯操作變成了明確的構建操作。
在這個例子中,通過內部迭代,我們把整個過程被拆分成兩步:
- 找到大于 5 的元素;
- 統計這些元素的個數。
這樣一來我們代碼的可讀性是不是大大提升了呢?
2. 實現機制
在前面的內部迭代例子中,整個操作過程被分成了過濾和計數兩個簡單的操作。我們再來看一下之前的例子,并做了一些改造:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
public class Test{
public static void main(String...s){
List<Integer> numbers = new ArrayList<Integer>();
Collections.addAll(numbers,new Integer[]{1,2,3,4,5,6,7});
Stream stream1 = numbers.stream();
numbers.remove(6);
//直接使用numbers的stream()
long counter = numbers.stream().filter(e->e>5).count();
System.out.println(counter);
//調用之前的stream1
counter = stream1.filter(ex-> (Integer)ex>5).count();
System.out.println(counter);
}
}
返回結果:
1
1
在這個例子中,我們在獲取到 Stream 對象 stream1 后刪除了數組 numbers
中的最后一個元素,隨后分別對 numbers 和 stream1 進行過濾統計操作,會發現兩個結果是一樣的,stream1 中的內容跟隨 numbers
一起做相應的改變。這說明 Stream 對象不是一個新的集合,而是創建新集合的配方。同樣,像 filter()
雖然返回 Stream 對象,但也只是對 Stream 的刻畫,并沒有產生新的集合。
我們通常對于這種不產生集合的方法叫做 惰性求值方法,相對應的類似于 count()
這種返回結果的方法我們叫做 及早求值方法。
我們可以把多個惰性求值方法組合起來形成一個惰性求值鏈,最后通過及早求值操作返回想要的結果。這類似建造者模式,使用一系列操作設置屬性和配置,最后通過一個 build
的方法來創建對象。通過這樣的一個過程我們可以讓我們對集合的構建過程一目了然。這也就是 Java 8 風格的集合操作。
3. 常用的流操作
為了更好地理解 Stream API,我們需要掌握它的一些常用操作,接下來我們將逐個學習幾種常用的操作。
3.1 collect
collect
操作是根據 Stream 里面的值生成一個列表,它是一個求值操作。
Tips:
collect
方法通常會結合Collectors
接口一起使用,是一個通用的強大結構,可以滿足數據轉換、數據分塊、字符串處理等操作。
我們來看一些例子:
- 生成集合:
import java.util.stream.Stream;
import java.util.List;
import java.util.stream.Collectors;
public class Test{
public static void main(String...s){
List<String> collected = Stream.of("a","b","c").collect(Collectors.toList());
System.out.println(collected);
}
}
輸出:[a, b, c]
使用 collect(Collectors.toList())
方法從 Stream 中生成一個列表。
- 集合轉換:
使用 collect
來定制集合收集元素。
import java.util.List;
import java.util.stream.Stream;
import java.util.stream.Collectors;
import java.util.TreeSet;
public class Test{
public static void main(String...s){
List<String> collected = Stream.of("a","b","c","c").collect(Collectors.toList());
TreeSet<String> treeSet = collected.stream().collect(Collectors.toCollection(TreeSet::new));
System.out.println(collected);
System.out.println(treeSet);
}
}
輸出結果:
[a, b, c, c]
[a, b, c]
使用 toCollection
來定制集合收集元素,這樣就把 List
集合轉換成了 TreeSet
- 轉換成值:
使用 collect
來對元素求值。
import java.util.List;
import java.util.stream.Stream;
import java.util.stream.Collectors;
public class Test{
public static void main(String...s){
List<String> collected = Stream.of("a","b","c").collect(Collectors.toList());
String maxChar = collected.stream().collect(Collectors.maxBy(String::compareTo)).get();
System.out.println(maxChar);
}
}
輸出: c
上面我們使用 maxBy
接口讓收集器生成一個值,通過方法引用調用了 String
的 compareTo
方法來比較元素的大小。同樣還可以使用 minBy
來獲取最小值。
- 數據分塊:
比如我們對于數據 1-7 想把他們分成兩組,一組大于 5 另外一組小于等于 5,我們可以這么做:
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import java.util.stream.Collectors;
public class Test{
public static void main(String...s){
List<Integer> collected = Stream.of(1,2,3,4,5,6,7).collect(Collectors.toList());
Map<Boolean,List<Integer>> divided = collected.stream().collect(Collectors.partitioningBy(e -> e>5));
System.out.println(divided.get(true));
System.out.println(divided.get(false));
}
}
輸出結果:
[6, 7]
[1, 2, 3, 4, 5]
通過 partitioningBy
接口可以把數據分成兩類,即滿足條件的和不滿足條件的,最后將其收集成為一個 Map
對象,其 Key
為 Boolean
類型,Value
為相應的集合元素。
同樣我們還可以使用 groupingBy
方法來對數據進行分組收集,這類似于 SQL 中的 group by
操作。
- 字符串處理:
collect
還可以來將元素組合成一個字符串。
import java.util.List;
import java.util.stream.Stream;
import java.util.stream.Collectors;
public class Test{
public static void main(String...s){
List<String> collected = Stream.of("a","b","c").collect(Collectors.toList());
String formatted = collected.stream().collect(Collectors.joining(",","[","]"));
System.out.println(formatted);
}
}
輸出:[a,b,c]
這里我們把 collected
數組的每個元素拼接起來,并用 [
]
包裹。
3.2 map
map
操作是將流中的對象換成一個新的流對象,是 Stream 上常用操作之一。 其示意圖如下:
?
比如我們把小寫字母改成大寫,通常我們會使用 for
循環:
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
public class Test{
public static void main(String...s){
List<String> collected = new ArrayList<>();
List<String> newArr = new ArrayList<>();
Collections.addAll(newArr,new String[]{"a","b","c"});
for(String string : newArr){
collected.add(string.toUpperCase());
}
System.out.println(collected);
}
}
輸出: [A, B, C]
此時,我們可以使用 map
操作來進行轉換:
import java.util.List;
import java.util.stream.Stream;
import java.util.stream.Collectors;
public class Test{
public static void main(String...s){
List<String> collected = Stream.of("a","b","c").collect(Collectors.toList());
List<String> upperCaseList = collected.stream().map(e->e.toUpperCase()).collect(Collectors.toList());
System.out.println(upperCaseList);
}
}
輸出: [A, B, C]
在 map
操作中,我們 把 collected
中的每一個元素轉換成大寫,并返回。
3.3 flatmap
flatmap
與 map
功能類似,只不過 map
對應的是一個流,而 flatmap
可以對應多個流。
我們來看一個例子:
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
public class Test{
public static void main(String...s){
List<String> nameA = Arrays.asList("Mahela", "Sanga", "Dilshan");
List<String> nameB = Arrays.asList("Misbah", "Afridi", "Shehzad");
List<List<String>> nameSets = Arrays.asList(nameA,nameB);
List<String> flatMapList = nameSets.stream()
.flatMap(pList -> pList.stream())
.collect(Collectors.toList());
System.out.println(flatMapList);
}
}
返回結果: [Mahela, Sanga, Dilshan, Misbah, Afridi, Shehzad]
通過 flatmap
我們把集合 nameSets
中的字集合合并成了一個集合。
3.4 filter
filter
用來過濾元素,在元素遍歷時,可以使用 filter
來提取我們想要的內容,這也是集合常用的方法之一。其示意圖如下:
?
我們來看一個例子:
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.stream.Collectors;
public class Test{
public static void main(String...s) {
List<Integer> numbers = new ArrayList<>();
Collections.addAll(numbers,new Integer[]{1,2,3,4,5,6,7});
List<Integer> collected = numbers.stream()
.filter(e->e>5).collect(Collectors.toList());
System.out.println(collected);
}
}
輸出:[6, 7]
此時,filter
會遍歷整個集合,將滿足將滿足條件的元素提取出來,并通過收集器收集成新的集合。
3.5 max/min
max/min
求最大值和最小值也是集合上常用的操作。它通常會與 Comparator
接口一起使用來比較元素的大小。示例如下:
import java.util.List;
import java.util.Collections;
public class Test{
public static void main(String...s) {
List<Integer> numbers = new ArrayList<>();
Collections.addAll(numbers,new Integer[]{1,2,3,4,5,6,7});
Integer max = numbers.stream().max(Comparator.comparing(k->k)).get();
Integer min = numbers.stream().min(Comparator.comparing(k->k)).get();
System.out.println("max:"+max);
System.out.println("min:"+min);
}
}
輸出:
max:7
min:1
我們可以在 Comparator 接口中定制比較條件,來獲得想要的結果。
3.6 reduce
reduce
操作是可以實現從流中生成一個值,我們前面提到的如 count
、max
、min
這種及早求值就是由reduce
提供的。我們來看一個例子:
import java.util.stream.Stream;
public class Test{
public static void main(String...s) {
int sum = Stream.of(1,2,3,4,5,6,7).reduce(0,(acc,e)-> acc + e);
System.out.println(sum);
}
}
輸出:28
上面的例子是對數組元素進行求和,這個時候我們就要使用 reduce
方法。這個方法,接收兩個參數,第一個參數相當于是一個初始值,第二參數則為具體的業務邏輯。 上面的例子中,我們給 acc
參數賦予一個初始值 0 ,隨后將 acc
參數與各元素求和。
4. 小結

以上我們學習了 Java 8 的流及常用的一些集合操作。我們需要常用的函數式接口和流操作非常熟悉才能更好地使用這些新特性。
另外,請思考一個問題,在本節關于集合的操作中都將集合通過 stream()
方法轉換成了 Stream 對象,那么我們還有必要對外暴露一個集合對象(List 或者 Set)嗎?
Tips: 在編程過程中,使用 Stream 工廠比對外暴露集合對象要更好一些。僅需要暴露 Stream 接口,在實際操作中無論怎么使用都影響內部的集合。
所以,Java 8 風格不是一蹴而就的,我們可以對已有的代碼進行重構來練習和強化 Java 8 的編程風格,時間長了自然就對 Stream 對象有更深的理解了。