1. 程式人生 > 實用技巧 >Java 8 Lambda表示式介紹(二)

Java 8 Lambda表示式介紹(二)

接著上篇內容

函數語言程式設計介面

  從上面的程式碼例子可以看出,我們使用Lambda表示式建立執行緒的時候,並不關心介面名,方法名,引數名。我們只關注他的引數型別,引數個數,返回值

  JDK原生就給我們提供了一些函數語言程式設計介面方便我們去使用,下面是一些常用的介面:

簡單說明一下:

  • 表格中的一元介面表示只有一個入參,二元介面表示有兩個入參

使用Lambda時,要記住的就兩點:

  1. Lambda返回的是介面的例項物件
  2. 有沒有引數、引數有多少個、需不需要有返回值、返回值的型別是什麼---->選擇自己合適的函式式介面

1.2 方法引用

在學Lambda的時候,還可能會發現一種比較奇怪的寫法,例如下面的程式碼:

// 方法引用寫法
Consumer<String> consumer = System.out::println;
consumer.accept("Java3y");

如果按正常Lambda的寫法可能是這樣的:

// 普通的Lambda寫法
Consumer<String> consumer = s -> System.out.println(s);
consumer.accept("Java3y");

顯然使用方法引用比普通的Lambda表示式又簡潔了一些。

如果函式式介面的實現恰好可以通過呼叫一個方法來實現,那麼我們可以使用方法引用

方法引用又分了幾種:

  • 靜態方法的方法引用
  • 非靜態方法的方法引用
  • 建構函式的方法引用

方法引用Demo:

public class Demo {
    public static void main(String[] args) {
        // 靜態方法引用--通過類名呼叫
        Consumer<String> consumerStatic = Java3y::MyNameStatic;
        consumerStatic.accept("3y---static");

        //例項方法引用--通過例項呼叫
        Java3y java3y = new
Java3y(); Consumer<String> consumer = java3y::myName; consumer.accept("3y---instance"); // 構造方法方法引用--無引數 Supplier<Java3y> supplier = Java3y::new; System.out.println(supplier.get()); } } class Java3y { // 靜態方法 public static void MyNameStatic(String name) { System.out.println(name); } // 例項方法 public void myName(String name) { System.out.println(name); } // 無參構造方法 public Java3y() { } }

結果如下:

關於@FunctionalInterface

我們常用的一些介面Callable、Runnable、Comparator等在JDK8中都添加了@FunctionalInterface註解。

通過JDK8原始碼javadoc,可以知道這個註解有以下特點:

1、該註解只能標記在"有且僅有一個抽象方法"的介面上。

2、JDK8介面中的靜態方法和預設方法,都不算是抽象方法。

3、介面預設繼承java.lang.Object,所以如果介面顯示宣告覆蓋了Object中方法,那麼也不算抽象方法。

4、該註解不是必須的,如果一個介面符合"函式式介面"定義,那麼加不加該註解都沒有影響。加上該註解能夠更好地讓編譯器進行檢查。如果編寫的不是函式式介面,但是加上了@FunctionInterface,那麼編譯器會報錯。

@FunctionalInterface標記在介面上,“函式式介面”是指僅僅只包含一個抽象方法的介面。

如果一個介面中包含不止一個抽象方法,那麼不能使用@FunctionalInterface,編譯會報錯。

比如下面這個介面就是一個正確的函式式介面:

// 正確的函式式介面
@FunctionalInterface
public interface TestInterface {
 
    
    // 抽象方法
    public void sub();
 
    // java.lang.Object中的方法不是抽象方法
    public boolean equals(Object var1);
 
    // default不是抽象方法
    public default void defaultMethod(){
 
    }
 
    // static不是抽象方法
    public static void staticMethod(){
 
    }
}

使用Lambda表示式的要求

也許你已經想到了,能夠使用Lambda的依據是必須有相應的函式介面

函式介面,是指內部只有一個抽象方法的介面。這一點跟Java是強型別語言吻合,也就是說你並不能在程式碼的任何地方任性的寫Lambda表示式。實際上Lambda的型別就是對應函式介面的型別。Lambda表示式另一個依據是型別推斷機制,在上下文資訊足夠的情況下,編譯器可以推斷出引數表的型別,而不需要顯式指名。

自定義函式介面

自定義函式介面很容易,只需要編寫一個只有一個抽象方法的介面即可。

  // 自定義函式介面
    @FunctionalInterface
    public interface ConsumerInterface<T>{
        void accept(T t);
    }

上面程式碼中的@FunctionalInterface是可選的,但加上該標註編譯器會幫你檢查介面是否符合函式介面規範。就像加入@Override標註會檢查是否過載了函式一樣。有了上述介面定義,就可以寫出類似如下的程式碼:

ConsumerInterface<String> consumer = str -> System.out.println(str);
consumer.accept("我是自定義函式式介面");

詳細例子參考:

public class Test {
          public static void main(String[] args) {
              TestStream<String> stream = new TestStream<String>();
              List list = Arrays.asList("11", "22", "33");
              stream.setList(list);
              stream.myForEach(str -> System.out.println(str));// 使用自定義函式介面書寫Lambda表示式
          }
      }

      @FunctionalInterface
      interface ConsumerInterface<T>{
          void accept(T t);
      }

      class TestStream<T>{
          private List<T> list;
          public void myForEach(ConsumerInterface<T> consumer){// 1
              for(T t : list){
                  consumer.accept(t);
              }
          }

          public void setList(List<T> list) {
              this.list = list;
          }
      }

Java 內建四大核心函式式介面

Consumer<T> 消費型介面

void accept(T t);

  @Test
    public void test1(){
        hello("張三", (m) -> System.out.println("你好:" + m));
    }
    public void hello(String st, Consumer<String> con){
        con.accept(st);
    }

Supplier<T> 供給型介面

T get();

  //Supplier<T> 供給型介面 :
    @Test
    public void test2(){
        List list = Arrays.asList(121, 1231, 455, 56, 67,78);
        List<Integer> numList = getNumList(1, () -> (int)(Math.random() * 100));
        for (Integer num : numList) {
            System.out.println(num);
        }
    }
    //需求:產生指定個數的整數,並放入集合中
    public List<Integer> getNumList(int num, Supplier<Integer> sup){
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            Integer n = sup.get();
            list.add(n);
        }
        return list;
    }

Function<T, R> 函式型介面

R apply(T t);

