天天看點

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