Java8 Stream学习笔记

一、什么是Stream流(WHAT)

在Java中,集合和数组是我们经常会用到的数据结构,需要经常对他们做增、删、改、查、聚合、统计、过滤等操作。相比之下,关系型数据库中也同样有这些操作,但是在Java 8之前,集合和数组的处理并不是很便捷。

不过,这一问题在Java 8中得到了改善,Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream是Java 8新增的接口,Stream可以认为是一个高级版本的 Iterator。它代表着数据流,流中的数据元素的数量可以是有限的,也可以是无限的。

Stream作为java8的新特性,基于lambda表达式,是对集合对象功能的增强,它专注于对集合对象进行各种高效、便利的聚合操作或者大批量的数据操作,提高了编程效率和代码可读性。如果在项目中经常用到集合,遍历集合可以试下lambda表达式,经常还要对集合进行过滤和排序,Stream就派上用场了。

本文就来介绍下如何使用Stream。特别说明一下,关于Stream的性能及原理不是本文的重点,这里简单提一下:

Stream的原理:将要处理的元素看做一种流,流在管道中传输,并且可以在管道的节点上处理,包括过滤筛选、去重、排序、聚合等。元素流在管道中经过中间操作的处理,最后由最终操作得到前面处理的结果。

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。

Stream有以下特性及优点:

无存储。Stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。

为函数式编程而生。对Stream的任何修改都不会修改背后的数据源,比如对Stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新Stream。

惰式执行。Stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。

可消费性。Stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

我们举一个例子,来看一下到底Stream可以做什么事情:

Java8 Stream学习笔记_第1张图片
image.png

上面的例子中,获取一些带颜色塑料球作为数据源,首先过滤掉红色的、把它们融化成随机的三角形。再过滤器并删除小的三角形。最后计算出剩余图形的周长。

如上图,对于流的处理,主要有三种关键性操作:分别是流的创建中间操作(intermediate operation)以及最终操作(terminal operation)。

二、为什么要使用Stream流(WHY)

那我们为什么要使用Stream流呢?什么场景下会使用到它呢?不会用它会死么?我先不试着去回答这些问题,我们来直接看一下Demo,这个Demo展示的是一些常见的场景下,原始的写法以及使用Stream流之后的写法,相信看完这个Demo之后大家对于上述的问题心中都会有自己的答案了,OK,show me the code:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;

public class Java8Tester {
    public static void main(String args[]){
        System.out.println("使用 Java 7: ");

        // 计算空字符串
        List strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
        System.out.println("列表: " +strings);
        long count = getCountEmptyStringUsingJava7(strings);

        System.out.println("空字符数量为: " + count);
        count = getCountLength3UsingJava7(strings);

        System.out.println("字符串长度为 3 的数量为: " + count);

        // 删除空字符串
        List filtered = deleteEmptyStringsUsingJava7(strings);
        System.out.println("筛选后的列表: " + filtered);

        // 删除空字符串,并使用逗号把它们合并起来
        String mergedString = getMergedStringUsingJava7(strings,", ");
        System.out.println("合并字符串: " + mergedString);
        List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);

        // 获取列表元素平方数
        List squaresList = getSquares(numbers);
        System.out.println("平方数列表: " + squaresList);
        List integers = Arrays.asList(1,2,13,4,15,6,17,8,19);

        System.out.println("列表: " +integers);
        System.out.println("列表中最大的数 : " + getMax(integers));
        System.out.println("列表中最小的数 : " + getMin(integers));
        System.out.println("所有数之和 : " + getSum(integers));
        System.out.println("平均数 : " + getAverage(integers));
        System.out.println("随机数: ");

        // 输出10个随机数
        Random random = new Random();

        for(int i=0; i < 10; i++){
            System.out.println(random.nextInt());
        }

        System.out.println("使用 Java 8: ");
        System.out.println("列表: " +strings);

        count = strings.stream().filter(string->string.isEmpty()).count();
        System.out.println("空字符串数量为: " + count);

