第十三节 Java8的Stream API

亮子 2021-09-14 21:21:59 10789 0 0 0

Stream是Java8中处理集合的关键抽象概念,它可以指定希望对集合的操作,可以执行复杂的查找、过滤和映射数据等操作。

1、为什么要用Stream API

(1)、实际开发中,项目中多数数据源都来自于Mysql,Oracle等。但现在数据源可以更多了,有MongDB,Radis等,而这些NoSQL的数据就需要Java层面去处理。
(2)、Stream和 Collection集合的区别:Collection是一种静态的内存数据结构,而Stream是有关计算的。前者是主要面向内存,存储在内存中,后者主要是面向CPU,通过CPU实现计算。

使用Stream API 对集合的数据进行操作,类似于SQL执行的数据库查询,也可以用来并行执行操作,其提供了一种高效且易于使用的处理数据方式。

2、Stream到底是什么呢?

是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
集合讲的是数据,Stream讲的是计算!

注意:

  • Stream自身不会存储元素
  • Stream不会改变数据源对象,相反会返回产生一个持有结果的新Stream
  • Steam操作是延迟执行的,这意味着他们会等到需要结果的时候才执行。

3、Stream的操作三个步骤

(1)创建Stream
一个数据源(如:集合、数组〉,获取一个流

(2)中间操作
一个中间操作链,对数据源的数据进行处理3-终止操作(终端操作)

(3)终止操作(终端操作)
一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用

Stream的操作三个步骤

4、创建Stream对象的方式

(1)、通过集合创建

Java8中的Collection接口被扩展,提供了两个获取流的方法:

  • default Stream stream():返回一个顺序流
  • default Stream parallelStream():返回一个并行流

(2)通过数组创建

Java8中的Arrays的静态方法 stream()可以获取数组流:

static Stream stream(T array):返回一个流

重载形式,能够处理对应基本类型的数组:

public static IntStream stream(int[] array)
public static LongStream stream(long[] array)
public static DoubleStream stream(double[] array)

(3)通过Stream的of()函数创建

可以调用Stream类静态方法 of(),通过显示值创建一个流。它可以接收任意数量的参数。

  • public static Stream of(T… values):返回一个流

(4)创建无限流

可以使用静态方法Stream.iterate()和 Stream.generate(),创建无限流。

  • 迭代

public static Stream iterate(final T seed, final UnaryOperator f)

  • 生成

public static Streamgenerate(Supplier s)

5、创建流的代码示例

(1)示例1

@Test
public void test1(){
	// 1、通过Collection集合提供的stream()或parallelStream()
	List<String> list = new ArrayList<String>();
	Stream<String> stream = list.parallelStream();
	
	// 2、通过Arrays的静态方法stream()获取
	Student[] students = new Student[10];
	Stream<Student> stream2 = Arrays.stream(students);
	
	// 3、通过Stream类的静态方法of()
	Stream<String> stream3 = Stream.of("1","2");
	
	// 4、创建无限流(迭代)
	Stream<Integer> stream4 = Stream.iterate(0, (x) -> x+2);
	stream4.limit(5).forEach(System.out::println);
	
	// 5、生成
	Stream.generate(() -> Math.random()).limit(4).forEach(System.out::println);
}

(2)示例2

package com.shenmazong.stream;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

/**
 * @author 军哥
 * @version 1.0
 * @description: 演示创建流的四种方式
 * @date 2021/10/8 11:18
 */

class Student {
    private String name;
    private Integer age;

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class StreamApiTest {

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();

        list.add(new Student("武大郎", 50));
        list.add(new Student("潘金莲", 19));
        list.add(new Student("武松", 29));
        list.add(new Student("西门庆", 35));

        //--1 通过集合创建
        // default Stream<E> stream():返回一个顺序流
        Stream<Student> stream = list.stream();

        // default Stream<E> parallelStream():返回一个并行流
        Stream<Student> studentStream = list.parallelStream();

        //--2 通过数据创建
        Student[] students = {};
        Stream<Student> stream1 = Arrays.stream(students);


        //--3 通过Stream的of()函数创建
        Stream<Integer> integerStream = Stream.of(1, 2, 3);

        //--4 创建无限流

        // 迭代:输出10个偶数
        Stream<Integer> iterate = Stream.iterate(0, t -> t + 2);
        iterate.limit(10).forEach(System.out::println);

        // 生成:输出10个随机数
        Stream<Double> generate = Stream.generate(Math::random);
        generate.limit(10).forEach(System.out::println);

    }
}

6、流特点的代码演示

1)、演示延迟加载

package com.shenmazong.lambda;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

