Lambda表達式和Java集合框架

EzraHaddon 7年前發布 | 14K 次閱讀 Lambda Java Java開發

Java8為容器新增一些有用的方法,這些方法有些是為完善原有功能,有些是為引入函數式編程(Lambda表達式),學習和使用這些方法有助于我們寫出更加簡潔有效的代碼.本文分別以ArrayList和HashMap為例,講解Java8集合框架(Java Collections Framework)中新加入方法的使用.

前言

我們先從最熟悉的 Java集合框架(Java Collections Framework, JCF) 開始說起。

為引入Lambda表達式,Java8新增了 java.util.funcion 包,里面包含常用的 函數接口 ,這是Lambda表達式的基礎,Java集合框架也新增部分接口,以便與Lambda表達式對接。

首先回顧一下Java集合框架的接口繼承結構:

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

接口名 Java8新加入的方法
Collection removeIf() spliterator() stream() parallelStream() forEach()
List replaceAll() sort()
Map getOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge()

這些新加入的方法大部分要用到 java.util.function 包下的接口,這意味著這些方法大部分都跟Lambda表達式相關。我們將逐一學習這些方法。

Collection中的新方法

如上所示,接口 Collection 和 List 新加入了一些方法,我們以是 List 的子類 ArrayList 為例來說明。了解 Java7 ArrayList 實現原理 ,將有助于理解下文。

forEach()

該方法的簽名為 void forEach(Consumer<? super E> action) ,作用是對容器中的每個元素執行 action 指定的動作,其中 Consumer 是個函數接口,里面只有一個待實現方法 void accept(T t) (后面我們會看到,這個方法叫什么根本不重要,你甚至不需要記憶它的名字)。

需求: 假設有一個字符串列表,需要打印出其中所有長度大于3的字符串.

Java7及以前我們可以用增強的for循環實現:

// 使用曾強for循環迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(String str : list){
    if(str.length()>3)
        System.out.println(str);
}

現在使用 forEach() 方法結合匿名內部類,可以這樣實現:

// 使用forEach()結合匿名內部類迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(new Consumer<String>(){
    @Override
    public void accept(String str){
        if(str.length()>3)
            System.out.println(str);
    }
});

上述代碼調用 forEach() 方法,并使用匿名內部類實現 Comsumer 接口。到目前為止我們沒看到這種設計有什么好處,但是不要忘記Lambda表達式,使用Lambda表達式實現如下:

// 使用forEach()結合Lambda表達式迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach( str -> {
        if(str.length()>3)
            System.out.println(str);
    });

上述代碼給 forEach() 方法傳入一個Lambda表達式,我們不需要知道 accept() 方法,也不需要知道 Consumer 接口,類型推導幫我們做了一切。

removeIf()

該方法簽名為 boolean removeIf(Predicate<? super E> filter) ,作用是 刪除容器中所有滿足 filter 指定條件的元素 ,其中 Predicate 是一個函數接口,里面只有一個待實現方法 boolean test(T t) ,同樣的這個方法的名字根本不重要,因為用的時候不需要書寫這個名字。

需求: 假設有一個字符串列表,需要刪除其中所有長度大于3的字符串。

我們知道如果需要在迭代過程沖對容器進行刪除操作必須使用迭代器,否則會拋出 ConcurrentModificationException ,所以上述任務傳統的寫法是:

// 使用迭代器刪除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while(it.hasNext()){
    if(it.next().length()>3) // 刪除長度大于3的元素
        it.remove();
}

現在使用 removeIf() 方法結合匿名內部類,我們可是這樣實現:

// 使用removeIf()結合匿名名內部類實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(new Predicate<String>(){ // 刪除長度大于3的元素
    @Override
    public boolean test(String str){
        return str.length()>3;
    }
});

上述代碼使用 removeIf() 方法,并使用匿名內部類實現 Precicate 接口。相信你已經想到用Lambda表達式該怎么寫了:

// 使用removeIf()結合Lambda表達式實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(str -> str.length()>3); // 刪除長度大于3的元素

使用Lambda表達式不需要記憶 Predicate 接口名,也不需要記憶 test() 方法名,只需要知道此處需要一個返回布爾類型的Lambda表達式就行了。

replaceAll()

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

需求: 假設有一個字符串列表,將其中所有長度大于3的元素轉換成大寫,其余元素不變。

Java7及之前似乎沒有優雅的辦法:

// 使用下標實現元素替換
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(int i=0; i<list.size(); i++){
    String str = list.get(i);
    if(str.length()>3)
        list.set(i, str.toUpperCase());
}

使用 replaceAll() 方法結合匿名內部類可以實現如下:

// 使用匿名內部類實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(new UnaryOperator<String>(){
    @Override
    public String apply(String str){
        if(str.length()>3)
            return str.toUpperCase();
        return str;
    }
});

上述代碼調用 replaceAll() 方法,并使用匿名內部類實現 UnaryOperator 接口。我們知道可以用更為簡潔的Lambda表達式實現:

// 使用Lambda表達式實現
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
    if(str.length()>3)
        return str.toUpperCase();
    return str;
});

sort()

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

需求: 假設有一個字符串列表,按照字符串長度增序對元素排序。

由于Java7以及之前 sort() 方法在 Collections 工具類中,所以代碼要這樣寫:

// Collections.sort()方法
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>(){
    @Override
    public int compare(String str1, String str2){
        return str1.length()-str2.length();
    }
});

現在可以直接使用 List.sort()方法 ,結合Lambda表達式,可以這樣寫:

// List.sort()方法結合Lambda表達式
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length()-str2.length());

