1. 程式人生 > 實用技巧 >Java8 Stream

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\ 進行元素生成。由於結果流是無限的,因此開發人員應指定所需的大小,否則generate()方法執行後會達到記憶體的上限:

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