/**
 * @program: server-java-demo
 * @description: 验证是否是延迟加载
 * @author: 亮子说编程
 * @create: 2020-10-19 16:33
 **/

@Data
@NoArgsConstructor
@AllArgsConstructor
class Student {
    private String name;
    private Integer age;
}

public class StreamApiTest {

    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();

        //
        list.add(new Student("武大郎", 50));
        list.add(new Student("潘金莲", 19));
        list.add(new Student("武松", 29));
        list.add(new Student("西门庆", 35));

        //
        Stream<Student> stream = list.stream().filter((i) -> {
            System.out.println("验证是否是延迟加载");
            return  i.getAge() > 30;
        });

        //
        System.out.println("=== end ===");

        //stream.forEach(System.out::println);
        //System.out.println("=== finish ===");
    }
}

2)、generate创建无限流

使用Stream类的静态方法 generate创建无限流
generate方法参数为Supplier 供给型接口

package com.shenmazong.lambda;

import java.util.Random;
import java.util.stream.Stream;

/**
 * @program: server-java-demo
 * @description: 演示创建无限流
 * @author: 亮子说编程
 * @create: 2020-10-19 16:33
 **/

public class StreamApiTest2 {

    public static void main(String[] args) {
        //--1
        Stream<Double> doubleStream = Stream.generate(() -> Math.random());
        doubleStream.limit(5).forEach(System.out::println);

        //--2
        Stream<Integer> integerStream = Stream.generate(() -> new Random().nextInt(100));
        integerStream.limit(5).forEach(System.out::println);
    }
}

3)、iterate 创建无限流

public class StreamApiTest3 {

    public static void main(String[] args) {
        //使用Stream类的静态方法 iterate 创建无限流
        Stream<Integer> stream = Stream.iterate(0, (x) -> x + 2);

        //中间操作和终止操作
        stream.limit(5).forEach(System.out::println);
    }
}

4)、of创建有限流

public class StreamApiTest3 {

    public static void main(String[] args) {

        Stream<String> stream3 = Stream.of("hxh", "aj", "hhh");
        stream3.forEach(System.out::println);
    }
}

6、Stream API的操作

流:是数据渠道,用于操作数据源(集合,数组等)所生成的元素序列;
集合讲的是数据,流讲的是计算;

①Stream 自己不会存储元素。
②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。

1)、筛选与切片

  • filter:接收Lambda,从流中排除某些元素;
  • limit(n):截断流,使其不超过给定元素;
  • skip(n):跳过元素,返回跳过前n个元素。若元素不足n个,则返回一个空流;
  • distinct:筛选,通过流生成的hashcode()和equals()方法去重;
package com.shenmazong.stream;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 军哥
 * @version 1.0
 * @description: StreamAPI测试类
 * @date 2021/10/8 19:44
 */

@Data
@NoArgsConstructor
@AllArgsConstructor
class Student {
    private String name;
    private Integer age;
}

public class StreamApiTest {

    @Test
    public void testFilter() {
        // 初始化数据
        List<Student> list = new ArrayList<>();

        list.add(new Student("武大郎", 50));
        list.add(new Student("潘金莲", 19));
        list.add(new Student("武松", 29));
        list.add(new Student("西门庆", 35));
        list.add(new Student("西门庆", 35));

        System.out.println("0 ==================");

        // 筛选年龄大于30岁的人
        list.stream().filter((s)->{
            return s.getAge()>30;
        }).forEach(System.out::println);

        System.out.println("1 ==================");

        // 筛选出1个年龄大于30岁的人
        list.stream().limit(1).filter((s)->{
            return s.getAge()>30;
        }).forEach(System.out::println);

        System.out.println("2 ==================");

        // 跳过前2个元素,然后筛选出1个年龄大于30岁的人
        // 这里需要注意skip函数的位置
        list.stream().skip(2).limit(10).filter((s)->{
            return s.getAge()>30;
        }).forEach(System.out::println);

        // 实现去重:如果是自定义对象需要实现hashCode()和equals()函数
        System.out.println("3 ==================");
        list.stream().distinct().forEach(System.out::println);
    }
}

完善Student代码如下:

    @Override
    public boolean equals(Object obj) {
        TbStudent o = (TbStudent)obj;
        if(!this.getName().equals(o.getName())) {
            return false;
        }
        if(!this.getAge().equals(o.getAge())) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        return this.getName().hashCode()+this.getAge().hashCode();
    }

2)、映射

一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选
择一列。Stream API也通过map和flatMap方法提供了类似的工具。

  • map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • mapToDouble( ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
  • mapTolnt( ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的IntStream。
  • mapToLong( ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的LongStream。
  • flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

(1)map映射

map方法接收一个函数作为函数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

1、下面实例,把每个字符串转换成大写字母,把所有的序列组成一个流

    @Test
    public void test4(){
        List<String> list = Arrays.asList("aaa", "bbb", "ccc");
        /*map方法中接收一个Function类型的对象,该对象用Lambda表达式实现,输入一个字符串类型,返回一个字符串类型,并把返回的字符串放到流中*/
        Stream<String> stream = list.stream()
                                    .map((x) -> {
                                        String s = x.toUpperCase();
                                        return s;
                                    });
        stream.forEach(System.out::println);
    }

运行上述方法,输出结果:

AAA
BBB
CCC

2、把每个字符串中的字符转化为大写字母,把每个序列转化为一个流

    public static Stream<Character> filterString(String str){
        List<Character> list = new ArrayList<Character>();
        for(Character ch : str.toCharArray()){  //char[]
            list.add(Character.toUpperCase(ch));
        }
        return list.stream();
    }

    @Test
    public void test5(){
        List<String> list = Arrays.asList("aaa", "bbb", "ccc");
        /*每个序列生成一个流,然后分别把每个流组成一个大流*/
        Stream<Stream<Character>> stream = list.stream()
                                               .map(TestStream::filterString);
        /*遍历流中的流的每个序列*/
        stream.forEach((s) -> {
            s.forEach(System.out::println);
        });
    }

运行上述方法,输出结果:

A
A
A
B
B
B
C
C
C

3、映射后再筛选

    @Test
    public void testMap() {
        //--1 字符转换为大写
        List<String> list = Arrays.asList("aa", "bb", "cc");
        list.stream().map((s)->{
            return s.toUpperCase();
        }).forEach(System.out::println);

        //--2 筛选名字长度大于2的人
        List<Student> students = new ArrayList<>();

        students.add(new Student("武大郎", 50));
        students.add(new Student("潘金莲", 19));
        students.add(new Student("武松", 29));
        students.add(new Student("西门庆", 35));
        students.add(new Student("西门庆", 35));

        Stream<String> nameStream = students.stream().map(Student::getName);
        nameStream.filter((s) -> {return s.length()>2;}).forEach(System.out::println);
    }

(2)、flatMap映射

上面一个实例,把每个子流放入一个流中,相当于流中嵌套了一个流,如果用flatMap方法的话,会把每个子流中的元素组成一个流,然后遍历一个流就可以了,而不是遍历一个流中的每个流。

这个描述是非常难以理解的,但是我们可以通过list接口的add和addAll来理解,代码如下:

    @Test
    public void testArrayDemo() {
        ArrayList<Object> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);
        list1.add(3);

        ArrayList<Object> list2 = new ArrayList<>();
        list2.add(4);
        list2.add(5);
        list2.add(6);

//        list1.add(list2);
        list1.addAll(list2);
        System.out.println(list1);
    }

下面是具体的例子:

    public static Stream<Character> filterString(String str){
        List<Character> list = new ArrayList<Character>();
        for(Character ch : str.toCharArray()){  //char[]
            list.add(Character.toUpperCase(ch));
        }
        return list.stream();
    }

    @Test
    public void test6(){
        List<String> list = Arrays.asList("aaa", "bbb", "ccc");
        Stream<Character> stream = list.stream()
            .flatMap(TestStream::filterString);
        stream.forEach(System.out::println);
    }

运行上述方法,输出结果:

A
A
A
B
B
B
C
C
C

3)、排序

Stream通道流支持两种排除方式:sorted()自然排序;sorted(Comparator comp)。
sorted()自然排序 :产生一个新流,流中每个序列按自然排序的方式进行排序。
sorted(Comparator comp) :产生一个新流,流中的每个序列按自定义的比较器排序进行排序。

1、 sorted()自然排序

    @Test
    public void test1(){
        List<String> list = Arrays.asList("aaa", "bbb", "ccc");
        Stream<String> stream = list.stream()
                                    .sorted(); //把流中的每个序列进行自然排序
        stream.forEach(System.out::println);
    }

运行方法,输出结果如下

aaa
bbb
ccc

2、sorted(Comparator comp)定制排序

首先按用户的年龄进行比较,如果年龄相等,则按照用户的姓名进行比较。

    @Test
    public void test2(){
        List<User> users = Arrays.asList(
                new User("lzj", 28),
                new User("zhangsan", 30),
                new User("lisi", 28),
                new User("wanger", 28));

        Stream<User> stream = users.stream()
                                    .sorted((x, y) -> { //使流中的序列两两进行比较
                                        if (x.getAge() == y.getAge()) {
                                            return x.getName().compareTo(y.getName());
                                        }else{
                                            return x.getAge() > y.getAge() ? 1 : -1;
                                        }
                                    });
        stream.forEach(System.out::println);
    }

Sorted方法接受一个Comparator类型的参数,实例用Lambda表达式进行实现了该接口,重写的该接口中的compare方法。

运行方法,输出结果如下:

User [name=lisi, age=28]
User [name=lzj, age=28]
User [name=wanger, age=28]
User [name=zhangsan, age=30]

4)、终止操作:查找与匹配

终端操作会从通道流的流水线生成结果。其结果可以是任何不是流的值,例如int、List、void等。如果流操作后返回的值还是Stream流类型的,则是开始操作和中间操作。
有以下经常用到的查找与匹配操作:

  • allMatch——检查是否匹配所有元素
  • anyMatch——检查是否至少匹配一个元素
  • noneMatch——检查是否没有匹配的元素
  • findFirst——返回第一个元素
  • findAny——返回当前流中的任意元素
  • count——返回流中元素的总个数
  • max——返回流中最大值
  • min——返回流中最小值

下面进行示例演示,首先创建数据源为:

    private static List<User> usrs = Arrays.asList(
            new User("lzj", 20),
            new User("zhangsan", 22),
            new User("lisi", 25),
            new User("suner", 30));

1、 allMatch——检查是否匹配所有元素

检查是否流中的所有User对象的年龄是否大于18.

    @Test
    public void test1(){
        Boolean flag = usrs.stream()
            .allMatch((x) -> x.getAge() > 18);
        System.out.println(flag);   //输出true
    }

2、 anyMatch——检查是否至少匹配一个元素

检查是否流中的User对象有没有年龄大于25岁的。

    @Test
    public void test2(){
        Boolean flag = usrs.stream()
                .anyMatch((x) -> x.getAge() > 25);
        System.out.println(flag);   //输出true
    }

3、 noneMatch——检查是否没有匹配的元素

检查流中是否User类型的对象年龄都不大于35岁。

    @Test
    public void test3(){
        Boolean flag = usrs.stream()
                .noneMatch((x) -> x.getAge() > 35);
        System.out.println(flag); //输出true
    }

4、 findFirst——返回第一个元素

或得满足条件的流中的第一个元素并放到Optional容器中。

    @Test
    public void test4(){
        Optional<User> user = usrs.stream()
            .filter((x) -> x.getName().contains("s"))
            .findFirst();
        System.out.println(user.get());//输出:User [name=zhangsan, age=22]
    }

5、 findAny——返回当前流中的任意元素

过滤出姓名包含s字符的User类型对象,获取满足条件的任何一个对象,并放到Optional容器中。

    @Test
    public void test5(){
        Optional<User> user = usrs.stream()
            .filter((x) -> x.getName().contains("s"))
            .findAny();
        System.out.println(user.get());//输出:User [name=zhangsan, age=22]
    }

6、 count——返回流中元素的总个数

过滤出名字包含s字符的所有User类型对象,并返回总个数。

    @Test
    public void test6(){
        Long total = usrs.stream()
                .filter((x) -> x.getName().contains("s"))
                .count();
        System.out.println(total);
    }

7、 max——返回流中最大值

获取年龄最大的那个User类型对象,并放到Optional容器中

    @Test
    public void test7(){
        Optional<User> user = usrs.stream()
            .max((x, y) -> x.getAge().compareTo(y.getAge()));
        System.out.println(user);
    }

8、 min——返回流中最小值

获取年龄最小的User类型对象,并放置到Optional容器中

    @Test
    public void test8(){
        Optional<User> user = usrs.stream()
                .min((x, y) -> x.getAge().compareTo(y.getAge()));
        System.out.println(user);
    }

5)、终止操作:归约

reduce(T identity, BinaryOperator) / reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。
获取流中User对象年龄的总和。

    /*reduce*/
    @Test
    public void test9(){
        Integer total = usrs.stream()
            .map(User::getAge)
            .reduce(0, (x, y) -> x + y);
           /*其中0、x、y和total的类型一致,都是Integer类型*/
        System.out.println(total);//输出:97
    }

开始流中放置的是每个user对象,经过map后,把每个user对象的年龄获取到放在流中,然后分别把年龄进行加起来,起始值是0,加总后的结果放在赋给total变量。

6)、终止操作:收集

collect——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法。
获取流中User类型对象的年龄,并把年龄重组成List类型

    @Test
    public void test10(){
        List<String> names = usrs.stream()
            .map(User::getName)
            .collect(Collectors.toList());
        System.out.println(names);
    }

运行方法,输出结果:

[lzj, zhangsan, lisi, suner]

参考文章