• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

Java8新特性(二) Stream(流)

java 来源:zeroxes 1次浏览

引例

还记得上一篇文章中的农场主吗?这一次农场主又提出了新的需求——找出所有果园里150g以上的绿苹果,除此之外还要按照重量从大到小给苹果排序。然而这并难不倒掌握Lambda表达式的我们,下面就用代码实现他的需求:

public class Apple {
	 
    private String color;
    private int weight;
 
    public Apple(String color, int weight) {
        this.color = color;
        this.weight = weight;
    }
    
    public String getColor() {
    	return color;
    }
    public void setColor(String color) {
    	this.color = color;
    }
    public int getWeight() {
    	return weight;
    }
    public void setWeight(int weight) {
    	this.weight = weight;
    }
    
    @Override
    public String toString() {
    	return "Apple [color=" + color + ", weight=" + weight + "]";
    }
    
}
public class FindApple1 {
	// 果园
	public static List<Apple> orchard = Arrays.asList(new Apple("green", 150), 
			new Apple("green", 200), new Apple("yellow", 150),new Apple("red", 170));
	
	public static void main(String[] args) {
		List<Apple> basket = appleFilterThenSort(orchard, 
				apple -> "green".equals(apple.getColor()) ? 
						(apple.getWeight() > 150 ? true : false) : false);
		basket.forEach(System.out::println);
	}

	private static List<Apple> appleFilterThenSort(List<Apple> orchard, Predicate<Apple> predicate) {
		List<Apple> temp = new ArrayList<>();
		for (Apple apple : orchard) {
			if(predicate.test(apple)) {
				temp.add(apple);
			}
		}
		temp.sort((o1,o2) -> o2.getWeight() - o1.getWeight());
		return temp;
	}

}

尽管我们已经尽可能的使用Lambda表达式,但程序中的代码量仍然不少,那么还有方法可以让我们的程序变得更加简洁呢?答案是肯定的,下面是使用流修改后的代码的样子:

public class FindApple2 {
	
	public static void main(String[] args) {
		List<Apple> basket = FindApple1.orchard.stream()
			.filter(apple -> "green".equals(apple.getColor()) ? 
					(apple.getWeight() > 150 ? true : false) : false)
			.sorted((o1, o2) -> o2.getWeight()- o1.getWeight())
			.collect(Collectors.toList());
		basket.forEach(System.out::println);
	}
	
}

使用流之后我们的代码变得更加精简、更加易读,即使是没有接触过流的人,也能大概明白代码所表达的含义:首先过滤数据,然后对数据进行排序,最后将收集到的数据返回。换而言之,使用流之后代码所表达的含义发生了变化——从命令式编程变成函数式编程。

命令式编程:制定做一件事情的步骤(FindApple1)——创建新集合,遍历原始集合并找出符合条件的数据添加到新集合中,对新集合排序,将新集合作为结果返回。

函数式编程描述一件事情怎么做(FindApple2)——将符合条件的数据排序之后收集到集合中返回。

 

什么是流

流到底是什么呢?JavaDoc上给出的定义是这样的:

A sequence of elements supporting sequential and parallel aggregate operations.
流是一个支持串行和并行的聚合操作的元素序列。

《Java8实战》中给流下了这样的定义:

从支持数据处理操作的源生成的元素序列。

纵观二者, 他们都认为流是一个元素序列。提到元素序列我们很容易就可以联想到集合,同为元素序列,集合和流之间是什么关系呢?区别在于,集合负责的是元素的存储和访问,流的目的则在于计算。

流可以认为是对集合功能上的增强,能对集合对象实现更高效、更便利的操作。但是流不是数据结构,流本身不储存数据,只是从源(集合是流使用最多的源,下面会介绍其他的源)中获取数据, 并进行相应的计算——对流的操作会生成一个结果,不会修改数据源。

上面所说的都是理论上的流,具体到代码中,流指的就是Stream对象。