spliterator()

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

  1. Spliterator 既可以像 Iterator 那樣逐個迭代,也可以批量迭代。批量迭代可以降低迭代的開銷。
  2. Spliterator 是可拆分的,一個 Spliterator 可以通過調用 Spliterator<T> trySplit() 方法來嘗試分成兩個。一個是 this ,另一個是新返回的那個,這兩個迭代器代表的元素沒有重疊。

可通過(多次)調用 Spliterator.trySplit() 方法來分解負載,以便多線程處理。

stream()和parallelStream()

stream() 和 parallelStream() 分別 返回該容器的 Stream 視圖表示 ,不同之處在于 parallelStream() 返回并行的 Stream 。 Stream 是Java函數式編程的核心類 ,我們會在后面章節中學習。

Map中的新方法

相比 Collection , Map 中加入了更多的方法,我們以 HashMap 為例來逐一探秘。了解 Java7 HashMap 實現原理 ,將有助于理解下文。

forEach()

該方法簽名為 void forEach(BiConsumer<? super K,? super V> action) ,作用是 對 Map 中的每個映射執行 action 指定的操作 ,其中 BiConsumer 是一個函數接口,里面有一個待實現方法 void accept(T t, U u) 。 BinConsumer 接口名字和 accept() 方法名字都不重要,請不要記憶他們。

需求: 假設有一個數字到對應英文單詞的Map,請輸出Map中的所有映射關系.

Java7以及之前經典的代碼如下:

// Java7以及之前迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
    System.out.println(entry.getKey() + "=" + entry.getValue());
}

使用 Map.forEach() 方法,結合匿名內部類,代碼如下:

// 使用forEach()結合匿名內部類迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach(new BiConsumer<Integer, String>(){
    @Override
    public void accept(Integer k, String v){
        System.out.println(k + "=" + v);
    }
});

上述代碼調用 forEach() 方法,并使用匿名內部類實現 BiConsumer 接口。當然,實際場景中沒人使用匿名內部類寫法,因為有Lambda表達式:

// 使用forEach()結合Lambda表達式迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
}

getOrDefault()

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

需求; 假設有一個數字到對應英文單詞的Map,輸出4對應的英文單詞,如果不存在則輸出NoValue

// 查詢Map中指定的值,不存在時使用默認值
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
// Java7以及之前做法
if(map.containsKey(4)){ // 1
    System.out.println(map.get(4));
}else{
    System.out.println("NoValue");
}
// Java8使用Map.getOrDefault()
System.out.println(map.getOrDefault(4, "NoValue")); // 2

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<? super K,? super V,? extends V> function) ,作用是對 Map 中的每個映射執行 function 指定的操作,并用 function 的執行結果替換原來的 value ,其中 BiFunction 是一個函數接口,里面有一個待實現方法 R apply(T t, U u) .不要被如此多的函數接口嚇到,因為使用的時候根本不需要知道他們的名字.

需求: 假設有一個數字到對應英文單詞的Map,請將原來映射關系中的單詞都轉換成大寫.

Java7以及之前經典的代碼如下:

// Java7以及之前替換所有Map中所有映射關系
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
    entry.setValue(entry.getValue().toUpperCase());
}

使用 replaceAll() 方法結合匿名內部類,實現如下:

// 使用replaceAll()結合匿名內部類實現
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll(new BiFunction<Integer, String, String>(){
    @Override
    public String apply(Integer k, String v){
        return v.toUpperCase();
    }
});

上述代碼調用 replaceAll() 方法,并使用匿名內部類實現 BiFunction 接口。更進一步的,使用Lambda表達式實現如下:

// 使用replaceAll()結合Lambda表達式實現
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());

簡潔到讓人難以置信.

merge()

該方法簽名為 merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction) ,作用是:

  1. 如果 Map 中 key 對應的映射不存在或者為 null ,則將 value (不能是 null )關聯到 key 上;
  2. 否則執行 remappingFunction ,如果執行結果非 null 則用該結果跟 key 關聯,否則在 Map 中刪除 key 的映射.

參數中 BiFunction 函數接口前面已經介紹過,里面有一個待實現方法 R apply(T t, U u) .

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

map.merge(key, newMsg, (v1, v2) -> v1+v2);

compute()

該方法簽名為 compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) ,作用是把 remappingFunction 的計算結果關聯到 key 上,如果計算結果為 null ,則在 Map 中刪除 key 的映射.

要實現上述 merge() 方法中錯誤信息拼接的例子,使用 compute() 代碼如下:

map.compute(key, (k,v) -> v==null ? newMsg : v.concat(newMsg));

computeIfAbsent()

該方法簽名為 V computeIfAbsent(K key, Function<? super K,? extends V> 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("one");
}else{
    Set<String> valueSet = new HashSet<String>();
    valueSet.add("one");
    map.put(1, valueSet);
}
// Java8的實現方式
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");

使用 computeIfAbsent() 將條件判斷和添加操作合二為一,使代碼更加簡潔.

computeIfPresent()

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

這個函數的功能跟如下代碼是等效的:

// Java7及以前跟computeIfPresent()等效的代碼
if (map.get(key) != null) {
    V oldValue = map.get(key);
    V newValue = remappingFunction.apply(key, oldValue);
    if (newValue != null)
        map.put(key, newValue);
    else
        map.remove(key);
    return newValue;
}
return null;

總結

  1. Java8為容器新增一些有用的方法,這些方法有些是為 完善原有功能 ,有些是為 引入函數式編程 ,學習和使用這些方法有助于我們寫出更加簡潔有效的代碼.
  2. 函數接口 雖然很多,但絕大多數時候我們根本不需要知道它們的名字,書寫Lambda表達式時類型推斷幫我們做了一切.

 

 

 本文由用戶 EzraHaddon 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!