    //Function<T, R> 函式型介面:
    @Test
    public void test3(){
        String newStr = strHandler("ttt 這是一個函式型介面 ", (str) -> str.trim());
        System.out.println(newStr);
        String subStr = strHandler("這是一個函式型介面", (str) -> str.substring(4, 7));
        System.out.println(subStr);
    }
    //需求:用於處理字串
    public String strHandler(String str, Function<String, String> fun){
        return fun.apply(str);
    }

Predicate<T> 斷定型介面

boolean test(T t);

  // Predicate<T> 斷言型介面:
    @Test
    public void test4(){
        List<String> list = Arrays.asList("Hello", "Java8", "Lambda", "www", "ok");
        List<String> strList = filterStr(list, (s) -> s.length() > 3);
        for (String str : strList) {
            System.out.println(str);
        }
    }
    //需求:將滿足條件的字串,放入集合中
    public List<String> filterStr(List<String> list, Predicate<String> pre){
        List<String> strList = new ArrayList<>();
        for (String str : list) {
            if(pre.test(str)){
                strList.add(str);
            }
        }
        return strList;
    }

其他介面

Collections中的常用函式介面

Java8新增了java.util.funcion包,裡面包含常用的函式介面,這是Lambda表示式的基礎,Java集合框架也新增部分介面,以便與Lambda表示式對接。

Java集合框架的介面繼承結構:

上圖中綠色標註的介面類,表示在Java8中加入了新的介面方法,當然由於繼承關係,他們相應的子類也都會繼承這些新方法。下表詳細列舉了這些方法。

這些新加入的方法大部分要用到java.util.function包下的介面,這意味著這些方法大部分都跟Lambda表示式相關。

Collection中的新方法

forEach()

該方法的簽名為void forEach(Consumer action),作用是對容器中的每個元素執行action指定的動作,其中Consumer是個函式介面,裡面只有一個待實現方法void accept(T t)。

匿名內部類實現:

ArrayList<Integer> list = new ArrayList<>(Arrays.asList(3, 6, 9, 10));
  list.forEach(new Consumer<Integer>(){
      @Override
      public void accept(Integer integer){
          if(integer % 3 == 0){
              System.out.println(integer);
          }
      }
  });

lambda表示式實現:

ArrayList<Integer> list = new ArrayList<>(Arrays.asList(3, 6, 9, 10));
  list.forEach((s) -> {
      if (s % 3 == 0){
          System.out.println(s);
      }
  });

removeIf()

該方法簽名為boolean removeIf(Predicate filter),作用是刪除容器中所有滿足filter指定條件的元素,其中Predicate是一個函式介面,裡面只有一個待實現方法boolean test(T t)。

匿名內部類實現:

ArrayList<Integer> list = new ArrayList<>(Arrays.asList(3, 6, 9, 10));
  list.removeIf(new Predicate<Integer>(){ // 刪除長度大於3的元素
      @Override
      public boolean test(Integer sum){
          return sum % 3 == 0;
      }
  });
  System.out.println(list);

lambda表示式實現:

 ArrayList<Integer> list = new ArrayList<>(Arrays.asList(3, 6, 9, 10));
  list.removeIf(s -> s % 3 == 0);
  System.out.println(list);

replaceAll()

該方法簽名為void replaceAll(UnaryOperator<E> operator),作用是對每個元素執行operator指定的操作,並用操作結果來替換原來的元素。其中UnaryOperator是一個函式介面,裡面只有一個待實現函式T apply(T t)。

匿名內部類實現:

  ArrayList<Integer> list = new ArrayList<>(Arrays.asList(3, 6, 9, 10));
  list.replaceAll(new UnaryOperator<Integer>(){
      @Override
      public Integer apply(Integer sum){
          if(sum % 3 == 0){
              return ++sum;
          }
          return --sum;
      }
  });
  System.out.println(list);

lambda表示式實現:

  ArrayList<Integer> list = new ArrayList<>(Arrays.asList(3, 6, 9, 10));
  list.replaceAll(sum -> {
      if (sum % 3 == 0){
          return ++sum;
      }else {
          return --sum;
      }
  });
  System.out.println(list);

sort()

該方法定義在List介面中,方法簽名為void sort(Comparator c),該方法根據c指定的比較規則對容器元素進行排序。Comparator介面我們並不陌生,其中有一個方法int compare(T o1, T o2)需要實現,顯然該介面是個函式介面。

匿名內部類實現:

ArrayList<Integer> list = new ArrayList<>(Arrays.asList(6, 10, 9, 3));
  Collections.sort(list, new Comparator<Integer>(){
      @Override
      public int compare(Integer sum1, Integer sum2){
          return sum1 - sum2;
      }
  });
  System.out.println(list);

lambda表示式實現:

  ArrayList<Integer> list = new ArrayList<>(Arrays.asList(6, 10, 9, 3));
  System.out.println(list);
  list.sort((sum1, sum2) -> sum1 - sum2);
  System.out.println(list);

spliterator()

方法簽名為Spliterator<E> spliterator(),該方法返回容器的可拆分迭代器。從名字來看該方法跟iterator()方法有點像,我們知道Iterator是用來迭代容器的,Spliterator也有類似作用,但二者有如下不同:

Spliterator既可以像Iterator那樣逐個迭代,也可以批量迭代。批量迭代可以降低迭代的開銷。

Spliterator是可拆分的,一個Spliterator可以通過呼叫SpliteratortrySplit()方法來嘗試分成兩個。一個是this,另一個是新返回的那個,這兩個迭代器代表的元素沒有重疊。
可通過(多次)呼叫Spliterator.trySplit()方法來分解負載,以便多執行緒處理。

stream()和parallelStream()

stream()和parallelStream()分別返回該容器的Stream視圖表示,不同之處在於parallelStream()返回並行的Stream。Stream是Java函數語言程式設計的核心類,具體內容後面單獨介紹。

Map中的新方法

forEach()

該方法簽名為void forEach(BiConsumer action),作用是對Map中的每個對映執行action指定的操作,其中BiConsumer是一個函式介面,裡面有一個待實現方法void accept(T t, U u)。

匿名內部類實現:

HashMap<Integer, String> map = new HashMap<>();
  map.put(1, "我");
  map.put(2, "拒絕");
  map.put(3, "996");
  map.forEach(new BiConsumer<Integer, String>(){
      @Override
      public void accept(Integer key, String value){
          System.out.println(key + "=" + value);
      }
  });

lambda表示式實現:

HashMap<Integer, String> map = new HashMap<>();
  map.put(1, "我");
  map.put(2, "拒絕");
  map.put(3, "996");
  map.forEach((key, value) -> System.out.println(key + "=" + value));

getOrDefault()

