Spring Boot 2.x实战2 - 函数式编程2

5 Stream

我们在前面学习“函数接口”中执行的演示都是手动调用函数接口中的函数(如test()、apply()等)执行的,这一节所要学习的Stream的操作就是“函数接口”的使用环境。

5.1 什么是Stream

Stream是用声明式的方式来操作数据集合的Java API。从一个数据源(集合类、数组)获得的Stream,而Stream就是数据序列,我们可以对数据序列进行各种数据处理操作(过滤、转换、排序、查询等)。

很多Stream数据处理操作方法的返回值还是一个Stream,我们可以再对新的Stream再进行数据处理操作,这就意味着我们可以将多个数据处理串成一条大的处理管道(pipeline),这些返回值还是Stream的数据处理操作我们称之为中间操作;中间操作并没有对数据做运算处理,而是对数据处理的方式做了声明。还有另外一类操作,在Stream进行了一些管道处理后,将Stream转换成你所需要结果的数据操作,这些操作我们称之为终结操作

那我们进行Stream开发只需要三步:

  • 从数据源获得Stream;
  • 中间操作组成的处理管道;
  • 终结操作从管道中产生处理结果。

5.2 获得Stream

  1. 从普通值获取:Stream.of

    Stream<String> singleStringStream = Stream.of("wyf");
    Stream<String> stringStream = Stream.of("www", "wyf", "foo", "bar");
    
  2. 空Stream:Stream.empty

    Stream<String> emptyStringStream = Stream.empty();
    
  3. 从可空值中获取:Stream.ofNullable

    String str1 = "wyf";
    String str2 = null;
    Stream<String> str1Stream = Stream.ofNullable(str1); //等同于Stream.of
    Stream<String> str2Stream = Stream.ofNullable(str2); //等同于Stream.empty
    
  4. 从数组中获取:Arrays.stream

    Integer[] numArray = {1, 2, 3, 4, 5};
    Stream<Integer> numStream = Arrays.stream(numArray);
    
  5. 来自文件:Files类的静态方法

    String filePathStr = "文件路径字符串";
    Path path = Paths.get(filePathStr);
    Stream<String> lineStream = Files.lines(path);//返回一个字符串行的Stream
    
  6. 来自集合类:Collection、List、Set

    Collection<String> collection = Arrays.asList("www", "wyf", "foo", "bar");
    Stream<String> collectionStream = collection.stream();
    
  7. 使用建造者模式(builder pattern)构建:Stream.builder

    Stream<String> streamBuilder = Stream.<String>builder().add("www").add("wyf").add("foo").add("bar").build();
    
  8. 来自函数:Stream.generate、Stream.iterate

    • Stream.generate接受一个Supplier函数接口作为参数,且产生的Stream是无限的,使用时请限制数量:

      Stream<String> generatedStream = Stream.generate(() -> "wyf").limit(10);
      
    • Stream.iterate接受的第一个参数是起始值,第二个是个UnaryOperator,产生的Stream也是无限的,也需要限制数量:

      Stream<Integer> iteratedStream = Stream.iterate(20, n -> n+1 ).limit(5);//20,21,22,23,24
      
  9. 原始数据类型的Stream(只包含某类原始数据类型的Stream):IntStream、LongStream、DoubleStream的静态方法

    IntStream intStream = IntStream.range(1,5); //开始包含,结束不包含
    LongStream longStreamClosed = LongStream.rangeClosed(1,5);//开始包含,结束包含
    DoubleStream doubleStream = new Random().doubles(3);//3个随机的double数据
    
  10. 来自字符串

    IntStream intStream1 = "wyf".chars();
    
  11. 来自Optional:

    Optional<String> nameOptional = Optional.of("wyf");
    Stream<String> stringOptionalStream = nameOptional.stream();
    

5.3 中间操作

中间操作(Intermediate operations)不会得到最终的结果,只会返回一个新的Stream。中间参数接受函数接口作为参数,我们可以使用“Lambda表达式”和“方法引用”来作为实现。