引例部分的最后这样说过:使用流之后代码所表达的含义发生了变化。其实流操作的参数含义也发生了变化:在Java8之前参数传递的是值,而Java8中流操作将Lambda表达式作为参数传递,传递的是一种行为

 

使用流

流的使用一般包括下面三件事情:

  1. 一个数据源(如集合)
  2. 零个或多个中间操作(中间操作会返回一个新的流)
  3. 一个终止操作(终止操作会关闭流)

 

流是一次性的

从名字上来看,流是河流的意思,而流使用起来正如流水一般,一去不返。是的,流是一次性的,每个流只能使用一次。

public class TestStream1 {
	
	public static void main(String[] args) {
		Stream<Double> stream = Stream.generate(Math::random).limit(10);
		stream.forEach(System.out::println);
//		stream.forEach(System.out::println);	// 错误: 流只能使用一次
	}
	
}

既然流是一次性的,那么在FindApple2中为什么可以对流进行多次操作(filter、sorted、collect)呢?这就和流是一次性的说法相矛盾了。这是因为流的中间操作会返回一个新的流,这样一来我们就可以将多个中间操作串联起来,实现聚合操作。

部分中间操作举例:

Stream<T> filter(Predicate<? super T> predicate);

Stream<T> sorted(Comparator<? super T> comparator);

 

惰性求值与及早求值

惰性求值(Lazy Evaluation)是在需要时才进行求值的计算方式,与惰性求值相对立的就是及早求值(Eager Evaluation),及早求值需要立即求值。

流的中间操作属于惰性求值,而终止操作属于及早求值

由于流本身是不存储数据的,所以如果使用流时只有中间操作计算数据,而没有终止操作返回计算结果,使用流就是毫无意义的。因此如果使用流时没有终止操作(及早求值),那么流的中间操作(惰性求值)都不会被执行。看下面一个例子:

public class TestStream2 {

	public static void main(String[] args) {
		// filter和map是中间操作
		List<String> data = Arrays.asList("hello", "world", "hello world");
		data.stream().filter(item -> {
			System.out.println("filter invoked");
			return item.length() > 5;
		}).map(item -> {
			System.out.println("map invoked");
			return item + " java";
		});
	}
	
}

运行上面的程序,并不会看到任何的打印结果。这是因为使用流时只使用中间操作(filter和map)而没有终止操作,所以流的中间操作都不会执行。

惰性求值最重要的好处是它可以构造一个无限的流,而这是集合做不到的。集合中的每个元素只有先计算出来,才能添加到集合中去,因此集合只能存储有限个元素。

List<Integer> list = new ArrayList<>();
list.add(1); 
list.add(2); 
……   // 集合中只能存放有限个元素

Stream.generate(Math::random);   // 一个无限流

 

内部迭代与外部迭代

在流出现之前的,我们对集合所做的迭代(如for-each)都是外部迭代,而流使用的自然就是内部迭代。区分外部迭代和内部迭代很容易,外部迭代是显式的,你可以清楚的看到完整的迭代过程;相反内部迭代则是隐式的,你看不到迭代的过程。

public class TestStream3 {

	public static void main(String[] args) {
		List<Integer> data = Arrays.asList(1,2,3,4,5,6);
		// 外部迭代,可以看到迭代的过程
		for (Integer item : data) {
			if (item > 3) System.out.println(item);
		}
		// 内部迭代,看不到迭代的过程
		data.stream().filter(item -> item > 3).forEach(System.out::println);
	}
	
}

使用外部迭代时,整个迭代过程全部都是由我们自己搭建的。我们编写的迭代逻辑和集合之间的关联很弱,迭代逻辑始终游离在集合之外。

而使用内部迭代时,我们并不需要去搭建整个迭代过程,只需要将一些关键的迭代逻辑通过Lambda表达式传递给流就可以了。所以内部迭代的本质是流框架,流框架已经事先为我们搭建好了完整的迭代过程,它会将流从源中获取到的元素和我们编写的迭代逻辑整合到一起。