  該方法跟Lambda表示式沒關係,但是很有用。方法簽名為V getOrDefault(Object key, V defaultValue),作用是按照給定的key查詢Map中對應的value,如果沒有找到則返回defaultValue。使用該方法可以省去查詢指定鍵值是否存在的麻煩。

實現:

HashMap<Integer, String> map = new HashMap<>();
  map.put(1, "我");
  map.put(2, "拒絕");
  map.put(3, "996");
  // Java7以及之前做法
  if(map.containsKey(4)){
      System.out.println(map.get(4));
  }else{
      System.out.println("NoValue");
  }

  // Java8使用Map.getOrDefault()
  System.out.println(map.getOrDefault(4, "NoValue"));

putIfAbsent()

該方法跟Lambda表示式沒關係,但是很有用。方法簽名為V putIfAbsent(K key, V value),作用是隻有在不存在key值的對映或對映值為null時,才將value指定的值放入到Map中,否則不對Map做更改.該方法將條件判斷和賦值合二為一,使用起來更加方便。

remove()

我們都知道Map中有一個remove(Object key)方法,來根據指定key值刪除Map中的對映關係;Java8新增了remove(Object key, Object value)方法,只有在當前Map中key正好對映到value時才刪除該對映,否則什麼也不做。

replace()

在Java7及以前,要想替換Map中的對映關係可通過put(K key, V value)方法實現,該方法總是會用新值替換原來的值.為了更精確的控制替換行為,Java8在Map中加入了兩個replace()方法,分別如下:

  • replace(K key, V value),只有在當前Map中key的對映存在時才用value去替換原來的值,否則什麼也不做。

  • replace(K key, V oldValue, V newValue),只有在當前Map中key的對映存在且等於oldValue時才用newValue去替換原來的值,否則什麼也不做。

replaceAll()

該方法簽名為replaceAll(BiFunction function),作用是對Map中的每個對映執行function指定的操作,並用function的執行結果替換原來的value,其中BiFunction是一個函式介面,裡面有一個待實現方法R apply(T t, U u)。

匿名內部類實現:

HashMap<Integer, String> map = new HashMap<>();
  map.put(1, "我");
  map.put(2, "拒絕");
  map.put(3, "996");
  map.replaceAll(new BiFunction<Integer, String, String>(){
      @Override
      public String apply(Integer k, String v){
          if (v.equals("我")){
              v = "你";
          }
          return v.toUpperCase();
      }
  });
  map.forEach((key, value) -> System.out.println(key + "=" + value));

lambda表示式實現:

HashMap<Integer, String> map = new HashMap<>();
  map.put(1, "我");
  map.put(2, "拒絕");
  map.put(3, "996");
  map.replaceAll((k, v) -> {
      if (v.equals("我")){
          v = "你";
      }
      return v.toUpperCase();
  });
  map.forEach((key, value) -> System.out.println(key + "=" + value));

merge()

該方法簽名為merge(K key, V value, BiFunction remappingFunction)。

作用是:

  • 如果Map中key對應的對映不存在或者為null,則將value(不能是null)關聯到key上;

  • 否則執行remappingFunction,如果執行結果非null則用該結果跟key關聯,否則在Map中刪除key的對映。

    引數中BiFunction函式介面前面已經介紹過,裡面有一個待實現方法R apply(T t, U u)。

merge()方法雖然語義有些複雜,但該方法的用方式很明確,一個比較常見的場景是將新的錯誤資訊拼接到原來的資訊上,比如:

HashMap<Integer, String> map = new HashMap<>();
map.put(1, "我");
map.put(2, "拒絕");
map.put(3, "996");

map.forEach((key, value) -> System.out.println(key + "=" + value));
map.merge(1, "和你", (v1, v2) -> v1+v2);
map.forEach((key, value) -> System.out.println(key + "=" + value));

compute()

該方法簽名為compute(K key, BiFunction remappingFunction),作用是把remappingFunction的計算結果關聯到key上,如果計算結果為null,則在Map中刪除key的對映。

HashMap<Integer, String> map = new HashMap<>();
  map.put(1, "我");
  map.put(2, "拒絕");
  map.put(3, "996");

