Java8 Stream 入门篇

281 阅读6分钟

简介

Stream 是Java 8 提供用来处理数组、集合等数据的新方式。Stream就像工厂的流水线一样,开发人员可以在流水线上增加对元素的处理操作。

参考资料

  1. 深入理解Java8中Stream的实现原理
  2. JAVA Stream流有状态操作和无状态操作

概念

Lambda

Lambda 是 Java 8 提供的新特性,开发人员可以通过编写函数式接口,使用Lambda实现函数式编程。Stream中的大量方法使用了函数式接口,因此理解Lambda是理解后续Stream代码的基础。

函数式接口

/**
 * 单一未实现的方法接口(函数式接口)
 * @FunctionalInterface注解约束接口只能有一个未实现的方法
 */
@FunctionalInterface 	//标识为函数式接口
interface TestFunctionalInterface{
    int add(int b);
}

Lambda

// b 是接口中add方法的参数,与接口方法参数数量相同,类型可以不指定
// -> 指向实现的方法
// {} 接口的实现方法
TestFunctionalInterface testFunc = (b)->{
    return b+1;
};

Lambda的使用

package com.studyjava.stream;

public class Java8Lambda {
    /**
     * 接口中只有单一且需要实现的方法,使用@FunctionInterface注解后,就成了函数式接口
     */
    @FunctionalInterface
    interface TestFunctionalInterface{
        int add(int b);
    }


    public static void main(String[] args){
        //单独声明变量
        System.out.println("单独声明变量使用");
        TestFunctionalInterface testFunc = (b)->{   //使用Lambda
            return b+1;
        };
        int result = testFunc.add(10);
        System.out.println("result=>" + result + "\n");

        //使用在方法参数中
        System.out.println("在方法参数中使用");
        result = addOne((b)->{ return b+1;});   	//使用Lambda
        System.out.println("result=>" + result + "\n");

        //声明变量传递给方法参数使用
        System.out.println("声明变量传递给方法参数使用");
        TestFunctionalInterface testFunc2 = (b)->{ //使用Lambda
            return b+1;
        };
        result = addOne(testFunc2);
        System.out.println("result=>" + result + "\n");

    }
    public static int addOne(TestFunctionalInterface testFunc){
        return testFunc.add(10)+1;
    }

}

Stream

image-20210628134720553.png

如上图,这样就构成了一个Stream(流)。可以看出Stream的组成部分:

  1. 数据源 - List
  2. 流动的元素 - List中的元素
  3. 中间操作 -(map、filter)
  4. 结束操作 -(collect)

与上图相对应的示例代码:

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Java8Stream {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");

        System.out.println("before list ===>" + list);
        List<Integer> results = list.stream()
                .map(item -> Integer.parseInt(item)) //将String转换为Integer
                .filter(item -> item < 4)			 //过滤列表中小于4的
                .collect(Collectors.toList());       //收集结果
        System.out.println("after list ===>" + results);
    }
}
//程序运行结果
/*
before list ===>[1, 2, 3, 4, 5]
after list ===>[1, 2, 3]
*/

PS:代码中map().filter();不是元素先在map方法中执行一遍循环,返回结果后,再到filter中执行一遍循环。这两个方法共用一个循环。

相当于以下代码:

List<Integer> result = new ArrayList<>();        //收集结果
for(String item: list){
	Integer int = Integer.parseInt(item);    //数据转换
	if( item > 4 ){				 //数据过滤
    	result.add(item);
	}
}

就像上图的流水线一样,数组中的元素先到map方法处理,再到filter方法。

数据源

数据源是Stream中要处理元素的来源。数据源可以是集合或数组。

image-20210628211605071.png

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("3");
        list.add("2");
        list.add("5");
        list.add("4");
        Stream<String> listStream = list.stream(); //list做为数据源

	String[] strArr = new String[10];
	strArr[0]="1";
	strArr[1]="2";
	strArr[2]="3";
	strArr[3]="4";
	strArr[4]="5";
	Stream<String> arrStream = Arrays.stream(strArr); //数组做为数据源

中间操作

中间操作:在Stream中,对元素进行处理的操作。如:转换(map)、过滤(filter)、排序(sorted)

