Java函数式编程

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!


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表达式可以写为

Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));

Lambda表达式的参数和返回值均可由编译器自动推断。


FunctionalInterface:单方法接口

我们把只定义了单方法的接口称之为FunctionalInterface,用注解@FunctionalInterface标记。
虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)是Object定义的方法,不算在接口方法内。因此,Comparator也是一个FunctionalInterface。接收FunctionalInterface作为参数的时候,可以把实例化的匿名类改写为Lambda表达式,能大大简化代码。

函数式接口的特点

  • 接口有且仅有一个抽象方法,如抽象方法compare
  • 允许定义静态非抽象方法
  • 允许定义默认defalut非抽象方法
  • 允许定义java.lang.Object中的public方法,如equals
  • FunctionInterface注解不是必须的,如果一个接口符合"函数式接口"定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错

函数式接口是专门为lambda表达式准备的,lambda表达式是只实现接口中唯一的抽象方法的匿名实现类

default关键字

  • default方法可以有自己的默认实现,即有方法体
  • 接口实现类可以不去实现default方法,并且可以使用default方法

Stream:惰性计算

这个Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java对象的序列。这个Stream和List也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。

Stream看上去有点不好理解,但我们举个例子就明白了。

如果我们要表示一个全体自然数的集合,显然,用List是不可能写出来的,因为自然数是无限的,内存再大也没法放到List中。但是,用Stream可以做到。

Stream<BigInteger> naturals = createNaturalStream(); // 全体自然数

因为这个streamNxN也有无限多个元素,要打印它,必须首先把无限多个元素变成有限个元素,可以用limit()方法截取前100个元素,最后用forEach()处理每个元素,这样,我们就打印出了前100个自然数的平方:

Stream<BigInteger> naturals = createNaturalStream();
naturals.map(n -> n.multiply(n)) // 1, 4, 9, 16, 25...
    .limit(100)
    .forEach(System.out::println);

我们总结一下Stream的特点:它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。

Stream的另一个特点是,一个Stream可以轻易地转换为另一个Stream,而不是修改原Stream本身。

最后,真正的计算通常发生在最后结果的获取,也就是惰性计算。

Stream<BigInteger> naturals = createNaturalStream(); // 不计算
Stream<BigInteger> s2 = naturals.map(BigInteger::multiply); // 不计算
Stream<BigInteger> s3 = s2.limit(100); // 不计算
s3.forEach(System.out::println); // 计算

惰性计算的特点是:一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生。


创建Stream

(1)

Stream<String> stream = Stream.of("A", "B", "C", "D");//虽然这种方式基本上没啥实质性用途,但测试的时候很方便。

(2)

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);

(3)创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象,基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。

Stream<String> s = Stream.generate(Supplier<String> sp);

因为Java的范型不支持基本类型,所以我们无法用Stream< int>这样的类型,会发生编译错误。为了保存int,只能使用Stream< Integer>,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream、LongStream和DoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率


map

Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream。

List.of("  Apple ", " pear ", " ORANGE", " BaNaNa ")
    .stream()
    .map(String::trim) // 去空格
    .map(String::toLowerCase) // 变小写
    .forEach(System.out::println); // 打印

通过若干步map转换,可以写出逻辑简单、清晰的代码。

flatMap

map可以对管道流中的数据进行转换操作,但是如果管道中还有管道该如何处理?即:如何处理二维数组及二维集合类。实现一个简单的需求:将“hello”,“world”两个字符串组成的集合,元素的每一个字母打印出来。

words.stream()
        .flatMap(w -> Arrays.stream(w.split(""))) // [h,e,l,l,o,w,o,r,l,d]
        .forEach(System.out::println);

filter

IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
            .filter(n -> n % 2 != 0)
            .forEach(System.out::println);

谓词逻辑的复用

通常情况下,filter函数中lambda表达式为一次性使用的谓词逻辑。如果我们的谓词逻辑需要被多处、多场景、多代码中使用,通常将它抽取出来单独定义到它所限定的主语实体中。
比如:将下面的谓词逻辑定义在Employee实体class中。

public static Predicate<Employee> ageGreaterThan70 = x -> x.getAge() >70;
public static Predicate<Employee> genderM = x -> x.getGender().equals("M");

List<Employee> filtered = employees.stream()
        .filter(Employee.ageGreaterThan70.and(Employee.genderM))
        .collect(Collectors.toList());

List<Employee> filtered1 = employees.stream()
        .filter(Employee.ageGreaterThan70.or(Employee.genderM))
        .collect(Collectors.toList());

List<Employee> filtered2 = employees.stream()
        .filter(Employee.ageGreaterThan70.or(Employee.genderM).negate())//negate语法(取反)
        .collect(Collectors.toList());

reduce(归约)

Stream提供的操作分为两类:转换操作(map filter)和聚合操作(reduce)

Stream API为我们提供了Stream.reduce用来实现集合元素的归约。reduce函数有三个参数:

  • Identity标识:一个元素,它是归约操作的初始值,如果流为空,则为默认结果
  • Accumulator累加器:具有两个参数的函数:归约运算的部分结果和流的下一个元素
  • Combiner合并器(可选):当归约并行化时,或当累加器参数的类型与累加器实现的类型不匹配时,用于合并归约操作的部分结果的函数

Integer类型归约

reduce初始值为0,累加器可以是lambda表达式,也可以是方法引用。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int result = numbers
        .stream()
        .reduce(0, (subtotal, element) -> subtotal + element);
System.out.println(result);  //21

int result = numbers
        .stream()
        .reduce(0, Integer::sum);
System.out.println(result); //21

String类型归约

不仅可以归约Integer类型,只要累加器参数类型能够匹配,可以对任何类型的集合进行归约计算。

List<String> letters = Arrays.asList("a", "b", "c", "d", "e");
String result = letters
        .stream()
        .reduce("", (partialString, element) -> partialString + element);
System.out.println(result);  //abcde


String result = letters
        .stream()
        .reduce("", String::concat);
System.out.println(result);  //ancde

复杂对象归约

Employee e1 = new Employee(1,23,"M","Rick","Beethovan");
Employee e2 = new Employee(2,13,"F","Martina","Hengis");
Employee e3 = new Employee(3,43,"M","Ricky","Martin");
Employee e4 = new Employee(4,26,"M","Jon","Lowman");
Employee e5 = new Employee(5,19,"F","Cristine","Maria");
Employee e6 = new Employee(6,15,"M","David","Feezor");
Employee e7 = new Employee(7,68,"F","Melissa","Roy");
Employee e8 = new Employee(8,79,"M","Alex","Gussin");
Employee e9 = new Employee(9,15,"F","Neetu","Singh");
Employee e10 = new Employee(10,45,"M","Naveen","Jain");


List<Employee> employees = Arrays.asList(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10);


Integer total = employees.stream().map(Employee::getAge).reduce(0,Integer::sum);
System.out.println(total); //346

Combiner合并器的使用

除了使用map函数实现类型转换后的集合归约,我们还可以用Combiner合并器来实现,这里第一次使用到了Combiner合并器。
因为Stream流中的元素是Employee,累加器的返回值是Integer,所以二者的类型不匹配。这种情况下可以使用Combiner合并器对累加器的结果进行二次归约,相当于做了类型转换。

Integer total3 = employees.stream()
        .reduce(0,(totalAge,emp) -> totalAge + emp.getAge(),Integer::sum); //注意这里reduce方法有三个参数
System.out.println(total); //346

计算结果和使用map进行数据类型转换的方式是一样的。

并行流数据归约(使用合并器)

对于大数据量的集合元素归约计算,更能体现出Stream并行流计算的威力。在进行并行流计算的时候,可能会将集合元素分成多个组计算。为了更快的将分组计算结果累加,可以使用合并器

Integer total2 = employees
        .parallelStream()
        .map(Employee::getAge)
        .reduce(0,Integer::sum,Integer::sum);  //注意这里reduce方法有三个参数

System.out.println(total); //346

Stream查找和匹配元素

不用stream,用for循环的做法:

boolean isExistAgeThan70 = false;
for(Employee employee:employees){
  if(employee.getAge() > 70){
    isExistAgeThan70 = true;
    break;
  }
}

使用Stream API来实现查找和匹配:

boolean isExistAgeThan70 = employees.stream().anyMatch(Employee.ageGreaterThan70);//使用之前定义的谓词逻辑

boolean isExistAgeThan72 = employees.stream().anyMatch(e -> e.getAge() > 72);//lambda表达式

boolean isExistAgeThan10 = employees.stream().allMatch(e -> e.getAge() > 10);//AllMatch

boolean isExistAgeLess18 = employees.stream().noneMatch(e -> e.getAge() < 18);//noneMatch

总结

Stream提供的常用操作有:

转换操作:map(),filter(),sorted(),distinct();

合并操作:concat(),flatMap();

并行处理:parallel();Stream中并行操作不一定比串行快

聚合操作:reduce(),collect(),count(),max(),min(),sum(),average();

其他操作:allMatch(), anyMatch(), forEach()

posted @ 2020-03-25 11:18  swifthao  阅读(345)  评论(0编辑  收藏  举报
Live2D