天天看点

Java——函数式编程十、函数式编程

十、函数式编程

文章目录

  • 十、函数式编程
    • 1、Lambda表达式
      • 1.1 FunctionalInterface
    • 2、方法引用
      • 2.1 构造方法引用
    • 3、使用Stream
      • 3.1 创建Stream
        • Stream.of()
        • 基于数组或Collection
        • 基于Supplier
        • 其他方法
        • 基本类型
      • 3.2 使用map
      • 3.3 使用filter
      • 3.4 使用reduce
      • 3.5 输出集合
        • 输出为List
        • 输出为数组
        • 输出为Map
        • 分组输出

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

从 Java8 开始支持函数式编程。

1、Lambda表达式

Java的方法分为实例方法,例如

Integer

定义的

equals()

方法:

public final class Integer {
    boolean equals(Object o) {
        ...
    }
}
           

以及静态方法,例如

Integer

定义的

parseInt()

方法:

public final class Integer {
    public static int parseInt(String s) {
        ...
    }
}
           

上面的方法,本质上都相当于过程式语言的函数。只不过Java的实例方法隐含的传入了一个

this

变量。

函数式编程是把函数作为基本元算单元,函数可以作为变量,可以接受函数,还可以返回函数。

在Java程序中,我们经常遇到一些但方法接口,即一个接口只定义一个方法:

  • Comparator
  • Runnable
  • Callable

Comparator

为例,从 Java8 开始,我们可以使用 Lambda表达式替换单方法接口:

@Test
public void m0() {
  String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
  Arrays.sort(array, (s1, s2) -> {
    return s1.compareTo(s2);
  });
  System.out.println(String.join(", ", array));
}
           

JavaScript

的箭头函数一样,在方法中只有一句时,可以省略

{}

在Lambda 表达式中,它只需要写出方法定义,参数为(s1, s2),参数的类型可以省略,因为编译器可以自动推断出

String

类型。

返回值的类型也是由编译器自动推断。

1.1 FunctionalInterface

只定义了单方法的接口称之为

FunctionalInterface

,用注解

@FunctionalInterface

标记。例如,

Callable

接口:

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    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

定义的方法,不算在接口方法内。

2、方法引用

使用 Lambda表达式,我们可以不必编写

FunctionalInterface

接口的实现类,从而简化了代码。

当然,除了 Lambda 表达式,还可以直接传入方法引用,例如:

public class LambdaTest {

    @Test
    public void m1() {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, LambdaTest::cmp);
        System.out.println(String.join(", ", array));
    }

    static int cmp(String s1, String s2) {
        return s1.compareTo(s2);
    }


}
           

上述代码在

Arrays.sort()

中传入了静态方法

cmp

的引用,用

LambdaTest::cmp

表示。

方法引用,就是说某个方法签名和接口一样,就可以直接传入方法引用。

ps:方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。

2.1 构造方法引用

如果要把一个

String

数组转化为

Person

数组,在以前,我们可能会:

@Test
public void m2() {
  Stream<String> stream = Stream.of("Bob", "Alice", "Tim");
  List<Person> list = new ArrayList<>();
  stream.forEach(s -> {
    list.add(new Person(s));
  });
  System.out.println(list);
}
           

现在我们可以引用

Person

的构造方法来实现

String

Person

的转化:

public class Main { 
    public static void main(String[] args) {
        List<String> names = List.of("Bob", "Alice", "Tim");
        List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
        System.out.println(persons);
    }
}

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
    public String toString() {
        return "Person:" + this.name;
    }
}
           

3、使用Stream

从 Java8 开始,引入了一种全新的流式API:Stream API。位于

java.util.stream

包中。

这个

stream

java.io

中的

InputStram

OutputStream

不一样,它代表的是任意Java对象的序列。

java.io java.util.stream
存储 顺序读写的

byte

char

顺序输出的任意Java对象实例
用途 序列化至文件或网络 内存计算/业务逻辑

java.util.stream

List

也不一样,

List

存储的每个元素都已经在内存中存储,而

stream

输出的对象可能并没有存储在内存中,而是实时计算得到的,且是惰性计算的。

简单来说,

List

就是一个个实实在在的元素,这些元素也已经存储在内存中,用户可以用它来操作其中的元素(例如,遍历、排序等)。而

stream

可能就根本没有分配内存。下面,看一个例子:

如果想用

List

表示全体的自然数,这是不可能的,因为自然数是无穷的,但内存是有限的。

如果我们使用

stram

就可以做到,如下:

上面的 createNaturalStram() 方法没有实现。

也可以对

Stream

计算,例如,对每个自然数做一个平方:

Stream<BigInteger> naturals = createNaturalStream(); // 全体自然数
Stream<BigInteger> streamNxN = naturals.map(n -> n.multiply(n)); // 全体自然数的平方
           

上面的

streamNxN

也有无限多个元素,如果要打印它,可以用

limit()

方法截取前100个元素,最后用

forEach()

处理每个元素。

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

Stream

惰性计算的特点:一个

Stream

转换为另一个时,实际上只存储了转化规则,并不会有任何的计算。例如,上面的例子中,只有在调用

