JDK1.8新特性簡單介紹

Java8爲容器新增一些有用的方法,這些方法有些是爲完善原有功能,有些是爲引入函數式編程(Lambda表達式),學習和使用這些方法有助於我們寫出更加簡潔有效的代碼(性能上先不說尷尬).本文分別以ArrayList和HashMap爲例,講解Java8集合框架(Java Collections Framework)中新加入方法的使用.在講解這些的時候,順帶參雜一些jdk1.8新增加的其它特性

前言

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

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

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

JCF_Collection_Interfaces

上圖中綠色標註的接口類,表示在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中的新方法

如上所示,接口CollectionList新加入了一些方法,我們以是List的子類ArrayList爲例來說明。瞭解Java7ArrayList實現原理,將有助於理解下文。

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接口,類型推導幫我們做了一切。

對於函數體只有一行代碼的,你可以去掉大括號{}以及return關鍵字,但是你還可以寫得更短點:

//排序
Collections.sort(names, (a, b) -> b.compareTo(a));
//打印
Collections.forEach(System.out::println);


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()返回並行的StreamStream是Java函數式編程的核心類,我們會在後面章節中學習。

Map中的新方法

相比CollectionMap中加入了更多的方法,我們以HashMap爲例來逐一探祕。瞭解Java7HashMap實現原理,將有助於理解下文。

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表達式:

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)方法,只有在當前Mapkey正好映射到value才刪除該映射,否則什麼也不做.

replace()

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

  • replace(K key, V value),只有在當前Mapkey的映射存在時才用value去替換原來的值,否則什麼也不做.
  • replace(K key, V oldValue, V newValue),只有在當前Mapkey的映射存在且等於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. 如果Mapkey對應的映射不存在或者爲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表達式時類型推斷幫我們做了一切

附加:

1:Deafult Interface

上述中,我們講解了Stream以及集合中的一些用法如:forEach,我們來看下他的源碼:

package java.lang;
import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;

public interface Iterable<T> {
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

疑問接口中存在實現方法,對的

Java 8允許我們給接口添加一個非抽象的方法實現,只需要使用 default關鍵字即可,這個特徵又叫做擴展方法,默認方法在子類上可以直接使用


2:Stream API

java.util.Stream 表示能應用在一組元素上一次執行的操作序列。Stream 操作分爲中間操作或者最終操作兩種,最終操作返回一特定類型的計算結果,而中間操作返回Stream本身,這樣你就可以將多個操作依次串起來。Stream 的創建需要指定一個數據源,比如 java.util.Collection的子類,List或者Set, Map不支持。Stream的操作可以串行執行或者並行執行。

首先看看Stream是怎麼用,首先創建實例代碼的用到的數據List:

List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");
Java 8擴展了集合類,可以通過串行計算- Collection.stream() 或者並行計算- Collection.parallelStream() 來創建一個Stream。下面幾節將詳細解釋常用的Stream操作:
1)Filter 過濾

過濾通過一個predicate接口來過濾並只保留符合條件的元素,該操作屬於中間操作,所以我們可以在過濾後的結果來應用其他Stream操作(比如forEach)。forEach需要一個函數來對過濾後的元素依次執行。forEach是一個最終操作,所以我們不能在forEach之後來執行其他Stream操作。

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa2", "aaa1"

2)Sort 排序

排序是一箇中間操作,返回的是排序好後的Stream。如果你不指定一個自定義的Comparator則會使用默認排序。

stringCollection
			.stream()
			.filter((s) -> s.startsWith("a"))
			.sorted()
			.forEach(System.out::println);
// aaa1 aaa2

3)Map 映射

中間操作map會將元素根據指定的Function接口來依次將元素轉成另外的對象,下面的示例展示了將字符串轉換爲大寫字符串。你也可以通過map來講對象轉換成其他類型,map返回的Stream類型是根據你map傳遞進去的函數的返回值決定的。

stringCollection
			.stream()
			.filter((s) -> s.startsWith("a"))
			.map(String::toUpperCase)
			.sorted()
			.forEach(System.out::println);
AAA1
AAA2

4)Match 匹配

Stream提供了多種匹配操作,允許檢測指定的Predicate是否匹配整個Stream。所有的匹配操作都是最終操作,並返回一個boolean類型的值。

boolean anyStartsWithA =
    stringCollection
        .stream()
        .anyMatch((s) -> s.startsWith("a"));

System.out.println(anyStartsWithA);      // true

boolean allStartsWithA =
    stringCollection
        .stream()
        .allMatch((s) -> s.startsWith("a"));

System.out.println(allStartsWithA);      // false

boolean noneStartsWithZ =
    stringCollection
        .stream()
        .noneMatch((s) -> s.startsWith("z"));

System.out.println(noneStartsWithZ);      // true

5)Count 計數

計數是一個最終操作,返回Stream中元素的個數,返回值類型是long。

long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

System.out.println(startsWithB);    // 3

6)Reduce 規約

這是一個最終操作,允許通過指定的函數來講stream中的多個元素規約爲一個元素,規越後的結果是通過Optional接口表示的

Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

3:Annotation 註解

在Java 8中支持多重註解了,先看個例子來理解一下是什麼意思。
首先定義一個包裝類Hints註解用來放置一組具體的Hint註解:

@interface Hints {
    Hint[] value();
}

@Repeatable(Hints.class)
@interface Hint {
    String value();
}
Java 8允許我們把同一個類型的註解使用多次,只需要給該註解標註一下@Repeatable即可。

例 1: 使用包裝類當容器來存多個註解(老方法)

@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}
例 2:使用多重註解(新方法)
@Hint("hint1")
@Hint("hint2")
class Person {}

第二個例子裏java編譯器會隱性的幫你定義好@Hints註解,瞭解這一點有助於你用反射來獲取這些信息:
Hint hint = Person.class.getAnnotation(Hint.class);
System.out.println(hint);                   // null

Hints hints1 = Person.class.getAnnotation(Hints.class);
System.out.println(hints1.value().length);  // 2

Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
System.out.println(hints2.length);          // 2
即便我們沒有在Person類上定義@Hints註解,我們還是可以通過 getAnnotation(Hints.class) 來獲取 @Hints註解,更加方便的方法是使用 getAnnotationsByType 可以直接獲取到所有的@Hint註解。
另外Java 8的註解還增加到兩種新的target上了:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
關於Java 8的新特性就寫到這了,肯定還有更多的特性等待發掘。後面有用到或發現,我會陸續補上!害羞



發佈了102 篇原創文章 · 獲贊 220 · 訪問量 37萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章