  map.forEach((key, value) -> System.out.println(key + "=" + value));
  map.compute(1, (k,v) -> v == null ? "值為空" : v.concat("和你"));
  map.forEach((key, value) -> System.out.println(key + "=" + value));

computeIfAbsent()

該方法簽名為V computeIfAbsent(K key, Function mappingFunction),作用是:只有在當前Map中不存在key值的對映或對映值為null時,才呼叫mappingFunction,並在mappingFunction執行結果非null時,將結果跟key關聯。

Function是一個函式介面,裡面有一個待實現方法R apply(T t)。

computeIfAbsent()常用來對Map的某個key值建立初始化對映.比如我們要實現一個多值對映,Map的定義可能是Map<K,Set<V>>,要向Map中放入新值,可通過如下程式碼實現:

實現:

Map<Integer, Set<String>> map = new HashMap<>();
  // Java7及以前的實現方式
  if(map.containsKey(1)){
      map.get(1).add("123");
  }else{
      Set<String> valueSet = new HashSet<String>();
      valueSet.add("123");
      map.put(1, valueSet);
  }
  // Java8的實現方式
  map.computeIfAbsent(1, v -> new HashSet<String>()).add("345");
  map.forEach((key, value) -> System.out.println(key + "=" + value));

使用computeIfAbsent()將條件判斷和新增操作合二為一,使程式碼更加簡潔。

computeIfPresent()

該方法簽名為V computeIfPresent(K key, BiFunction remappingFunction),作用跟computeIfAbsent()相反。即只有在當前Map中存在key值的對映且非null時,才呼叫remappingFunction,如果remappingFunction執行結果為null,則刪除key的對映,否則使用該結果替換key原來的對映。

Stream

對於Java 7來說stream完全是個陌生東西,stream並不是某種資料結構,它只是資料來源的一種檢視。這裡的資料來源可以是一個數組,Java容器或I/O channel等。

常見的stream介面繼承關係如圖:

圖中4種stream介面繼承自BaseStream,其中IntStream, LongStream, DoubleStream對應三種基本型別(int, long, double,注意不是包裝型別),Stream對應所有剩餘型別的stream檢視。

為不同資料型別設定不同stream介面,可以:

  1. 提高效能

  2. 增加特定介面函式。

為什麼不把IntStream等設計成Stream的子介面?

答案是這些方法的名字雖然相同,但是返回型別不同,如果設計成父子介面關係,這些方法將不能共存,因為Java不允許只有返回型別不同的方法過載。

雖然大部分情況下stream是容器呼叫Collection.stream()方法得到的,但stream和collections有以下不同:

  • 無儲存。stream不是一種資料結構,它只是某種資料來源的一個檢視,資料來源可以是一個數組,Java容器或I/O channel等。

  • 為函數語言程式設計而生。對stream的任何修改都不會修改背後的資料來源,比如對stream執行過濾操作並不會刪除被過濾的元素,而是會產生一個不包含被過濾元素的新stream。

  • 惰式執行。stream上的操作並不會立即執行,只有等到使用者真正需要結果的時候才會執行。

  • 可消費性。stream只能被“消費”一次,一旦遍歷過就會失效,就像容器的迭代器那樣,想要再次遍歷必須重新生成。

對stream的操作分為為兩類,中間操作(intermediate operations)和結束操作(terminal operations),二者特點是:

  • 中間操作總是會惰式執行,呼叫中間操作只會生成一個標記了該操作的新stream,僅此而已。

  • 結束操作會觸發實際計算,計算髮生時會把所有中間操作積攢的操作以pipeline的方式執行,這樣可以減少迭代次數。計算完成之後stream就會失效。

下表彙總了Stream介面的部分常見方法:

區分中間操作和結束操作最簡單的方法,就是看方法的返回值,返回值為stream的大都是中間操作,否則是結束操作。

stream方法使用

stream跟函式介面關係非常緊密,沒有函式介面stream就無法工作。

函式介面是指內部只有一個抽象方法的介面。通常函式接口出現的地方都可以使用Lambda表示式,所以不必記憶函式介面的名字。

forEach()

方法簽名為void forEach(Consumer action),作用是對容器中的每個元素執行action指定的動作,也就是對元素進行遍歷。

  // 使用Stream.forEach()迭代
  Stream<String> stream = Stream.of("I", "love", "Java");
  stream.forEach(str -> System.out.println(str));

由於forEach()是結束方法,上述程式碼會立即執行,輸出所有字串。

filter()

函式原型為Stream<T> filter(Predicate predicate),作用是返回一個只包含滿足predicate條件元素的Stream。