特点

  1. 设置完中间操作后,会返回一个Stream对象。开发人员可以再次添加中间操作或者是结束操作。
  2. 设置中间操作时,不会执行中间操作。

中间操作分为:有状态的和无状态

有状态的操作是指在执行操作时,操作内部会进行记录状态。

如:sorted,它需要把列表中所有的元素先收集起来,再排序。完成排序后,再遍历进行输出给下一个中间操作或结束操作

image-20210628211433489.png

        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(3);
        list.add(2);
        list.add(5);
        list.add(4);

        System.out.println("before list ===>" + list);
        List<Integer> results = list.stream()
                .sorted()			                 //先收集,再排序
                .collect(Collectors.toList());       //收集结果
        System.out.println("after list ===>" + results);

无状态的操作是指在执行操作时,只关注当前元素本身的处理。

如:map、filter

image-20210628211508009.png

结束操作

image-20210628211631460.png

结束操作:最后元素的收集操作。如:collect、reduce、forEach

特点

  1. 结束操作,不会返回Stream对象
  2. 执行结果操作时,才会去执行之前设置好的中间操作

结束操作分为 :短路操作和非短路操作

  1. 短路操作是指,遍历元素时,遇到满足条件元素,直接返回,不再进行遍历
    1. 如:allMatch()、 noneMatch() 、findFirst() 、findAny()
  2. 非短路操作,与短路操作相反,会完成数据源的遍历。
    1. 如:forEach()、forEachOrdered()、toArray()、reduce()、collect()、max()、min()、count()

并行计算

image-20210630112507811.png

Stream在默认情况下是串行的,并行计算的开启方式如下:

        List<Integer> results = list.stream()
                .parallel()							 //开启并行
                .map(item -> Integer.parseInt(item)) 
                .filter(item -> item < 4)			 
                .collect(Collectors.toList());    

只需在结束操作前调用parallel方法即可开启并行。

并行计算依靠Java7提供的ForkJoin框架,把一个任务拆分为多个小任务执行,执行完后合并小任务的结果。

使用并行时注意:

  1. 避免在中间操作中使用外部的状态
    1. 原因:多线程安全性的根本问题是与其它线程共享的状态。
    2. 并行执行可能会出现各个线程观察到的状态是不一致的
  2. 处理基本类型数据,使用包装好的Stream。如:LongStream、IntStream
  3. 简单的数据处理使用串行比并行要快
    1. ForkJoin框架需要对任务进行拆分,需要耗时。
    2. 多线程上下文切换需要时间

测试代码:

package com.studyjava.stream;

import com.sun.deploy.util.StringUtils;
import org.junit.Test;

import java.util.*;
import java.util.stream.BaseStream;
import java.util.stream.LongStream;

public class Java8StreamParallel {
    /**
     * 在longStream中,使用并行比串行快
     */
    @Test
    public void longStreamParallel() {
        //装箱类并行计算
        //并行计算
        long time = System.currentTimeMillis();
        long sum1 = LongStream.rangeClosed(1, 100000000000l).parallel().map(log -> log * 10).sum();
        System.out.println(System.currentTimeMillis() - time);
        //串行计算
        time = System.currentTimeMillis();
        long sum2 = LongStream.rangeClosed(1, 100000000000l).map(log -> log * 10).sum();
        System.out.println(System.currentTimeMillis() - time);

        System.out.println("sum1 = " + sum1 + " sum2 = " + sum2);
    }

    /**
     * 在List中,简单的数据处理使用并行处理比串行处理要慢
     */
    @Test
    public void singleParallel() {
        //ArrayList并行计算
        List<Long> arrayList = new LinkedList<>();
        for (long i = 0; i < 1000; i++) {
            arrayList.add(i);
        }
        long time = System.currentTimeMillis();
        arrayList.parallelStream()
                .map(log ->
                        log * 10
                ).reduce(0L, Long::sum);
        long end = System.currentTimeMillis() - time;
        System.out.println("Parallel end time--->" + end);
        time = System.currentTimeMillis();
        arrayList.stream()
                .map(log ->
                        log * 10
                ).reduce(0L, Long::sum);
        end = System.currentTimeMillis() - time;
        System.out.println("Sequential end time--->" + end);
    }