forEach

确实需要输出元素时,才会进行计算。

3.1 创建Stream

Stream.of()

使用

Stream.of()

创建虽然没有实质性用途,但在测试时很方便。

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

基于数组或Collection

可以基于一个数组或者

Collection

创建

Stream

,这样

Stream

在输出时的元素也就是数组或

Collection

的元素。

@Test
public void m1() {
  //数组变成 `Stream` 使用 `Arrays.stream()` 方法
  Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
  //对于 `Collection`(List/Set/Queue等),直接调用 `stream()` 方法即可。
  Stream<String> stream2 = List.of("X", "Y", "Z").stream();
  stream1.forEach(System.out::println);
  stream2.forEach(System.out::println);
}
           
上述创建

Stream

的方法都是把一个现有的序列变成

Stream

,它的元素都是固定的。

基于Supplier

创建

Stream

还可以通过

Stream.generate()

方法,它需要传入的是一个

Supplier

对象:

这时

Stream

保存的不是具体的元素,而是一种规则,在需要产生一个元素时,

Stream

自己回去调用

Supplier.get()

方法。

例子,通过

Stream

不断的产生自然数:

public class StreamTest {
    @Test
    public void m1() {
        Stream<Integer> natural = Stream.generate(new NaturalSupplier());
        natural.limit(10).forEach(System.out::println);
    }
}

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

即使

int

的范围有限,但如果用

List

存储,也会占用巨大的内存,而使用

Stream

时,因为只保存计算规则,所以几乎不占用空间。

在调用

forEach()

或者

count()

进行最终求值前,一定要把

Stream

的无限序列变成有限序列,否则会因为不能完成这个计算进入死循环。

其他方法

创建

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>

这种类型。为了方便,Java标准库提供了

IntStream

LongStream

DoubleStream

这3种使用基本类型的

Stream

,它们的使用方法和范型

Stream

没有大的区别。

3.2 使用map

Java中的

map

filter

reduce

类似于

JavaScript

中高阶函数的用法。

类似的用法,可以写出下面的例子:

@Test
public void m2() {
  List.of(1, 2, 3, 4)
    .stream()
    .map(n -> n * n)	//求平方
    .forEach(System.out::println); // 打印
}
           

3.3 使用filter

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

3.4 使用reduce

map()

filter()

都是

Stream

的转换方法,而

Stream.reduce()

则是

Stream

的一个聚合方法。

@Test
public void m2() {
  int n = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
    .reduce(0, (x, y) -> x + y);
  System.out.println(n);
}
           
上面的代码如果去掉初始值,会返回一个

Optional<Integer>

Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent) {
    System.out.println(opt.get());
}
           
这是因为

Stream

的元素有可能是0个,这样就没法调用

reduce()

方法,因此返回

Optional

对象,需要怕断结果是否存在。

Stream

的操作分为两类:
  • 转换操作:把一个

    Stream

    转化为 另一个

    Stream

    ,例如

    map()

    filter()

  • 聚合操作:会对

    Stream

    的每个元素进行计算,得到一个确定的结果,例如

    reduce()

    ,这类操作会触发计算。

3.5 输出集合

输出为List

因为需要把

Stream

的元素保存到集合,而集合保存的都是确定的 Java对象,所以把

Stream

变成

List

是一个聚合操作。

@Test
public void m2() {
  Stream<String> stream = Stream.of("Apple", "", null, "Pear", "  ", "Orange");
  List<String> lists = 
    stream.filter(s -> s != null && !s.trim().isEmpty()).collect(Collectors.toList());
  System.out.println(lists);
}
           

Stream

的每个元素收集到

List

的方法是调用

collect()

并传入

Collectors.toList()

对象,它实际上是一个

Collector

实例,通过类似

reduce()

的操作,把每个元素添加到一个收集器中(实际上是

ArrayList

)。

类似的,

collect(Collectors.toSet())

可以把

Stream

的每个元素收集到

Set

中。

输出为数组

@Test
public void m2() {
  Stream<String> stream = Stream.of("Apple", "Pear", "Orange");
  String[] array = stream.toArray(String[]::new);
  System.out.println(String.join(", ", array));
}
           
传入的“构造方法”是

String[]::new

,它的签名实际上是

IntFunction

定义的

String[] apply(int)

,即传入

int

参数,获得

String[]

数组的返回值

输出为Map

对于

Stream

的元素输出到

Map

,需要分别把元素映射为key和value:

@Test
public void m2() {
  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);
}
           

分组输出

Stream

还有一个强大的功能就是可以按组输出。

@Test
public void m2() {
  Stream<String> stream = 
    Stream.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
  	Map<String, List<String>> groups = stream
    	.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
  	System.out.println(groups);
}
           
//输出结果
{A=[Apple, Avocado, Apricots], B=[Banana, Blackberry], C=[Coconut, Cherry]}
           

在上面使用到的

Collectors.groupinigBy()

方法,需要提供两个函数:

  • 第一个是分组的key,

    s -> s.substring(0, 1)

    表示只要首字母相同的

    String

    分到一组。
  • 第二个是分组的value,这里直接输出为

    List