同时,使用外部迭代是比较难并行化的,一旦你选择了外部迭代,那么你就需要自己管理所有的并行问题,而使用内部迭代则可以自动为你实现并行(并行流)。

 

创建流

前面说了很多理论上的东西,接下来就让我们实际创建流。创建流的方法:

1、通过集合创建流:集合根接口Collection中定义有默认方法stream(),所以Collection的所有直接或间接实现类都会自动继承该方法,调用集合对象的stream()方法就可以创建流。

2、通过给定值创建流Stream接口中提供静态方法of(),该方法接收一到任意个参数,调用该方法就可以创建流。当然你也可以通过Stream接口提供的静态方法empty()来创建一个空流。

3、通过数组创建流Arrays类提供静态方法stream()创建一个流,该方法接受一个数组作为参数。

4、通过函数创建流Stream接口提供两个静态方法iterate()generate(),通过这两个方法可以创建无限流(一般会同时调用limit()方法对无限流加以限制)。

  • iterate():该方法接受两个参数——一个初始值和一个依次应用在每个产生的新值上的Lambda表达式。
  • generate():该方法只接受一个Supplier类型的Lambda表达式用于源源不断的提供新值。

下面通过一个例子来演示上述的方法:

public class TestStream4 {

	public static void main(String[] args) {
		createStreamByCollection().forEach(System.out::println);
		createStreamByValues().forEach(System.out::println);
		createStreamByArrays().forEach(System.out::println);
		createStreamByIterator().forEach(System.out::println);
		createStreamByGenerate().forEach(System.out::println);
	}
	// 通过集合创建流
	private static Stream<String> createStreamByCollection() {	
		List<String> list = Arrays.asList("hello", "world");
		return list.stream();
	}
	// 通过给定值创建流
	private static Stream<String> createStreamByValues() {		
		return Stream.of("hello", "world");
	}
	// 通过数组创建流
	private static Stream<String> createStreamByArrays() {		
		String[] arr = {"hello", "world"};
		return Arrays.stream(arr);
	}
	// 通过iterator创建流,无限流
	private static Stream<Integer> createStreamByIterator() {   
		return Stream.iterate(0, i -> i + 2).limit(10);        	 // 这里限制生成10个
	}
	// 通过generate创建流,无限流
	private static Stream<Double> createStreamByGenerate () {	 
		return Stream.generate(Math::random).limit(10);          // 这里限制生成10个
	}
	
}

 

流操作

流操作可以分为两类:中间操作(intermediate operation)和终止操作(terminal operation)。

通过操作的返回值就可以区分这两类操作:如果操作返回一个Stream对象,那么该操作就是中间操作;反之,该操作就是终止操作。

中间操作

过滤:filter

该操作可以过滤掉不符合条件的元素,并返回一个新的由符合条件的的元素组成的流。filter()方法使用Predicate接口作为参数:

Stream<T> filter(Predicate<? super T> predicate);

public class TestStream5 {
	
	public static void main(String[] args) {
		List<Integer> data = Arrays.asList(1,5,4,2,3);
		// filter操作并没有改变流中元素的顺序
		Stream<Integer> stream = data.stream().filter(i -> i%2 ==0);
		stream.forEach(System.out::println);	
	}
	
}

打印结果如下:

4
2

从打印结果可以发现,filter操作并没有改变流中元素的顺序。

映射:map

编码时经常需要这样的操作:从一堆数据中挑选我们需要的数据,比如从表中选择一列。这种两个元素集之间的对应关系就是映射,流也给我们提供了相关的映射操作。

该操作会对流中元素进行映射,并返回一个新的由映射产生元素组成的流。map()方法使用Function接口作为参数:

 <R> Stream<R> map(Function<? super T, ? extends R> mapper);

public class TestStream6 {
	
	public static void main(String[] args) {
		List<Apple> data = Arrays.asList(new Apple("red", 150), 
				new Apple("green", 170), new Apple("yellow", 200));
		Stream<Apple> appleStream = data.stream();
		// 流的类型发生了变化Stream<Apple> -> Stream<String>
		Stream<String> stringStream = appleStream.map(apple -> apple.getColor());
		stringStream.forEach(System.out::println);	
	}
	
}

