Java8 Stream
@目錄
1、什麼是流?
Stream 作為 Java 8 的一大亮點,它與 java.io 包裡的 InputStream 和 OutputStream 是完全不相關的東西。
Java 8 中的 Stream 是對集合(Collection)物件功能的增強,它專注於對集合物件進行各種非常便利、高效的聚合操作(aggregate operation),或者大批量資料操作 (bulk data operation)。
Java 8 中出現的 java.util.stream 是一個函式式語言+多核時代綜合影響的產物。
這裡一個簡單的示例——對陣列求和。
在引入流之前:
int[] nums = { 1, 2, 3 };
//迴圈計算求職
int sum = 0;
for (int i : nums) {
sum += i;
}
System.out.println("結果為:" + sum);
邏輯也比較簡單,引入流之後:
//使用流 int sum2 = IntStream.of(nums).sum(); System.out.println("結果為:" + sum2);
程式碼相對而言要簡潔一些。
這只是一個簡單的迭代求和,如果是一些複雜的聚合或批量操作,那麼流在程式碼簡潔性上就更有優勢了。
2、建立流
當我們使用一個流的時候,通常包括三個基本步驟:
獲取一個數據源(source)→ 資料轉換→ 執行操作獲取想要的結果。
每次轉換原有 Stream 物件不改變,返回一個新的 Stream 物件(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道,如下圖所示。
有很多方法可以建立不同資料來源的流例項。
2.1、空的流
建立空的流,使用empty()方法:
Stream<String> streamEmpty = Stream.empty();
使用empty()方法建立來避免沒有元素的流返回null的問題:
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}
2.2、集合的流
可以建立任何型別的集合(Collection, List, Set)的流:
Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();
2.3、陣列的流
陣列也可以作為流的資料來源:
Stream<String> streamOfArray = Stream.of("a", "b", "c");
也可以從現有陣列或陣列的一部分中建立流:
String[] arr = new String[]{"a", "b", "c"};
Stream<String> streamOfArrayFull = Arrays.stream(arr);
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);
2.4、 Stream.builder()
使用builder時,應在語句的右側另外使用的型別,否則build()方法將建立 Stream <Object >的例項:
Stream<String> streamBuilder =
Stream.<String>builder().add("a").add("b").add("c").build();
2.5、 Stream.generate()
generate()方法接受Supplier\
Stream<String> streamGenerated =
Stream.generate(() -> "element").limit(10);
上面的程式碼建立了一個由十個字串組成的序列,其值是“ element”。
2.6、 Stream.iterate()
建立無限流的另一種方法是使用iterate()方法:
Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);
結果流的第一個元素是iterate()方法的第一個引數。為了建立後續的元素,使用了上一個元素。在上面的示例中,第二個元素為42。
2.7、 基本型別的流
Java 8提供了從三種基本型別中建立流的方式:int,long和double。
由於Stream <T>是泛型介面,無法將基本型別用作泛型的型別引數,因此建立了三個新的特殊介面:IntStream,LongStream和DoubleStream。使用新介面可以減輕不必要的自動裝箱,從而提效率:
IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);
range(int startInclusive,int endExclusive)方法建立從第一個引數到第二個引數的有序流。它以等於1的步長遞增後續元素的值。結果不包括最後一個引數,它只是序列的上限。
2.8、字串的流
字串也可以用作建立流的資料來源。
由於JDK中沒有介面CharStream,因此使用IntStream表示字元流。用到了String類的chars()方法。
IntStream streamOfChars = "abc".chars();
下面的示例根據指定的正則表示式將String細分為子字串:
Stream<String> streamOfString =
Pattern.compile(", ").splitAsStream("a, b, c");
2.9、檔案的流
Java NIO類 Files 允許通過lines()方法生成文字檔案的Stream <String>。文字的每一行都成為流的元素:
Path path = Paths.get("C:\\file.txt");
Stream<String> streamOfStrings = Files.lines(path);
Stream<String> streamWithCharset =
Files.lines(path, Charset.forName("UTF-8"));
可以將字元編碼指定為lines()方法的引數。
最常用的就是從集合中創建出流。
3、流操作
上面學習了流的建立方式,接下來學習流的操作。
它們分為中間操作(返回Stream <T>)和終端操作(返回確定型別的結果)。中間操作允許鏈式傳遞。
我們來看一看常用的非終端操作。
3.1、中間操作
中間操作也叫非終端操作。
Java Stream API的非終端流操作是對流中的元素進行轉換或過濾的操作。
當向流新增非終端操作時,將得到一個新的流。新流表示應用了非終端操作的原始流產生的元素流。這是新增到流中的非終端操作的示例——會產生新的流:
List<String> stringList = new ArrayList<String>();
stringList.add("ONE");
stringList.add("TWO");
stringList.add("THREE");
Stream<String> stream = stringList.stream();
Stream<String> stringStream =
stream.map((value) -> { return value.toLowerCase(); });
注意對stream.map()的呼叫。該呼叫實際上返回一個新的Stream例項,該例項表示已使用map操作原來的字元流。
只能將單個操作新增到給定的Stream例項。
如果需要將多個操作彼此連結在一起,則需要將第二個操作應用第一個操作產生的Stream流。例項如下:
Stream<String> stringStream1 =
stream.map((value) -> { return value.toLowerCase(); });
Stream<½String> stringStream2 =
stringStream1.map((value) -> { return value.toUpperCase(); });
鏈式呼叫是非常常見的,這是鏈式呼叫的例項:
Stream<String> stream1 = stream
.map((value) -> { return value.toLowerCase(); })
.map((value) -> { return value.toUpperCase(); })
.map((value) -> { return value.substring(0,3); });
許多非終端Stream操作可以將Java Lambda表示式作為引數。該lambda表示式實現了適合給定非終端操作的Java函式式介面。非終端操作方法引數的引數通常是函式式介面——這就是為什麼它也可以由Java lambda表示式實現的原因。
3.1.1、filter()
Java Stream filter()可用於過濾Java Stream中的元素。filter方法採用一個Predicate,該Predicate被流中的每個元素被呼叫。如果元素要包含在結果流中,則Predicate返回true。如果不應包含該元素,則Predicate返回false。
下面是一個filter()例項:
Stream<String> longStringsStream = stream.filter((value) -> {
return value.length() >= 3;
});
3.1.2、map()
Java Stream map()方法將一個元素轉換(對映)到另一個物件。例如,如果你有一個字串列表,則可以將每個字串轉換為小寫,大寫或原始字串的子字串,或者完全轉換成其他字串。
這是一個Java Stream map()示例:
List<String> list = new ArrayList<String>();
Stream<String> stream = list.stream();
Stream<String> streamMapped = stream.map((value) -> value.toUpperCase());
3.1.3、flatMap()
Java Stream flatMap()方法將單個元素對映到多個元素。意思是將每個元素從由多個內部元素組成的複雜結構“展平”到僅由這些內部元素組成的“展平”流。
例如,假設你有一個帶有巢狀物件(子物件)的物件。然後,你可以將該物件對映到一個“平”流,該流由自身加上其巢狀物件——或僅巢狀物件組成。你還可以將元素列表流對映到元素本身。或將字串流對映到這些字串中的字元流——或對映到這些字串中的各個Character例項。
這是一個將字串列表平面對映到每個字串中的字元的示例。
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
stream.flatMap((value) -> {
String[] split = value.split(" ");
return (Stream<String>) Arrays.asList(split).stream();
}).forEach((value) -> System.out.println(value));
此Java Stream flatMap() 示例首先建立一個包含3個包含書名的字串的List。然後,獲取List的Stream,並呼叫flatMap()。
3.1.4、distinct()
Java Stream unique()方法是一種非終端操作,返回一個新的Stream,與來源的流不同,它去掉了重複的元素。這是Java Streamdisting()方法的示例:
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
List<String> distinctStrings = stream
.distinct()
.collect(Collectors.toList());
System.out.println(distinctStrings);
在此示例中,元素 " one " 在原來的流中出現2次。在新的流中只會出現第一次出現的元素。因此,結果列表(通過呼叫collect()將僅包含 "one" , "two" 和"three"。從此示例列印的輸出將是:
[one, two, three]
3.1.5、limit()
Java Stream limit()方法可以將流中的元素數量限制為指定給limit()方法的數量。limit() 方法返回一個新的Stream,該Stream最多包含給定數量的元素。這是一個Java Stream limit() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
stream
.limit(2)
.forEach( element -> { System.out.println(element); });
本示例首先建立一個Stream流,然後在其上呼叫limit(),然後使用forEach()來打印出該流中的元素。由於呼叫了limit(2),僅將列印前兩個元素。
3.1.6、peek()
Java Stream peek()方法是一種非終端操作,它以 Consumer(java.util.function.Consumer)作為引數。將為流中的每個元素呼叫Consumer。peek()方法返回一個新的Stream,其中包含原來的流中的所有元素。
正如方法所說,peek() 方法的目的是見識流中的元素,而不是對其進行轉換。peek方法不會啟動流中元素的內部迭代。要這是一個Java Stream peek()示例:
List<String> stringList = new ArrayList<String>();
stringList.add("abc");
stringList.add("def");
Stream<String> stream = stringList.stream();
Stream<String> streamPeeked = stream.peek((value) -> {
System.out.println("value");
});
3.2、終端操作
Java Stream介面的終端操作通常返回單個值。一旦在Stream上呼叫了終端操作,就將開始Stream的迭代以及鏈路上的流。迭代完成後,將返回終端操作的結果。
終端操作通常不返回新的Stream例項。因此,一旦在流上呼叫了終端操作,來自非終端操作的Stream例項鏈就結束了。
這是在Java Stream上呼叫終端操作的示例:
long count = stream
.map((value) -> { return value.toLowerCase(); })
.map((value) -> { return value.toUpperCase(); })
.map((value) -> { return value.substring(0,3); })
.count();
該示例末尾的對count()的呼叫是終端操作。由於count()返回long,因此非終端操作的Stream鏈路(map()呼叫)結束。
3.2.1、anyMatch()
Java Stream anyMatch()方法是一種終端操作,它以單個Predicate作為引數,啟動Stream的內部迭代,並將Predicate引數應用於每個元素。如果Predicate對任意一個元素返回true,則anyMatch()方法返回true。如果沒有元素與Predicate匹配,則anyMatch()將返回false。
這是一個Java Stream anyMatch()示例:
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
boolean anyMatch = stream.anyMatch((value) -> { return value.startsWith("One"); });
System.out.println(anyMatch);
在上面的示例中,anyMatch() 方法呼叫將返回true,因為流中的第一個字串元素以“ One”開頭。
3.2.2、allMatch()
Java Stream allMatch() 方法是一種終端操作,該操作以單個Predicate作為引數,啟動Stream中元素的內部迭代,並將Predicate引數應用於每個元素。如果Predicate對於Stream中的所有元素都返回true,則allMatch() 將返回true。如果不是所有元素都與Predicate匹配,則allMatch() 方法將返回false。
這是一個Java Stream allMatch() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
boolean allMatch = stream.allMatch((value) -> { return value.startsWith("One"); });
System.out.println(allMatch);
在上面的示例中,allMatch()方法將返回false,因為Stream中只有一個字串以“ One”開頭。
3.2.3、noneMatch()
Java Stream noneMatch() 方法是一個終端操作,它將對流中的元素進行迭代並返回true或false,這取決於流中是否沒有元素與作為引數傳遞給noneMatch() 的謂詞相匹配。如果謂詞不匹配任何元素,則noneMatch() 方法將返回true;如果匹配一個或多個元素,則方法將返回false。
這是一個Java Stream noneMatch() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("abc");
stringList.add("def");
Stream<String> stream = stringList.stream();
boolean noneMatch = stream.noneMatch((element) -> {
return "xyz".equals(element);
});
System.out.println("noneMatch = " + noneMatch);
3.2.4、collect()
Java Stream collect() 方法是一種終端操作,它開始元素的內部迭代,並以某種型別的集合或物件接收流中的元素。
這是一個簡單的Java Stream collect()方法示例:
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
List<String> stringsAsUppercaseList = stream
.map(value -> value.toUpperCase())
.collect(Collectors.toList());
System.out.println(stringsAsUppercaseList);
collect() 方法採用Collector(java.util.stream.Collector)作為引數。實現Collector需要對Collector介面進行一些研究。幸運的是,Java類java.util.stream.Collectors包含了一組可以用於最常用操作的預先實現的Collector實現。在上面的示例中,使用的是Collectors.toList() 返回的Collector實現。該Collector只是將流中的所有元素收集到標準Java List中。
3.2.5、 count()
Java Stream count() 方法是一種終端操作,用於啟動Stream中元素的內部迭代並計算元素。這是一個Java Stream count() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
long count = stream.flatMap((value) -> {
String[] split = value.split(" ");
return (Stream<String>) Arrays.asList(split).stream();
})
.count();
System.out.println("count = " + count);
此示例首先建立一個字串列表,然後獲取該列表的Stream,為其新增一個flatMap()操作,然後完成對count()的呼叫。count()方法將開始Stream中元素的迭代,flatMap()操作中將字串元素拆分為單詞,然後進行計數。最終打印出來的結果是14。
3.2.6、findAny()
Java Stream findAny() 方法可以從Stream中查詢單個元素。找到的元素可以來自Stream中的任何位置。無法保證從流中何處獲取元素。
這是一個Java Stream findAny()示例:
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
Optional<String> anyElement = stream.findAny();
System.out.println(anyElement.get());
注意findAny()方法返回了Optional。Stream可能為空——因此無法返回任何元素。可以檢查是否通過可選的isPresent()方法找到元素。
3.2.7、findFirst()
如果Stream中存在任何元素,則Java Stream findFirst()方法將查詢Stream中的第一個元素。findFirst()方法返回一個Optional,可以從中獲取元素(如果存在)。
這是一個Java Stream findFirst() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
Optional<String> result = stream.findFirst();
System.out.println(result.get());
可以通過isPresent() 方法檢查Optional返回的元素是否包含元素。
3.2.8、forEach()
Java Stream forEach() 方法是一種終端操作,它對Stream中元素迭代,並將Consumer(java.util.function.Consumer)應用於Stream中的每個元素。forEach() 無返回值。
這是一個Java Stream forEach() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("one");
stringList.add("two");
stringList.add("three");
stringList.add("one");
Stream<String> stream = stringList.stream();
stream.forEach( element -> { System.out.println(element); });
3.2.9、min()
Java Stream min() 方法是一種終端操作,它返回Stream中的最小元素。哪個元素最小是由傳遞給min() 方法的Comparator實現確定的。
這是一個Java Stream min() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("abc");
stringList.add("def");
Stream<String> stream = stringList.stream();
Optional<String> min = stream.min((val1, val2) -> {
return val1.compareTo(val2);
});
String minString = min.get();
System.out.println(minString);
注意min() 方法返回一個Optional,它可能包含也可能不包含結果。如果Stream為空,則Optional get()方法將丟擲NoSuchElementException。
3.2.10、max()
Java Stream max() 方法是一種終端操作,它返回Stream中最大的元素。哪個元素最大,取決於傳遞給max()方法的Comparator實現。
這是一個Java Stream max() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("abc");
stringList.add("def");
Stream<String> stream = stringList.stream();
Optional<String> max = stream.max((val1, val2) -> {
return val1.compareTo(val2);
});
String maxString = max.get();
System.out.println(maxString);
注意max() 方法如何返回一個Optional,它可以包含也可以不包含結果。如果Stream為空,則Optional get()方法將丟擲NoSuchElementException。
3.2.11、reduce()
Java Stream reduce() 方法是一種終端操作,可以將流中的所有元素縮減為單個元素。
這是一個Java Stream reduce() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
Optional<String> reduced = stream.reduce((value, combinedValue) -> {
return combinedValue + " + " + value;
});
System.out.println(reduced.get());
3.2.12、toArray()
Java Stream toArray() 方法是一種終端操作,它迭代流中元素,並返回包含所有元素的Object陣列。
這是一個Java Stream toArray() 示例:
List<String> stringList = new ArrayList<String>();
stringList.add("One flew over the cuckoo's nest");
stringList.add("To kill a muckingbird");
stringList.add("Gone with the wind");
Stream<String> stream = stringList.stream();
Object[] objects = stream.toArray();
3.2.13、sorted()
Collection 需要排序的時候可以使用Comparator和Comparable實現。在Java 8中,同樣可以使用Comparator對Stream進行排序。
示例如下:
public class Human {
private String name;
private int age;
}
ArrayList<Human> humans = new ArrayList<Human>();
humans.add(new Human("李四", 4));
humans.add(new Human("王二", 2));
humans.add(new Human("張三", 3));
System.out.println(humans);
List<Human> sortHumans = humans.stream().sorted(Comparator.comparing(Human::getAge))
.collect(Collectors.toList());
System.out.println(sortHumans);
4、總結
Stream API是一組功能強大但易於理解的工具,用於處理元素序列。如果使用得當,我們可以減少大量的重複程式碼,建立更具可讀性的程式,並提高應用的工作效率。
參考:
【1】:Java 8 中的 Streams API 詳解
【2】:[譯] 一文帶你玩轉 Java8 Stream 流,從此操作集合 So Easy
【3】:A Guide to Streams in Java 8: In-Depth Tutorial With Examples
【4】:The Java 8 Stream API Tutorial
【5】:java.util.stream
【6】:Introduction to Java 8 Streams
【7】:Java Stream API
【8】:Java8 使用 stream().sorted()對List集合進行排序
【9】:Java 8 Stream sorted() Example