讀古今文學網 > Java 8實戰 > 附錄 B 類庫的更新 >

附錄 B 類庫的更新

本附錄會審視Java 8方法庫中重要的更新。

B.1 集合

Collection API在Java 8中最重大的更新就是引入了流,我們已經在第4章到6章進行了介紹。當然,除此之外,Collection API還有一部分更新,本附錄會簡要地討論。

B.1.1 其他新增的方法

Java API的設計者們充分利用默認方法,為集合接口和類新增了多個新的方法。這些新增的方法我們已經列在表B-1中了。

表B-1 集合類和接口中新增的方法

類/接口

新方法

Map

getOrDefaultforEachcomputecomputeIfAbsentcomputeIfPresentmergeputIfAbsentremove(key,value)replacereplaceAll

Iterable

forEachspliterator

Iterator

forEachRemaining

Collection

removeIfstreamparallelStream

List

replaceAllsort

BitSet

stream

1. Map

Map接口的變化最大,它增加了多個新方法,利用這些新方法能更加便利地操縱Map中的數據。比如,getOrDefault方法就可以替換現在檢測Map中是否包含給定鍵映射的慣用方法。如果Map中不存在這樣的鍵映射,你可以提供一個默認值,方法會返回該默認值。使用之前版本的Java,要實現這一目的,你可能會如下編這段代碼:

Map<String, Integer> carInventory = new HashMap<>;
Integer count = 0;
if(map.containsKey("Aston Martin")){
  count = map.get("Aston Martin");
}

  

使用新的Map接口之後,你只需要簡單地編寫一行代碼就能實現這一功能,代碼如下:

Integer count = map.getOrDefault("Aston Martin", 0);

  

注意,這一方法僅在沒有映射時才生效。比如,如果鍵被顯式地映射到了空值,那麼該方法是不會返回你設定的默認值的。

另一個特別有用的方法是computeIfAbsent,這個方法在第14章解釋記憶表時曾經簡要地提到過。它能幫助你非常方便地使用緩存模式。比如,我們假設你需要從不同的網站抓取和處理數據。這種場景下,如果能夠緩存數據是非常有幫助的,這樣你就不需要每次都執行(代價極高的)數據抓取操作了:

public String getData(String url){
    String data = cache.get(url);
    if(data == null){           ←─檢查數據是否已經緩存
        data = getData(url);
        cache.put(url, data);    ←─如果數據沒有緩存,那就訪問網站抓取數據,緊接著對Map中的數據進行緩存,以備將來使用之需
    }
    return data;
}

  

這段代碼,你現在可以通過computeIfAbsent用更加精煉的方式實現,代碼如下所示:

public String getData(String url){
    return cache.computeIfAbsent(url, this::getData);
}

  

上面介紹的這些方法,其更詳細的內容都能在Java API的官方文檔中找到1。注意,ConcurrentHashMap也進行了更新,提供了新的方法。我們會在B.2節討論。

1更多細節請參考http://docs.oracle.com/javase/8/docs/api/java/util/Map.html。

2. 集合

removeIf方法可以移除集合中滿足某個謂詞的所有元素。注意,這一方法與我們在介紹Stream API時提到的filter方法不大一樣。Stream API中的filter方法會產生一個新的流,不會對當前作為數據源的流做任何變更。

3. 列表

replaceAll方法會對列表中的每一個元素執行特定的操作,並用處理的結果替換該元素。它的功能和Stream中的map方法非常相似,不過replaceAll會修改列表中的元素。與此相反,map方法會生成新的元素。

比如,下面這段代碼會打印輸出[2,4,6,8,10],因為列表中的元素被原地修改了:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.replaceAll(x -> x * 2);
System.out.println(numbers);    ←─打印輸出[2,4,6,8,10]

  

B.1.2 Collections

Collections類已經存在了很長的時間,它的主要功能是操作或者返回集合。Java 8中它又新增了一個方法,該方法可以返回不可修改的、同步的、受檢查的或者是空的NavigableMapNavigableSet。除此之外,它還引入了checkedQueue方法,該方法返回一個隊列視圖,可以擴展進行動態類型檢查。

