Java8 用法优雅的函数式编程与stream,你学会了吗?

Posted 郎涯技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8 用法优雅的函数式编程与stream,你学会了吗?相关的知识,希望对你有一定的参考价值。

Java 不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带 this 参数的函数。
Java 平台从 Java 8 开始,支持函数式编程。函数式编程(Functional Programming)就是一种抽象程度很高的编程范式,把函数作为基本运算单元,函数可以作为变量,还可以返回函数。Java 8 中的函数(方法)是“值”的一种新的形式,可以将方法作为参数进行传递。而作为参数进行传递的方法主要是 Lambda表达式方法引用
其中:
  • 单方法接口被称为 FunctionalInterface
  • 接收 FunctionalInterface 作为参数的时候,可以把实例化的匿名类改写为 Lambda 表达式,能大大简化代码
  • Lambda 表达式的参数和返回值均可由编译器自动推断
历史上研究函数式编程的理论是 Lambda 演算,所以我们经常把支持函数式编程的编码风格称为 Lambda 表达式。    


1、Lambda 表达式


Lambda表达式是一种匿名函数,在函数式编程里,它可以作为参数进行传递。
在 Java 程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:
  • Comparator
  • Runnable
  • Callable
Comparator为例,我们想要调用Arrays.sort()时,可以传入一个Comparator实例
匿名类
以匿名类方式编写如下:
 
   
   
 
String[] array = ...
Arrays.sort(array, new Comparator<String>() {
   public int compare(String s1, String s2) {
       return s1.compareTo(s2);
  }
});
Lambda 表达式
上述写法非常繁琐。从 Java 8 开始,我们可以用 Lambda 表达式替换单方法接口。改写上述代码如下:
 
   
   
 
public class Main {
   public static void main(String[] args) {
       String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
       Arrays.sort(array, (s1, s2) -> {
           return s1.compareTo(s2);
      });
       System.out.println(String.join(", ", array));
  }
}
观察 Lambda 表达式的写法,它只需要写出方法定义:
即 Lambada表达式作为 sort 方法的参数,是 Comparator 函数接口的实现
 
   
   
 
(s1, s2) -> {
   return s1.compareTo(s2);
}


2、FunctionalInterface 函数接口


上面的例子中涉及的函数接口都标记了 @FunctionalInterface 注解。我们把只定义了单方法的接口称之为 FunctionalInterface,用注解 @FunctionalInterface 标记。例如,Callable接口:
 
   
   
 
@FunctionalInterface
public interface Callable<V> {
   V call() throws Exception;
}
从 Java 8 开始,接口内不仅可以有抽象方法,还可以有静态方法和默认方法。只要符合定义,即使没有标记@FunctionalInterface,它也是函数接口。如果不符合函数接口的定义,那么即使标记了 @FunctionalInterface,编译器也会报错,这就是 @FunctionalInterface 的作用。


类型

函数接口主要位于 java.util.function 包下,可分成下面几类:
  • Predicate
    有输入且只输出布尔值的函数。
  • Function
    有输入有输出的函数。
  • Consumer
    有输入无输出的函数。
  • Supplier
    无输入有输出的函数。
  • Operator
    输入和输出为相同类型的函数。

Function

这里以 Function 为例详细说明,其余类型类似。Function(函数)的源码定义如下:
 
   
   
 
@FunctionalInterface
public interface Function<T, R> {
   R apply(T t);

   default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
       Objects.requireNonNull(before);
       return (V v) -> apply(before.apply(v));
  }

   default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
       Objects.requireNonNull(after);
       return (T t) -> after.apply(apply(t));
  }

static <T> Function<T, T> identity() {
       return t -> t;
  }
}
Lambda 表达式是 apply 方法的实现,apply 方法可接收任意类型的参数 T,返回值类型为 R。
例如:入参 T 类型为 String,返回值 R 类型为Integer
 
   
   
 
Function<String, Integer> lenFunction = (str) -> {return str.length();};
//输出的返回值的长度为 5
System.out.println("apple length:" + lenFunction.apply("apple"));

组合Function
Function 接口函数还提供了 andThencompose 方法来组合已有的 Function,组合 Function 的返回值仍为 Function
andThen:新的Function是把组合中第一个函数的返回值作为第二个函数的输入
compose:新的Function是把组合中第二个函数的返回值作为第一个函数的输入
例如:
 
   
   
 
//functional
Function<String, Integer> lenFunction = (str) -> {return str.length();};
Function<Integer, Integer> multiFunction = x -> x*x;

//第一个函数的返回值作为第二个函数的输入
Function<String, Integer> andFunction = lenFunction.andThen(multiFunction);
//打印 25
System.out.println("andFunction:" + andFunction.apply("apple"));