  // 保留長度大於等於3的字串
  Stream<String> stream = Stream.of("I", "love", "Java");
  stream.filter(str -> str.length() >= 3).forEach(str -> System.out.println(str));

上述程式碼將輸出為長度大於等於3的字串love和Java。注意,由於filter()是個中間操作,如果只調用filter()不會有實際計算,因此也不會輸出任何資訊。

distinct()

函式原型為Stream<T> distinct(),作用是返回一個去除重複元素之後的Stream。

  // 元素去重
  Stream<String> stream = Stream.of("I", "love", "you", "Java", "you");
  stream.distinct().forEach(str -> System.out.println(str));

上述程式碼會輸出去掉一個you之後的其餘字串。

sorted()

排序函式有兩個,一個是用自然順序排序,一個是使用自定義比較器排序,函式原型分別為Stream<T> sorted()和Stream<T> sorted(Comparator comparator)。

  // 排序
  Stream<String> stream = Stream.of("I", "love", "you", "too", "Java");
  stream.sorted((str1, str2) -> str1.length() - str2.length()).forEach(str -> System.out.println(str));

map()

函式原型為<R> Stream<R> map(Function mapper),作用是返回一個對當前所有元素執行執行mapper之後的結果組成的Stream。直觀的說,就是對每個元素按照某種操作進行轉換,轉換前後Stream中元素的個數不會改變,但元素的型別取決於轉換之後的型別。

  // 將字串轉換成大寫
  Stream<String> stream = Stream.of("i", "love", "java", "too");
  stream.map(str -> str.toUpperCase()).forEach(str -> System.out.println(str));

flatMap()

函式原型為<R> Stream<R> flatMap(Function> mapper),作用是對每個元素執行mapper指定的操作,並用所有mapper返回的Stream中的元素組成一個新的Stream作為最終返回結果。說起來太拗口,通俗的講flatMap()的作用就相當於把原stream中的所有元素都”攤平”之後組成的Stream,轉換前後元素的個數和型別都可能會改變。

  // 將兩個集合中大於等於2的數重新組成Stream,然後輸出
  Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
  stream.flatMap(list -> list.stream().filter(integer -> integer >= 2)).forEach(i -> System.out.println(i));

流的規約操作

規約操作(reduction operation)又被稱作摺疊操作(fold),是通過某個連線動作將所有元素彙總成一個彙總結果的過程。元素求和、求最大值或最小值、求出元素總個數、將所有元素轉換成一個列表或集合,都屬於規約操作。Stream類庫有兩個通用的規約操作reduce()和collect(),也有一些為簡化書寫而設計的專用規約操作,比如sum()、max()、min()、count()等。

最大或最小值這類規約操作很好理解(至少方法語義上是這樣),我們著重介紹reduce()和collect(),這是比較有魔法的地方。

reduce()

reduce操作可以實現從一組元素中生成一個值,sum()、max()、min()、count()等都是reduce操作,將他們單獨設為函式只是因為常用。reduce()的方法定義有三種重寫形式:

  • Optional<T> reduce(BinaryOperator<T> accumulator)

  • T reduce(T identity, BinaryOperator<T> accumulator)

  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

雖然函式定義越來越長,但語義不曾改變,多的引數只是為了指明初始值(引數identity),或者是指定並行執行時多個部分結果的合併方式(引數combiner)。reduce()最常用的場景就是從一堆值中生成一個值。

具體實踐:

  // 找出最長的單詞
  Stream<String> stream1 = Stream.of("I", "love", "you", "too");
  Optional<String> longest = stream1.reduce((s1, s2) -> s1.length() >= s2.length() ? s1 : s2);
  // Optional<String> longest = stream.max((s1, s2) -> s1.length() - s2.length());
  System.out.println(longest.get());

上述程式碼會選出最長的單詞love,其中Optional是(一個)值的容器,使用它可以避免null值的麻煩。當然可以使用Stream.max(Comparator comparator)方法來達到同等效果,但reduce()自有其存在的理由。

  // 求單詞長度之和
  // (引數1)初始值
  // (引數2)累加器
  // (引數3)部分和拼接器,並行執行時才會用到
  Stream<String> stream2 = Stream.of("I", "love", "you", "too");
  Integer lengthSum = stream2.reduce(0, (sum, str) -> sum + str.length(), (a, b) -> a + b);
  // int lengthSum = stream.mapToInt(str -> str.length()).sum();
  System.out.println(lengthSum);

引數2處:

  1. 字串對映成長度。