5.3.1 演示所用Stream

从集合类获得Stream:

Stream<Person> peopleStream = Arrays.asList(new Person("wyf", Gender.MALE, 100),
                                            new Person("www", Gender.FEMALE, 80),
                                            new Person("foo", Gender.FEMALE, 90),
                                            new Person("foo", Gender.FEMALE, 90)).stream();
5.3.2 过滤
  • filter方法

Stream的过滤是主要通过Stream的filter方法来实现的,filter接受一个Predicate函数接口作为参数,Predicate接受一个参数T,返回值为boolean,当数据运算的结果为true时,数据保留。我们过滤出性别是男性数据。为了演示,我们先引入forEach这个终结操作来展示filter操作的结果。

peopleStream.filter(person -> person.getGender().equals(Gender.MALE))
					  .forEach(person -> System.out.println(person.getName()));

除了Lambda表达式可以作为函数接口的实现以外,我们还可以使用方法引用来作为函数接口的实现:

peopleStream.filter(Person::isMale)
						.forEach(System.out::println);//只剩下性别为男性的数据

使用方法引用形式的实现代码简洁性和可读性大大增强,这里的方法引用方式都是属于“引用特定类的任意对象的方法”,即引用的是Stream中任意对象的方法。

  • distinct方法

我们可以使用distinct过滤掉相同的数据,只剩下唯一一个。当然我们需要重载Person类的equals和hashCode方法来标识数据是相同的。

Person类重载equals和hashCode方法

    @Override
    public boolean equals(Object obj) {
        Person person = (Person) obj;
        if(this.getGender().equals(person.getGender()) &&
                this.getName().equals(person.getName()) &&
                this.getWeight() == person.getWeight())
            return true;
        else
            return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, weight, gender);
    }

distinct方法的使用也是很简单的,Stream直接调用返回一个新的Stream。

peopleStream.distinct()
  			.forEach(System.out::println); //去掉重复的数据,只剩唯一的数据
  • 中间操作管道

    当然我们可以将filter和distinct操作串成操作管道,如:

    peopleStream.distinct()
    				.filter(person -> person.getGender().equals(Gender.MALE))
    				.forEach(System.out::println);
    
5.3.3 转换处理

Stream API通过mapflatMap方法来对已有数据进行转换处理操作。

  • map方法

    map方法的参数是一个Function函数接口,接受一个参数T,并有返回值类型R,map方法得到的新Stream将是包含R类型数据。如:

    Stream<String> weightStream = peopleStream.map(person -> person.getName());
    

    入参为Person类型的person,返回值是字符串,那么新的Stream包含的数据类型为String,也可使用方法引用简写成:

    Stream<String> weightStream = peopleStream.map(Person::getName);
    

    当前我们也可以链起多个map方法的管道,如:

    Stream<Integer> lengthStream = peopleStream.map(Person::getName)
                                           .map(String::length);
    
  • flatMap方法

    flatMap用来处理Stream嵌套的问题,如:

    List<Person> people1 = Arrays.asList(new Person("wyf", Gender.MALE, 100),
    																		 new Person("www", Gender.FEMALE, 80));
    List<Person> people2 = Arrays.asList(new Person("foo", Gender.MALE, 90),
    																		 new Person("bar", Gender.FEMALE, 110));
    List<List<Person>> peopleList = Arrays.asList(people1, people2);
    
    peopleList.stream()
    			   .flatMap(Collection::stream) //1
             .forEach(System.out::println);//2
    
    1. 此时flatMap方法将Stream中包含的List转换成2个Stream,并将两个Stream合并成一个Stream
    2. 获取合并的Stream,打印出来的结果在people1和people2里所有Person。

    若采用map处理的话,需要处理嵌套的Stream:

    peopleList.stream()
    				.map(Collection::stream)
    				.forEach(personStream -> {personStream.forEach(System.out::println);
    });
    
  • 原始数据类型方法

    Stream中有很多针对原始数据类型的转换处理方法:mapToInt、mapToLong、mapToDouble、flatMapToInt、flatMapToLong、flatMapToDouble,如:

    IntStream intStream = peopleStream.mapToInt(Person::getWeight);
    