B.1.3 Comparator

Comparator接口現在同時包含了默認方法和靜態方法。你可以使用第3章中介紹的靜態方法Comparator.comparing返回一個Comparator對象,該對像提供了一個函數可以提取排序關鍵字。

新的實例方法包含了下面這些。

  • reversed——對當前的Comparator對像進行逆序排序,並返回排序之後新的Comparator對象。

  • thenComparing——當兩個對像相同時,返回使用另一個Comparator進行比較的Comparator對象。

  • thenComparingIntthenComparingDoublethenComparingLong——這些方法的工作方式和thenComparing方法類似,不過它們的處理函數是特別針對某些基本數據類型(分別對應於ToIntFunctionToDoubleFunctionToLongFunction)的。

新的靜態方法包括下面這些。

  • comparingIntcomparingDoublecomparingLong——它們的工作方式和comparing類似,但接受的函數特別針對某些基本數據類型(分別對應於ToIntFunctionToDoubleFunctionToLongFunction)。

  • naturalOrder——對Comparable對像進行自然排序,返回一個Comparator對象。

  • nullsFirstnullsLast——對空對像和非空對像進行比較,你可以指定空對像(null)比非空對像(non-null)小或者比非空對像大,返回值是一個Comparator對象。

  • reverseOrder——和naturalOrder.reversed方法類似。

B.2 並發

Java 8中引入了多個與並發相關的更新。首當其衝的當然是並行流,我們在第7章詳細討論過。另外一個就是第11章中介紹的CompletableFuture類。

除此之外,還有一些值得注意的更新。比如,Arrays類現在支持並發操作了。我們會在B.3節討論這些內容。

這一節,我們想要圍繞java.util.concurrent.atomic包的更新展開討論。這個包的主要功能是處理原子變量(atomic variable)。除此之外,我們還會討論ConcurrentHashMap類的更新,它現在又新增了幾個方法。

B.2.1 原子操作

java.util.concurrent.atomic包提供了多個對數字類型進行操作的類,比如AtomicIntegerAtomicLong,它們支持對單一變量的原子操作。這些類在Java 8中新增了更多的方法支持。

  • getAndUpdate——以原子方式用給定的方法更新當前值,並返回變更之前的值。

  • updateAndGet——以原子方式用給定的方法更新當前值,並返回變更之後的值。

  • getAndAccumulate——以原子方式用給定的方法對當前及給定的值進行更新,並返回變更之前的值。

  • accumulateAndGet——以原子方式用給定的方法對當前及給定的值進行更新,並返回變更之後的值。

下面的例子向我們展示了如何以原子方式比較一個現存的原子整型值和一個給定的觀測值(比如10),並將變量設定為二者中較小的一個。

int min = atomicInteger.accumulateAndGet(10, Integer::min);

  

AdderAccumulator

多線程的環境中,如果多個線程需要頻繁地進行更新操作,且很少有讀取的動作(比如,在統計計算的上下文中),Java API文檔中推薦大家使用新的類LongAdderLongAccumulatorDouble-Adder以及DoubleAccumulator,盡量避免使用它們對應的原子類型。這些新的類在設計之初就考慮了動態增長的需求,可以有效地減少線程間的競爭。

LongAddrDoubleAdder類都支持加法操作,而LongAccumulatorDoubleAccumulator可以使用給定的方法整合多個值。比如,可以像下面這樣使用LongAdder計算多個值的總和。

代碼清單B-1 使用LongAdder計算多個值之和

LongAdder adder = new LongAdder;    ←─使用默認構造器,初始的sum值被置為0
adder.add(10);    ←─在多個不同的線程中進行加法運算
// …
long sum = adder.sum;    ←─到某個時刻得出sum的值

  

或者,你也可以像下面這樣使用LongAccumulator實現同樣的功能。

代碼清單B-2 使用LongAccumulator計算多個值之和