        count = strings.stream().filter(string -> string.length() == 3).count();
        System.out.println("字符串长度为 3 的数量为: " + count);

        filtered = strings.stream().filter(string ->!string.isEmpty()).collect(Collectors.toList());
        System.out.println("筛选后的列表: " + filtered);

        mergedString = strings.stream().filter(string ->!string.isEmpty()).collect(Collectors.joining(", "));
        System.out.println("合并字符串: " + mergedString);

        squaresList = numbers.stream().map( i ->i*i).distinct().collect(Collectors.toList());
        System.out.println("Squares List: " + squaresList);
        System.out.println("列表: " +integers);

        IntSummaryStatistics stats = integers.stream().mapToInt((x) ->x).summaryStatistics();

        System.out.println("列表中最大的数 : " + stats.getMax());
        System.out.println("列表中最小的数 : " + stats.getMin());
        System.out.println("所有数之和 : " + stats.getSum());
        System.out.println("平均数 : " + stats.getAverage());
        System.out.println("随机数: ");

        random.ints().limit(10).sorted().forEach(System.out::println);

        // 并行处理
        count = strings.parallelStream().filter(string -> string.isEmpty()).count();
        System.out.println("空字符串的数量为: " + count);
    }

    private static int getCountEmptyStringUsingJava7(List strings){
        int count = 0;

        for(String string: strings){

            if(string.isEmpty()){
                count++;
            }
        }
        return count;
    }

    private static int getCountLength3UsingJava7(List strings){
        int count = 0;

        for(String string: strings){

            if(string.length() == 3){
                count++;
            }
        }
        return count;
    }

    private static List deleteEmptyStringsUsingJava7(List strings){
        List filteredList = new ArrayList();

        for(String string: strings){

            if(!string.isEmpty()){
                filteredList.add(string);
            }
        }
        return filteredList;
    }

    private static String getMergedStringUsingJava7(List strings, String separator){
        StringBuilder stringBuilder = new StringBuilder();

        for(String string: strings){

            if(!string.isEmpty()){
                stringBuilder.append(string);
                stringBuilder.append(separator);
            }
        }
        String mergedString = stringBuilder.toString();
        return mergedString.substring(0, mergedString.length()-2);
    }

    private static List getSquares(List numbers){
        List squaresList = new ArrayList();

        for(Integer number: numbers){
            Integer square = new Integer(number.intValue() * number.intValue());

            if(!squaresList.contains(square)){
                squaresList.add(square);
            }
        }
        return squaresList;
    }

    private static int getMax(List numbers){
        int max = numbers.get(0);

        for(int i=1;i < numbers.size();i++){

            Integer number = numbers.get(i);

            if(number.intValue() > max){
                max = number.intValue();
            }
        }
        return max;
    }

    private static int getMin(List numbers){
        int min = numbers.get(0);

        for(int i=1;i < numbers.size();i++){
            Integer number = numbers.get(i);

            if(number.intValue() < min){
                min = number.intValue();
            }
        }
        return min;
    }

    private static int getSum(List numbers){
        int sum = (int)(numbers.get(0));

        for(int i=1;i < numbers.size();i++){
            sum += (int)numbers.get(i);
        }
        return sum;
    }

    private static int getAverage(List numbers){
        return getSum(numbers) / numbers.size();
    }
}

个人认为使用Stream的优势:

  • 充分JDK库提供现有的API,代码写起来简洁优化,且减少逻辑错误的可能性
  • 方便实现并发。在多核情况下,可以使用并行Stream API来发挥多核优势。在单核的情况下,我们自己写的for性能不比Stream API 差多少。

个人认为使用Stream的劣势:

  • 调试的难度增大,但是可以通过打thread级别的断点来实现调试

总之,综合比较,推荐在做批量数据集处理的时候,在项目JDK版本允许的情况下(如果JDK版本小于8,先升级一下版本到8),尽量地去使用Java 8 Stream的特性,你的代码质量会有一个比较高的level的提升。