5.3.4 其他操作
  • skip:忽略前n条数据

    peopleStream.skip(1).forEach(System.out::println);
    
  • limit:限制只需要前n条数据

    peopleStream.limit(2).forEach(System.out::println);
    
  • sorted:将数据排序

    peopleStream.sorted(Comparator.comparing(Person::getWeight)).forEach(System.out::println);
    

5.4 终结操作

5.4.1 聚合操作
  • count:获得Stream中的数据数量

    peopleStream.count()
    
  • max:获得Stream中按照规则最大的值

    peopleStream.max(Comparator.comparing(Person::getWeight))
    
  • min:获得Stream按照规则最小的值

    peopleStream.min(Comparator.comparing(Person::getWeight))
    
5.4.2 循环
  • forEach:对Stream中的数据循环处理,forEach的参数是Consumer函数接口,只接受参数,没有返回值。

    peopleStream.forEach(System.out::println);
    

    打印peopleStream里的每个Person对象。

5.4.3 匹配
  • allMatch:Stream数据是否全部匹配,返回布尔值

    boolean isAllMale = peopleStream.allMatch(Person::isMale);
    
  • anyMatch:Stream数据是否有任意匹配,返回布尔值

  • boolean isAnyMale = peopleStream.anyMatch(Person::isMale);
    
  • nonMatch:Stream数据是否全部不匹配,返回布尔值

    boolean isNonMale = peopleStream.noneMatch(Person::isMale);
    
5.3.4 查找
  • findAny:获得Stream中的任意数据

    Person person = peopleStream.findAny().get();
    

    findAny的返回值是Optional,我们会在后面讲到,使用Optional的get方法可以获得数据。

  • findFirst:获得Stream中的第一条数据

    Person firstPerson = peopleStream.findFirst().get();
    
5.5.5 获得数组

toArray方法将Stream中的数据转换为Object数组,如:

Object[] people =  peopleStream.toArray();
5.4.6 reduce

Stream的reduce方法让我们可以进行累计的聚合操作。reduce的参数主要分成两个:

  • 第一个是初始值(可选);
  • 第二个为累计方法:按步两两计算,计算的结果作为下一步的累计计算的开始值。
int reduced1 = Stream.of(1, 2, 3).reduce((a, b) -> a + b).get(); //1 求和
int reduced2 = Stream.of(1, 2, 3).reduce(10, (a, b) -> a + b); //2 带初始值的求和
int reduced3 = Stream.of(1, 2, 3).reduce((a, b) -> a * b).get(); //3 累乘
int reduced4 =Stream.of(1, 2, 3).reduce(10, (a, b) -> a * b); //4 带初始值的类乘
int reduced5 = Stream.of(1, 2, 3).reduce(Integer::max).get(); //5 求最大值
int reduced6 = Stream.of(1, 2, 3).reduce(10,Integer::max); //6 带初始值的求最大值
int reduced7 = Stream.of(1, 2, 3).reduce(Integer::min).get(); //7 求最小值
int reduced8 = Stream.of(1, 2, 3).reduce(10,Integer::min); //8 带初始值的求最小值
  1. 数据有1、2、3,第一步计算1 + 2 = 3,第二步将得到的结果3与3相加得到6;
  2. 初始值为10,数据有1、2、3,第一步10 + 1 = 11,第二步 11 + 2 = 13,第三部13 + 3 = 16;
  3. 数组有1、2、3,第一步计算1 * 2 = 2,第二步将得到的结果2 与3相乘得到6;
  4. 初始值为10,数据有1、2、3,第一步10 * 1 = 10,第二步 10 * 2 = 20,第三部20 * 3 = 60;
  5. 数组有1、2、3,第一步比较1和2大小,第二步将第一步较大值2与3相比,得到最大值为3;
  6. 初始值为10,数据有1、2、3,第一步比较10 和 1,第二步将第一步较大值10与2相比较大值为10,第三步将第二步较大值10与3相比,得到最大值为10;
  7. 数组有1、2、3,第一步比较1和2大小,第二步将第一步较小值1与3相比,得到最小值为1;
  8. 初始值为10,数据有1、2、3,第一步比较10 和 1,第二步将第一步较小值1与2相比较小值为1,第三步将第二步较小值1与3相比,得到最小值为1;
