本附錄會審視Java 8方法庫中重要的更新。
B.1 集合
Collection API在Java 8中最重大的更新就是引入了流,我們已經在第4章到6章進行了介紹。當然,除此之外,Collection API還有一部分更新,本附錄會簡要地討論。
B.1.1 其他新增的方法
Java API的設計者們充分利用默認方法,為集合接口和類新增了多個新的方法。這些新增的方法我們已經列在表B-1中了。
表B-1 集合類和接口中新增的方法
類/接口
新方法
Map
getOrDefault
,forEach
,compute
,computeIfAbsent
,computeIfPresent
,merge
,putIfAbsent
,remove(key,value)
,replace
,replaceAll
Iterable
forEach
,spliterator
Iterator
forEachRemaining
Collection
removeIf
,stream
,parallelStream
List
replaceAll
,sort
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中它又新增了一個方法,該方法可以返回不可修改的、同步的、受檢查的或者是空的NavigableMap
或NavigableSet
。除此之外,它還引入了checkedQueue
方法,該方法返回一個隊列視圖,可以擴展進行動態類型檢查。
B.1.3 Comparator
Comparator
接口現在同時包含了默認方法和靜態方法。你可以使用第3章中介紹的靜態方法Comparator.comparing
返回一個Comparator
對象,該對像提供了一個函數可以提取排序關鍵字。
新的實例方法包含了下面這些。
-
reversed
——對當前的Comparator
對像進行逆序排序,並返回排序之後新的Comparator
對象。 -
thenComparing
——當兩個對像相同時,返回使用另一個Comparator
進行比較的Comparator
對象。 -
thenComparingInt
、thenComparingDouble
、thenComparingLong
——這些方法的工作方式和thenComparing
方法類似,不過它們的處理函數是特別針對某些基本數據類型(分別對應於ToIntFunction
、ToDoubleFunction
和ToLongFunction
)的。
新的靜態方法包括下面這些。
-
comparingInt
、comparingDouble
、comparingLong
——它們的工作方式和comparing
類似,但接受的函數特別針對某些基本數據類型(分別對應於ToIntFunction
、ToDoubleFunction
和ToLongFunction
)。 -
naturalOrder
——對Comparable
對像進行自然排序,返回一個Comparator
對象。 -
nullsFirst
、nullsLast
——對空對像和非空對像進行比較,你可以指定空對像(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
包提供了多個對數字類型進行操作的類,比如AtomicInteger
和AtomicLong
,它們支持對單一變量的原子操作。這些類在Java 8中新增了更多的方法支持。
-
getAndUpdate
——以原子方式用給定的方法更新當前值,並返回變更之前的值。 -
updateAndGet
——以原子方式用給定的方法更新當前值,並返回變更之後的值。 -
getAndAccumulate
——以原子方式用給定的方法對當前及給定的值進行更新,並返回變更之前的值。 -
accumulateAndGet
——以原子方式用給定的方法對當前及給定的值進行更新,並返回變更之後的值。
下面的例子向我們展示了如何以原子方式比較一個現存的原子整型值和一個給定的觀測值(比如10),並將變量設定為二者中較小的一個。
int min = atomicInteger.accumulateAndGet(10, Integer::min);
Adder
和Accumulator
多線程的環境中,如果多個線程需要頻繁地進行更新操作,且很少有讀取的動作(比如,在統計計算的上下文中),Java API文檔中推薦大家使用新的類LongAdder
、LongAccumulator
、Double-Adder
以及DoubleAccumulator
,盡量避免使用它們對應的原子類型。這些新的類在設計之初就考慮了動態增長的需求,可以有效地減少線程間的競爭。
LongAddr
和DoubleAdder
類都支持加法操作,而LongAccumulator
和DoubleAccumulator
可以使用給定的方法整合多個值。比如,可以像下面這樣使用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
以及鍵值對的函數:
-
使用鍵和值的操作(
forEach
、reduce
、search
) -
使用鍵的操作(
forEachKey
、reduceKeys
、searchKeys
) -
使用值的操作 (
forEachValue
、reduceValues
、searchValues
) -
使用
Map.Entry
對象的操作(forEachEntry
、reduceEntries
、searchEntries
)
注意,這些操作不會對ConcurrentHashMap
的狀態上鎖。它們只會在運行過程中對元素進行操作。應用到這些操作上的函數不應該對任何的順序,或者其他對象,抑或在計算過程發生變化的值,有依賴。
除此之外,你需要為這些操作指定一個並發閾值。如果經過預估當前map
的大小小於設定的閾值,操作會順序執行。使用值1
開啟基於通用線程池的最大並行。使用值Long.MAX_VALUE
設定程序以單線程執行操作。
下面這個例子中,我們使用reduceValues
試圖找出map
中的最大值:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>;
Optional<Integer> maxValue =
Optional.of(map.reduceValues(1, Integer::max));
注意,對int
、long
和double
,它們的reduce
操作各有不同(比如reduceValuesToInt
、reduceKeysToLong
等)。
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 使用setAll
和parallelSetAll
setAll
和parallelSetAll
方法可以以順序的方式也可以用並發的方式,使用提供的函數計算每一個元素的值,對指定數組中的所有元素進行設置。該函數接受元素的索引,返回該索引元素對應的值。由於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 Number
和Math
Java 8 API對Number
和Math
也做了改進,為它們增加了新的方法。
B.4.1 Number
Number
類中新增的方法如下。
-
Short
、Integer
、Long
、Float
和Double
類提供了靜態方法sum
、min
和max
。在第5章介紹reduce
操作時,你已經見過這些方法。 -
Integer
和Long
類提供了compareUnsigned
、pideUnsigned
、remainderUnsigned
和toUnsignedLong
方法來處理無符號數。 -
Integer
和Long
類也分別提供了靜態方法parseUnsignedInt
和parseUnsignedLong
將字符解析為無符號int
或者long
類型。 -
Byte
和Short
類提供了toUnsignedInt
和toUnsignedLong
方法通過無符號轉換將參數轉化為int
或者long
類型。類似地,Integer
類現在也提供了靜態方法toUnsignedLong
。 -
Double
和Float
類提供了靜態方法isFinite
,可以檢查參數是否為有限浮點數。 -
Boolean
類現在提供了靜態方法logicalAnd
、logicalOr
和logicalXor
,可以在兩個boolean
之間執行and
、or
和xor
操作。 -
BigInteger
類提供了byteValueExact
、shortValueExact
、intValueExact
和longValueExact
,可以將BigInteger
類型的值轉換為對應的基礎類型。不過,如果在轉換過程中有信息的丟失,方法會拋出算術異常。
B.4.2 Math
如果Math
中的方法在操作中出現溢出,Math
類提供了新的方法可以拋出算術異常。支持這一異常的方法包括使用int
和long
參數的addExact
、subtractExact
、multipleExact
、incrementExact
、decrementExact
和negateExact
。此外,Math
類還新增了一個靜態方法toIntExact
,可以將long
值轉換為int
值。其他的新增內容包括靜態方法floorMod
、floorDiv
和nextDown
。
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