Java 8 流 – Java 流

欢迎来到Java 8 Stream API教程。在过去的几篇Java 8文章中,我们深入研究了Java 8接口变更函数接口与Lambda表达式。今天我们将深入了解Java 8引入的主要API之一 – Java Stream

Java 8 Stream

  1. Java 8 Stream
  2. 集合与Java Stream
  3. Java 8 Stream中的函数接口
    1. Function和BiFunction
    2. Predicate和BiPredicate
    3. Consumer和BiConsumer
    4. Supplier
  4. java.util.Optional
  5. java.util.Spliterator
  6. Java Stream 中间和终端操作
  7. Java Stream 短路操作
  8. Java Stream 示例
    1. 创建 Java Streams
    2. 将 Java Stream 转换为集合或数组
    3. Java Stream 中间操作
    4. Java Stream 终端操作
  9. Java 8 Stream API 限制

Java Stream

在我们深入了解 Java Stream API 示例之前,让我们看看为什么它是必需的。假设我们想迭代一个整数列表并找出所有大于 10 的整数的总和。在 Java 8 之前,执行此操作的方法是:

private static int sumIterator(List<Integer> list) {
	Iterator<Integer> it = list.iterator();
	int sum = 0;
	while (it.hasNext()) {
		int num = it.next();
		if (num > 10) {
			sum += num;
		}
	}
	return sum;
}

上述方法存在三个主要问题:

  1. 我们只想知道整数的总和,但我们也必须提供迭代将如何进行,这也被称为外部迭代,因为客户端程序正在处理迭代列表的算法。
  2. 该程序是顺序性的,我们无法轻易地并行执行此操作。
  3. 即使是简单的任务也需要大量的代码。

为了克服以上所有缺点,引入了Java 8 Stream API。我们可以使用Java Stream API来实现内部迭代,这更好,因为Java框架控制着迭代。内部迭代提供了几个功能,例如基于给定条件的顺序和并行执行、过滤、映射等。大多数Java 8 Stream API方法参数都是函数接口,因此lambda表达式与它们非常配合。让我们看看如何使用Java Streams在一行语句中编写上述逻辑。

private static int sumStream(List<Integer> list) {
	return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}

请注意,以上程序利用了Java框架的迭代策略、过滤和映射方法,并将提高效率。首先,我们将深入了解Java 8 Stream API的核心概念,然后我们将通过一些示例了解最常用的方法。

集合和Java Stream

A collection is an in-memory data structure to hold values and before we start using collection, all the values should have been populated. Whereas a java Stream is a data structure that is computed on-demand. Java Stream doesn’t store data, it operates on the source data structure (collection and array) and produce pipelined data that we can use and perform specific operations. Such as we can create a stream from the list and filter it based on a condition. Java Stream operations use functional interfaces, that makes it a very good fit for functional programming using lambda expression. As you can see in the above example that using lambda expressions make our code readable and short. Java 8 Stream internal iteration principle helps in achieving lazy-seeking in some of the stream operations. For example filtering, mapping, or duplicate removal can be implemented lazily, allowing higher performance and scope for optimization. Java Streams are consumable, so there is no way to create a reference to stream for future usage. Since the data is on-demand, it’s not possible to reuse the same stream multiple times. Java 8 Stream support sequential as well as parallel processing, parallel processing can be very helpful in achieving high performance for large collections. All the Java Stream API interfaces and classes are in the java.util.stream package. Since we can use primitive data types such as int, long in the collections using auto-boxing and these operations could take a lot of time, there are specific classes for primitive types – IntStream, LongStream and DoubleStream.

Java 8 Stream 中的函数接口

