Java 函數式編程

由 JS 轉 Java,寫慣了 React,習慣了函數式,因此轉 Java 時也是先學函數式。

語法糖「Syntactic Sugar」

起初,Java 的函數式看起來是匿名類的一個語法糖。

Stream.of(1, 2, 3).filter(new Predicate<Integer>() {
  @Override
  public boolean test(Integer integer) {
    return integer % 2 == 0;
  }
}).collect(Collectors.toList());
Stream.of(1, 2, 3).filter(x -> x % 2 == 0).collect(Collectors.toList());

後來,看到也能傳方法引用時,我陷入了懷疑。

Stream.of(1, 2, 3).forEach(System.out::println);
StringBuilder sb = new StringBuilder();
Stream.of(1, 2, 3).forEach(sb::append);
System.out.println(sb.toString());
Stream.of(4, 2, 3).sorted(Integer::compareTo);

不過再想想,還是可以理解成語法糖:

Stream.of(1, 2, 3).forEach(System.out::println);

Stream.of(1, 2, 3).forEach(new Consumer<Integer>() {
  @Override
  public void accept(Integer integer) {
    System.out.println(integer);
  }
});

閉包「Closure」

Java 閉包也是假的,和匿名類的限制一樣,要求閉包訪問的外部作用域變數是 final 的。實現應該都是:基礎類型值傳遞,引用類型引用傳遞。

以下 Java 程式碼編譯會報錯:

int index = 0;
Stream.of("a", "b", "c")
  .forEach(x -> System.out.println((index++) + ":" + x));

Error: Variable used in lambda expression should be final or effectively final

JS 閉包毫無問題:

let index = 0;
["a", "b", "c"].forEach(x => console.log(index++ + ":" + x));

儘管如此,但函數式習慣還是可以帶到 Java 了。

第一個要帶過來的是:少量的數據結構搭配大量的操作。

在 OOP 的世界裡,開發者被鼓勵針對具體的問題建立專門的數據結構,並以方法的形式,將專門的操作關聯在數據結構上。函數式程式語言選擇了另一種重用思路。它們用很少的一組關鍵數據結構( 如 list、 set、 map)來搭配專為這些數據結構深度優化過的操作。我們在這些關鍵數據結構和操作組成的一套運轉機構上面,按需要「 插入」 另外的數據結構和高階函數來調整機器,以適應具體的問題。

Neal Ford. 函數式編程思維

在 Java 中,關鍵數據結構就是指 Stream。

Stream 操作三板斧:map、filter、reduce

Java 和 JS 有些不一樣。

不同點一:index 的取法

JS map、filter、reduce 都能拿到 index:

["a", "b", "c"]
  .map((s, index) => `${index}: ${s}`)
  .forEach(s => console.log(s));

Java 想要 index 的資訊,如果只是單執行緒,可以考慮利用引用類型保存迭代的索引:

AtomicInteger i = new AtomicInteger(0);
Stream.of("a", "b", "c")
  .map(x -> i.getAndIncrement() + ":" + x)
  .forEach(System.out::println);

如果想支援多執行緒,可以先通過 zip 讓流中的數據帶上 index:

public static <T, R> List<Pair<T, R>> zip(List<T> s1, List<R> s2) {
  return IntStream.range(0, Math.min(s1.size(), s2.size()))
    .mapToObj(index -> new Pair<T, R>(s1.get(index), s2.get(index)))
    .collect(Collectors.toList());
}

public static void main(String[] args) {
  List<String> list = Arrays.asList("a", "b", "c");
  zip(IntStream.range(0, list.size()).boxed().collect(Collectors.toList()), list)
    .parallelStream()
    .map(p -> p.getKey() + ":" + p.getValue())
    .forEach(System.out::println);
}

不同點二:reduce 的用法

Java 分為了 reduction 和 mutable reduction,在 JS 里是不區分的。

A mutable reduction operation accumulates input elements into a mutable result container, such as a Collection or StringBuilder, as it processes the elements in the stream.

  • JS reduction:

    [1, 2, 3].reduce((acc, cur) => acc + cur, 0);
    
  • Java reduction:

    Stream.of(1, 2, 3).reduce(0, Integer::sum);
    
  • JS mutable reduction:

    [1, 2, 2].reduce((acc, cur) => {
      acc.add(cur);
      return acc;
    }, new Set());
    
  • Java mutable reduction:

    Stream.of(1, 2, 2).collect(
      () -> new HashSet<>(),
      (set, el) -> set.add(el),
      (s1, s2) -> s1.addAll(s2)
    );
    

    也可以簡寫為:

    Stream.of(1, 2, 2).collect(
      HashSet::new,
      HashSet::add,
      HashSet::addAll
    );
    

    參數比 JS 多了個 HashSet::addAll 是為了並行處理。


後續繼續總結高階函數、柯里化、函子在 Java 中的應用。

Tags: