先看一段代碼
List<String> names = Arrays.asList("Jack", "Jill", "Nate", "Kara", "Kim", "Jullie", "Paul", "Peter");
List<String> subList = new ArrayList<>();
for(String name : names) {
if(name.length() == 4)
subList.add(name);
}
StringBuilder namesOfLength4 = new StringBuilder();
for(int i = 0; i < subList.size() - 1; i++) {
namesOfLength4.append(subList.get(i));
namesOfLength4.append(", ");
}
if(subList.size() > 1)
namesOfLength4.append(subList.get(subList.size() - 1));
System.out.println(namesOfLength4);
Stream API的歷史
- 在java8引入
- 受益于lambda表達式
lambda表達式
接口常被用于傳遞代碼,如sort接收一個Comparator接口用于給文件數(shù)組按名稱排序
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
return f1.getName().compareTo(f2.getName());
}
});
不過上述代碼需要new一個匿名類,而實際上sort()方法只應該知道如何對兩個文件做比較即可,也就是說只要一個函數(shù)體,但是java卻非要一個類!
Java 8提供只需要傳入函數(shù)即可進行排序的方案,這就是lambda表達式:
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
上面的代碼就自然多了,只需給出一個函數(shù)就能完成排序,(f1, f2)表示傳入的參數(shù),類型由java根據前面的files自行推斷出來。如果無參數(shù),則可以寫成()。
函數(shù)式接口
接口替換為lambda表達式不是無條件的,要求該接口是所謂的函數(shù)式接口。
函數(shù)式接口就是一個具有一個方法的普通接口
可以用@FunctionalInterface來注解某個接口,強行要求某個接口為函數(shù)式接口,如果該接口含有2個方法,則編譯會失敗;默認方法與靜態(tài)方法并不影響函數(shù)式接口的契約,可以任意使用:
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); default Comparator<T> reversed() { return Collections.reverseOrder(this); } public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() { return Collections.reverseOrder(); } ...... }
一句話,lambda表達式的目的是讓大家編程不用再記憶類名和函數(shù)名,也不需要寫參數(shù)類型,自然得傳入需要的函數(shù)體即可。
比如這段sort()這段代碼:
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
根本不需要記憶sort需要傳入Comparator接口,也不需要記憶它的方法名稱compare(),也不需要寫參數(shù)類型File,方便吧。
Stream API
針對常見的集合數(shù)據處理,Java 8引入了一套新的類庫,位于包java.util.stream下,稱之為Stream API。Java 8給Collection接口增加默認方法,可以返回一個Stream。
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
在介紹Stream API之前,先看2個函數(shù)式接口,定義在包java.util.function下:

可以把它們想象提供和
Comparator類似的功能,就是提供一個函數(shù)體。Predicate叫謂詞函數(shù),測試輸入是否滿足條件,返回true或false;Function叫函數(shù)變換,將輸入值1->1映射為另一個值。我們完全不用在意它們的接口名和方法名,只要知道它們的用途即可。
為便于舉例,我們先定義一個簡單的學生類Student,有name和score兩個屬性,如下所示,我們省略了getter/setter方法。
static class Student {
String name;
double score;
public Student(String name, double score) {
this.name = name;
this.score = score;
}
}
有一個學生列表:
List<Student> students = Arrays.asList(new Student[] {
new Student("zhangsan", 89d),
new Student("lisi", 89d),
new Student("wangwu", 98d) });
map 變換

返回學生的姓名列表:
List<String> nameList = students.stream()
.map(s->s.getName())
.collect(Collectors.toList());
map()接收一個Function接口,將學生變換為姓名;在collect做實際的處理,將學生對象挨個變換為姓名,并構成列表輸出。
filter變換

過濾得分在90以上的學生:
List<Student> above90List = students.stream()
.filter(t->t.getScore()>90)
.collect(Collectors.toList());
filter()接收一個Predicate接口,過濾出90分以上的學生,在collect做實際的處理,構成列表輸出。
復合變換
返回90分以上的學生姓名:
- 過濾:得到90分以上的學生列表
- 轉換:將學生列表轉換為姓名列表
List<String> above90Names = students.stream()
.filter(t->t.getScore()>90)
.map(s->s.getName())
.collect(Collectors.toList());
效率問題探討
對復合變換,如果用舊方法,會在一次遍歷中做過濾和變換操作:
List<String> above90NamesList = new ArrayList<>();
for (Student t : students) {
if (t.getScore() > 90) {
above90NamesList.add(t.getName());
}
}
而用Stream的方式看起來似乎先filter遍歷了一次,map又遍歷了一次,這樣速度豈不會變慢?其實不會,java的Stream用了一種巧妙(tricky)的技術,實現(xiàn)了惰性求值,直到最后一步collect的時候才會做遍歷操作。要向做到這一點,應該采用某種方式記錄用戶每一步的操作,當用戶調用結束操作時將之前記錄的操作疊加到一起在一次迭代中全部執(zhí)行掉,具體實現(xiàn)可以看這篇文章:深入理解Java Stream流水線。
stream操作分類
前面講到filter,map,collect都是stream的方法,但是它們是有所不同的,stream的操作分為2類中間操作(intermediate operations)和結束操作(terminal operations),歸納如下:
| 操作類型 | 接口方法 |
|---|---|
| 中間操作 | concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered() |
| 結束操作 | allMatch() anyMatch() collect() count() findAny() findFirst()forEach() forEachOrdered() max() min() noneMatch() reduce() toArray() |
- 中間操作總是會惰式執(zhí)行,調用中間操作只會生成一個標記了該操作的新stream,僅此而已。
- 結束操作會觸發(fā)實際計算,計算發(fā)生時會把所有中間操作積攢的操作以流水線的方式執(zhí)行,這樣可以減少迭代次數(shù)。計算完成之后stream就會失效。
更多的例子
不僅是List有stream操作,Set也有steam操作,下面是個例子:
Map<Integer, String> HOSTING = new HashMap<>();
HOSTING.put(1, "linode.com");
HOSTING.put(2, "heroku.com");
HOSTING.put(3, "digitalocean.com");
HOSTING.put(4, "aws.amazon.com");
//Map -> Set -> Stream -> Filter -> List
List<String> strings = HOSTING.entrySet().stream()
.filter(map -> "aws.amazon.com".equals(map.getValue()))
.map(map -> map.getValue())
.collect(Collectors.toList());
System.out.println(strings);
回到開頭的例子
List<String> names = Arrays.asList("Jack", "Jill", "Nate", "Kara", "Kim", "Jullie", "Paul", "Peter");
// 給定一個名稱集合,僅選擇長度為 4 的名稱,然后通過逗號將它們連接起來。
System.out.println(
names.stream()
.filter(name -> name.length() == 4)
.collect(Collectors.joining(", ")));
問題:給定名為 numbers 的列表,此代碼將計算大于 3 且小于 8 的偶數(shù)并將該數(shù)字乘以 2,然后輸出結果。
int result = 0;
for(int e : numbers) {
if(e > 3 && e % 2 == 0 && e < 8) {
result += e * 2;
}
}
System.out.println(result);
System.out.println(
numbers.stream()
.filter(e -> e > 3)
.filter(e -> e % 2 == 0)
.filter(e -> e < 8)
.mapToInt(e -> e * 2)
.sum());
參考文獻
Java Stream API入門篇
深入理解Java Stream流水線
計算機程序的思維邏輯 (91) - Lambda表達式
計算機程序的思維邏輯 (92) - 函數(shù)式數(shù)據處理 (上)
Java 8 Stream Tutorial
提倡使用有幫助的編碼