Java時(shí)間操作類庫-Joda-Time
前言
上一周在做一個(gè)產(chǎn)品的需求的時(shí)候有個(gè)動(dòng)態(tài)計(jì)算時(shí)間段(如現(xiàn)在是13:00,則時(shí)間段為15:10-17:10、17:10-19:10、19:10-21:10;即最早的出發(fā)時(shí)間為當(dāng)前時(shí)間+參數(shù)【2h10min】,最遲的時(shí)間段為開始時(shí)間在20點(diǎn)前結(jié)束時(shí)間在20點(diǎn)后的時(shí)間段),期間大量使用到了日期時(shí)間類庫,本著熟悉日期時(shí)間類庫才有了這篇文章,文章最后我會(huì)把我如何實(shí)現(xiàn)的這個(gè)需求的一個(gè)算法貼出來。
一、JDK8以前版本中的時(shí)間類庫
1.1 原始時(shí)間類庫存在的缺陷與不足
我們?cè)谑褂肑ava8之前的類庫時(shí),都會(huì)在處理日期-時(shí)間的時(shí)候總是不爽,這其中包括且不限于以下的槽點(diǎn):
在Java 1.0版本中,對(duì)時(shí)間、日期的操作完全依賴于 java.util.Data 類,只能以毫秒的精度表示時(shí)間,無法表示日期。
- 在易用性方面有著很大的缺陷,年份的起始時(shí)間選擇是1900年,月份是從0開始。
- toString 方法返回值不直觀,帶有時(shí)區(qū)。
在Java1.1 版本中,廢棄了很多Date 類中的很多方法,并且新增了 java.util.Calendar。但是與Date相同,Calendar 類也有類似的問題和設(shè)計(jì)缺陷,導(dǎo)致在使用這些類寫出的代碼也很容易出錯(cuò)。
- 月份依然是從0開始計(jì)算。
- 常用的日期、時(shí)間操作需要同時(shí)使用Date、Canendar、SimpleDateFormat,比較繁瑣。
- 部分特性只存在于某一個(gè)類(解析和格式化日期或時(shí)間的DateFormat方法只存在于Date類中)。
- DateFormat 不是線程安全的,如果兩個(gè)線程嘗試使用同一個(gè)formatter 解析日期,可能會(huì)得到無法預(yù)期的結(jié)果。
- Date 和 Canendar 都是可變的。
1.2 關(guān)于SimpleDateFormat 線程不安全的原因
由于 parse 方法使用的貢獻(xiàn)變量 calendar 不是線程安全的。在 format (subFormat) 方法中進(jìn)行了 calendar 的賦值,在 parse 進(jìn)行了值得處理,因此在并發(fā)的情況下會(huì)造成 calendar 清理不及時(shí),值被覆蓋的情況。
- /**
- * The {@link Calendar} instance used for calculating the date-time fields
- * and the instant of time. This field is used for both formatting and
- * parsing.
- *
- * <p>Subclasses should initialize this field to a {@link Calendar}
- * appropriate for the {@link Locale} associated with this
- * <code>DateFormat</code>.
- * @serial
- */
- protected Calendar calendar;
- @Override
- public StringBuffer format(Date date, StringBuffer toAppendTo,
- FieldPosition pos){
- pos.beginIndex = pos.endIndex = 0;
- return format(date, toAppendTo, pos.getFieldDelegate());
- }
- // Called from Format after creating a FieldDelegate
- private StringBuffer format(Date date, StringBuffer toAppendTo,
- FieldDelegate delegate) {
- // Convert input date to time field list
- calendar.setTime(date);
- // At this point the fields of Calendar have been set. Calendar
- // will fill in default values for missing fields when the time
- // is computed.
- pos.index = start;
- Date parsedDate;
- try {
- parsedDate = calb.establish(calendar).getTime();
- // If the year value is ambiguous,
- // then the two-digit year == the default start year
- if (ambiguousYear[0]) {
- if (parsedDate.before(defaultCenturyStart)) {
- parsedDate = calb.addYear(100).establish(calendar).getTime();
- }
- }
- }
- }
1.3 如何解決上述線程不安全問題?
- 使用ThreadLocal 為每個(gè)線程都創(chuàng)建一個(gè)線程獨(dú)享 SimpleDateFormat 變量;
- 需要的時(shí)候創(chuàng)建局部變量;
- 使用 org.apacle.commons.lang3.time.DateFormatUtils
- 使用Joda-Time (后面介紹)
二、Joda-Time 日期時(shí)間類庫
2.1 簡介
Joda-Time 是Joda提供的一個(gè)遵循Apache2.0 開源協(xié)議的 JDK以外的優(yōu)質(zhì)日期和時(shí)間開發(fā)庫。
- Joda除Joda-Time之外的項(xiàng)目有Joda-Money、Joda-Beans、Joda-Convert、Joda-Collect Joda官網(wǎng)
2.1.1 為什么使用Joda-Time
- 使用方便:Calendar 訪問“正常的”日期困難,并且缺乏簡單的防腐,Joda-Time 擁有簡單的字段訪問,比如獲得年的 getYear() 和 獲得星期 中的天 getDayOfWeek() 。
- 易于擴(kuò)展:JDK支持通過使用子類實(shí)現(xiàn)多個(gè)日歷系統(tǒng),但是這是非常笨重的,并且在實(shí)現(xiàn)中很難選出另一個(gè)日歷系統(tǒng)。Joda-Time 支持基于 Chronology 類實(shí)現(xiàn)多個(gè)可插拔的日歷系統(tǒng)。
- 功能全面:Joda-Time 提供了所有的日期和時(shí)間計(jì)算的必須功能,它提供了即裝即用的特性。
- 最新的時(shí)區(qū)計(jì)算:時(shí)區(qū)的實(shí)現(xiàn)基于公共時(shí)區(qū)信息數(shù)據(jù)庫,每年更新數(shù)次。新版本的Joda-Time 包括了這個(gè)數(shù)據(jù)庫的所有更改,應(yīng)盡早進(jìn)行必要的更新,手動(dòng)更新區(qū)域數(shù)據(jù)很容易。
- 日歷支持:提供了8種日歷系統(tǒng)。
- 互通性:內(nèi)部使用毫秒進(jìn)行標(biāo)識(shí),這與JDK或者其他公共的時(shí)間表示相一致。
- 性能良好:支持針對(duì)所有訪問的域進(jìn)行最小的計(jì)算。
- 良好的測試覆蓋率:有全方位的測試人員保證庫的質(zhì)量、
- 具有完整文檔:有一個(gè)完整的用戶指南,該指南提供了一個(gè)概述,涵蓋常見的使用場景。javadoc 非常詳細(xì),涵蓋API的其余部分。
- 發(fā)展:自2002年以來積極發(fā)展,是一個(gè)成熟的可靠的代碼庫,一些相關(guān)的項(xiàng)目目前也是可用的。
- 開源:遵循Apache 2.0開源協(xié)議發(fā)布。
2.1.2 Joda-Time 的關(guān)鍵優(yōu)點(diǎn)
- LocalDate:只包含日期
- LocalTime:只包含時(shí)間
- Instant:時(shí)間軸上的時(shí)間點(diǎn)
- DateTime:時(shí)區(qū)中完整的日期和時(shí)間
- DateTimeZone:更好的時(shí)區(qū)
- Duration和Period:持續(xù)時(shí)間
- Interval:兩個(gè)時(shí)間點(diǎn)之間的時(shí)間
- 全面并且靈活的時(shí)間格式化與轉(zhuǎn)換
正因?yàn)镴oda-Time 與 Java8 之前的時(shí)間類庫相比,具備了如此多的優(yōu)點(diǎn),所以 Joda-Time 成為事實(shí)上的標(biāo)準(zhǔn)的Java日期和時(shí)間庫。
2.2 特性解讀
2.2.1 Joda-Time和JDK的互操作性
互操作性是指:Joda 類能夠生成 java.util.Date 的實(shí)例(以及Calendar),這可以讓我們保留現(xiàn)有對(duì)JDK的依賴,又能夠使用Joda處理復(fù)雜的日期/時(shí)間計(jì)算。
Date To Joda-Time
- Date date = new Date();
- DateTime dateTime = new DateTime(date);
Canendar To Joda-Time
- Calendar calendar = Calendar.getInstance();
- DateTime dateTime = new DateTime(calendar);
Joda-Time To Date
- Date date = new Date();
- DateTime dateTime = new DateTime(date);
- Date date2 = dateTime.toDate();
Joda-Time To Calendar
- Calendar calendar = Calendar.getInstance();
- dateTime = new DateTime(calendar);
- Calendar calendar2 = dateTime.toCalendar(Locale.CHINA);
2.2.2 Joda的關(guān)鍵日期/時(shí)間概念理解
Joda 使用了以下概念,使得它們可以應(yīng)用到任何日期/時(shí)間庫:
不可變性(Immutability)
Joda-Time與java.lang.String類似,它們的實(shí)例均無法修改(因?yàn)槿我鈱?duì)其值改變的操作都會(huì)生成新的對(duì)象),這也代表了它們是線程安全的。
瞬時(shí)性(Instant)
如接口 org.joda.time.ReadableInstant 中所表示的那樣,Instant 表示的是一個(gè)精確的時(shí)間點(diǎn),是從 epoch:1970-01-01T00:00:00Z 開始計(jì)算的毫秒數(shù),這也的設(shè)計(jì)也使得其子類都可以與JDK Date 以及 Calendar 類兼容。
- /**
- * Defines an instant in the datetime continuum.
- * This interface expresses the datetime as milliseconds from 1970-01-01T00:00:00Z.
- * <p>
- * The implementation of this interface may be mutable or immutable.
- * This interface only gives access to retrieve data, never to change it.
- * <p>
- * Methods in your application should be defined using <code>ReadableInstant</code>
- * as a parameter if the method only wants to read the instant without needing to know
- * the specific datetime fields.
- * <p>
- * The {@code compareTo} method is no longer defined in this class in version 2.0.
- * Instead, the definition is simply inherited from the {@code Comparable} interface.
- * This approach is necessary to preserve binary compatibility.
- * The definition of the comparison is ascending order by millisecond instant.
- * Implementors are recommended to extend {@code AbstractInstant} instead of this interface.
- *
- * @author Stephen Colebourne
- * @since 1.0
- */
- public interface ReadableInstant extends Comparable<ReadableInstant> {
- /**
- * Get the value as the number of milliseconds since
- * the epoch, 1970-01-01T00:00:00Z.
- *
- * @return the value as milliseconds
- */
- long getMillis();
- ······
- }
DateTime 類繼承圖如下:

