1. 簡介
Java中的時間相關庫真是一言難盡,以至於被逼出了一個joda庫,Java時間處理庫一直被吐槽當然也是有原因的。
java.util.Date、java.util.Calendar不好用,但是也勉強夠用了。
Java8的時候,也許是KPI要求,他們終於決定對時間庫做點重構,哦,不,已經不叫重構了,叫重寫。
功能是完善了,但是對像我這樣的API調用愛好者來說卻太過複雜,不信你看,下面的類和接口你知道幾個:
- LocalTime
- LocalDate
- LocalDateTime
- OffsetDateTime
- ZonedDateTime
- TemporalAccessor
- Temporal
- ChronoField
- TemporalAdjusters
- TemporalAdjuster
- TemporalUnit
- ChronoUnit
- Year
- YearMonth
- ZoneOffset
- ZoneId
- Instant
- Period
- ChronoPeriod
- Clock
- TemporalAmount
- DateTimeFormatter
當然,不瞭解上面這些類和接口也並不影響我們調用API,但是多瞭解一點總是沒錯的。
當然,這裏我們也不會介紹全部的類和接口,我們會簡單的說一下它們的邏輯,重點介紹一點常用且使用的類,如下面這些類。
- LocalDate
- LocalDateTime
- DateTimeFormatter
- ZoneId
- Instant
- Duration
- Period
- TemporalAdjusters
2. LocalDate
2.1 創建
LocalDate的構造方法是私有的,創建只能通過靜態工程方法,主要是of和parse。
@Test
public void createLocalDate(){
LocalDate.now();//今天
LocalDate.of(2021, 1, 1);
LocalDate.of(2021, Month.JANUARY, 1);
LocalDate.parse("2021-01-01");
}
2.2 計算
新的日期庫最大的特點是日期計算真的非常方便,基本可以執行任何你需要的。
@Test
public void calcLocalDate(){
LocalDate now = LocalDate.now();
now.plusDays(7);//7天之後
now.plusYears(1);//1年之後
now.minusMonths(1);//1月之前
now.minusWeeks(1);//1周之前
}
2.3 比較
@Test
public void compareLocalDate(){
LocalDate now = LocalDate.now();
LocalDate first = LocalDate.of(2021, 1, 1);
System.out.println(now.isBefore(first));//now日期是不是早於first
System.out.println(now.isAfter(first));//now日期是不是晚於first
System.out.println(now.isEqual(first));//now和first是不是同一天
}
2.4 其他
@Test
public void otherLocalDate(){
LocalDate now = LocalDate.now();
now.toEpochDay();//當前日期時間戳對應的天
now.withYear(2021);//指定年份
now.until(LocalDate.now().plusDays(36));//計算2個日期的時間區間
now.atStartOfDay();//一天的開始時間
now.format(DateTimeFormatter.ISO_DATE_TIME);//格式化時間
}
3. LocalDateTime
LocalDateTime與LocalDate的使用方法基本一致,只是多了比LocalDate多了時間部分,他們之前的方法基本可以相互套用。
@Test
public void chronoFieldTest(){
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime.getLong(ChronoField.MINUTE_OF_HOUR));//當前的分鐘
System.out.println(localDateTime.getLong(ChronoField.HOUR_OF_DAY));//當前是一天的第多少個小時
System.out.println(localDateTime.getLong(ChronoField.DAY_OF_MONTH));//當前是一個月中的第多少天
System.out.println(localDateTime.getLong(ChronoField.DAY_OF_YEAR));//當前是一年中的第多少天
System.out.println(localDateTime.getLong(ChronoField.ALIGNED_WEEK_OF_MONTH));//當前是這個月的第多少周,不是按完整的周計算
System.out.println(localDateTime.getLong(ChronoField.EPOCH_DAY));//時間戳對應的天
}
@Test
public void chronoUnitTest(){
LocalDateTime localDateTime = LocalDateTime.now();
localDateTime.plus(1,ChronoUnit.YEARS);//一年之後
localDateTime.plus(1,ChronoUnit.HOURS);//一小時之後
localDateTime.plus(1,ChronoUnit.HALF_DAYS);//半天之後
localDateTime.plus(1,ChronoUnit.MONTHS);//一月之後
}
4. DateTimeFormatter
DateTimeFormatter很簡單,也很複雜,基本就是使用ofPattern方法指定格式化時間的格式就可以了。
@Test
public void datetimeFormatter(){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDateTime date = LocalDateTime.now();
System.out.println(formatter.format(date));
System.out.println(DateTimeFormatter.ISO_LOCAL_DATE.format(date));
System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(date));
System.out.println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(date));
System.out.println(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM).format(date));
System.out.println(DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG).format(date));
}
5. ZoneId與ZoneOffset
ZoneId是一個抽象類,主要是爲了處理時區問題的,ZoneOffset是ZoneId的實現類。
其實主要就涉及本地時間和時間戳的一個轉換問題。
@Test
public void zoneId(){
long now = System.currentTimeMillis();
Instant instant = Instant.ofEpochMilli(now);
ZoneId zoneId = ZoneId.of("Asia/Shanghai");//東八區北京時間
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zoneId);
System.out.println(now);
System.out.println(localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli());
System.out.println(localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());
System.out.println(localDateTime.toInstant(ZoneOffset.UTC).toEpochMilli());
}
對於上面的代碼,如果你能解釋爲什麼使用ZoneOffset.UTC,比正確的時區換算出來的毫秒要大,你就理解了Java時區這個問題。
如果對於時區還不太瞭解,可以看一下後面GTM、UTC與時間戳的內容。
6. Instant
@Test
public void instant() throws InterruptedException {
//1毫秒
Instant instant = Instant.ofEpochMilli(1);
System.out.println(instant.toEpochMilli());
//1秒再過100萬納秒
instant = Instant.ofEpochSecond(1,1_000_000_000);
System.out.println(instant.toEpochMilli());
Instant start = Instant.now();
TimeUnit.SECONDS.sleep(2);
Instant end = Instant.now();
Duration duration = Duration.between(start,end);
System.out.println(duration.toMillis());
}
7. Duration與Period
Duration和Period都有時間區間、時間段的意味,但是Period更強調週期,例如一年、一個月、一週等。
思考這樣一個問題:女朋友把每一年的第99天定爲愛情紀念日,2019年的第99天是2019-04-09,那麼明年的愛情紀念日應該用下面哪一種方式計算:
- Duration.ofDays(365)
- Period.ofYears(1)
如果,女朋友今天心情不好,想要問一下距離下一次愛情紀念日還有多少天?又應該用哪一種方式計算?
用下面的代碼測試一下吧,我只能幫你到這裏了。
@Test
public void test(){
LocalDate localDate = LocalDate.parse("2019-04-09");
System.out.println(localDate.getLong(ChronoField.DAY_OF_YEAR));
LocalDate durationLocalDate = localDate.plusDays(365);
System.out.println(durationLocalDate.format(DateTimeFormatter.ISO_DATE));
System.out.println(durationLocalDate.getLong(ChronoField.DAY_OF_YEAR));
LocalDate periodLocalDate = localDate.plus(Period.ofYears(1));
System.out.println(periodLocalDate.format(DateTimeFormatter.ISO_DATE));
System.out.println(periodLocalDate.getLong(ChronoField.DAY_OF_YEAR));
}
另外LocalDateTime使用Duration,LocalDate使用Period。
@Test
public void duration(){
LocalDateTime now = LocalDateTime.now();
LocalDateTime localDateTime = now.plusDays(1);
Duration duration = Duration.between(now, localDateTime);
System.out.println(duration.toDays());
Period period = Period.between(now.toLocalDate(), localDateTime.toLocalDate());
System.out.println(period.getDays());
}
@Test
public void duration(){
LocalDateTime now = LocalDateTime.now();
LocalDateTime localDateTime = now.plusYears(1);
Duration duration = Duration.between(now, localDateTime);
System.out.println(duration.toMillis());
System.out.println(Duration.ofDays(1).toHours());
}
8. TemporalAdjuster與TemporalAdjusters
如果你要日程安排類的相關功能,TemporalAdjuster絕對是一個好的選擇。
一看TemporalAdjusters,我們就能猜到應該是TemporalAdjuster的工具類,它提供了下面這些方法:
方法 | 說明 |
---|---|
lastDayOfYear | 今年的最後一天 |
firstDayOfYear | 當年的第一天 |
1astDayOLMonth | 下月的最後一天 |
next(DayOfWeek) | 下一個星期X |
firstDayOfMonth | 當月的第1天 |
lastDayOfNextYear | 明年的最後一天 |
lastDayOfNextMonth | 下月的最後一天 |
firstDayOfNextYear | 明年的第一天 |
firstDayOfNextMonth | 下個月的第1天 |
previous(DayOfWeek) | 前一個星期X |
nextOrSame(DayOfWeek) | 下一個星期X,如果當前就是星期X,那就是當前日期 |
lastInMonth(DayOfWeek) | 當月最後一個星期X |
firstInMonth(DayOfWeek) | 當月第一個星期X |
previousOrSame(DayOfWeek) | 前一個星期X,如果當前就是星期X,那就是當前日期 |
dayOfWeekInMonth(int,DayOfWeek) | 當月第Y個星期X,例如,母親節,5月的第2個星期天 |
到底怎麼使用呢?
看一些小例子就清楚了:
@Test
public void temporalAdjusters(){
LocalDate now = LocalDate.now();
LocalDate tmp;
//下一個星期五
TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.FRIDAY);
tmp = now.with(temporalAdjuster);
System.out.println(tmp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
//下一個星期五,如果當前日期是星期五,那麼就是當前日期
temporalAdjuster = TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY);
tmp = now.with(temporalAdjuster);
System.out.println(tmp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
temporalAdjuster = TemporalAdjusters.lastDayOfMonth();
tmp = now.with(temporalAdjuster);
System.out.println(tmp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
//當月月的最後一個週五
temporalAdjuster = TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY);
tmp = now.with(temporalAdjuster);
System.out.println(tmp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
//當月月第一個週六
temporalAdjuster = TemporalAdjusters.firstInMonth(DayOfWeek.SATURDAY);
tmp = now.with(temporalAdjuster);
System.out.println(tmp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
//母親節:5月第2個星期天
temporalAdjuster = TemporalAdjusters.dayOfWeekInMonth(2,DayOfWeek.SUNDAY);
tmp = now.withMonth(5).with(temporalAdjuster);
System.out.println(tmp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
//父親節:6月第3個星期天
temporalAdjuster = TemporalAdjusters.dayOfWeekInMonth(3,DayOfWeek.SUNDAY);
tmp = now.withMonth(6).with(temporalAdjuster);
System.out.println(tmp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
}
9. GTM、UTC與時間戳
9.1 GMT
GMT(Greenwish Mean Time): 格林威治標準時間,指位於倫敦郊區的皇家格林尼治天文臺的標準時間。
爲啥使用這個時間做標準時間呢?
因爲本初子午線被定義在通過那裏的經線。
爲啥本初子午線被定義在通過那裏的經線呢?
你的問題太多了,電影中知道太多的角色都沒有什麼好結局。
格林威治標準時間的正午是指當太陽橫穿格林尼治子午線時,也就是在格林尼治上空最高點的時間。由於地球在它的橢圓軌道里的運動速度不均勻,這個時刻可能和實際的太陽時相差16分鐘。地球每天的自轉是有些不規則的,而且正在緩慢減速。
所以,格林尼治時間已經不再被作爲標準時間使用。現在的標準時間是協調世界時(UTC),它由原子鐘提供。
9.2 UTC
UTC(Coordinated Universal Time):世界統一時間,世界標準時間、國際協調時間
雖然GMT不夠精確,但是我們通常讓我GMT與UTC是等價的。
9.3 時間戳
時間戳是指格林威治時間(GMT)自:1970-01-01 00:00:00至當前所經過的秒(10位)或者毫秒(13位)數。
所以,從時間戳的定義我們就知道了時間戳和時區沒有任何關係,不存在本地時間轉化問題,因此,在存儲時間戳的時候最好不要做加減換算,使用本地時間的時候才需要。
時間戳雖然不需要加減換算,但是時間戳到本地時間卻需要,因爲從時間戳定義我們知道,它關聯了本地時間GMT。
所以,時間戳轉本地時間是需要時區的,大致的流程是:
- 時間戳 -> GMT時間
- GMT時間 + 時區偏移 -> 本地時間,例如,我們在東八區,所以我們的時間就是GMT+8
當然我們計算的時候通常是先執行時間戳加減,然後再轉換爲時間,結果上是沒有問題的,邏輯上容易讓人誤解。