Java 8 Stream API 方法中常用的一些函数接口包括:

  1. Function 和 BiFunction:Function 表示接受一种类型的参数并返回另一种类型参数的函数。Function<T, R> 是通用形式,其中 T 是函数输入的类型,R 是函数结果的类型。为了处理原始类型,有特定的 Function 接口 – ToIntFunctionToLongFunctionToDoubleFunctionToIntBiFunctionToLongBiFunctionToDoubleBiFunctionLongToIntFunctionLongToDoubleFunctionIntToLongFunctionIntToDoubleFunction 等。一些使用 Function 或其原始类型特化的 Stream 方法包括:
    • <R> Stream<R> map(Function<? super T, ? extends R> mapper)
    • IntStream mapToInt(ToIntFunction<? super T> mapper) – 类似的方法适用于返回 long 和 double 的原始类型特化流。
    • IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) – 类似的方法适用于返回 long 和 double。
    • <A> A[] toArray(IntFunction<A[]> generator)
    • <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
  2. Predicate 和 BiPredicate: 它表示流中元素被测试的谓词。这用于从 Java 流中过滤元素。就像 Function 一样,有针对 int、long 和 double 的特定原语接口。一些使用 PredicateBiPredicate 特化的流方法包括:
    • Stream<T> filter(Predicate<? super T> predicate)
    • boolean anyMatch(Predicate<? super T> predicate)
    • boolean allMatch(Predicate<? super T> predicate)
    • boolean noneMatch(Predicate<? super T> predicate)
  3. Consumer 和 BiConsumer:它表示接受单个输入参数并不返回结果的操作。它可用于在 Java 流的所有元素上执行某些操作。一些 Java 8 Stream 方法中使用了 ConsumerBiConsumer 或其原始专用接口的地方包括:
    • Stream<T> peek(Consumer<? super T> action)
    • void forEach(Consumer<? super T> action)
    • void forEachOrdered(Consumer<? super T> action)
  4. Supplier:Supplier 表示我们可以在流中生成新值的操作。一些在 Stream 中采用 Supplier 参数的方法包括:
    • public static<T> Stream<T> generate(Supplier<T> s)
    • <R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner)

java.util.Optional

Java Optional是一个容器对象,可以包含或不包含非空值。如果存在值,则isPresent()将返回true,get()将返回该值。流终端操作返回Optional对象。其中一些方法包括:

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • Optional<T> min(Comparator<? super T> comparator)
  • Optional<T> max(Comparator<? super T> comparator)
  • Optional<T> findFirst()
  • Optional<T> findAny()

java.util.Spliterator

为了支持Java 8 Stream API的并行执行,使用Spliterator接口。Spliterator的trySplit方法返回管理原始Spliterator元素子集的新Spliterator。

Java Stream中间和终端操作

Java Stream API操作返回新Stream的操作被称为中间操作。大多数情况下,这些操作是惰性的,因此它们开始生成新的流元素并将其发送到下一个操作。中间操作永远不是最终的结果生成操作。常用的中间操作包括filtermap。Java 8 Stream API操作返回结果或产生副作用。一旦在流上调用了终端方法,它就会消耗流,之后我们就无法再使用流了。终端操作是急切的,即它们在返回结果之前处理流中的所有元素。常用的终端方法包括forEachtoArrayminmaxfindFirstanyMatchallMatch等。您可以从返回类型中识别终端方法,它们永远不会返回一个Stream。

Java Stream短路操作

如果中间操作可能为无限流产生有限流,则称为短路操作。例如limit()skip()是两个短路中间操作。如果终端操作可能在有限时间内终止无限流,则称为短路终端操作。例如anyMatchallMatchnoneMatchfindFirstfindAny都是短路终端操作。

Java Stream示例

I have covered almost all the important parts of the Java 8 Stream API. It’s exciting to use this new API features and let’s see it in action with some java stream examples.

创建Java Streams