局部性(Partial)
瞬時(shí)性表達(dá)的是與epoch相對(duì)的時(shí)間上的一個(gè)精確時(shí)刻,而一個(gè)局部時(shí)間指的是一個(gè)時(shí)間的一部分片段,其可以通過一些方法使得時(shí)間產(chǎn)生變動(dòng)(本質(zhì)上還是生成了新的類),這樣可以把它當(dāng)做重復(fù)周期中的一點(diǎn),用到多個(gè)地方。
年表(Chronology)
Joda-Time的設(shè)計(jì)核心就是年表(org.joda.time.Chronology),從根本上將,年表是一種日歷系統(tǒng),是一種計(jì)算時(shí)間的特殊方式,并且在其中執(zhí)行日歷算法的框架。Joda-Time支持的8種年表如下所示:
- ISO(默認(rèn)) - org.joda.time.chrono.ISOChronology
- GJ - org.joda.time.chrono.GJChronology
- Gregorian - org.joda.time.chrono.GregorianChronology
- Julian - org.joda.time.chrono.JulianChronology
- Coptic - org.joda.time.chrono.CopticChronology
- Buddhist - org.joda.time.chrono.BuddhistChronology
- Ethiopic - org.joda.time.chrono.EthiopicChronology
- Islamic - org.joda.time.chrono.IslamicChronology
以上的每一種年表都可以作為特定日歷系統(tǒng)的計(jì)算引擎,是可插拔的實(shí)現(xiàn)。
時(shí)區(qū)(Time zone)
具體定義詳見百科解釋,在實(shí)際編碼過程中任何嚴(yán)格的時(shí)間計(jì)算都必須涉及時(shí)區(qū)(或者相對(duì)于GMT),Joda-Time中對(duì)應(yīng)的核心類為org.joda.time.DateTimeZone,雖然日常的使用過程中,并未涉及到對(duì)時(shí)區(qū)的操作,但是DateTimeZone如何對(duì)DateTime產(chǎn)生影響是比較值得注意的,此處不進(jìn)行贅述。
2.3 具體使用方法
上面介紹我完了Joda-Time的一些概念,接下來具體使用我們來進(jìn)行說明:
2.3.1 創(chuàng)建 Joda-Time 對(duì)象
瞬時(shí)性-ReadableInstant
- // 1.使用系統(tǒng)時(shí)間
- DateTime dateTime1 = new DateTime();
- // 2.使用jdk中的date
- Date jdkDate1 = new Date();
- DateTime dateTime2 = new DateTime(jdkDate1);
- // 3.使用毫秒數(shù)指定
- Date jdkDate2 = new Date();
- long millis = jdkDate.getTime();
- DateTime dateTime3 = new DateTime(millis);
- // 4.使用Calendar
- Calendar calendar = Calendar.getInstance();
- DateTime dateTime4 = new DateTime(calendar);
- // 5.使用多個(gè)字段指定一個(gè)瞬間時(shí)刻(局部時(shí)間片段)
- // year month day hour(midnight is zero) minute second milliseconds
- DateTime dateTime5 = new DateTime(2000, 1, 1, 0, 0, 0, 0);
- // 6.由一個(gè)DateTime生成另一個(gè)DateTime
- DateTime dateTime6 = new DateTime(dateTime1);
- // 7.有時(shí)間字符串生成DateTime
- String timeString = "2019-01-01T00:00:00-06:00";
- DateTime dateTime7 = DateTime.parse(timeString);
局部性-ReadablePartial
當(dāng)程序中處理的日期、時(shí)間并不需要是完整時(shí)刻的時(shí)候,可以創(chuàng)建一個(gè)局部時(shí)間,比如只希望專注于年/月/日, 或者一天中的時(shí)間,或者是一周中的某天。Joda-Time中有表示這些時(shí)間的是org.joda.time.ReadablePartial接口,實(shí)現(xiàn)它的兩個(gè)類LocalDate和LocalTime是分別用來表示年/月/日和一天中的某個(gè)時(shí)間的。
- // 顯示地提供所含的每個(gè)字段
- LocalDate localDate = new LocalDate(2019, 1, 1);
- // 6:30:06 PM
- LocalTime localTime = new LocalTime(18, 30, 6, 0);
LocalDate是替代了早期Joda-Time版本中使用的org.joda.time.YearMonthDay,LocalTime是替代早期版本的org.joda.time.TimeOfDay。(均已被標(biāo)注為過時(shí)狀態(tài))。
時(shí)間跨度
Joda-Time提供了三個(gè)類用于表示時(shí)間跨度(在某些業(yè)務(wù)需求中,它們可能會(huì)非常有用)。
Duration
這個(gè)類表示以毫秒為單位的絕對(duì)精度,提供標(biāo)準(zhǔn)數(shù)學(xué)轉(zhuǎn)換的方法,同時(shí)把時(shí)間跨度轉(zhuǎn)換為標(biāo)準(zhǔn)單位。
Period
這個(gè)類表示以年月日單位表示。
Interval
這個(gè)類表示一個(gè)特定的時(shí)間跨度,使用一個(gè)明確的時(shí)刻界定這段時(shí)間跨度的范圍。Interval 為半開 區(qū)間,所以由其封裝的時(shí)間跨度包括這段時(shí)間的起始時(shí)刻,但是不包含結(jié)束時(shí)刻。
2.3.2 使用Joda-Time的方法處理時(shí)間
- DateTime today = new DateTime();
- // 獲取777秒之前的時(shí)間
- DateTime dateTime1 = today.minus(777 * 1000);
- // 獲取明天的時(shí)間
- DateTime tomorrow = today.plusDays(1);
- // 獲取當(dāng)月第一天的日期
- DateTime dateTime2 = today.withDayOfMonth(1);
- // 獲取當(dāng)前時(shí)間三個(gè)月后的月份的最后一天
- DateTime dateTime3 = today.plusMonths(3).dayOfMonth().withMaximumValue();
下面列出部分DateTime方法列表: plus/minus開頭的方法(比如:plusDay, minusMonths):用來返回在DateTime實(shí)例上增加或減少一段時(shí)間后的實(shí)例
- plus(long duration) 增加指定毫秒數(shù)并返回
- plusYears(int years) 增加指定年份并返回
- plusMonths(int months) 增加指定月份并返回
- plusWeeks(int weeks) 增加指定星期并返回
- plusDays(int days) 增加指定天數(shù)并返回
- plusHours(int hours) 增加指定小時(shí)并返回
- plusMinutes(int minutes) 增加指定分鐘并返回
- plusSeconds(int seconds) 增加指定秒數(shù)并返回
- plusMillis(int millis) 增加指定毫秒并返回
與之相反的是minus前綴的 plus是增加 minus是減少
with開頭的方法:用來返回在DateTime實(shí)例更新指定日期單位后的實(shí)例
- withCenturyOfEra(int centuryOfEra) 更新時(shí)間世紀(jì)單位并返回
- withYearOfCentury(int yearOfCentury)更新世紀(jì)年并返回
- withYear(int year) 更新時(shí)間年并返回
- withWeekyear(int weekyear) 更新時(shí)間周數(shù)并返回
- withMonthOfYear(int monthOfYear)更新時(shí)間月份并返回
- withDayOfYear(int dayOfYear) 更新時(shí)間天數(shù)并返回
- withDayOfMonth(int dayOfMonth) 更新時(shí)間天數(shù)并返回
- withDayOfWeek(int dayOfWeek) 更新時(shí)間天數(shù)并返回
- withHourOfDay(int hour) 更新時(shí)間小時(shí)并返回
- withMinuteOfHour(int minute) 更新時(shí)間分鐘并返回
- withSecondOfMinute(int second) 更新時(shí)間秒數(shù)并返回
- withMillisOfSecond(int millis) 更新時(shí)間毫秒并返回
- withMillisOfDay(int millis) 更新時(shí)間毫秒并返回
- withTimeAtStartOfDay() 獲取當(dāng)天最早時(shí)間
判斷DateTime對(duì)象大小狀態(tài)的一些操作方法
- compareTo(DateTime d) 比較兩時(shí)間大小 時(shí)間大于指定時(shí)間返回 1 時(shí)間小于指定時(shí)間返回-1 相等返回0
- equals(DateTime d) 比較兩時(shí)間是否相等
- isAfter(long instant) 判斷時(shí)間是否大于指定時(shí)間
- isAfterNow() 判斷時(shí)間是否大于當(dāng)前時(shí)間
- isBefore(long instant) 判斷時(shí)間是否小于指定時(shí)間
- isBeforeNow() 判斷時(shí)間是否小于當(dāng)前時(shí)間
- isEqual(long instant) 判斷時(shí)間是否等于指定時(shí)間
- isEqualNow() 判斷時(shí)間是否等于當(dāng)前時(shí)間
2.3.3 以Joda-Time的方式格式化時(shí)間
- // 傳入的格式化模板只需與JDK SimpleDateFormat兼容的格式字符串即可
- public static String convert(Date date,String dateFormat){
- return new DateTime(date).toString(dateFormat);
- }
- // 將JDK中的Date轉(zhuǎn)化為UTC時(shí)區(qū)的DateTime
- DateTime dateTime = new DateTime(new Date(), DateTimeZone.UTC);
- // 將String轉(zhuǎn)換為DateTime
- public static Date convertUTC2Date(String utcDate){
- DateTime dateTime =DateTime.parse(utcDate, DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"));
- return dateTime.toDate();
- }
更多使用方法請(qǐng)參考官方文檔。
三、JAVA 8中新的時(shí)間類庫
3.1 簡介
由于JDK之前版本的類庫的缺陷和糟糕的使用體驗(yàn),再加上已經(jīng)成為事實(shí)標(biāo)準(zhǔn)Joda-Time的影響力,Oracle決定在JAVA API中提供高質(zhì)量的日期和時(shí)間支持,這也就是整合了大部分Joda-Time特性的JDK 8新的時(shí)間類庫。(Joda-Time的作者實(shí)際參與開發(fā),并且實(shí)現(xiàn)了JSR310的全部內(nèi)容,新的API位于java.time下。常用的類有以下幾個(gè):LocalDate、LocalTime、Instant、Duration和Period。)
由于JDK 8 新的時(shí)間類庫大量借鑒了Joda-Time的設(shè)計(jì)思想乃至命名,因此如果你是Joda-Time的使用者,那你可以無學(xué)習(xí)成本的使用新的API(當(dāng)然,它們之間也存在些許差別需要注意到)。
3.2 使用方法
3.2.1 使用LocalDate 和LocalTime
首先是LocalDate,該類的實(shí)例是一個(gè)不可變對(duì)象,它只提供了簡單的日期,并不含當(dāng)天的時(shí)間信息。另外,它也不附帶任何與時(shí)區(qū)相關(guān)的信息。
- // 使用指定的日期創(chuàng)建LocalDate
- LocalDate date = LocalDate.of(2019, 1, 1);
- // 獲取當(dāng)前日期
- LocalDate today = LocalDate.now();
- // 獲取今日的屬性
- int year = date.getYear();
- Month month = date.getMonth();
- int day = date.getDayOfMonth();
- DayOfWeek dow = date.getDayOfWeek();
- int len = date.lengthOfMonth();
- boolean leap = date.isLeapYear();
- // 通過ChronoField的枚舉值獲取需要的屬性字段
- int year = date.get(ChronoField.YEAR);
接著是LocalTime,它表示了一天內(nèi)的某個(gè)時(shí)刻。
- LocalTime time = LocalTime.of(18, 18, 18);
- int hour = time.getHour();
- int minute = time.getMinute();
- int second = time.getSecond();
LocalDate和LocalTime都可以通過使用靜態(tài)方法parse來解析字符串進(jìn)行創(chuàng)建。
- LocalDate date = LocalDate.parse("2019-01-01");
- LocalTime time = LocalTime.parse("18:18:18");
也可以向parse方法傳遞一個(gè)DateTimeFormatter,該類的實(shí)例定義了如何格式化一個(gè)日期或者時(shí)間對(duì)象。它其實(shí)是老版java.util.DateFormat的替代品。
3.2.2 LocalDateTime
- // 直接創(chuàng)建LocalDateTime
- LocalDateTime dt1 = LocalDateTime.of(2019, Month.JANUARY, 1, 18, 18, 18);
- // 合并日期和時(shí)間
- LocalDate date = LocalDate.parse("2019-01-01");
- LocalTime time = LocalTime.parse("18:18:18");
- LocalDateTime dt2 = LocalDateTime.of(date, time);
- LocalDateTime dt3 = date.atTime(18, 18, 18);
- LocalDateTime dt4 = date.atTime(time);
- LocalDateTime dt5 = time.atDate(date);
- // 從LocalDateTime中提取LocalDate或者LocalTime
- LocalDate date1 = dt1.toLocalDate();
- LocalTime time1 = dt1.toLocalTime();
3.3.3 Instant
Instant類是為了方便計(jì)算機(jī)理解的而設(shè)計(jì)的,它表示一個(gè)持續(xù)時(shí)間段上某個(gè)點(diǎn)的單一大整型數(shù),實(shí)際上它是以Unix元年時(shí)間(傳統(tǒng)的設(shè)定為UTC時(shí)區(qū)1970年1月1日午夜時(shí)分)開始所經(jīng)歷的秒數(shù)進(jìn)行計(jì)算(最小計(jì)算單位為納秒)。
- // 傳遞一個(gè)秒數(shù)已創(chuàng)建該類的實(shí)例
- Instant.ofEpochSecond(3);
- // 傳遞一個(gè)秒數(shù)+納秒 2 秒之后再加上100萬納秒(1秒)
- Instant.ofEpochSecond(2, 1_000_000_000);
3.3.4 Duration與Period
Duration是用于比較兩個(gè)LocalTime對(duì)象或者兩個(gè)Instant之間的時(shí)間差值。
- Duration d1 = Duration.between(time1, time2);
- Duration d1 = Duration.between(dateTime1, dateTime2);
- Duration d2 = Duration.between(instant1, instant2);
Period是用于對(duì)年月日的方式對(duì)多個(gè)時(shí)間進(jìn)行比較。
- Period tenDays = Period.between(LocalDate.of(2019, 1, 1), lcalDate.of(2019, 2, 2));
當(dāng)然,Duration和Period類都提供了很多非常方便的工廠類,直接創(chuàng)建對(duì)應(yīng)的實(shí)例。
- 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);
3.3.5 操作、解析和格式化日期
- // 直接使用withAttribute的方法修改
- LocalDate date1 = LocalDate.of(2019, 1, 1);
- LocalDate date2 = date1.withYear(2019);
- LocalDate date3 = date2.withDayOfMonth(1);
- LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 1);
所有聲明了Temporal接口的類LocalDate、LocalTime、LocalDateTime以及Instant,它們都使用get和with方法,將對(duì)象值的讀取和修改區(qū)分開,如果使用了不支持的字段訪問字段,會(huì)拋出一個(gè)UnsupportedTemporalTypeException異常。類似的,plus方法和minus方法都聲明于Temporal接口。通過這些方法,對(duì)TemporalUnit對(duì)象加上或者減去一個(gè)數(shù)字,我們能非常方便地將Temporal對(duì)象前溯或者回滾至某個(gè)時(shí)間段,通過ChronoUnit枚舉我們可以非常方便地實(shí)現(xiàn)TemporalUnit接口。
3.3.6 更多定制化的處理時(shí)間
向重載的with方法傳遞一個(gè)定制化的TemporalAdjuster對(duì)象,可以更加靈活地處理日期。時(shí)間和日期的API已經(jīng)提供了大量預(yù)定義的TemporalAdjuster,可以通過TemporalAdjuster類的靜態(tài)工廠方法訪問它們。這些方法的名稱非常直觀,方法名就是問題描述。某些情況下,如果你需要定義自己的TemporalAdjuster,只需要聲明TemporalAdjuster接口并且自己實(shí)現(xiàn)對(duì)應(yīng)的方法即可。
- LocalDate date1 = LocalDate.of(2014, 3, 18);
- LocalDate date2 = date1.with(TemporalAdjuster.nextOrSame(DayOfWeek.SUNDAY));
- LocalDate date3 = date2.with(TemporalAdjuster.lastDayOfMonth());
3.3.7 解析日期-時(shí)間對(duì)象
日常工作中,格式化以及解析日期-時(shí)間對(duì)象是另一個(gè)非常重要的功能,而新的java.time.format包就是特別為我們達(dá)到這個(gè)目的而設(shè)計(jì)的。這其中,最重要的類是DateTimeFormatter。所有的DateTimeFormatter實(shí)例都能用于以一定的格式創(chuàng)建代表特定日期或時(shí)間的字符串。(與老的java.util.DateFormat相比較,所有的DateTimeFormatter實(shí)例都是線程安全的)
- // 使用不同的格式器生成字符串
- LocalDate date = LocalDate.of(2019, 1, 1);
- String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
- String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
- // 生成LocalDate對(duì)象
- LocalDate date1 = LocalDate.parse("20190101", DateTimeFormatter.BASIC_ISO_DATE);
- LocalDate date2 = LocalDate.parse("2019-01-01", DateTimeFormatter.ISO_LOCAL_DATE);
- // 使用特定的模式創(chuàng)建格式器
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
- LocalDate date1 = LocalDate.of(2019, 1, 1);
- String formattedDate = date1.format(formatter);
- LocalDate date2 = LocalDate.parse(formattedDate, formatter);
3.3.8 處理不同的時(shí)區(qū)和日歷系統(tǒng)
在新的日期-時(shí)間類庫中,為了最大程度上的減少在處理時(shí)區(qū)帶來的繁瑣和復(fù)雜而使用了新的java.time.ZoneId類(與其他日期和時(shí)間類一樣,ZoneId類也是無法修改的) 來替代老版的java.util.TimeZone。時(shí)區(qū)是按照一定的規(guī)則將區(qū)域劃分成標(biāo)準(zhǔn)時(shí)間相同的區(qū)間。在ZoneRules這個(gè)類中包含了40個(gè)這樣的實(shí)例。可以簡單地通過調(diào)用ZoneId的getRules()得到指定時(shí)區(qū)的規(guī)則。每個(gè)特定的ZoneId對(duì)象都由一個(gè)地區(qū)ID標(biāo)識(shí),地區(qū)ID都為“{區(qū)域}/{城市}”的格式。比如:
- ZoneId romeZone = ZoneId.of("Asia/Shanghai");
Java 8中在原先的TimeZone中加入了新的方法toZoneId,其作用是將一個(gè)老的時(shí)區(qū)對(duì)象轉(zhuǎn)換為ZoneId:
- ZoneId zoneId = TimeZone.getDefault().toZoneId();
得到的ZoneId對(duì)象后可以將它與LocalDate、LocalDateTime或者是Instant對(duì)象整合起來,構(gòu)造為一個(gè)ZonedDateTime實(shí)例,它代表了相對(duì)于指定時(shí)區(qū)的時(shí)間點(diǎn):
- LocalDate date = LocalDate.of(2019, Month.JANUARY, 1);
- ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
- LocalDateTime dateTime = LocalDateTime.of(2019, Month.JANUARY, 18, 13, 45);
- ZonedDateTime zdt2 = dateTime.atZone(romeZone);
- Instant instant = Instant.now();
- ZonedDateTime zdt3 = instant.atZone(romeZone);
通過ZoneId,還可以將LocalDateTime轉(zhuǎn)換為Instant:
- LocalDateTime dateTime = LocalDateTime.of(2019, Month.JANUARY, 18, 13, 45);
- Instant instantFromDateTime = dateTime.toInstant(romeZone);
同樣可以通過反向的方式得到LocalDateTime對(duì)象:
- Instant instant = Instant.now();
- LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);
與Joda-Time所不同的是,Java8中的日期-時(shí)間類庫提供了4種其他的日歷系統(tǒng),這些日歷系統(tǒng)中的每一個(gè)都有一個(gè)對(duì)應(yīng)的日志類,分別是ThaiBuddhistDate、MinguoDate 、JapaneseDate 以及HijrahDate 。所有這些類以及LocalDate 都實(shí)現(xiàn)了ChronoLocalDate接口,能夠?qū)珰v的日期進(jìn)行建模。利用LocalDate對(duì)象,你可以創(chuàng)建這些類的實(shí)例。同樣的,利用它們提供的靜態(tài)工廠方法,你可以創(chuàng)建任何一個(gè)Temporal對(duì)象的實(shí)例。
- LocalDate date = LocalDate.of(2019, Month.JANUARY, 1);
- JapaneseDate japaneseDate = JapaneseDate.from(date);
參考資料
Joda-Time 簡介 Joda Time項(xiàng)目和java8時(shí)間api
動(dòng)態(tài)計(jì)算時(shí)間段
需求:如現(xiàn)在是13:00,則時(shí)間段為15:10-17:10、17:10-19:10、19:10-21:10;即最早的出發(fā)時(shí)間為當(dāng)前時(shí)間+參數(shù)【2h10min】,最遲的時(shí)間段為開始時(shí)間在20點(diǎn)前結(jié)束時(shí)間在20點(diǎn)后的時(shí)間段),求解共有多少個(gè)時(shí)間段?
分析:
- 第一個(gè)時(shí)間段的開始時(shí)間:當(dāng)前時(shí)間+參數(shù)【2h10min】,中間的時(shí)間段是2h;
- 通過理解這句:最遲的時(shí)間段為開始時(shí)間在20點(diǎn)前結(jié)束時(shí)間在20點(diǎn)后的時(shí)間段,我們可以假設(shè)最大的時(shí)間變量為 max
- 假設(shè)當(dāng)前時(shí)間為now,總共有n個(gè)時(shí)間段,可以推導(dǎo)出公式:now + (2h * n) + 10min <= max;
注意:計(jì)算過程都轉(zhuǎn)換成毫秒
- public class Test {
- // 毫秒
- static final long slot = 130 * 60 * 1000;
- private static List<TimeSelectItem> buildStartEndTime(Long now, Long max) {
- // now + (2h * n) + 10min <= max;
- Long n = (max - now - 60 * 1000) / (120 * 60 * 1000);
- System.out.println("max:" + max);
- System.out.println("now:" + now);
- System.out.println(" max - now:" + (max - now));
- System.out.println("n:" + n);
- List<TimeSelectItem> timeSelectItems = new ArrayList<>();
- Long startTimestamp = now + slot;
- Long endTimestamp = startTimestamp + 120 * 60 * 1000;
- for (int i = 1; i <= n; i++) {
- // 起始時(shí)間
- // startTimestamp = startTimestamp + i * (120 * 60 * 1000);
- // 結(jié)束時(shí)間
- endTimestamp = startTimestamp + (120 * 60 * 1000);
- System.out.println(startTimestamp);
- System.out.println(endTimestamp);
- TimeSelectItem item = new TimeSelectItem();
- DateTime dt = new DateTime(startTimestamp);
- int hour = dt.hourOfDay().get();
- int millis = dt.getMinuteOfHour();
- String startTag = hour + ":" + millis;
- DateTime dt1 = new DateTime(endTimestamp);
- int hour1 = dt1.hourOfDay().get();
- long millis1 = dt1.getMinuteOfHour();
- String enTag = hour1 + ":" + millis1;
- item.setDisplayName(startTag + " - " + enTag);
- item.setStartTimestamp(startTimestamp);
- item.setEndTimestamp(endTimestamp);
- timeSelectItems.add(item);
- startTimestamp = endTimestamp;
- }
- return timeSelectItems;
- }
- public static void main(String[] args) {
- Long start = DateTime.now().getMillis();
- Calendar c = Calendar.getInstance();
- c.setTime(new Date());
- c.set(Calendar.HOUR_OF_DAY, 20);
- c.set(Calendar.MINUTE, 0);
- c.set(Calendar.SECOND, 0);
- DateTime dt = new DateTime();
- dt.withHourOfDay(20);
- Long end = c.getTimeInMillis();
- // List<TimeSelectItem> list = buildStartEndTime(1614747600000L, 1614772800000L);
- List<TimeSelectItem> list = buildStartEndTime(1614834000000L, end);
- for (TimeSelectItem item : list ) {
- System.out.println(item);
- }
- }
- }
【編輯推薦】