数据类型 Function
为了解决包装类型的性能损失问题,针对原始数据类型提供了一些特殊定义的Function
  • 入参固定类型
    IntFunction、LongFunction、DoubleFunction
  • 返回值固定类型
    ToIntFunction、ToLongFunction、ToDoubleFunction
  • 入参和返回值都固定类型
    IntToLongFunction、IntToDoubleFunction、LongToIntFunction等

多参数 Function
同时还提供了 2 个入参的 BiFunction

自定义函数接口

函数接口的定义主要是看入参和返回值,例如定义3个入参+1个返回值的接口:
 
   
   
 
/**
* 接收三个入参
*/
@FunctionalInterface
public interface TriFunction <T, U, K, R>
{
   R apply(T t, U u, K k);
}
调用示例:
 
   
   
 
//自定义Function
TriFunction<String, String, String, Integer> allLenFunction =
      (str1, str2, str3) -> str1.length() + str2.length() + str3.length();
System.out.println("all length:" + allLenFunction.apply("Apple", "Orange", "Banana"));


3、方法引用


可以把方法引用作为方法的参数使用,在Java中,方法引用使用 :: 表示。
FunctionalInterface允许传入:
  • 接口的实现类(很繁琐)
  • Lambda 表达式
  • 静态方法
    类名::静态方法
  • 实例方法
    实例对象名::实例方法。实例类型this隐式被看做第一个参数类型
  • 构造方法
    类名::new。实例类型被看做返回类型

静态方式、构造方法比较好理解,这里以实例方法举例说明:
 
   
   
 
Arrays.sort(array, String::compareTo);
因为实例方法本质上有一个隐含的 this 参数,String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:
 
   
   
 
public static int compareTo(this, String o);
所以,String.compareTo()方法也可作为方法引用传入。
在调用现有类的已有方法时,方法引用比 Lambda 表达式更自然,可读性更强


4、Stream


Java 8 开始,引入了一个全新的流式 Stream API,特点是:
  • 提供了一套新的流式处理的抽象序列
  • 支持函数式编程和链式操作
  • 可以表示无限序列,并且大多数情况下是 惰性求值 的
不同于java.ioInputStreamOutputStream,它代表的是任意Java对象的序列

java.io java.util.stream
存储
顺序读写的bytechar
顺序输出的任意 Java 对象实例
用途
序列化至文件或网络
内存计算/业务逻辑
不同于List,List存储的每个元素都是已经存储在内存中的某个 Java 对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的

java.util.List java.util.stream
元素
已分配并存储在内存
可能未分配,实时计算
用途
操作一组已存在的Java对象
惰性计算

Stream提供的常用操作有:
转换操作:
map()filter()sorted()distinct()
合并操作:
concat()flatMap()
并行处理:
parallel()
聚合操作:
reduce()collect()count()max()min()sum()average()
其他操作:
allMatch(), anyMatch(), forEach()findAnyfindFirst

一般来说,可以从数据源(集合类、数组)获得 Stream,而 Stream 就是数据序列,我们可以对数据序列进行各种数据处理操作(过滤、转换、排序、查询等)。
在进行 Stream 开发时只需以下三步:
  • 从数据源获得Stream
  • 组成处理管道
  • 从管道中产生处理结果
 
   
   
 
int result = createNaturalStream() // 从数据源获得Stream
            .filter(n -> n % 2 == 0) // 组成处理管道
            .map(n -> n * n) // 组成处理管道
            .limit(100) // 组成处理管道
            .sum(); // 从管道中产生处理结果

创建 Stream

  • 通过指定元素 or 数组、Collection 创建 Stream
  • 通过 Supplier 创建 Stream,可以是无限序列
  • 通过其他类的相关方法创建
  • 基本类型的StreamIntStreamLongStreamDoubleStream

基于Stream.of
 
   
   
 
Stream<String> stream = Stream.of("A", "B", "C", "D");

基于数组Arrays.stream或Collection
 
   
   
 
public class Main {
   public static void main(String[] args) {
       Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
       Stream<String> stream2 = List.of("X", "Y", "Z").stream();
       stream1.forEach(System.out::println);
       stream2.forEach(System.out::println);
  }
}
对于Collection(List、Set、Queue等),直接调用stream()方法就可以获得Stream。

基于Supplier
创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象:
 
   
   
 
Stream<String> s = Stream.generate(Supplier<String> sp);
基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素
 
   
   
 
public class Main {
   public static void main(String[] args) {
       Stream<Integer> natual = Stream.generate(new NatualSupplier());
       
       // 注意:无限序列必须先变成有限序列再打印:
       natual.limit(20).forEach(System.out::println);
  }
}

class NatualSupplier implements Supplier<Integer> {
   int n = 0;
   public Integer get() {
       n++;
       return n;
  }
}

其他方法
创建Stream的第三种方法是通过类提供的接口,直接获得Stream
例如,Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:
 
   
   
 
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
  ...
}