5.4.7 collect

Stream的collect方法是终结操作中功能最丰富的操作,它接受一个java.util.stream.Collector参数,Collector指定了Stream转换成值的方式。Java为我们预先定义了大量的Collector来解决常用的问题。通过java.util.stream.Collectors类的静态方法构造。

Collectors的静态方法主要分为以下几类:

  • 转换成集合类

    • Collectors.toList()

      List<String> streamToList = Stream.of("wyf", "www", "foo", "bar")
                                      .collect(Collectors.toList());
      
    • Collectors.toSet()

      Set<String> streamToSet = Stream.of("wyf", "wyf", "foo", "bar")
                                      .collect(Collectors.toSet());
      
    • Collectors.toCollection(集合类型)

      List<String> streamToCollection = Stream.of("wyf", "www", "foo", "bar")
              .collect(Collectors.toCollection(ArrayList::new));
      
  • 转换成String

    • Collectors.joining():支持将Stream中的字符串数据指定分隔符、前缀、后缀连接成一个字符串。

      String streamToString = Stream.of("wyf", "www", "foo", "bar")
              .collect(Collectors.joining(",", "[", "]"));
      

      第一个参数“,”为分隔符,第二个参数“[”为前缀,第二个参数“]”为后缀,字符串streamToString为“[wyf,www,foo,bar]”。joining还有另外一个方法是只有分隔符没有前缀和后缀,如:

      String streamToStringOnlyDelimiter = Stream.of("wyf", "www", "foo", "bar")
              .collect(Collectors.joining(","));
      

      字符串streamToStringOnlyDelimiter为“wyf,www,foo,bar”。

  • 聚合操作

    • Collectors.counting():Stream中的数据数量

      long count = Stream.of("wyf", "www", "foo", "bar").collect(Collectors.counting());
      
    • Collectors.maxBy():Stream中数据按照某种规则的最大值,下面是比较字符串长度的最大值

      String maxLengthString = Stream.of("wyf", "w", "fooo", "barbar").collect(Collectors.maxBy(Comparator.comparing(String::length))).get();
      
    • Collectors.minBy():Stream中数据按照某种规则的最小值,下面是比较字符串长度的最小值

      String minLengthString = Stream.of("wyf", "ww", "fooo", "barbar").collect(Collectors.minBy(Comparator.comparing(String::length))).get();
      
    • Collectors.averagingInt/Long/Double:求Stream中数据的平均值,下面是求字符串长度的平均值

      double average =  Stream.of("wyf", "ww", "fooo", "barbar").collect(Collectors.averagingInt(String::length));
      
    • Collectors.summingInt/Long/Double:求Stream中数据的和,下面是求字符串长度的和

      int sum = Stream.of("wyf", "ww", "fooo", "barbar").collect(Collectors.summingInt(String::length));
      
    • Collectors.summarizingInt/Long/Double:汇总Stream数据情况,包括:平均值、求和、最大值、最小值、数量,所有的值都包含在返回值类型IntSummaryStatistics(LongSummaryStatistics/DoubleSummaryStatistics)中

      IntSummaryStatistics summary = Stream.of("wyf", "ww", "fooo", "barbar").collect(Collectors.summarizingInt(String::length));
      
      System.out.println(summary.getMax());
      System.out.println(summary.getMin());
      System.out.println(summary.getAverage());
      System.out.println(summary.getSum());
      System.out.println(summary.getCount());
      
  • 转换成Map

    • Collectors.toMap():toMap方法将Stream转换成Map,toMap方法两个参数:keyMapper(Map实例key生成方式)、valueMapper(Map实例value生成方式),这两个参数的类型都是Function函数接口。

      Map<String, Integer> map = Stream.of("wyf", "www", "foo", "bar")
              .collect(Collectors.toMap(Function.identity(), String::length));
      

      本例将字符串作为key,Function.identity()keyMapper;字符串的长度作为value,String::lengthvalueMapper。其中Function.identity()是返回输入的数据,即字符串本身。

    • Collectors.groupingBy():按照特定功能处理分组,接受一个Function函数接口。

      Map<Integer,List<String>> map  =  Stream.of("wyf", "www", "foobar", "barfoo")
              .collect(Collectors.groupingBy(String::length));
      

      按照字符串的长度分组,长度相等的分到一组(List<String>),字符串长度为key。所以上面例子分为两组,第一组key为3,List<String>的值是:wyf、www;第二组key为6,List<String>里的值为:foobar和barfoo

    • Collectors.partitioningBy():partitioningBy是一种特殊的groupingBy方法,它接受Predicate作为参数,只将数据分成false和true两组。

      Map<Boolean,List<String>> partitioningByMap = Stream.of("wyf", "www", "foobar", "barfoo")
                 .collect(Collectors.partitioningBy(str -> str.length()== 6));
      

      条件为字符串长度是否为6,字符穿长度不为6的进入key为false的分组,字符串长度为6的进入key为true的分组。

  • Collectors.collectingAndThen:collect执行后将值再进行转换