三、如何使用Stream流?(HOW)
使用Stream流分为三步:

  • 创建Stream流
  • 通过Stream流对象执行中间操作
  • 执行最终操作,得到结果
Java8 Stream学习笔记_第2张图片
image.png

创建Stream流

Java 8 JDK中为我们提供了多种方式来创建或转化成Stream流:

  • 通过集合的stream()方法为集合创建串行流或者parallelStream()为集合创建并行流
  • 使用流的静态方法,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator)。
  • 通过Arrays.stream(Object[])方法。
  • BufferedReader.lines()从文件中获得行的流。
  • Files类的操作路径的方法,如list、find、walk等。
  • 随机数流Random.ints()。
  • 其它一些类提供了创建流的方法,如BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), 和 JarFile.stream()。

最底层都是依赖底层的StreamSupport类来完成Stream创建。

  1. 通过已有的集合来创建流
List strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");
Stream stream = strings.stream();

通过一个已有的List创建一个流,这种通过集合创建出一个Stream的方式也是比较常用的一种方式。

  1. 通过Stream创建流
    可以使用Stream类提供的方法,直接返回一个由指定元素组成的流。
Stream stream = Stream.of("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");

下面我们列举一些常见的创建流方式的Demo:

public class StreamTest {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        // 从集合创建,stream()返回的是串行流,parallelStream()返回的是并行流
        Stream stream = list.stream();
        Stream stream1 = list.parallelStream();

        // 从数组创建
        IntStream stream2 = Arrays.stream(new int[]{1, 2, 3});

        // 创建数字流
        IntStream intStream = IntStream.of(1, 2, 3);

        // 使用random创建包含3个随机数的流
        IntStream randomStream = new Random().ints().limit(3);
    }
}

中间操作

Stream有很多中间操作,多个中间操作可以连接起来形成一个流水线,每一个中间操作就像流水线上的一个工人,每人工人都可以对流进行加工,加工后得到的结果还是一个流。

Java8 Stream学习笔记_第3张图片
image.png

怎么理解中间操作?意思是这样的:在上面我们已经能创建出Stream了,我们是对Stream进行操作,对Stream操作返回完返回的还是Stream,那么我们称这个操作为中间操作。

Java8 Stream学习笔记_第4张图片
image.png

下表罗列了一些比较常用的中间操作函数:

函数 含义
filter 根据给定规则过滤元素
map 处理并转换元素
limit 限制输出结果的数目
skip 跳过前n个元素不处理
sorted 对流中的元素进行排序
distinct 根据eqals()方法移除重复的元素
flatMap 流格式转换
allMatch 匹配所有
anyMathc 匹配其中一个
noneMathc 全部不匹配

Filter

filter 方法用于通过设置的条件过滤出元素。

Java8 Stream学习笔记_第5张图片
Filter示意图

以下代码片段使用 filter 方法过滤掉空字符串:

List strings = Arrays.asList("A", "", "B", "C", "D");
strings.stream().filter(string -> !string.isEmpty()).forEach(System.out::println);
//A, ,B, C, D

map

map 方法用于映射每个元素到对应的结果。

Java8 Stream学习笔记_第6张图片
map

以下代码片段使用 map 输出了元素对应的平方数:

List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().map( i -> i*i).forEach(System.out::println);
//9,4,4,9,49,9,25

limit/skip

limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。

以下代码片段使用 limit 方法保理4个元素:

List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().limit(4).forEach(System.out::println);
//3,2,2,3

sorted

sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法进行排序:

List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().sorted().forEach(System.out::println);
//2,2,3,3,3,5,7

distinct

distinct主要用来去重,以下代码片段使用 distinct 对元素进行去重:

List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().distinct().forEach(System.out::println);
//3,2,7,5

flatMap

flatMap用于流转换,将一个流中的每个值都转换为另一个流

List wordList = Arrays.asList("Hello", "World");
List strList = wordList.stream()
        .map(w -> w.split(" "))
        .flatMap(Arrays::stream)
        .distinct()
        .collect(Collectors.toList());

map(w -> w.split(" "))的返回值为Stream,我们想获取Stream,可以通过flatMap方法完成Stream ->Stream的转换

元素匹配

提供了三种匹配方式

1.allMatch匹配所有

List integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().allMatch(i -> i > 3)) {
    System.out.println("值都大于3");
}

2.anyMatch匹配其中一个

List integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().anyMatch(i -> i > 3)) {
    System.out.println("存在大于3的值");
}

3. noneMatch全部不匹配

List integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().noneMatch(i -> i > 3)) {
    System.out.println("值都小于3");
}

接下来我们通过一个例子和一张图,来演示下,当一个Stream先后通过filter、map、sort、limit以及distinct处理后会发生什么。

List strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");
Stream s = strings.stream().filter(string -> string.length()<= 6).map(String::length).sorted().limit(3)
            .distinct();

过程及每一步得到的结果如下图:

Java8 Stream学习笔记_第7张图片
image.png

最终操作

一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流。终端操作的执行,才会真正开始流的遍历。

Stream的中间操作得到的结果还是一个Stream,那么如何把一个Stream转换成我们需要的类型呢?比如计算出流中元素的个数、将流装换成集合等。这就需要最终操作(terminal operation)

下图总结了一些常用的最终操作:

函数 含义
forEach 对于每一个元素输出点什么
count 统计当前元素个数
collect 转化为某种集合数据类型

forEach

Stream 提供了方法 'forEach' 来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

count

count用来统计流中的元素个数。

List strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis");
System.out.println(strings.stream().count());
//7

collect

collect就是一个归约操作,可以接受各种做法作为参数,将流中的元素累积成一个汇总结果:

List strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis");
strings  = strings.stream().filter(string -> string.startsWith("Hollis")).collect(Collectors.toList());
System.out.println(strings);
//Hollis, HollisChuang, Hollis666, Hollis

couting

最后一种统计元素个数的方法在与collect联合使用的时候特别有用。

List integerList = Arrays.asList(1, 2, 3, 4, 5);
Long result = integerList.stream().collect(counting());

查找

findFirst

findFirst用于查找第一个

List integerList = Arrays.asList(1, 2, 3, 4, 5);
Optional result = integerList.stream().filter(i -> i > 3).findFirst();

通过findFirst方法查找到第一个大于三的元素并打印

findAny

findAny用于找到任意一个

List integerList = Arrays.asList(1, 2, 3, 4, 5);
Optional result = integerList.stream().filter(i -> i > 3).findAny();

通过findAny方法查找到其中一个大于三的元素并打印,因为内部进行优化的原因,当找到第一个满足大于三的元素时就结束,该方法结果和findFirst方法结果一样。提供findAny方法是为了更好的利用并行流。

reduce

reduce用于将流中的元素组合起来。

下面我们看一个Demo:

int sum = integerList.stream().reduce(0, (a, b) -> (a + b));

int sum = integerList.stream().reduce(0, Integer::sum);

reduce接受两个参数,一个初始值这里是0,一个BinaryOperator accumulator来将两个元素结合起来产生一个新值,另外reduce方法还有一个没有初始化值的重载方法。

joining

通过joining拼接流中的元素

String result = menu.stream().map(Dish::getName).collect(Collectors.joining(", "));

默认如果不通过map方法进行映射处理拼接的toString方法返回的字符串,joining的方法参数为元素的分界符,如果不指定生成的字符串将是一串的,可读性不强。

获取流中最小最大值

通过min/max获取最小最大值

Optional min = menu.stream().map(Dish::getCalories).min(Integer::compareTo);
Optional max = menu.stream().map(Dish::getCalories).max(Integer::compareTo);

toMap

将结果转化为map。为了介绍用方法,下面我们看一个例子:
UserBo.java