有几种方式可以从数组和集合创建Java流。让我们通过简单的示例来了解这些。

  1. 我们可以使用Stream.of()从相似类型的数据创建流。例如,我们可以从一组int或Integer对象创建整数的Java流。

    Stream<Integer> stream = Stream.of(1,2,3,4);
    
  2. 我们可以使用Stream.of()与对象数组一起返回流。请注意,它不支持自动装箱,因此我们不能传递原始类型数组。

    Stream<Integer> stream = Stream.of(new Integer[]{1,2,3,4}); 
    //运行正常
    
    Stream<Integer> stream1 = Stream.of(new int[]{1,2,3,4}); 
    //编译时错误,类型不匹配:无法从Stream<int[]>转换为Stream<Integer>
    
  3. 我们可以使用集合stream()创建顺序流,使用parallelStream()创建并行流。

    List<Integer> myList = new ArrayList<>();
    for(int i=0; i<100; i++) myList.add(i);
    		
    // 顺序流
    Stream<Integer> sequentialStream = myList.stream();
    		
    // 并行流
    Stream<Integer> parallelStream = myList.parallelStream();
    
  4. 我们可以使用Stream.generate()Stream.iterate()方法创建流。

    Stream<String> stream1 = Stream.generate(() -> {return "abc";});
    Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
    
  5. 使用Arrays.stream()String.chars()方法。

    LongStream is = Arrays.stream(new long[]{1,2,3,4});
    IntStream is2 = "abc".chars();
    

将Java流转换为集合或数组

有几种方法可以从Java流中获取集合或数组。

  1. 我们可以使用java Stream collect() 方法从流中获取List、Map或Set。

    Stream<Integer> intStream = Stream.of(1,2,3,4);
    List<Integer> intList = intStream.collect(Collectors.toList());
    System.out.println(intList); //输出 [1, 2, 3, 4]
    
    intStream = Stream.of(1,2,3,4); //流已关闭,因此需要重新创建
    Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10));
    System.out.println(intMap); //输出 {1=11, 2=12, 3=13, 4=14}
    
  2. 我们可以使用流的 toArray() 方法从流中创建数组。

    Stream<Integer> intStream = Stream.of(1,2,3,4);
    Integer[] intArray = intStream.toArray(Integer[]::new);
    System.out.println(Arrays.toString(intArray)); //输出 [1, 2, 3, 4]
    

Java流中间操作

让我们看一下常用的java流中间操作示例。

  1. 流过滤器(Stream filter())示例:我们可以使用filter()方法测试流元素是否符合条件并生成过滤后的列表。

    List<Integer> myList = new ArrayList<>();
    for(int i=0; i<100; i++) myList.add(i);
    Stream<Integer> sequentialStream = myList.stream();
    
    Stream<Integer> highNums = sequentialStream.filter(p -> p > 90); //过滤大于90的数字
    System.out.print("大于90的高数字=");
    highNums.forEach(p -> System.out.print(p+" "));
    //输出 "大于90的高数字=91 92 93 94 95 96 97 98 99 "
    
  2. 流映射(Stream map())示例:我们可以使用map()将函数应用于流。让我们看看如何将其应用于一组字符串列表以转换为大写。

    Stream<String> names = Stream.of("aBc", "d", "ef");
    System.out.println(names.map(s -> {
    		return s.toUpperCase();
    	}).collect(Collectors.toList()));
    //输出 [ABC, D, EF]
    
  3. 流 sorted() 示例: 我们可以使用 sorted() 通过传递 Comparator 参数来对流元素进行排序。

    Stream<String> names2 = Stream.of("aBc", "d", "ef", "123456");
    List<String> reverseSorted = names2.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
    System.out.println(reverseSorted); // [ef, d, aBc, 123456]
    
    Stream<String> names3 = Stream.of("aBc", "d", "ef", "123456");
    List<String> naturalSorted = names3.sorted().collect(Collectors.toList());
    System.out.println(naturalSorted); //[123456, aBc, d, ef]
    
  4. 流 flatMap() 示例: 我们可以使用 flatMap() 从列表流中创建一个流。让我们看一个简单的例子来澄清这个疑惑。

    Stream<List<String>> namesOriginalList = Stream.of(
    	Arrays.asList("Pankaj"), 
    	Arrays.asList("David", "Lisa"),
    	Arrays.asList("Amit"));
    //flat the stream from List<String> to String stream
    Stream<String> flatStream = namesOriginalList
    	.flatMap(strList -> strList.stream());
    
    flatStream.forEach(System.out::println);
    