int size = Stream.of("wyf", "www", "foo", "bar")
        .collect(Collectors.collectingAndThen(Collectors.toList(), List::size));

将Stream转成List(Collectors.toList())后的下一步的处理是获取List的大小(List::size)。

前面所有例子为了演示全部使用Collectors静态方法,我们可以用静态导入让代码更简洁,如:

import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toList;

代码可以写成

int size = Stream.of("wyf", "www", "foo", "bar")
        .collect(collectingAndThen(toList(),List::size));

6 Optional

Optional类是为类帮我们解决空指针异常(NullPointException)的问题。它可以作为任意类型T的对象的容器,它可以再对象值不为空的时候返回值,值为空的时候可以预先做处理而不是抛出空指针异常。

6.1 获得Optional

  • Optional.emtpty():获得空的Optional

    Optional<String> emptyOptional = Optional.empty();
    
  • Optional.of(参数):包含非null值的Optional

    String str = "wyf";
    Optional<String> nonNullOptional = Optional.of(str);
    
  • Optional.ofNullable(参数):包含可为null的Optional。参数若不为null,则返回包含参数的Optional;若为null,则返回空的Optional。

    String str1 = "wyf";
    String str2 = null;
    Optional<String> nullableOptional1 = Optional.ofNullable(str1);
    Optional<String> nullableOptional2 = Optional.ofNullable(str2);
    System.out.println(emptyOptional.equals(nullableOptional2)); //nullableOptional2与emptyOptional相同,输出true
    