LongAccumulator acc = new LongAccumulator(Long::sum, 0);
acc.accumulate(10);                 ←─在幾個不同的線程中累計計算值
// …
long result = acc.get;          ←─在某個時刻得出結果

  

B.2.2 ConcurrentHashMap

ConcurrentHashMap類的引入極大地提升了HashMap現代化的程度,新引入的ConcurrentHashMap對並發的支持非常友好。ConcurrentHashMap允許並發地進行新增和更新操作,因為它僅對內部數據結構的某些部分上鎖。因此,和另一種選擇,即同步式的Hashtable比較起來,它具有更高的讀寫性能。

1. 性能

為了改善性能,要對ConcurrentHashMap的內部數據結構進行調整。典型情況下,map的條目會被存儲在桶中,依據鍵生成哈希值進行訪問。但是,如果大量鍵返回相同的哈希值,由於桶是由List實現的,它的查詢複雜度為O(n),這種情況下性能會惡化。在Java 8中,當桶過於臃腫時,它們會被動態地替換為排序樹(sorted tree),新的數據結構具有更好的查詢性能(排序樹的查詢複雜度為O(log(n)))。注意,這種優化只有當鍵是可以比較的(比如String或者Number類)時才可能發生。

2. 類流操作

ConcurrentHashMap支持三種新的操作,這些操作和你之前在流中所見的很像:

  • forEach——對每個鍵值對進行特定的操作

  • reduce——使用給定的精簡函數(reduction function),將所有的鍵值對整合出一個結果

  • search——對每一個鍵值對執行一個函數,直到函數的返回值為一個非空值