class UserBo{
    private int UserId;
    private String UserName;
    public UserBo(int userId, String userName) {
        super();
        UserId = userId;
        UserName = userName;
    }
    public int getUserId() {
        return UserId;
    }
    public void setUserId(int userId) {
        UserId = userId;
    }
    public String getUserName() {
        return UserName;
    }
    public void setUserName(String userName) {
        UserName = userName;
    }
    @Override
    public String toString() {
        return "UserBo [UserId=" + UserId + ", UserName=" + UserName + "]";
    }     
}
public class ToMapTest {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        List list = new ArrayList<>();
        list.add(new UserBo(100, "Mohan"));
        list.add(new UserBo(100, "Sohan"));
        list.add(new UserBo(300, "Mahesh"));
        Map map = list.stream().collect(Collectors.toMap(UserBo::getUserId, v -> v, (k, v) -> k));
        map.forEach((k, v) -> System.out.println("Key: " + k + ", value: " + v));
    }
}

输出:
·```java
Key: 100, value: UserBo(UserId=100, UserName=Mohan)
Key: 300, value: UserBo(UserId=300, UserName=Mahesh)

修改代码:
```java
public class ToMapTest {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        List list = new ArrayList<>();
        list.add(new UserBo(100, "Mohan"));
        list.add(new UserBo(100, "Sohan"));
        list.add(new UserBo(300, "Mahesh"));
        Map map = list.stream().collect(Collectors.toMap(UserBo::getUserId, v -> v, (k, v) -> v));
        map.forEach((k, v) -> System.out.println("Key: " + k + ", value: " + v));
    }
}

输出结果:

Key: 100, value: UserBo(UserId=100, UserName=Sohan)
Key: 300, value: UserBo(UserId=300, UserName=Mahesh)

我们看到toMap接受三个参数,第一个参数是转换后的map的key,第二个参数是转换后的map的value,第三个参数是当key发生冲突的时候是选择留下前面的元素还是候选的元素:其中(k, v) -> k表示选择留下前面的元素,(k, v) -> v表示选择留下后面的元素。

最后,我们还是使用一张图,来演示下,前文的例子中,当一个Stream先后通过filter、map、sort、limit以及distinct处理后会,在分别使用不同的最终操作可以得到怎样的结果。

下图,展示了文中介绍的所有操作的位置、输入、输出以及使用一个案例展示了其结果。

Java8 Stream学习笔记_第8张图片
image.png

前面说了好多Stream流的中间操作和最终操作,由于Stream流相关的接口众多,可能短时间大家全部记住。不过没关系,下图为我们展示的JDK提供的Stream相关的接口方法,返回值为stream的为中间操作,不是stream的为最终操作,这样就比较方便查找了。

Java8 Stream学习笔记_第9张图片
image.png

下图是Stream类的类结构图,里面包含了大部分的中间和终止操作。

  • 中间操作主要有以下方法(此类型方法返回的都是Stream):map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
  • 终止操作主要有以下方法:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator


    Java8 Stream学习笔记_第10张图片
    image.png

总结

本文介绍了本人在学习Java 8 Stream中的一些笔记。

学习使用Stream要分别了解Stream创建、中间操作和最终操作。

Stream的创建有两种方式,分别是通过集合类的stream方法、通过Stream的of方法。

Stream的中间操作可以用来处理Stream,中间操作的输入和输出都是Stream,中间操作可以是过滤、转换、排序等。

Stream的最终操作可以将Stream转成其他形式,如计算出流中元素的个数、将流装换成集合、以及元素的遍历等。

最后,在Java开发中,如果使用了Java 8,那么强烈建议使用Stream。因为Stream的每个操作都可以依赖Lambda表达式,它是一种声明式的数据处理方式,并且Stream提高了数据处理效率和开发效率。

参考资料

  1. Java8 Stream的总结
  2. java8的Stream对集合操作飞起来
  3. Java 8 Stream
  4. Java 8的Stream代码,你能看懂吗?
  5. Java 8 中的 Streams API 详解
  6. 使用 Stream API 优化你的代码)

你可能感兴趣的:(Java8 Stream学习笔记)