【函数式编程】

函数式编程

函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式。

Lambda基础

Lambda表达式

正常写法:

String[] array = ...
Arrays.sort(array, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
});

lambda写法:

// Lambda
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        //此处为lambda表达式写法
        Arrays.sort(array, (s1, s2) -> {
            return s1.compareTo(s2);
        });
        System.out.println(String.join(", ", array));
    }
}

FunctionalInterface

把只定义了单方法的接口称之为FunctionalInterface,用注解@FunctionalInterface标记。例如,Callable接口:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

再来看Comparator接口:

@FunctionalInterface
public interface Comparator<T> {

    int compare(T o1, T o2);

    boolean equals(Object obj);

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        ...
    }
    ...
}

虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)Object定义的方法,不算在接口方法内。因此,Comparator也是一个FunctionalInterface


小结

单方法接口被称为FunctionalInterface

接收FunctionalInterface作为参数的时候,可以把实例化的匿名类改写为Lambda表达式,能大大简化代码。

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

方法引用

  • 传递的方法有默认this
  • 方法引用,是指如果某个方法签名(只看参数类型和返回类型)和接口恰好一致,就可以直接传入方法引用

构造方法引用

构造方法的引用写法是类名::new


小结

FunctionalInterface允许传入:

  • 接口的实现类(传统写法,代码较繁琐);
  • Lambda表达式(只需列出参数名,由编译器推断类型);
  • 符合方法签名的静态方法;
  • 符合方法签名的实例方法(实例类型被看做第一个参数类型);
  • 符合方法签名的构造方法(实例类型被看做返回类型)。

FunctionalInterface不强制继承关系,不需要方法名称相同,只要求方法参数(类型和数量)与方法返回类型相同,即认为方法签名相同。

使用Stream

  • java.util.stream

划重点:这个Stream不同于java.ioInputStreamOutputStream,它代表的是任意Java对象的序列。两者对比如下:

java.iojava.util.stream
存储顺序读写的bytechar顺序输出的任意Java对象实例
用途序列化至文件或网络内存计算/业务逻辑

再次划重点:这个StreamList也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。

换句话说,List的用途是操作一组已存在的Java对象,而Stream实现的是惰性计算,两者对比如下:

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

例:

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

链式

createNaturalStream()
    .map(n -> n.multiply(n))
    .limit(100)
    .forEach(System.out::println);

Stream API的基本用法就是:创建一个Stream,然后做若干次转换,最后调用一个求值方法获取真正计算的结果:

int result = createNaturalStream() // 创建Stream
             .filter(n -> n % 2 == 0) // 任意个转换
             .map(n -> n * n) // 任意个转换
             .limit(100) // 任意个转换
             .sum(); // 最终计算结果

小结

Stream API的特点是:

  • Stream API提供了一套新的流式处理的抽象序列;
  • Stream API支持函数式编程和链式操作;
  • Stream可以表示无限序列,并且大多数情况下是惰性求值的。

创建Stream

Stream.of()静态方法

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("A", "B", "C", "D");
        // forEach()方法相当于内部循环调用,
        // 可传入符合Consumer接口的void accept(T t)的方法引用:
        stream.forEach(System.out::println);
    }
}

基于数组或Collection

基于一个数组或者Collection,这样该Stream输出的元素就是数组或者Collection持有的元素:

import java.util.*;
import java.util.stream.*;

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

把数组变成Stream使用Arrays.stream()方法。对于CollectionListSetQueue等),直接调用stream()方法就可以获得Stream

上述创建Stream的方法都是把一个现有的序列变为Stream,它的元素是固定的。


基于Supplier

创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象:

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

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

例如,我们编写一个能不断生成自然数的Supplier,它的代码非常简单,每次调用get()方法,就生成下一个自然数:

import java.util.function.*;
import java.util.stream.*;

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

上述代码我们用一个Supplier<Integer>模拟了一个无限序列(当然受int范围限制不是真的无限大)。如果用List表示,即便在int范围内,也会占用巨大的内存,而Stream几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。

对于无限序列,如果直接调用forEach()或者count()这些最终求值操作,会进入死循环,因为永远无法计算完这个序列,所以正确的方法是先把无限序列变成有限序列,例如,用limit()方法可以截取前面若干个元素,这样就变成了一个有限序列,对这个有限序列调用forEach()或者count()操作就没有问题。


其他方法

创建Stream的第三种方法是通过一些API提供的接口,直接获得Stream

例如,Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:

try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
    ...
}

此方法对于按行遍历文本文件十分有用。

另外,正则表达式的Pattern对象有一个splitAsStream()方法,可以直接把一个长字符串分割成Stream序列而不是数组:

Pattern p = Pattern.compile("\\s+");
Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");
s.forEach(System.out::println);

基本类型

因为Java的泛型不支持基本类型,所以我们无法用Stream<int>这样的类型,会发生编译错误。为了保存int,只能使用Stream<Integer>,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStreamLongStreamDoubleStream这三种使用基本类型的Stream,它们的使用方法和泛型Stream没有大的区别,设计这三个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);

