先來看一個程式片段:
String fileName = args[0];
String prefix = args[1];
String firstMatchdLine = "no matched line";
for (String line : Files.readAllLines(Paths.get(fileName))) {
if(line.startsWith(prefix)) {
firstMatchdLine = line;
break;
}
}
out.println(firstMatchdLine);
這個程式片段會讀取指定的檔案,找到第一個符合條件的行,然後顯示小寫後離開迴圈。在JDK8中,這類的需求,可以用以下的程式片段來完成:
String fileName = args[0];
String prefix = args[1];
Optional<String> firstMatchdLine =
Files.lines(Paths.get(fileName))
.filter(line -> line.startsWith(prefix))
.findFirst();
out.println(firstMatchdLine.orElse("no matched line"));
一眼可見到最大的差別是沒有使用到for
迴圈與if
判斷式,以及使用了管線化(Pipeline)操作風格,而效能上也有所差異,如果讀取的檔案很大,第二個程式片段會比第一個程式片段來得有效率。
java.nio.file.Files
的lines()
方法,會傳回java.util.stream.Stream
實例,就這個例子來說就是Stream<String>
,使用Stream
的filter
方法會過濾留下符合條件的元素,findFirst()
方法會嘗試看看留下的元素有沒有首元素,因為也可能完全沒有元素,因此傳回Optional<String>
實例。
效能上的差異性在於,第一個程式片段的Files.readAllLines()
方法傳回的是List<String>
實例,當中包括了檔案中所有行,如果第一行就符合指定的條件了,那後續的行讀取就是多餘的;第二個程式片段的lines()
方法實際上沒有進行任何一行的讀取,filter()
也沒有作任何一行的過濾,直到呼叫findFirst()
時,filter()
指定的條件才會真正去執行,而此時才會要求lines()
傳回的Stream
進行第一行讀取,如果第一行就符合,那後續的行就不會再讀取,效率的差異性就在於此。
之所以能夠達到這類惰性求值(Lazy evaluation)的效果,也就是需要時findFirst()
要求filter()
,而filter()
再要求讀取檔案下一行這種你需要我再給的行為,功臣就是Stream
實例。第一個程式片段要取得List
傳回的Iterator
,以搭配for
迴圈進行外部迭代(External iteration),第二個程式片段則將迭代行為隱藏在lines()
、filter()
與findFirst()
方法之中,稱之為內部迭代(Internal iteration),因為內部迭代的行為是被隱藏的,因此多了很多可以實現效率的可能性。
在 Stream
的API文件 中談到,Stream
繼承了AutoClosable
,而BaseStream.close()
實作了close()
方法,然而基本上,絕大多數的Stream
並不需要呼叫close()
方法,除了一些IO操作之外,例如Files.lines()
、Files.list()
與Files.walk()
方法,建議這類操作可以搭配try-with-resource語法。
JDK8引入了Stream
API,也引入了管線操作風格,一個管線基本上包括了幾個部份:
- 來源(Source):可能是檔案、陣列、群集(
Collection
)、產生器(Generator)等,在這個例子就是指定的檔案。 - 零或多個中介操作(Intermediate operation):又稱為聚合操作(Aggregate operation),這些操作呼叫時,並不會立即進行手邊的資料處理,它們很懶惰(Lazy),只會在後續中介操作要求資料時才會動手處理下一筆資料,像是第二個程式片段中的
filter()
方法。 - 一個最終操作(Terminal operation):最後真正需要結果的操作,這個操作會要求之前懶惰的中介操作開始動手。
這就是Stream
API之所以命名為Stream
的原因,Stream
實例銜接了來源,提到中介操作方法,每個中介操作方法都會傳回Stream
實例,但不會實際進行資料處理,每個中介操作後的Stream
實例會串連在一起,Stream
亦提供最終操作方法,不是傳回Stream
而是傳回真正需要的結果,最終操作方法會引發之前串連在一起的Stream
實例進行資料處理。
實際上從來源進行一些運算,以求得最終結果,正是程式設計時最常進行的動作,因此JDK8在不少具有來源概念的API上,都增加了可傳回Stream
的方法,除了這邊看到的BufferedReader
之外,你還可以使用Stream
上的靜態方法來建立Stream
實例,像是of()
方法,對於陣列,也可以使用Arrays
的stream()
方法來建立Stream
實例。
Collection
也是個例子,其定義了stream()
方法會傳回Stream
實例,只要是Collection
都可以進行中介操作。例如,原本有個程式片段:
List<Person> persons = ...;
List<String> names = new ArrayList<>();
for(Person person : persons) {
if(person.getAge() > 15) {
names.add(person.getName().toUpperCase());
}
}
在JDK8中可以改為以下的風格:
List<Person> persons = ...;
List<String> names = persons.stream()
.filter(person -> person.getAge() > 15)
.map(person -> person.getName().toUpperCase())
.collect(toList()); // 使用了 Collectors.toList() 方法
每個中介操作隱藏了細節,除了增加更多效率改進的空間之外,也鼓勵開發者多利用這類風格,來避免撰寫一些重複流程,或思考目前的複雜演算中,實際上會是由哪些小任務完成。
例如,如果你的程式在for
迴圈中使用了if
:
for(Person person : persons) {
if(person.getAge() > 15) {
// 這是下一個小任務
}
}
也許就有改用filter()
方法的可能性:
persons.stream()
.filter(person -> person.getAge() > 15);
如果你的程式在for
迴圈中從一個型態對應至另一個型態:
for(Person person : persons) {
...
...person.getName().toUpperCase()...
...
}
也許就有改用map()
方法的可能性:
persons.stream()
.map(person -> person.getName().toUpperCase());
許多時候,for
迴圈中就是滲雜了許多小任務,從而使for
迴圈中的程式碼艱澀難懂,辨識出這些小任務,運用中介操作,形成管線化操作風格,就能增加程式碼閱讀時的流暢性。
Stream
實際上繼承自java.util.stream.BaseStream
,而BaseStream
還有DoubleStream
、IntStream
與LongStream
這三個用於基本型態操作的子介面。
Stream
只能迭代一次,重複對Stream
進行迭代,會引發IllegalStateException
。