    /**
     * 在List中,复杂的数据处理使用并行处理比串行处理要快
     */
    @Test
    public void arrayParallel() {
        //ArrayList并行计算
        List<Long> arrayList = new LinkedList<>();
        for (long i = 0; i < 1000; i++) {
            arrayList.add(i);
        }
        long time = System.currentTimeMillis();
        arrayList.parallelStream().map(log -> {
            try {
                //模拟处理复杂数据处理所需1ms
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return log * 10;
        }).reduce(0L, (i, a) -> i + a);
        long end = System.currentTimeMillis() - time;
        System.out.println("Parallel end time--->" + end);
        time = System.currentTimeMillis();
        arrayList.stream().map(log -> {
            try {
                //模拟处理复杂数据处理所需1ms
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return log * 10;
        }).reduce(0L, (i, a) -> i + a);
        end = System.currentTimeMillis() - time;
        System.out.println("Sequential end time--->" + end);
    }
}

常用

数据源

  1. 基本类型包含Stream
    1. IntStream
    2. LongStream
    3. DoubleStream
  2. 集合类
    1. List
    2. Set
    3. Map
  3. 数组(需要转换)
    1. Stream.of()
      1. 内部调用Arrays.stream()
    2. Arrays.stream()

中间操作

  1. map
    1. mapToInt
    2. mapToLong
    3. mapToDouble
  2. filter
  3. sorted

结果操作

  1. collect - 收集结果
    1. collect(Collectors.toList) - 结果转为List类型
  2. forEach - 遍历
  3. count - 计数
  4. sum - 求和 (要转为LongStream才能使用)

示例

package com.studyjava.stream;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;

public class Java8Stream {
    private static long getCurrentTime() {
        return System.currentTimeMillis();
    }
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");

        //collect - 收集
        System.out.println("collect - 收集");
        System.out.println("before list ===>" + list);
        List<Integer> intList = list.stream()
                .map(item -> Integer.parseInt(item))   //转换数据类型
                .filter(item -> item < 4)              //过滤数据
                .collect(Collectors.toList());         //收集结果
        System.out.println("after list ===>" + intList + "\n");
        /*
        	执行结果
		before list ===>[1, 2, 3, 4, 5]
		after list ===>[1, 2, 3]
        */

        //count - 计数
        System.out.println("count - 计数");
        System.out.println("before list ===>" + list);
        long count = list.stream()
                .map(item -> Integer.parseInt(item))   //转换数据类型
                .filter(item -> item < 4)              //过滤数据
                .count();                              //计数
        System.out.println("count ===>" + count + "\n");
        /*
        	执行结果
		before list ===>[1, 2, 3, 4, 5]
		count ===>3
	*/

        //sum - 求和
        System.out.println("sum - 求和");
        System.out.println("before list ===>" + list);
        long sum = list.stream()
                .mapToLong(item -> Long.parseLong(item)) //转换数据类型(mapToLong)
                .filter(item -> item < 4)              	 //过滤数据
                .sum();                              	 //计数
        System.out.println("sum ===>" + sum + "\n");
        /*
           	执行结果
                before list ===>[1, 2, 3, 4, 5]
		sum ===>6
        */

        //forEach - 遍历
        System.out.println("forEach - 遍历");
        list.stream()
                .mapToLong(item -> Integer.parseInt(item))   //转换数据类型
                .filter(item -> item < 4)                    //过滤数据
                .forEach(item -> System.out.println(item));  //遍历
	/*
		执行结果
		before list ===>[1, 2, 3, 4, 5]
		1
		2
		3
	*/

    }
}

总结

  1. 增强代码逻辑
    1. 函数式编程:使用Lambda配合Stream,简化代码,突出数据处理逻辑
    2. 链式调用:数据处理逻辑一节接到一节,线性逻辑符合思考方式
    3. 延迟执行:Stream只有在调用结束操作时,整个流程才会执行。这意味着,在调用结束操作之前,可以根据业务逻辑拼接不同的中间操作
  2. 并行计算实现简单
    1. Stream屏蔽了数据流的遍历步骤,让开发人员专注于元素的处理操作。实现并行计算交给底层代码,开发人员只需要调用底层代码提供的并行计算方法,即可开启并行计算。