小结

创建Stream的方法有 :

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

基本类型的StreamIntStreamLongStreamDoubleStream

使用map

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

Function的定义是:

@FunctionalInterface
public interface Function<T, R> {
    // 将T类型转换为R:
    R apply(T t);
}

map()方法接收的对象是Function接口对象,它定义了一个apply()方法,负责把一个T类型转换成R类型:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

小结

map()方法用于将一个Stream的每个元素映射成另一个元素并转换成一个新的Stream

可以将一种元素类型转换成另一种元素类型。

使用filter

  • Stream.filter()

    • 所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream
  • filter()方法接收的对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件:

    • @FunctionalInterface
      public interface Predicate<T> {
          // 判断元素t是否符合条件:
          boolean test(T t);
      }
      

小结

使用filter()方法可以对一个Stream的每个元素进行测试,通过测试的元素被过滤后生成一个新的Stream

使用reduce

  • map()filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。

  • reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:

@FunctionalInterface
public interface BinaryOperator<T> {
    // Bi操作:两个输入,一个输出
    T apply(T t, T u);
}

例:

import java.util.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
    }
}
// 计算过程:
acc = 0 // 初始化为指定值
acc = acc + n = 0 + 1 = 1 // n = 1
acc = acc + n = 1 + 2 = 3 // n = 2
acc = acc + n = 3 + 3 = 6 // n = 3
acc = acc + n = 6 + 4 = 10 // n = 4
acc = acc + n = 10 + 5 = 15 // n = 5
acc = acc + n = 15 + 6 = 21 // n = 6
acc = acc + n = 21 + 7 = 28 // n = 7
acc = acc + n = 28 + 8 = 36 // n = 8
acc = acc + n = 36 + 9 = 45 // n = 9

小结

reduce()方法将一个Stream的每个元素依次作用于BinaryOperator,并将结果合并。

reduce()是聚合方法,聚合方法会立刻对Stream进行计算。

输出集合

输出为List

public class Main {
    public static void main(String[] args) {
        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);
    }
}

Stream的每个元素收集到List的方法是调用collect()并传入Collectors.toList()对象,它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。

类似的,collect(Collectors.toSet())可以把Stream的每个元素收集到Set中。


输出为数组

把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”:

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

注意到传入的“构造方法”是String[]::new,它的签名实际上是IntFunction<String[]>定义的String[] apply(int),即传入int参数,获得String[]数组的返回值。


输出为Map

要把Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:

import java.util.*;
import java.util.stream.*;

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

分组输出使用Collectors.groupingBy(),它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List,上述代码运行结果如下:

{
    A=[Apple, Avocado, Apricots],
    B=[Banana, Blackberry],
    C=[Coconut, Cherry]
}

小结

Stream可以输出为集合:

Stream通过collect()方法可以方便地输出为ListSetMap,还可以分组输出

其他操作

排序

  • Stream.sorted();

  • 可以传入Comparator来自定义

  • 返回Stream

List<String> list = List.of("Orange", "apple", "Banana")
    .stream()
    .sorted(String::compareToIgnoreCase)
    .collect(Collectors.toList());

截取

  • Stream.skip()
  • Stream.limit()
  • 返回Stream
List.of("A", "B", "C", "D", "E", "F")
    .stream()
    .skip(2) // 跳过A, B
    .limit(3) // 截取C, D, E
    .collect(Collectors.toList()); // [C, D, E]

合并

  • Stream.concat() 静态方法
Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]

flatMap

  • Stream.flatMap()

如果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());

因此,所谓flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream

┌─────────────┬─────────────┬─────────────┐
│┌───┬───┬───┐│┌───┬───┬───┐│┌───┬───┬───┐│
││ 1 │ 2 │ 3 │││ 4 │ 5 │ 6 │││ 7 │ 8 │ 9 ││
│└───┴───┴───┘│└───┴───┴───┘│└───┴───┴───┘│
└─────────────┴─────────────┴─────────────┘
                     │
                     │flatMap(List -> Stream)
                     │
                     │
                     ▼
   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
   │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
   └───┴───┴───┴───┴───┴───┴───┴───┴───┘

并行

  • Stream.parallel() 尽可能的情况下自动并行处理
Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
                   .sorted() // 可以进行并行排序
                   .toArray(String[]::new);

其他聚合方法

  • reduce()

  • collect()

  • count() :返回元素个数;

  • max(Comparator<? super T> cp) :找出最大元素;

  • min(Comparator<? super T> cp):找出最小元素。

  • sum():对所有元素求和;

  • average():对所有元素求平均数。

还有一些方法,用来测试Stream的元素是否满足以下条件:

  • boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;

  • boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。

  • forEach(),它可以循环处理Stream的每个元素

小结

Stream提供的常用操作有:

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

合并操作:concat()flatMap()

并行处理:parallel()

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值