38. 函数式编程

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

1.Lambda

1.1 单方法接口

把只定义了单方法的接口称之为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

1.2 Lambda表达式,

使用Lambda表达式,我们就可以不必编写FunctionalInterface接口的实现类,从而简化代码:

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

        //匿名内部类写法:
        Collections.sort(list, new Comparator<String>() {
            public int compare(String o1, String o2) {
                return o1.length()-o2.length();
            }
        });

        //lambda表达式写法:
        Collections.sort(list, (String o1, String o2)->{
                return o1.length()-o2.length();
        });

        //lambda表达式中的参数类型可以不写
        Collections.sort(list, (o1,o2)->{
            return o1.length()-o2.length();
        });

        //lambda表达式方法体中只有一句代码时,方法体的{}可以不写,如果这句话有return,也一并不写
        Collections.sort(list, (o1,o2)->o1.length()-o2.length());

        //lambda表达式的方法参数只有1个,那么()可以忽略不写---本案例不适合
    }
}

1.3 方法引用

  • 引用静态方法
    除了Lambda表达式,我们还可以直接传入方法引用。
public class Main {
    public static void main(String[] args) {
        String[] arr = {"Apple","Orange", "Banana"};
        Arrays.sort(arr,Main::cmp);
        System.out.println(Arrays.toString(arr));
    }

    static int cmp(String a, String b) {
        return a.compareTo(b);
    }
}

所谓方法引用,是指如果某个方法签名(参数和返回类型)和接口恰好一致,就可以直接传入方法引用。
因为Comparator<String>接口定义的方法是int compare(String, String),和静态方法int cmp(String, String)相比,除了方法名外,方法参数一致,返回类型相同,因此,我们说两者的方法签名一致,可以直接把方法名作为Lambda表达式传入。

  • 引用实例方法
public class Main {
    public static void main(String[] args) {
        String[] arr = {"Apple","Orange", "Banana"};
        Arrays.sort(arr,String::compareTo);
        System.out.println(Arrays.toString(arr));
    }
}

实例方法也可以是因为实例方法有一个隐含的this参数,String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入thisthis也是String,相当于静态方法:

public static int compareTo(this, String o);

2. Stream

Java 8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中。

2.1 创建Stream

  • Stream.of()
/**
 * 创建 Stream.of
 */
public static void streamOf()
{
    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    stream.forEach(System.out::println);
}

2.2 基于数组或Collection

  • 把数组变成Stream使用Arrays.stream()方法。
  • 对于Collection(List、Set、Queue等),直接调用stream()方法就可以获得Stream
/**
 * 创建:数组和集合
 */
public static void collectionAndArray()
{
    // 使用数据
    Stream<Integer> stream = Arrays.stream(new Integer[]{1, 2, 3, 4, 5});
    stream.forEach(System.out::println);
    // 使用集合
    Stream<Integer> stream2 = List.of(1, 2, 3, 4, 5).stream();
    stream2.forEach(System.out::println);
}

2.3 基于Supplier

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

/**
 * 使用Supplier
 */
public static void useSupplier()
{
    Stream<Integer> stream = Stream.generate(new SupplierChild());
    // limit限制数量输出
    stream.limit(10).forEach(System.out::println);
}


import java.util.function.Supplier;

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

Stream几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。如果用List表示,即便在int范围内,也会占用巨大的内存。
对于无限序列,如果直接调用forEach()或者count()这些最终求值操作,会进入死循环,因为永远无法计算完这个序列,所以正确的方法是先把无限序列变成有限序列。

2.4 其他方法

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

  • Files类
    Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
    ...
}
  • 正则表达式的Pattern对象
    正则表达式的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);

3. Stream转换

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

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);

map操作,把一个Stream的每个元素一一对应到应用了目标函数的结果上。

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

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

4. 过滤

Stream.filter()是Stream的另一个常用过滤方法。

/**
 * Stream 过滤
 */
public static void filterStream()
{
    Stream<Integer> s = Stream.of(1, 2, 3, 4, 5).filter(n -> n % 2 == 0);
    s.forEach(System.out::println);
}