打印结果如下:

red
green
yellow

例子中我们通过map()方法将流中的Apple类型元素中映射成String类型元素,因此流的类型也发生了变化。但是只靠map()方法真的可以映射出所有我们需要的元素吗?

假设存在列表[“hello”,”world”],需要把列表中的数据处理这样[“h”,”e”,”l”, “o”,”w”,”r”,”d”],应该怎么做呢?可能你会这样做:

public class TestStream7 {
	
	static String[] arr = {"hello", "world"};
	
	public static void main(String[] args) {
		// 调用map()方法之后,流中元素类型为数组类型
		Stream<String[]> stream = Arrays.stream(arr).map(i -> i.split("")).distinct();	
		stream.forEach(System.out::println);
	}
	
}

用数组记录需要处理的数据,通过该数组构建一个流,然后对流中元素进行映射操作——分割字符串,最后将流中元素去重就可以了。这样真的可以吗?

通过打印结果就可以发现上面的程序是无法完成任务的。打印结果如下:

[Ljava.lang.String;@53d8d10a
[Ljava.lang.String;@e9e54c2

从打印结果和代码都可以看出来,调用map()方法之后得到的流中的元素是数组类型,而不是我们想要的字符串类型,所以map()方法并没有实现我们想要的效果:

调用map()方法之后流中的元素: [“h”,”e”,”l”,”l”,”o”], [“w”,”o”,”r”,”l”,”d”]

我们想要的流中元素:”h”,”e”,”l”,”l”,”o”,”w”,”o”,”r”,”l”,”d”

所以这里我们还需要用到另一种映射操作——flatMap

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); 

public class TestStream8 {
	
	public static void main(String[] args) {
		// 调用map()方法后,流的类型是数组类型
		Stream<String[]> stream = Arrays.stream(TestStream7.arr).map(i -> i.split(""));	
		// 对流中数组进行扁平化处理
		Stream<String> flatmapStream = stream.flatMap(Arrays::stream).distinct();
		flatmapStream.forEach(System.out::print);	
	}
	
}

打印结果如下:

helowrd

flatMap()方法也使用Function接口作为参数,只是调用该接口的返回值类型为Stream。flatMap操作将该接口应用于此流中的每个元素并生成多个映射流,然后将此流中被映射的元素替换成每个映射流中所有元素(每个映射流会在完成替换之后关闭),最后该操作返回一个新的由此流中替换之后的元素组成的流(不是返回此流,每个流只能使用一次)。

flat即扁平化,flatMap操作所做的事情就是将需要映射的数据扁平化。简单来说,flatMap操作会将流中每个元素都转换成一个流,然后将所有转换的流连接成一个新的流。

下面提供另一个例子来理解该方法:

public class TestStream9 {
	
	public static void main(String[] args) {
		List<String> data1 = Arrays.asList("hello! ","hi! ","你好! ");
		List<String> data2 = Arrays.asList("张三","李四","王五 ","赵六");
		// 将两个流中的元素拼接
		data1.stream()
			.flatMap(item1 -> data2.stream().map(item2 -> item1 + item2 + "。 "))
			.forEach(System.out::print);
	}
	
}

 打印结果如下:

hello! 张三。 hello! 李四。 hello! 王五 。 hello! 赵六。 hi! 张三。 hi! 李四。 hi! 王五 。 hi! 赵六。 你好! 张三。 你好! 李四。 你好! 王五 。 你好! 赵六。

其他中间操作

除了上述两种主要的中间操作,Stream还提供了一些其他的中间操作:

Stream<T> distinct();

Stream<T> sorted();

Stream<T> sorted(Comparator<? super T> comparator);

Stream<T> skip(long n);

Stream<T> limit(long maxSize);

这些操作作用如下: 

  1. distinct:去重
  2. sorted:排序(重载方法,一个是空参方法,另一个接收Comparator作为参数)
  3. skip:跳过指定个元素
  4. limit(short-circuiting):截取指定个元素
public class TestStream11 {
	
	public static void main(String[] args) {
		List<Integer> data = Arrays.asList(1,5,4,2,3,2,4,5,2,1);
		// distinct: 去重
		// sorted:   排序(可传入或不传比较器)
		data.stream().distinct().sorted(Integer::compare).forEach(System.out::print);	
		System.out.print(System.lineSeparator());   // 换行
		// skip: 跳过指定个数元素
		data.stream().skip(5).forEach(System.out::print);	
		System.out.print(System.lineSeparator());   
		// limit: 截取指定个元素
		data.stream().limit(3).forEach(System.out::print);
	}
	
}

打印结果如下:

12345
24521
154 

终止操作

匹配:match(short-circuiting

 Stream提供三种match操作,如下所示:

boolean allMatch(Predicate<? super T> predicate);

boolean anyMatch(Predicate<? super T> predicate);

boolean noneMatch(Predicate<? super T> predicate);

这三个方法都将Predicate接口作为参数,返回一个布尔值表示是否匹配。其具体含义如下:

  • allMatch():流中元素全部符合条件,返回true,否则返回false
  • anyMatch():流中存在任何一个元素符合条件,返回true,否则返回false
  • noneMatch():流中元素全都不符合条件,返回true,否则返回false

下面演示三种match操作: 

public class TestStream12 {
	
	static List<Integer> data = Arrays.asList(1,2,3,4,5,6,7,8,9);
	
	public static void main(String[] args) {
		// 流中所有元素大于0
		boolean allMatch = data.stream().allMatch(i -> i > 0);
		System.out.println(allMatch);
		// 流中任意元素大于10
		boolean anyMatch = data.stream().anyMatch(i -> i > 10);
		System.out.println(anyMatch);
		// 流中没有元素大于10
		boolean noneMatch =data.stream().noneMatch(i -> i > 10);
		System.out.println(noneMatch);
	}	
	
}

打印结果如下:

true
false
true

查找:find(short-circuiting

查找操作有两种:

Optional<T> findAny();

Optional<T> findFirst();

需要注意的是这两个方法的返回值是Optional类型, 其作用如下:

  • findAny():找到流中任意一个元素
  • findFirst():找到流中第一个元素

具体看下面的例子:

public class TestStream13 {
	
	public static void main(String[] args) {
		List<Integer> data = TestStream12.data;
		// 找到流中任意元素
		Optional<Integer> result1 = data.stream().filter(i -> i >5).findAny();
		result1.ifPresent(System.out::println);
		// 找到流中第一个元素
		Optional<Integer> result2 = data.stream().filter(i -> i >5).findFirst();
		System.out.println(result2.orElse(-1));		
	}
	
}

打印结果如下:

6
6

两个方法的调用结果一样。对于这个结果可能你会觉得只是一个巧合,于是我把findAny()方法在while循环中调用了1000次,每一次的调用结果都是6。这样的结果还是巧合吗?

其实这两个方法的区别体现在并行上。在串行流中调用findAny()方法和调用findFirst()方法并没有什么区别,都会取得流中第一个元素。但是在并行流中调用findAny()方法就会取得多个线程计算结果中的任意一个元素,而调用findFirst()方法则会严格保证取得多个线程计算结果中的一个元素。

所以如果你并不在意返回的元素是哪个,最好使用findAny()方法,因为它在并行流中的限制较少。

归约:reduce

归约是把流中元素汇聚成一个值的操作,该操作对应Stream接口中的reduce()方法。Stream中存在三个重载的reduce()方法:

Optional<T> reduce(BinaryOperator<T> accumulator);

T reduce(T identity, BinaryOperator<T> accumulator);

<U> U reduce(U identity,  BiFunction<U, ? super T, U> accumulator,  BinaryOperator<U> combiner);

 这三个reduce()方法都使用BinaryOperator接口作为参数:

public interface BinaryOperator<T> extends BiFunction<T,T,T> 

BinaryOperator是一种特殊的BiFunction接口,更准的说,是三个泛型全都相同的BiFunction接口。这也不难理解,因为归约操作是将多个值合并成一个值的操作,所以操作的肯定是相同类型的数据,那么返回值也应该是同类型的数据。

下面演示前两种的reduce()方法,:

public class TestStream14 {
	
	public static void main(String[] args) {
		List<Integer> data = TestStream12.data;
		// 求最小值
		Optional<Integer> result = data.stream().reduce(Integer::min);
		result.ifPresent(System.out::println);
		// 求和
		Integer result2 = data.stream().reduce(0, Integer::sum);
		System.out.print(result2);
	}
	
}

这两个reduce()方法的区别在于:接收两个参数的reduce()方法将第一个参数作为返回值的初始值,这样的话即使是一个空的流调用该方法,返回值也不会是null(返回值是初始值);而接收一个参数的reduce()方法,调用的结果有可能是一个null,该方法的返回值类型是Optional

接收三个参数的reduce()方法,参数combiner和并行相关。在串行流中combiner并不会被调用;而在并行流中combiner会用于将多个线程产生的汇聚结果进行合并,并作为最终的汇聚结果。

聚合:collect

聚合操作可以将流中元素收集整理后返回。该操作对应Stream接口中的collect()方法,Stream提供两个重载的collect()方法。

<R> R collect(Supplier<R> supplier,  BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);

<R, A> R collect(Collector<? super T, A, R> collector);

第一种collect()方法接收三个参数,作用如下:

  • supplier:提供结果容器
  • accumulator:将流中元素添加到结果容器中
  • combiner:合并两个结果容器(并行流使用,合并多个线程产生的结果容器)

看下面一个例子: 

public class TestStream15 {
	
	public static void main(String[] args) {
		Stream<String> stream = Stream.of("hello", "world", "hello world");
		ArrayList<String> result = stream
				.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
		System.out.println(result);
	}
	
}

打印结果如下:

[hello, world, hello world]

例子中调用collect()方法后执行操作是这样的:创建结果容器——ArrayList,调用结果容器的add()方法将流中元素添加到结果容器中,返回结果容器(虽然我们给出了合并多个结果容器的方法——addAll(),但是由于是串行流,该方法并没有被调用)。

不难发现,collect()方法的三个参数是需要组合使用的。既然如此,完全可以将这些参数抽取成一个接口。于是,就有了第二种collect()方法——将Collector接口作为参数。

Collector接口中不仅包含了supplier、accumulator、combiner,还增加了finisher和characteristics,可以让聚合操作变得更加强大。你可以自定义Collector实现类,也可以使用Collectors工厂类中提供的Collector实现。我们在FindApple2中使用的第二种collect()方法。

 

流的拆装箱

相信大家都知道Integer和int的区别,一个引用类型数据所占用的内存会比一个基本类型数据大出很多,这其中的差距会随着数据量的增加而愈发明显。Stream只能处理引用类型数据,所以使用流处理数据时会面临内存开销的问题。

因此除了Stream,JDK8还提供了IntStream、LongStream、DoubleStream三种处理基本类型数据的Stream,以减少内存上的开销。通过mapTo方法(包括mapToInt、mapToLong和mapToDouble)我们就可以将引用类型流转化成基本类型流。下面通过一个例子演示两种不同类型流处理数据的用时差距: 

public class TestUnboxed {
	
	public static void main(String[] args) {
		List<Integer> data = Arrays.asList(new Integer[] {1,2,3,4,5,6,7});
		// Integer数据流求和
		long s1 = System.currentTimeMillis();
		Integer result = data.stream().filter(i -> i > 3).reduce(0, Integer::sum);
		long e1 = System.currentTimeMillis();
		System.out.println("Stream result: " + result + ", used time: " +  (e1 - s1));
		// int数据流求和
		long s2 = System.currentTimeMillis();
		IntStream intstream = data.stream().mapToInt(i -> i.intValue());
		int sum = intstream.filter(i -> i > 3).sum();
		long e2 = System.currentTimeMillis();
		System.out.println("IntStream result: "+ sum + ", used time: " + (e2 - s2));
	}
	
}

打印结果如下(不同的机器会有略微的差别):

Stream result: 22, used time: 86
IntStream result: 22, used time: 3

通过打印结果可以发现,引用类型流和基本类型流处理数据的用时相差了几十倍。因此处理数据量比较大的时候,一定要记得将引用类型流拆箱为基本类型流再进行运算。

既然可以拆箱那么就一定可以装箱,基本类型流也可以类型提升为引用类型流。下面提供两种做法:

  1. 调用boxed()方法,将流中基本类型数据装箱为对应的引用类型数据(其底层还是调用了mapToObj()方法)
  2. 调用mapToObj()方法,将流中基本类型数据映射成指定的数据类型
public class TestBoxed {
	
	public static void main(String[] args) {
		// 数据装箱
		IntStream intStream = IntStream.rangeClosed(1, 1000);
		Stream<Integer> stream1 = intStream.boxed();
		// 数据映射
		DoubleStream doubleStream = DoubleStream.generate(Math::random);
		Stream<Double> stream2 = doubleStream.mapToObj(Double::new);
	}
	
}

 

流的短路机制

看这样一个例子:

public class TestShortCircuiting1 {
	
	static List<String> data = Arrays.asList("hello","world","hello world");
	
	public static void main(String[] args) {
		data.stream()
			.mapToInt(item -> {
				System.out.print(item + ",");
				return item.length();})
			.filter(item -> {
				System.out.print(item + ",");
				return item == 5;})
			.findFirst()
			.ifPresent(System.out::print);
	}
	
}

可能在你看来,上面的代码中流操作是这样进行的:

  1. 遍历流中所有元素并进行mapToInt操作;
  2. 遍历mapToInt操作返回的流中的所有元素并进行filter操作;
  3. 遍历filter操作返回的流中所有元素并进行findFirst操作;
  4. 打印findFirst操作返回的结果。

因此理想中控制台打印的结果应该是这样的:

hello,world,hello world,5,5,11,5

然而结果真的是这样吗?眼见为实!运行程序,打印结果如下:

hello,5,5

所以我们之前所做的推论都是错误的。

真实的流操作是这样运行的:只对源中的所有元素进行一次遍历,遍历过程中对每一元素应用所有流操作(包括中间操作和终止操作)。看到这里你可能会说:即使是流操作是这样进行的,控制台的也应该打印所有的元素。

但是,对元素应用流操作时存在短路机制——如果对元素应用的流操作中存在短路操作(short-circuiting operation)就会触发短路机制。短路这一概念相信你肯定不陌生,我们在编程中经常会碰到。例如||和&&运算符:

A || B:若A成立,则B不会被执行
A && B:若A不成立,则B不会被执行

findFirst操作相当于流操作中||运算符。对流中第一个元素“hello”应用所有流操作之后,findFirst操作就触发了短路机制,流中其他的元素就不会再遍历。

那么哪些流操作是短路操作呢?在介绍流操作部分,标有short-circuiting的操作都是短路操作。短路操作包括查找(find)、匹配(match)和截取(limit)

我们将代码稍作修改就可以看到完全不同的打印结果:

public class TestShortCircuiting2 {
	
	public static void main(String[] args) {
		TestShortCircuiting1.data.stream()
			.mapToInt(item -> {
				System.out.print(item + ",");
				return item.length();})
			.filter(item -> {
				System.out.print(item + ",");
				return item == 11;})  // 修改过滤条件
			.findFirst()
			.ifPresent(System.out::print);
	}
	
}

打印结果如下:

hello,5,world,5,hello world,11,11

 

参考:

java8 实战》

https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/index.html

https://segmentfault.com/a/1190000015806792

https://www.cnblogs.com/webor2006/p/8302401.html

https://segmentfault.com/q/1010000004944450


版权声明:本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。
喜欢 (0)