  2. 並和當前累加和相加。

這顯然是兩步操作,使用reduce()函式將這兩步合二為一,更有助於提升效能。如果想要使用map()和sum()組合來達到上述目的,也是可以的。

reduce()擅長的是生成一個值,如果想要從Stream生成一個集合或者Map等複雜的物件該怎麼辦呢?

collect()

如果你發現某個功能在Stream介面中沒找到,十有八九可以通過collect()方法實現。collect()是Stream介面方法中最靈活的一個,學會它才算真正入門Java函數語言程式設計。

例子:

  Stream<String> stream = Stream.of("I", "love", "you", "too");
  // 轉換成list集合
  List<String> list = stream.collect(Collectors.toList());
  // 轉換成set集合
  // Set<String> set = stream.collect(Collectors.toSet());
  // 轉換成map集合
  // Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length));

上述程式碼分別列舉了如何將Stream轉換成List、Set和Map。雖然程式碼語義很明確,可是我們仍然會有幾個疑問:

  • Function.identity()是幹什麼的?

  • String::length是什麼意思?

  • Collectors是個什麼東西?

介面的靜態方法和預設方法

Function是一個介面,那麼Function.identity()是什麼意思呢?這要從兩方面解釋:

  • Java 8允許在介面中加入具體方法。介面中的具體方法有兩種,default方法和static方法,identity()就是Function介面的一個靜態方法。

  • Function.identity()返回一個輸出跟輸入一樣的Lambda表示式物件,等價於形如t -> t形式的Lambda表示式。

上面的解釋是不是讓你疑問更多?不要問我為什麼介面中可以有具體方法,也不要告訴我你覺得t -> t比identity()方法更直觀。我會告訴你介面中的default方法是一個無奈之舉,在Java 7及之前要想在定義好的介面中加入新的抽象方法是很困難甚至不可能的,因為所有實現了該介面的類都要重新實現。試想在Collection介面中加入一個stream()抽象方法會怎樣?default方法就是用來解決這個尷尬問題的,直接在介面中實現新加入的方法。既然已經引入了default方法,為何不再加入static方法來避免專門的工具類呢!

方法引用

諸如String::length的語法形式叫做方法引用(method references),這種語法用來替代某些特定形式Lambda表示式。如果Lambda表示式的全部內容就是呼叫一個已有的方法,那麼可以用方法引用來替代Lambda表示式。方法引用可以細分為四類:

收集器

收集器(Collector)是為Stream.collect()方法量身打造的工具介面(類)。考慮一下將一個Stream轉換成一個容器(或者Map)需要做哪些工作?我們至少需要兩樣東西:

  • 目標容器是什麼?是ArrayList還是HashSet,或者是個TreeMap。

  • 新元素如何新增到容器中?是List.add()還是Map.put()。

  • 如果並行的進行規約,還需要告訴collect(),多個部分結果如何合併成一個。

結合以上分析,collect()方法定義為 <R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner),三個引數依次對應上述三條分析。

不過每次呼叫collect()都要傳入這三個引數太麻煩,收集器Collector就是對這三個引數的簡單封裝,所以collect()的另一定義為 <R,A> R collect(Collector collector)。

Collectors工具類可通過靜態方法生成各種常用的Collector。舉例來說,如果要將Stream規約成List可以通過如下兩種方式實現:

  // 將Stream規約成List
  Stream<String> stream = Stream.of("I", "love", "Collector");
  List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
  // List<String> list = stream.collect(Collectors.toList());// 方式2
  System.out.println(list);

通常情況下我們不需要手動指定collect()的三個引數,而是呼叫collect(Collector collector)方法,並且引數中的Collector物件大都是直接通過Collectors工具類獲得。實際上傳入的收集器的行為決定了collect()的行為。

使用collect()生成Collection

有時候我們可能會想要人為指定容器的實際型別,這個需求可通過Collectors.toCollection(Supplier<C> collectionFactory)方法完成。

  // 使用toCollection()指定規約容器的型別
  Stream<String> stream = Stream.of("I", "love", "Collector");
  ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
  System.out.println(arrayList);
  // HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));
  // System.out.println(hashSet);

使用collect()生成Map

通常在三種情況下collect()的結果會是Map:

  • 使用Collectors.toMap()生成的收集器,使用者需要指定如何生成Map的key和value。

  • 使用Collectors.partitioningBy()生成的收集器,對元素進行二分割槽操作時用到。

  • 使用Collectors.groupingBy()生成的收集器,對元素做group操作時用到。