filter()方法接收的对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件:

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

filter()除了常用于数值外,也可应用于任何Java对象。例如,从一组给定的LocalDate中过滤掉工作日,以便得到休息日:

/**
 * Stream 过滤
 */
public static void filterStream()
{
    Stream<Integer> s = Stream.of(1, 2, 3, 4, 5).filter(n -> n % 2 == 0);
    s.forEach(System.out::println);

    Stream.generate(new LocalDateSupplier()).limit(10).filter(ldt -> !(ldt.getDayOfWeek()== DayOfWeek.SUNDAY || ldt.getDayOfWeek()==DayOfWeek.SATURDAY)).forEach(System.out::println);
}


import java.time.LocalDate;
import java.util.function.Supplier;

public class LocalDateSupplier implements Supplier<LocalDate> {
    LocalDate start = LocalDate.of(2025, 1, 1);
    int n =-1;

    public LocalDate get()
    {
        n = n + 1;
        return start.plusDays(n);
    }
}

5. 聚合

Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。
reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:

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

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
...
}
  • 实例
/**
 * 聚合
 */
public static void reduceStream()
{
    int stream = Stream.of(1, 2, 3, 4, 5).reduce(0, (m,n) -> m + n);
    System.out.println(stream);
}

reduce()操作首先初始化结果为指定值(这里是0),reduce()对每个元素依次调用(m, n) -> m+ n,计算过程如下:

// 计算过程:
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

6. 输出集合

6.1 输出为List

Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素。
把非空字符串保存到List中:

List<Integer> list = Stream.of(1, 2, 3, 4, 5).toList();
System.out.println(list);

6.2 输出为数组

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

6.3 输出为Map

Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要keyvalue:

Stream<String> stream = Stream.of("a:apple", "b:banana", "c:cat");
Map<String,String> map = stream.collect(Collectors.toMap(n -> n.substring(0,n.indexOf(":")), n -> n.substring(n.indexOf(":")+1)));
System.out.println(map);// {a=apple, b=banana, c=cat}

6.3 分组输出

List<String> list = List.of("apple", "banana", "cat", "orange", "dog","zero");
Map<String,List<String>> stream = list.stream().collect(Collectors.groupingBy(s -> s.substring(0,1),Collectors.toList()));
System.out.println(stream);// {a=[apple], b=[banana], c=[cat], d=[dog, duck], z=[zero], o=[orange]}

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

7. 排序

sorted()

Stream<String> s = Stream.of("apple", "banana", "cat", "orange", "dog","zero","duck");
s.sorted().forEach(System.out::println);

此方法要求Stream的每个元素必须实现Comparable接口。如果要自定义排序,传入指定的Comparator即可:

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

8.去重

对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct()

List.of("A", "B", "A", "C", "B", "D")
    .stream()
    .distinct()
    .collect(Collectors.toList()); // [A, B, C, D]

9.截取

截取操作常用于把一个无限的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]

10.合并

将两个Stream合并为一个Stream可以使用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]

11. 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());
┌─────────────┬─────────────┬─────────────┐
│┌───┬───┬───┐│┌───┬───┬───┐│┌───┬───┬───┐│
││ 1 │ 2 │ 3 │││ 4 │ 5 │ 6 │││ 7 │ 8 │ 9 ││
│└───┴───┴───┘│└───┴───┴───┘│└───┴───┴───┘│
└─────────────┴─────────────┴─────────────┘
                     │
                     │flatMap(List -> Stream)
                     │
                     │
                     ▼
   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
   │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
   └───┴───┴───┴───┴───┴───┴───┴───┴───┘

12. 并行

parallel()把一个普通Stream转换为可以并行处理的Stream

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

13. 其他聚合方法

  • count():用于返回元素个数;
  • max(Comparator<? super T> cp):找出最大元素;
  • min(Comparator<? super T> cp):找出最小元素。

针对IntStreamLongStreamDoubleStream,还额外提供了以下聚合方法:

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

测试Stream的元素是否满足以下条件:

  • boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
  • boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容