Java Stream 终端操作

让我们看一些Java Stream终端操作的例子。

  1. Stream reduce() 示例: 我们可以使用reduce()对流的元素执行规约操作,使用一个关联的累积函数,并返回一个Optional。让我们看看如何在流中使用它来将整数相乘。

    Stream<Integer> numbers = Stream.of(1,2,3,4,5);
    		
    Optional<Integer> intOptional = numbers.reduce((i,j) -> {return i*j;});
    if(intOptional.isPresent()) System.out.println("乘法结果 = "+intOptional.get()); //120
    
  2. Stream count() 示例: 我们可以使用这个终端操作来计算流中的项目数量。

    Stream<Integer> numbers1 = Stream.of(1,2,3,4,5);
    		
    System.out.println("流中的元素数量="+numbers1.count()); //5
    
  3. Stream forEach() example: 这可用于遍历流。我们可以用它来替代迭代器。让我们看看如何用它来打印流的所有元素。

    Stream<Integer> numbers2 = Stream.of(1,2,3,4,5);
    numbers2.forEach(i -> System.out.print(i+",")); //1,2,3,4,5,
    
  4. Stream match() examples: 让我们看一些Stream API中匹配方法的示例。

    Stream<Integer> numbers3 = Stream.of(1,2,3,4,5);
    System.out.println("Stream contains 4? "+numbers3.anyMatch(i -> i==4));
    //Stream contains 4? true
    
    Stream<Integer> numbers4 = Stream.of(1,2,3,4,5);
    System.out.println("Stream contains all elements less than 10? "+numbers4.allMatch(i -> i<10));
    //Stream contains all elements less than 10? true
    
    Stream<Integer> numbers5 = Stream.of(1,2,3,4,5);
    System.out.println("Stream doesn't contain 10? "+numbers5.noneMatch(i -> i==10));
    //Stream doesn't contain 10? true
    
  5. findFirst()示例:这是一个短路终端操作,让我们看看如何使用它来找到以D开头的流中的第一个字符串。

    Stream<String> names4 = Stream.of("Pankaj","Amit","David", "Lisa");
    Optional<String> firstNameWithD = names4.filter(i -> i.startsWith("D")).findFirst();
    if(firstNameWithD.isPresent()){
    	System.out.println("以D开头的第一个姓名="+firstNameWithD.get()); //David
    }
    

Java 8流API的局限性

Java 8流API为列表和数组带来了许多新功能,但它也有一些限制。

  1. 无状态的Lambda表达式: 如果您使用并行流而Lambda表达式是有状态的,可能导致随机响应。让我们通过一个简单的程序来看看。 StatefulParallelStream.java

    package com.journaldev.java8.stream;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    public class StatefulParallelStream {
    
    	public static void main(String[] args) {
    
    		List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
    		List<Integer> result = new ArrayList<Integer>();
    		 
    		Stream<Integer> stream = ss.parallelStream();
    		 
    		stream.map(s -> {
    		        synchronized (result) {
    		          if (result.size() < 10) {
    		            result.add(s);
    		          }
    		        }
    				return s;
    		    }).forEach( e -> {});
    		 System.out.println(result);   
    	}
    }
    

    如果我们运行上述程序,将会得到不同的结果,因为它取决于流的迭代方式,而且我们对并行处理没有定义任何顺序。如果我们使用顺序流,则不会出现这个问题。

  2. 一旦流被消耗,就无法后续使用。正如您在上面的示例中所看到的,每次我都在创建一个流。

  3. Stream API 中有很多方法,最令人困惑的部分是重载的方法。这使得学习曲线变得耗时。

这就是 Java 8 Stream 示例教程的全部内容。我期待着使用这个特性,并通过并行处理使代码更易读且性能更佳。参考:Java Stream API 文档

Source:
https://www.digitalocean.com/community/tutorials/java-8-stream