Java8學習小記
轉載自https://segmentfault.com/a/1190000006985405
2014年,Oracle釋出了Java8新版本。對於Java來說,這顯然是一個具有里程碑意義的版本。尤其是那函數語言程式設計的功能,避開了Java那煩瑣的語法所帶來的麻煩。
這可以算是一篇Java8的學習筆記。將Java8一些常見的一些特性作了一個概要的筆記。
行為引數化(Lambda以及方法引用)
為了編寫可重用的方法,比如filter,你需要為其指定一個引數,它能夠精確地描述過濾條件。雖然Java專家們使用之前的版本也能達到同樣的目的(將過濾條件封裝成類的一個方法,傳遞該類的一個例項),但這種方案卻很難推廣,因為它通常非常臃腫,既難於編寫,也不易於維護。
Java 8通過借鑑函數語言程式設計,提供了一種新的方式——通過向方法傳遞程式碼片段來解決這一問題。這種新的方法非常方便地提供了兩種變體。
- 傳遞一個Lambda表示式,即一段精簡的程式碼片段,比如
apple -> apple.getWeight() > 150
- 傳遞一個方法引用,該方法引用指向了一個現有的方法,比如這樣的程式碼:
Apple::isHeavy
這些值具有類似Function<T, R>、Predicate<T>或者BiFunction<T, U, R>這樣的型別,值的接收方可以通過apply、test或其他類似的方法執行這些方法。Lambda表示式自身是一個相當酷炫的概念,不過Java 8對它們的使用方式——將它們與全新的Stream API相結合,最終把它們推向了新一代Java的核心。
閉包
你可能已經聽說過閉包(closure,不要和Clojure程式語言混淆)這個詞,你可能會想Lambda是否滿足閉包的定義。用科學的說法來說,閉包就是一個函式的例項,且它可以無限制地訪問那個函式的非本地變數。例如,閉包可以作為引數傳遞給另一個函式。它也可以訪問和修改其作用域之外的變數。現在,Java 8的Lambda和匿名類可以做類似於閉包的事情:它們可以作為引數傳遞給方法,並且可以訪問其作用域之外的變數。但有一個限制:它們不能修改定義Lambda的方法的區域性變數的內容。這些變數必須是隱式最終的。可以認為Lambda是對值封閉,而不是對變數封閉。如前所述,這種限制存在的原因在於區域性變數儲存在棧上,並且隱式表示它們僅限於其所線上程。如果允許捕獲可改變的區域性變數,就會引發造成執行緒不安全的新的可能性,而這是我們不想看到的(例項變數可以,因為它們儲存在堆中,而堆是線上程之間共享的)。
函式介面
Java 8之前,介面主要用於定義方法簽名,現在它們還能為介面的使用者提供方法的預設實現,如果介面的設計者認為介面中宣告的某個方法並不需要每一個介面的使用者顯式地提供實現,他就可以考慮在介面的方法宣告中為其定義預設方法。
對類庫的設計者而言,這是個偉大的新工具,原因很簡單,它提供的能力能幫助類庫的設計者們定義新的操作,增強介面的能力,類庫的使用者們(即那些實現該介面的程式設計師們)不需要花費額外的精力重新實現該方法。因此,預設方法與庫的使用者也有關係,它們遮蔽了將來的變化對使用者的影響。第9章針對這一問題進行了更加深入的探討。
在介面上添加註解:@FunctionalInterface。即可宣告該介面為函式介面。
如果你去看看新的Java API,會發現函式式介面帶有@FunctionalInterface的標註。這個標註用於表示該介面會設計成一個函式式介面。如果你用@FunctionalInterface定義了一個介面,而它卻不是函式式介面的話,編譯器將返回一個提示原因的錯誤。例如,錯誤訊息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多個抽象方法。請注意,@FunctionalInterface不是必需的,但對於為此設計的介面而言,使用它是比較好的做法。它就像是@Override標註表示方法被重寫了。
Lambdas及函式式介面的例子:
使用案例 | Lambda例子 | 對應的函式式介面 |
---|---|---|
布林表示式 | (List<String> list) -> list.isEmpty() |
Predicate<List<String>> |
建立物件 | () -> new Apple(10) |
Supplier<Apple> |
消費一個物件 | (Apple a) ->System.out.println(a.getWeight()) |
Consumer<Apple> |
從一個物件中選擇/提取 | (String s) -> s.length() |
Function<String, Integer>或ToIntFunction<String> |
合併兩個值 | (int a, int b) -> a * b |
IntBinaryOperator |
比較兩個物件 | (Apple a1, Apple a2) ->a1.getWeight().compareTo(a2.getWeight()) |
Comparator<Apple>或BiFunction<Apple, Apple, Integer>或ToIntBiFunction<Apple, Apple> |
流
簡介
要討論流,我們先來談談集合,這是最容易上手的方式了。Java 8中的集合支援一個新的stream方法,它會返回一個流(介面定義在java.util.stream.Stream裡)。你在後面會看到,還有很多其他的方法可以得到流,比如利用數值範圍或從I/O資源生成流元素。
那麼,流到底是什麼呢?簡短的定義就是“從支援資料處理操作的源生成的元素序列”。讓我們一步步剖析這個定義。
- 元素序列——就像集合一樣,流也提供了一個介面,可以訪問特定元素型別的一組有序值。因為集合是資料結構,所以它的主要目的是以特定的時間/空間複雜度儲存和訪問元素(如ArrayList 與 LinkedList)。但流的目的在於表達計算,比如你前面見到的filter、sorted和map。集合講的是資料,流講的是計算。我們會在後面幾節中詳細解釋這個思想。
- 源——流會使用一個提供資料的源,如集合、陣列或輸入/輸出資源。 請注意,從有序集合生成流時會保留原有的順序。由列表生成的流,其元素順序與列表一致。
- 資料處理操作——流的資料處理功能支援類似於資料庫的操作,以及函數語言程式設計語言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以順序執行,也可並行執行。
此外,流操作有兩個重要的特點。
- 流水線——很多流操作本身會返回一個流,這樣多個操作就可以連結起來,形成一個大的流水線。這讓我們下一章中的一些優化成為可能,如延遲和短路。流水線的操作可以看作對資料來源進行資料庫式查詢。
- 內部迭代——與使用迭代器顯式迭代的集合不同,流的迭代操作是在背後進行的。
流與集合
Java現有的集合概念和新的流概念都提供了介面,來配合代表元素型有序值的資料介面。所謂有序,就是說我們一般是按順序取用值,而不是隨機取用的。那這兩者有什麼區別呢?
我們先來打個直觀的比方吧。比如說存在DVD裡的電影,這就是一個集合(也許是位元組,也許是幀,這個無所謂),因為它包含了整個資料結構。現在再來想想在網際網路上通過視訊流看同樣的電影。現在這是一個流(位元組流或幀流)。流媒體視訊播放器只要提前下載使用者觀看位置的那幾幀就可以了,這樣不用等到流中大部分值計算出來,你就可以顯示流的開始部分了(想想觀看直播足球賽)。特別要注意,視訊播放器可能沒有將整個流作為集合,儲存所需要的記憶體緩衝區——而且要是非得等到最後一幀出現才能開始看,那等待的時間就太長了。出於實現的考慮,你也可以讓視訊播放器把流的一部分快取在集合裡,但和概念上的差異不是一回事。
粗略地說,集合與流之間的差異就在於什麼時候進行計算。集合是一個記憶體中的資料結構,它包含資料結構中目前所有的值——集合中的每個元素都得先算出來才能新增到集合中。(你可以往集合里加東西或者刪東西,但是不管什麼時候,集合中的每個元素都是放在記憶體裡的,元素都得先算出來才能成為集合的一部分。)
相比之下,流則是在概念上固定的資料結構(你不能新增或刪除元素),其元素則是按需計算的。 這對程式設計有很大的好處。在第6章中,我們將展示構建一個質數流(2, 3, 5, 7, 11, …)有多簡單,儘管質數有無窮多個。這個思想就是使用者僅僅從流中提取需要的值,而這些值——在使用者看不見的地方——只會按需生成。這是一種生產者-消費者的關係。從另一個角度來說,流就像是一個延遲建立的集合:只有在消費者要求的時候才會計算值(用管理學的話說這就是需求驅動,甚至是實時製造)。
與此相反,集合則是急切建立的(供應商驅動:先把倉庫裝滿,再開始賣,就像那些曇花一現的聖誕新玩意兒一樣)。以質數為例,要是想建立一個包含所有質數的集合,那這個程式算起來就沒完沒了了,因為總有新的質數要算,然後把它加到集合裡面。當然這個集合是永遠也建立不完的,消費者這輩子都見不著了。
流的操作
操作 | 型別 | 返回型別 | 使用的型別、函式式介面 | 函式描述符 |
---|---|---|---|---|
filter |
中間 | Stream<T> |
Predicate<T> |
T -> boolean |
distinct |
中間(有狀態-無界) | Stream<T> |
`` | `` |
skip |
中間(有狀態-有界) | Stream<T> |
long |
`` |
limit |
中間(有狀態-有界) | Stream<T> |
long |
`` |
map |
中間 | Stream<R> |
Function<T, R> |
T -> R |
flatMap |
中間 | Stream<R> |
Function<T, Stream<R>> |
T -> Stream<R> |
sorted |
中間(有狀態-無界) | Stream<T> |
Comparator<T> |
(T, T) -> int |
anyMatch |
終端 | boolean |
Predicate<T> |
T -> boolean |
noneMatch |
終端 | boolean |
Predicate<T> |
T -> boolean |
allMatch |
終端 | boolean |
Predicate<T> |
T -> boolean |
findAny |
終端 | Optional<T> |
`` | `` |
findFirst |
終端 | Optional<T> |
`` | `` |
forEach |
終端 | void |
Consumer<T> |
T -> void |
collect |
終端 | R |
Collector<T, A, R> |
`` |
reduce`` | 終端(有狀態-有界) | Optional<T> |
BinaryOperator<T> |
(T, T)-> T |
count |
終端 | long |
`` | `` |
預定義收集器
即Collectors
類提供的工廠方法(例如groupingBy)建立的收集器。它們主要提供了三大功能:
- 將流元素歸約和彙總為一個值
- 元素分組
- 元素分割槽
Collectors類的靜態工廠方法
工廠方法 | 返回型別 | 用於 |
---|---|---|
toList | List<T> | 把流中所有專案收集到一個List |
使用示例:
List<Dish> dishes = menuStream.collect(toList());
工廠方法 | 返回型別 | 用於 |
---|---|---|
toSet | Set<T> | 把流中所有專案收集到一個Set,刪除重複項 |
使用示例:
Set<Dish> dishes = menuStream.collect(toSet());
工廠方法 | 返回型別 | 用於 |
---|---|---|
toCollection | Collection<T> | 把流中所有專案收集到給定的供應源建立的集合 |
使用示例:
Collection<Dish> dishes = menuStream.collect(toCollection(),ArrayList::new);
工廠方法 | 返回型別 | 用於 |
---|---|---|
counting | Long | 計算流中元素的個數 |
使用示例:
long howManyDishes = menuStream.collect(counting());
工廠方法 | 返回型別 | 用於 |
---|---|---|
summingInt | Integer | 對流中專案的一個整數屬性求和 |
使用示例:
int totalCalories =
menuStream.collect(summingInt(Dish::getCalories));
工廠方法 | 返回型別 | 用於 |
---|---|---|
averagingInt | Double | 計算流中專案Integer屬性的平均值 |
使用示例:
double avgCalories =
menuStream.collect(averagingInt(Dish::getCalories));
工廠方法 | 返回型別 | 用於 |
---|---|---|
summarizingInt | IntSummaryStatistics | 收集關於流中專案Integer屬性的統計值,例如最大、最小、總和與平均值 |
使用示例:
IntSummaryStatistics menuStatistics =
menuStream.collect(summarizingInt(Dish::getCalories));
工廠方法 | 返回型別 | 用於 |
---|---|---|
joining` | String | 連線對流中每個專案呼叫toString方法所生成的字串 |
使用示例:
String shortMenu =
menuStream.map(Dish::getName).collect(joining(", "));
工廠方法 | 返回型別 | 用於 |
---|---|---|
maxBy | Optional<T> | 一個包裹了流中按照給定比較器選出的最大元素的Optional,或如果流為空則為Optional.empty() |
使用示例:
Optional<Dish> fattest =
menuStream.collect(maxBy(comparingInt(Dish::getCalories)));
工廠方法 | 返回型別 | 用於 |
---|---|---|
minBy | Optional<T> | 一個包裹了流中按照給定比較器選出的最小元素的Optional,或如果流為空則為Optional.empty() |
使用示例:
Optional<Dish> lightest =
menuStream.collect(minBy(comparingInt(Dish::getCalories)));
工廠方法 | 返回型別 | 用於 |
---|---|---|
reducing | 歸約操作產生的型別 | 從一個作為累加器的初始值開始,利用BinaryOperator與流中的元素逐個結合,從而將流歸約為單個值 |
使用示例:
int totalCalories =
menuStream.collect(reducing(0, Dish::getCalories, Integer::sum));
工廠方法 | 返回型別 | 用於 |
---|---|---|
collectingAndThen | 轉換函式返回的型別 | 包裹另一個收集器,對其結果應用轉換函式 |
使用示例:
int howManyDishes =
menuStream.collect(collectingAndThen(toList(), List::size));
工廠方法 | 返回型別 | 用於 |
---|---|---|
groupingBy | Map<K, List<T>> | 根據專案的一個屬性的值對流中的專案作問組,並將屬性值作為結果Map 的鍵 |
使用示例:
Map<Dish.Type,List<Dish>> dishesByType =
menuStream.collect(groupingBy(Dish::getType));
工廠方法 | 返回型別 | 用於 |
---|---|---|
partitioningBy | Map<Boolean,List<T>> | 根據對流中每個專案應用謂詞的結果來對專案進行分割槽 |
使用示例:
Map<Boolean,List<Dish>> vegetarianDishes =
menuStream.collect(partitioningBy(Dish::isVegetarian));
並行流
在Java 7之前,並行處理資料集合非常麻煩。第一,你得明確地把包含資料的資料結構分成若干子部分。第二,你要給每個子部分分配一個獨立的執行緒。第三,你需要在恰當的時候對它們進行同步來避免不希望出現的競爭條件,等待所有執行緒完成,最後把這些部分結果合併起來。Java 7引入了一個叫作分支/合併的框架,讓這些操作更穩定、更不易出錯。
我們簡要地提到了Stream
介面可以讓你非常方便地處理它的元素:可以通過對收集源呼叫parallelStream
方法來把集合轉換為並行流。並行流就是一個把內容分成多個數據塊,並用不同的執行緒分別處理每個資料塊的流。這樣一來,你就可以自動把給定操作的工作負荷分配給多核處理器的所有核心,讓它們都忙起來。
高效使用並行流
一般而言,想給出任何關於什麼時候該用並行流的定量建議都是不可能也毫無意義的,因為任何類似於“僅當至少有一千個(或一百萬個或隨便什麼數字)元素的時候才用並行流)”的建議對於某臺特定機器上的某個特定操作可能是對的,但在略有差異的另一種情況下可能就是大錯特錯。儘管如此,我們至少可以提出一些定性意見,幫你決定某個特定情況下是否有必要使用並行流。
- 如果有疑問,測量。把順序流轉成並行流輕而易舉,但卻不一定是好事。我們在本節中已經指出,並行流並不總是比順序流快。此外,並行流有時候會和你的直覺不一致,所以在考慮選擇順序流還是並行流時,第一個也是最重要的建議就是用適當的基準來檢查其效能。
- 留意裝箱。自動裝箱和拆箱操作會大大降低效能。Java 8中有原始型別流(IntStream、LongStream、DoubleStream)來避免這種操作,但凡有可能都應該用這些流。
- 有些操作本身在並行流上的效能就比順序流差。特別是limit和findFirst等依賴於元素順序的操作,它們在並行流上執行的代價非常大。例如,findAny會比findFirst效能好,因為它不一定要按順序來執行。你總是可以呼叫unordered方法來把有序流變成無序流。那麼,如果你需要流中的n 個元素而不是專門要前n 個的話,對無序並行流呼叫limit可能會比單個有序流(比如資料來源是一個List)更高效。
- 還要考慮流的操作流水線的總計算成本。設 N 是要處理的元素的總數,Q 是一個元素通過流水線的大致處理成本,則 N*Q 就是這個對成本的一個粗略的定性估計。Q 值較高就意味著使用並行流時效能好的可能性比較大。
- 對於較小的資料量,選擇並行流幾乎從來都不是一個好的決定。並行處理少數幾個元素的好處還抵不上並行化造成的額外開銷。
- 要考慮流背後的資料結構是否易於分解。例如,ArrayList的拆分效率比LinkedList高得多,因為前者用不著遍歷就可以平均拆分,而後者則必須遍歷。另外,用range工廠方法建立的原始型別流也可以快速分解。
- 流自身的特點,以及流水線中的中間操作修改流的方式,都可能會改變分解過程的效能。例如,一個SIZED流可以分成大小相等的兩部分,這樣每個部分都可以比較高效地並行處理,但篩選操作可能丟棄的元素個數卻無法預測,導致流本身的大小未知。
- 還要考慮終端操作中合併步驟的代價是大是小(例如Collector中的combiner方法)。如果這一步代價很大,那麼組合每個子流產生的部分結果所付出的代價就可能會超出通過並行流得到的效能提升。
流的資料來源和可分解性
源 | 可分解性 |
---|---|
ArrayList | 極佳 |
LinkedList | 差 |
IntStream.range | 極佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |
Optional
Java 8的庫提供了Optional<T>類,這個類允許你在程式碼中指定哪一個變數的值既可能是型別T的值,也可能是由靜態方法Optional.empty表示的缺失值。無論是對於理解程式邏輯,抑或是對於編寫產品文件而言,這都是一個重大的好訊息,你現在可以通過一種資料型別表示顯式缺失的值——使用空指標的問題在於你無法確切瞭解出現空指標的原因,它是預期的情況,還是說由於之前的某一次計算出錯導致的一個偶然性的空值,有了Optional之後你就不需要再使用之前容易出錯的空指標來表示缺失的值了。
Optional類的方法
方法 | 描述 |
---|---|
empty | 返回一個空的Optional例項 |
filter | 如果值存在並且滿足提供的謂詞,就返回包含該值的Optional物件;否則返回一個空的Optional物件 |
flatMap | 如果值存在,就對該值執行提供的mapping函式呼叫,返回一個Optional型別的值,否則就返回一個空的Optional物件 |
get | 如果該值存在,將該值用Optional封裝返回,否則丟擲一個NoSuchElementException異常 |
ifPresent | 如果值存在,就執行使用該值的方法呼叫,否則什麼也不做 |
isPresent | 如果值存在就返回true,否則返回false |
map | 如果值存在,就對該值執行提供的mapping函式呼叫 |
of | 將指定值用Optional封裝之後返回,如果該值為null,則丟擲一個NullPointerException異常 |
ofNullable | 將指定值用Optional封裝之後返回,如果該值為null,則返回一個空的Optional物件 |
orElse | 如果有值則將其返回,否則返回一個預設值 |
orElseGet | 如果有值則將其返回,否則返回一個由指定的Supplier介面生成的值 |
orElseThrow | 如果有值則將其返回,否則丟擲一個由指定的Supplier介面生成的異常 |
小結
- null引用在歷史上被引入到程式設計語言中,目的是為了表示變數值的缺失。
- Java 8中引入了一個新的類java.util.Optional<T>,對存在或缺失的變數值進行建模。
- 你可以使用靜態工廠方法Optional.empty、Optional.of以及Optional.ofNullable建立Optional物件。
- Optional類支援多種方法,比如map、flatMap、filter,它們在概念上與Stream類中對應的方法十分相似。
- 使用Optional會迫使你更積極地解引用Optional物件,以應對變數值缺失的問題,最終,你能更有效地防止程式碼中出現不期而至的空指標異常。
- 使用Optional能幫助你設計更好的API,使用者只需要閱讀方法簽名,就能瞭解該方法是否接受一個Optional型別的值。
CompletableFuture
Java從Java 5版本就提供了Future介面。Future對於充分利用多核處理能力是非常有益的,因為它允許一個任務在一個新的核上生成一個新的子執行緒,新生成的任務可以和原來的任務同時執行。原來的任務需要結果時,它可以通過get方法等待Future執行結束(生成其計算的結果值)。
Future介面的侷限性
我們知道Future介面提供了方法來檢測非同步計算是否已經結束(使用isDone方法),等待非同步操作結束,以及獲取計算的結果。但是這些特性還不足以讓你編寫簡潔的併發程式碼。比如,我們很難表述Future結果之間的依賴性;從文字描述上這很簡單,“當長時間計算任務完成時,請將該計算的結果通知到另一個長時間執行的計算任務,這兩個計算任務都完成後,將計算的結果與另一個查詢操作結果合併”。但是,使用Future中提供的方法完成這樣的操作又是另外一回事。這也是我們需要更具描述能力的特性的原因,比如下面這些。
- 將兩個非同步計算合併為一個——這兩個非同步計算之間相互獨立,同時第二個又依賴於第一個的結果。
- 等待Future集合中的所有任務都完成。
- 僅等待Future集合中最快結束的任務完成(有可能因為它們試圖通過不同的方式計算同一個值),並返回它的結果。
- 通過程式設計方式完成一個Future任務的執行(即以手工設定非同步操作結果的方式)。
- 應對Future的完成事件(即當Future的完成事件發生時會收到通知,並能使用Future計算的結果進行下一步的操作,不只是簡單地阻塞等待操作的結果)。
一個非常有用,不過不那麼精確的格言這麼說:“Completable-Future對於Future的意義就像Stream之於Collection。”讓我們比較一下這二者。
- 通過Stream你可以對一系列的操作進行流水線,通過map、filter或者其他類似的方法提供行為引數化,它可有效避免使用迭代器時總是出現模板程式碼。
- 類似地,CompletableFuture提供了像thenCompose、thenCombine、allOf這樣的操作,對Future涉及的通用設計模式提供了函數語言程式設計的細粒度控制,有助於避免使用指令式程式設計的模板程式碼。
新的日期和時間API
Java的API提供了很多有用的元件,能幫助你構建複雜的應用。不過,Java API也不總是完美的。我們相信大多數有經驗的程式設計師都會贊同Java 8之前的庫對日期和時間的支援就非常不理想。然而,你也不用太擔心:Java 8中引入全新的日期和時間API就是要解決這一問題。
- LocalDate
- LocalTime
- LocalDateTime
- Instant
- Duration
- Period
使用LocalDate和LocalTime還有LocalDateTime
開始使用新的日期和時間API時,你最先碰到的可能是LocalDate類。該類的例項是一個不可變物件,它只提供了簡單的日期,並不含當天的時間資訊。另外,它也不附帶任何與時區相關的資訊。
你可以通過靜態工廠方法of建立一個LocalDate例項。LocalDate例項提供了多種方法來讀取常用的值,比如年份、月份、星期幾等,如下所示。
LocalDate date = LocalDate.of(2014, 3, 18); ←─2014-03-18 int year = date.getYear(); ←─2014 Month month = date.getMonth(); ←─MARCH int day = date.getDayOfMonth(); ←─18 DayOfWeek dow = date.getDayOfWeek(); ←─TUESDAY int len = date.lengthOfMonth(); ←─31 (days in March) boolean leap = date.isLeapYear(); ←─false (not a leap year) //你還可以使用工廠方法從系統時鐘中獲取當前的日期: LocalDate today = LocalDate.now();
LocalTime和LocalDateTime都提供了類似的方法。
機器的日期和時間格式
作為人,我們習慣於以星期幾、幾號、幾點、幾分這樣的方式理解日期和時間。毫無疑問,這種方式對於計算機而言並不容易理解。從計算機的角度來看,建模時間最自然的格式是表示一個持續時間段上某個點的單一大整型數。這也是新的java.time.Instant類對時間建模的方式,基本上它是以Unix元年時間(傳統的設定為UTC時區1970年1月1日午夜時分)開始所經歷的秒數進行計算。
你可以通過向靜態工廠方法ofEpochSecond傳遞一個代表秒數的值建立一個該類的例項。靜態工廠方法ofEpochSecond還有一個增強的過載版本,它接收第二個以納秒為單位的引數值,對傳入作為秒數的引數進行調整。過載的版本會調整納秒引數,確保儲存的納秒分片在0到999 999 999之間。這意味著下面這些對ofEpochSecond工廠方法的呼叫會返回幾乎同樣的Instant物件:
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000); ←─2 秒之後再加上100萬納秒(1秒) Instant.ofEpochSecond(4, -1_000_000_000); ←─4秒之前的100萬納秒(1秒)
正如你已經在LocalDate及其他為便於閱讀而設計的日期-時間類中所看到的那樣,Instant類也支援靜態工廠方法now,它能夠幫你獲取當前時刻的時間戳。我們想要特別強調一點,Instant的設計初衷是為了便於機器使用。它包含的是由秒及納秒所構成的數字。所以,它無法處理那些我們非常容易理解的時間單位。比如下面這段語句:
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
它會丟擲下面這樣的異常:
java.time.temporal.UnsupportedTemporalTypeException: Unsupported field:
DayOfMonth
但是你可以通過Duration和Period類使用Instant,接下來我們會對這部分內容進行介紹。
定義Duration或Period
目前為止,你看到的所有類都實現了Temporal介面,Temporal介面定義瞭如何讀取和操縱為時間建模的物件的值。之前的介紹中,我們已經瞭解了建立Temporal例項的幾種方法。很自然地你會想到,我們需要建立兩個Temporal物件之間的duration。Duration類的靜態工廠方法between就是為這個目的而設計的。你可以建立兩個LocalTimes物件、兩個LocalDateTimes物件,或者兩個Instant物件之間的duration,如下所示:
Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);
由於LocalDateTime和Instant是為不同的目的而設計的,一個是為了便於人閱讀使用,另一個是為了便於機器處理,所以你不能將二者混用。如果你試圖在這兩類物件之間建立duration,會觸發一個DateTimeException異常。此外,由於Duration類主要用於以秒和納秒衡量時間的長短,你不能僅向between方法傳遞一個LocalDate物件做引數。
如果你需要以年、月或者日的方式對多個時間單位建模,可以使用Period類。使用該類的工廠方法between,你可以使用得到兩個LocalDate之間的時長,如下所示:
Period tenDays = Period.between(LocalDate.of(2014, 3, 8),
LocalDate.of(2014, 3, 18));
最後,Duration和Period類都提供了很多非常方便的工廠類,直接建立對應的例項;換句話說,就像下面這段程式碼那樣,不再是隻能以兩個temporal物件的差值的方式來定義它們的物件。
建立Duration和Period物件
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
Duration類和Period類共享了很多相似的方法:
方法名 | 是否是靜態方法 | 方法描述 |
---|---|---|
between | 是 | 建立兩個時間點之間的interval |
from | 是 | 由一個臨時時間點建立interval |
of | 是 | 由它的組成部分建立interval的例項 |
parse | 是 | 由字串建立interval的例項 |
addTo | 否 | 建立該interval的副本,並將其疊加到某個指定的temporal物件 |
get | 否 | 讀取該interval的狀態 |
isNegative | 否 | 檢查該interval是否為負值,不包含零 |
isZero | 否 | 檢查該interval的時長是否為零 |
minus | 否 | 通過減去一定的時間建立該interval的副本 |
multipliedBy | 否 | 將interval的值乘以某個標量建立該interval的副本 |
negated | 否 | 以忽略某個時長的方式建立該interval的副本 |
plus | 否 | 以增加某個指定的時長的方式建立該interval的副本 |
subtractFrom | 否 | 從指定的temporal物件中減去該interval |