情況1:使用toMap()生成的收集器,這種情況是最直接的,前面例子中已提到,這是和Collectors.toCollection()並列的方法。如下程式碼將字元列表轉換成由<String,字串長度>組成的Map。

  // 使用toMap()統計字元長度
  Stream<String> stream = Stream.of("I", "love", "Collector");
  List<String> list = stream.collect(Collectors.toList());// 方式2
  Map<String, Integer> strLength = list.stream().collect(Collectors.toMap(Function.identity(), str -> str.length()));
  System.out.println(strLength);

情況2:使用partitioningBy()生成的收集器,這種情況適用於將Stream中的元素依據某個二值邏輯(滿足條件,或不滿足)分成互補相交的兩部分,比如男女性別、成績及格與否等。下列程式碼展示將字元列表分成長度大於2或不大於2的兩部分。

  // 對字串列表進行分組
  Stream<String> stream = Stream.of("I", "love", "Collector");
  List<String> list = stream.collect(Collectors.toList());// 方式2
  Map<Boolean, List<String>> listMap = list.stream().collect(Collectors.partitioningBy(str -> str.length() > 2));
  System.out.println(listMap);

情況3:使用groupingBy()生成的收集器,這是比較靈活的一種情況。跟SQL中的group by語句類似,這裡的groupingBy()也是按照某個屬性對資料進行分組,屬性相同的元素會被對應到Map的同一個key上。下列程式碼展示將字元列表按照字元長度進行分組。

  // 按照長度對字串列表進行分組
  Stream<String> stream = Stream.of("I", "love", "Collector", "you", "Java");
  List<String> list = stream.collect(Collectors.toList());// 方式2
  Map<Integer, List<String>> listMap = list.stream().collect(Collectors.groupingBy(String::length));
  System.out.println(listMap);

以上只是分組的最基本用法,有些時候僅僅分組是不夠的。在SQL中使用group by是為了協助其他查詢,比如:

  1. 先將員工按照部門分組。

  2. 然後統計每個部門員工的人數。

Java類庫設計者也考慮到了這種情況,增強版的groupingBy()能夠滿足這種需求。增強版的groupingBy()允許我們對元素分組之後再執行某種運算,比如求和、計數、平均值、型別轉換等。
這種先將元素分組的收集器叫做上游收集器,之後執行其他運算的收集器叫做下游收集器(downstream Collector)。

  // 對字串列表進行分組,並統計每組元素的個數
  Stream<String> stream = Stream.of("I", "love", "Collector", "you", "Java");
  List<String> list = stream.collect(toList());// 方式2
  Map<Integer, Long> listMap = list.stream().collect(groupingBy(String::length, Collectors.counting()));
  System.out.println(listMap);

上面程式碼的邏輯是不是越看越像SQL?高度非結構化。還有更狠的,下游收集器還可以包含更下游的收集器,這絕不是為了炫技而增加的把戲,而是實際場景需要。考慮將員工按照部門分組的場景,如果我們想得到每個員工的名字(字串),而不是一個個Employee物件,可通過如下方式做到:

  // 按照部門對員工分佈組,並只保留員工的名字
  Map<Department, List<String>> byDept = employees.stream()
                  .collect(Collectors.groupingBy(Employee::getDepartment,
                          Collectors.mapping(Employee::getName,// 下游收集器
                                  Collectors.toList())));// 更下游的收集器

使用collect()做字串join

字串拼接時使用Collectors.joining()生成的收集器,從此告別for迴圈。Collectors.joining()方法有三種重寫形式,分別對應三種不同的拼接方式。

  // 使用Collectors.joining()拼接字串
  Stream<String> stream = Stream.of("I", "love", "Collector");
  // String joined = stream.collect(Collectors.joining());// "IloveCollector"
  // String joined = stream.collect(Collectors.joining(","));// "I,love,Collector"
  String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,Collector}
  System.out.println(joined);

關於lambda並不是語法糖的問題

關於這個問題,有很多種說法,這裡說明一下:

Labmda表示式不是匿名內部類的語法糖,但是它也是一個語法糖。實現方式其實是依賴了幾個JVM底層提供的lambda相關api。為什麼說它不是內部類的語法糖呢?

如果是匿名內部類的語法糖,那麼編譯之後會有兩個class檔案,但是,包含lambda表示式的類編譯後只有一個檔案。這裡大家可以實際去操作一下,就可以論證這個問題了。這裡就不再詳細說明。

資料:

  關於Java Lambda表示式看這一篇就夠了