6.2 Optional的用法

  • 检查值是否存在/为空:存在检查使用isPresent;为空检查使用isEmpty

    String str1 = "wyf";
    String str2 = null;
    Optional<String> nullableOptional1 = Optional.ofNullable(str1);
    Optional<String> nullableOptional2 = Optional.ofNullable(str2);
    System.out.println(nullableOptional1.isPresent()); //1
    System.out.println(nullableOptional2.isEmpty()); 	//2
    
    1. nullableOptional1包含字符串wyf,所以检查是否存在是true;
    2. nullableOptional2是一个空的Optioanl,检查是否为空是true;
  • 条件运算:ifPresent,当满足数据存在的条件下,可执行自己处理语句

    Optional<String> nullableOptional1 = Optional.ofNullable("wyf");
    Optional<String> nullableOptional2 = Optional.ofNullable(null);
    nullableOptional1.ifPresent(System.out::println); //1
    nullableOptional2.ifPresent(System.out::println); //2
    
    1. 符合条件,输出字符串;
    2. 不符合条件,没有输出。
  • 默认值:orElse的参数设置当Optional为空时的默认值;orElseGet的参数是一个Supplier函数接口,它不指定默认值,而是使用函数接口实现算提供的值。

    Optional<String> nullableOptional1 = Optional.ofNullable("wyf");
    Optional<String> nullableOptional2 = Optional.ofNullable(null);
    String name1 = nullableOptional1.orElse("www"); //1
    String name2 = nullableOptional2.orElse("www"); //2
    String name3 = nullableOptional1.orElseGet(() -> "wwwFromOrElseGet"); //3
    String name4 = nullableOptional2.orElseGet(() -> "wwwFromOrElseGet"); //4
    
    1. nullableOptional1不为空,所以name1依然是wyf,不需要使用orElse设置的默认值www;
    2. nullableOptional2为空,所以name2使用的是orElse设置的默认值www;
    3. nullableOptional1不为空,所以name3依然是wyf,不需要使用orElseGet中Lambda表达值返回的wwwFromOrElseGet;
    4. nullableOptional2为空,所以name2使用的是oorElseGet中Lambda表达值返回的wwwFromOrElseGet。
  • 获得值:get方法只有当Optional不为null时才能获得包含的数据

    Optional<String> nameOptional = Optional.of("wyf");
    String name = nameOptional.get();//获得Optional中包含的name字符串
    
  • 数据过滤:可以使用filter对数据进行过滤

    Optional<String> nameOptional = Optional.of("wyf");
    boolean isWyf = nameOptional.filter(name -> name.equals("wyf")).isPresent();
    

    通过filter方法看Optional中包含的数据是否符合name -> name.equals("wyf")这个Predicate的实现,我们当前是符合的,所以运算的isWyf为true。

  • 转换处理:可以通过map和flatMap对数据进行转换处理

    Optional<String> nameOptional = Optional.of("wyf");
    String hello = nameOptional.map(name -> "Hello " + name).get();
    

    将Optional中包含的数据wyf处理成为"Hello " + name,并获得这个值。

总结

我们在这一章系统学习了Java函数式编程,函数式编程是将函数作为编程开发的第一等公民即函数也是值。我们可以将函数当作普通的对象值一样进行传递。在Java函数式编程的核心是函数接口,而函数接口的两大实现分别为Lambda表达式方法引用,当我熟悉这些概念和实践后,我们将在后面的章节中大量的使用到Lambda表达式方法引用Stream API、以及响应式编程

新书推荐:

我的新书《从企业级开发到云原生微服务:Spring Boot 实战》已出版,内容涵盖了丰富Spring Boot开发的相关知识
购买地址:https://item.jd.com/12760084.html

在这里插入图片描述

主要包含目录有:

第一章 初识Spring Boot(快速领略Spring Boot的美丽)
第二章 开发必备工具(对常用开发工具进行介绍:包含IntelliJ IDEA、Gradle、Lombok、Docker等)
第三章 函数式编程
第四章 Spring 5.x基础(以Spring 5.2.x为基础)
第五章 深入Spring Boot(以Spring Boot 2.2.x为基础)
第六章 Spring Web MVC
第七章 数据访问(包含Spring Data JPA、Spring Data Elasticsearch和数据缓存)
第八章 安全控制(包含Spring Security和OAuth2)
第九章 响应式编程(包含Project Reactor、Spring WebFlux、Reactive NoSQL、R2DBC、Reactive Spring Security)
第十章 事件驱动(包含JMS、RabbitMQ、Kafka、Websocket、RSocket)
第11章 系统集成和屁股里(包含Spring Integration和Spring Batch)
第12章 Spring Cloud与微服务
第13章 Kubernetes与微服务(包含Kubernetes、Helm、Jenkins、Istio)
多谢大家支持。

发布了116 篇原创文章 · 获赞 10 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/wiselyman/article/details/105732043