以上每一種操作都支持四種形式,接受使用鍵、值、Map.Entry以及鍵值對的函數:

  • 使用鍵和值的操作(forEachreducesearch

  • 使用鍵的操作(forEachKeyreduceKeyssearchKeys

  • 使用值的操作 (forEachValuereduceValuessearchValues

  • 使用Map.Entry對象的操作(forEachEntryreduceEntriessearchEntries

注意,這些操作不會對ConcurrentHashMap的狀態上鎖。它們只會在運行過程中對元素進行操作。應用到這些操作上的函數不應該對任何的順序,或者其他對象,抑或在計算過程發生變化的值,有依賴。

除此之外,你需要為這些操作指定一個並發閾值。如果經過預估當前map的大小小於設定的閾值,操作會順序執行。使用值1開啟基於通用線程池的最大並行。使用值Long.MAX_VALUE設定程序以單線程執行操作。

下面這個例子中,我們使用reduceValues試圖找出map中的最大值:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>;
Optional<Integer> maxValue =
    Optional.of(map.reduceValues(1, Integer::max));

  

注意,對intlongdouble,它們的reduce操作各有不同(比如reduceValuesToIntreduceKeysToLong等)。

3. 計數

ConcurrentHashMap類提供了一個新的方法,名叫mappingCount,它以長整型long返回map中映射的數目。我們應該盡量使用這個新方法,而不是老的size方法,size方法返回的類型為int。這是因為映射的數量可能是int 無法表示的。

4. 集合視圖

ConcurrentHashMap類還提供了一個名為KeySet的新方法,該方法以Set的形式返回ConcurrentHashMap的一個視圖(對map的修改會反映在該Set中,反之亦然)。你也可以使用新的靜態方法newKeySet,由ConcurrentHashMap創建一個Set

B.3 Arrays

Arrays類提供了不同的靜態方法對數組進行操作。現在,它又包括了四個新的方法(它們都有特別重載的變量)。

B.3.1 使用parallelSort

parallelSort方法會以並發的方式對指定的數組進行排序,你可以使用自然順序,也可以為數組對像定義特別的Comparator

B.3.2 使用setAllparallelSetAll

setAllparallelSetAll方法可以以順序的方式也可以用並發的方式,使用提供的函數計算每一個元素的值,對指定數組中的所有元素進行設置。該函數接受元素的索引,返回該索引元素對應的值。由於parallelSetAll需要並發執行,所以提供的函數必須沒有任何副作用,就如第7章和第13章中介紹的那樣。

舉例來說,你可以使用setAll方法生成一個值為0, 2, 4, 6, …的數組:

int evenNumbers = new int[10];
Arrays.setAll(evenNumbers, i -> i * 2);

  

B.3.3 使用parallelPrefix

parallelPrefix方法以並發的方式,用用戶提供的二進制操作符對給定數組中的每個元素進行累積計算。通過下面這段代碼,你會得到這樣的一些值:1, 2, 3, 4, 5, 6, 7, …。

代碼清單B-3 使用parallelPrefix並發地累積數組中的元素

int ones = new int[10];
Arrays.fill(ones, 1);
Arrays.parallelPrefix(ones, (a, b) -> a + b);    ←─ones現在的內容是[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  

B.4 NumberMath

Java 8 API對NumberMath也做了改進,為它們增加了新的方法。

B.4.1 Number

Number類中新增的方法如下。

  • ShortIntegerLongFloatDouble類提供了靜態方法summinmax。在第5章介紹reduce操作時,你已經見過這些方法。

  • IntegerLong類提供了compareUnsignedpideUnsignedremainderUnsignedtoUnsignedLong方法來處理無符號數。

  • IntegerLong類也分別提供了靜態方法parseUnsignedIntparseUnsignedLong將字符解析為無符號int或者long類型。

  • ByteShort類提供了toUnsignedInttoUnsignedLong方法通過無符號轉換將參數轉化為int或者long類型。類似地,Integer類現在也提供了靜態方法toUnsignedLong

  • DoubleFloat類提供了靜態方法isFinite,可以檢查參數是否為有限浮點數。

  • Boolean類現在提供了靜態方法logicalAndlogicalOrlogicalXor,可以在兩個boolean之間執行andorxor操作。

  • BigInteger類提供了byteValueExactshortValueExactintValueExactlongValueExact,可以將BigInteger類型的值轉換為對應的基礎類型。不過,如果在轉換過程中有信息的丟失,方法會拋出算術異常。

B.4.2 Math

如果Math中的方法在操作中出現溢出,Math類提供了新的方法可以拋出算術異常。支持這一異常的方法包括使用intlong參數的addExactsubtractExactmultipleExactincrementExactdecrementExactnegateExact。此外,Math類還新增了一個靜態方法toIntExact,可以將long值轉換為int值。其他的新增內容包括靜態方法floorModfloorDivnextDown

B.5 Files

Files類最引人注目的改變是,你現在可以用文件直接產生流。第5章中提到過新的靜態方法Files.lines,通過該方法你可以以延遲方式讀取文件的內容,並將其作為一個流。此外,還有一些非常有用的靜態方法可以返回流。

  • Files.list——生成由指定目錄中所有條目構成的Stream<Path>。這個列表不是遞歸包含的。由於流是延遲消費的,處理包含內容非常龐大的目錄時,這個方法非常有用。

  • Files.walk——和Files.list有些類似,它也生成包含給定目錄中所有條目的Stream<Path>。不過這個列表是遞歸的,你可以設定遞歸的深度。注意,該遍歷是依照深度優先進行的。

  • Files.find——通過遞歸地遍歷一個目錄找到符合條件的條目,並生成一個Stream<Path>對象。

B.6 Reflection

附錄A中已經討論過Java 8中註解機制的幾個變化。Reflection API的變化就是為了支撐這些改變。

除此之外,Relection接口的另一個變化是新增了可以查詢方法參數信息的API,比如,你現在可以使用新增的java.lang.reflect.Parameter類查詢方法參數的名稱和修飾符,這個類被新的java.lang.reflect.Executable類所引用,而java.lang.reflect.Executable通用函數和構造函數共享的父類。

B.7 String

String類也新增了一個靜態方法,名叫join。你大概已經猜出它的功能了,它可以用一個分隔符將多個字符串連接起來。你可以像下面這樣使用它:

String authors = String.join(", ", "Raoul", "Mario", "Alan");
System.out.println(authors);               ←─Raoul, Mario,Alan