基本类型
因为 Java 的范型不支持基本类型,所以我们无法用 Stream<int> 这样的类型,会发生编译错误。 为了保存int,只能使用Stream<Integer>,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java 标准库提供了IntStreamLongStreamDoubleStream这三种使用基本类型的Stream
 
   
   
 
// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });

// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3")
  .stream()
  .mapToLong(Long::parseLong);

中间操作

map
可以将一种元素类型转换成另一种元素类型
 
   
   
 
public class Main {
   public static void main(String[] args) {
       List.of("Apple", "Orange", "Banana")
          .stream()
          .map(String::trim)
          .map(String::toLowerCase)
          .filter((str) -> str.length() > 5)
          .forEach(System.out::println);
  }
}

flatMap
Stream的每个元素(例如List)映射为Stream,然后合并成一个新的Stream
例如Stream的元素是集合:
 
   
   
 
Stream<List<Integer>> s = Stream.of(
       Arrays.asList(1, 2, 3),
       Arrays.asList(4, 5, 6),
       Arrays.asList(7, 8, 9));
而我们希望把上述Stream转换为Stream<Integer>,就可以使用flatMap()
 
   
   
 
Stream<Integer> i = s.flatMap(list -> list.stream());
 
   
   
 
┌─────────────┬─────────────┬─────────────┐
│┌───┬───┬───┐│┌───┬───┬───┐│┌───┬───┬───┐│
││ 1    │ 2    │ 3   │ ││ 4   │ 5    │    6 │││   7  │    8 │   9  ││
│└───┴───┴───┘│└───┴───┴───┘│└───┴───┴───┘│
└─────────────┴─────────────┴─────────────┘
                    │
                    │flatMap(List -> Stream)
                    │
                    ▼
  ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
  │   1  │   2  │   3  │   4  │   5  │   6 │    7 │    8 │ 9    │
  └───┴───┴───┴───┴───┴───┴───┴───┴───┘

limit
截取操作常用于把一个无限的Stream转换成有限的Streamskip()用于跳过当前Stream的前N个元素,limit()用于截取当前Stream最多前N个元素:
 
   
   
 
List.of("A", "B", "C", "D", "E", "F")
  .stream()
  .skip(2) // 跳过A, B
  .limit(3) // 截取C, D, E
  .collect(Collectors.toList()); // [C, D, E]

filter
使用filter()方法可以对一个Stream的每个元素进行测试,通过测试的元素被过滤后生成一个新的Stream
例如,从一组给定的LocalDate中过滤掉工作日,以便得到休息日:
 
   
   
 
public class Main {
   public static void main(String[] args) {
       Stream.generate(new LocalDateSupplier())
              .limit(31)
              .filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
              .forEach(System.out::println);
  }
}

class LocalDateSupplier implements Supplier<LocalDate> {
   LocalDate start = LocalDate.of(2020, 1, 1);
   int n = -1;
   public LocalDate get() {
       n++;
       return start.plusDays(n);
  }
}

foreach
 
   
   
 
List<String> list = List.of("Apple", "Orange", "Banana");
list.stream().forEach(System.out::println);

输出操作

输出为List
 
   
   
 
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);
类似的,collect(Collectors.toSet())可以把Stream的每个元素收集到Set中。

输出为Map
因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:
 
   
   
 
public class Main {
   public static void main(String[] args) {
       Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
       Map<String, String> map = stream
              .collect(Collectors.toMap(
                       // 把元素s映射为key:
                       s -> s.substring(0, s.indexOf(':')),
                       // 把元素s映射为value:
                       s -> s.substring(s.indexOf(':') + 1)));
       System.out.println(map);
  }
}

分组输出
 
   
   
 
public class Main {
   public static void main(String[] args) {
       List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
       Map<String, List<String>> groups = list.stream()
              .collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
       System.out.println(groups);
  }
}

输出为数组
 
   
   
 
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);

使用reduce
reduce()方法将一个Stream的每个元素依次计算并将结果合并,例如:
 
   
   
 
public class Main {
   public static void main(String[] args) {
       int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
       System.out.println(sum); // 45
  }
}


5、Optional


Optional 类是可以解决空指针异常 NullPointException 的问题,可以作为任意类型的容器,在对象值不为空的时候返回值。当值为空时,可以预先做处理,而不是抛出空指针异常。
主要有以下方法:
  • Optional.of
    包含非 null 值的 Optional
  • Optional.ofNullable
    包含 null 值的 Optional。
    若参数不为 null,则返回包含参数的 Optional;若参数为 null,则返回空的 Optional
  • isPresent
    存在检查使用
  • isEmpty
    为空检查使用
 
   
   
 
String str = null;
Optional<String> stringOptional = Optional.ofNullable(str);
if(stringOptional.isPresent())
{
   //此时下面的代码不会执行
   System.out.println(stringOptional);
}

参考文档:廖雪峰等