接着上篇內容
函數式編程接口
從上面的代碼例子可以看出,我們使用Lambda表達式創建線程的時候,並不關心接口名,方法名,參數名。我們只關注他的參數類型,參數個數,返回值。
JDK原生就給我們提供了一些函數式編程接口方便我們去使用,下面是一些常用的接口:
簡單說明一下:
- 表格中的一元接口表示只有一個入參,二元接口表示有兩個入參
使用Lambda時,要記住的就兩點:
- Lambda返回的是接口的實例對象
- 有沒有參數、參數有多少個、需不需要有返回值、返回值的類型是什么---->選擇自己合適的函數式接口
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可以通過調用Spliterator trySplit()方法來嘗試分成兩個。一個是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接口,可以:
-
提高性能
-
增加特定接口函數。
為什么不把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處:
-
字符串映射成長度。
-
並和當前累加和相加。
這顯然是兩步操作,使用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是為了協助其他查詢,比如:
-
先將員工按照部門分組。
-
然后統計每個部門員工的人數。
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表達式看這一篇就夠了