1.時間線
閏秒,許多計算機系統使用“平滑”方式來人爲地緊鄰閏秒之前讓時間變慢或變快,以保證每天都是86400秒。
Java的Date和Time API規範要求Java使用的時間尺度爲:
1.每天86400秒
2.每天正午與官方時間精確匹配
3.在其他時間點上,以精確定義的方式與官方時間接近匹配
Java中,Instant表示時間線的某個點。被稱爲新紀元的時間線原點被設置爲穿過倫敦格林尼治皇家天文臺的本初子午線所在時區的1970年1月1日的午夜。從該原點開始,時間按照每天86400秒向前或向後度量,精確到納秒。最大值是Instant.MAX是公元1 000 000 000年的12月31日。
靜態方法調用Instant.now()會給出當前的時刻。
equals和compareTo方法用來比較兩個Instant對象,因此可以將Instant對象用作時間戳。
靜態方法Duration.between,可以得到兩個時刻的時間差。
Duration是兩個時刻之間的時間量。可以調用ToNanos、toMillis、getSeconds、toMinutes、toHours和toDays來獲得Duration按照傳統單位度量的時間長度。
Duration對象的內部存儲所需的空間超過了一個long值,所以秒數存儲在一個long中,而納秒數存儲在一個額外的int中。如果計算要精確到納秒級,實際上需要整個Duration的存儲內容(下圖)。如果不要求這個高的精度,可以用long值執行計算,然後調用toNanos。
Instant和Duration類都是不可修改的類,所以諸如multipliedBy和minus都會返回一個新的實例。
public class TimeLine {
public static void main(String[] args) {
Instant start = Instant.now();
runAlgorithm();
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();
System.out.printf("%d milliseconds\n", millis);
Instant start2 = Instant.now();
runAlgorithm2();
Instant end2 = Instant.now();
Duration timeElapsed2 = Duration.between(start2, end2);
System.out.printf("%d milliseconds\n", timeElapsed2.toMillis());
boolean overTenTimesFaster = timeElapsed.multipliedBy(10).
minus(timeElapsed2).isNegative();
System.out.printf("The first algorithm is %smore than ten times faster", overTenTimesFaster ? "" : "not ");
}
public static void runAlgorithm() {
int size = 10;
List<Integer> list = new Random().ints().map(i -> i % 100).limit(size)
.boxed().collect(Collectors.toList());
Collections.sort(list);
System.out.println(list);
}
public static void runAlgorithm2() {
int size = 10;
List<Integer> list = new Random().ints().map(i -> i % 100).limit(size)
.boxed().collect(Collectors.toList());
while (!IntStream.range(1, list.size()).allMatch(
i -> list.get(i - 1).compareTo(list.get(i)) <= 0)) {
Collections.sort(list);
}
System.out.println(list);
}
}
2.本地時間
Java API有兩種人類時間,本地日期/時間和時區時間。1903年6月14日就是一個本地日期。1969年7月16日 09:32:00 EDT是一個時區時間,表示的是時間線上的一個精確時刻。
API的設計者不推薦使用時區時間,除非確實想要表示絕對時間的實例。生日、假日、計劃時間等通常最好表示成本地日期和時間。
LocalDate是帶有年、月、日的日期。爲了構建LocalDate對象,可以使用now或of靜態方法。
與UNIX和java.util.Date中使用的月從0開始計算,而年從1900開始計算的不規則慣用法不同,LocalDate通常使用月份的數字,或者使用Month枚舉。
計算程序員日,每年的第256天:
LocalDate programmersDay = LocalDate.of(2019, 1, 1).plusDays(255);
本地時間的Period,它表示的是流逝的年、月或日的數量。
until方法會產生兩個本地日期之間的時長,但每個月的天數不盡相同,爲了確定到底有多少天:
independenceDay.until(christmas, ChronoUnit.DAYS);
LocalDate的有些方法可能會創建並不存在的時間,比如1月31日加上1個月,不會產生2月31日,也不會拋出異常,而是會返回該月有效的最後一天:
LocalDate.of(2020, 1, 31).plusMonths(1);
LocalDate.of(2020, 3, 31).minusMonths(1);
//都產生2020年2月29日
getDayOfWeek會產生星期日期,即DayOfWeek枚舉的某個值。DayOfWeek.SUNDAY=7
DayOfWeek枚舉具有便捷的plus和minus,以7位模進行計算星期日期。例如DayOfWeek.SATURDAY.plus(3)會產生DayOFWeek.TUESDAY。
除了LoaclDate之外,還有MonthDay、YearMonth和Year可以描述部分日期。12月25日(沒有年)可以表示成MonthDay對象。
public class LocalDates {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
System.out.println("today: " + today);
LocalDate alonzosBirthday = LocalDate.of(1903, 4, 14);
alonzosBirthday = LocalDate.of(1903, Month.JUNE, 14);
System.out.println("alonzosBirthday: " + alonzosBirthday);
LocalDate programmersDay = LocalDate.of(2018, 1, 1).plusDays(255);
System.out.println("programmersDay: " + programmersDay);
LocalDate nationalDay = LocalDate.of(2019, Month.NOVEMBER, 1);
LocalDate midAutumnFestival = LocalDate.of(2019, Month.SEPTEMBER, 13);
System.out.println("Until nationalDay: " + midAutumnFestival.until(nationalDay));
System.out.println("Until nationalDay: " + midAutumnFestival.until(nationalDay, ChronoUnit.DAYS));
System.out.println(LocalDate.of(2020, 1, 31).plusMonths(1));
System.out.println(LocalDate.of(2020, 3, 31).minusMonths(1));
DayOfWeek startOfLastMillennium = LocalDate.of(1900, 1, 1).getDayOfWeek();
System.out.println("startOfLastMillennium: " + startOfLastMillennium);
System.out.println(startOfLastMillennium.getValue());
System.out.println(DayOfWeek.SATURDAY.plus(3));
}
}
3.日期調整器
TemporalAdjusters類提供了大量用於常見調整的靜態方法。某個月的第一個星期二:
LocalDate firstTuesday = LocalDate.of(2020, 1, 1).with(
TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));
一如既往,with方法會返回一個新的LocalDate對象,而不會修改原來的對象。
還可以通過實現TemporalAdjuster接口來創建自己的調整器,用於計算下一個工作日的調整器:
TemporalAdjuster NEXT_WORKDAY = w -> {
// lambda的參數類型爲Temporal,必須強制轉型
LocalDate result = (LocalDate) w;
do {
result = result.plusDays(1);
} while (result.getDayOfWeek().getValue() >= 6);
return result;
};
LocalDate today = LocalDate.now();
LocalDate backToWork = today.with(NEXT_WORKDAY);
System.out.println(backToWork);
或者可以使用ofDateAdjuster方法來避免強轉,該方法期望得到的參數爲UnaryOperator< LocalDate >的lambda表達式:
TemporalAdjuster NEXT_WORKDAY = TemporalAdjusters.ofDateAdjuster(w -> {
LocalDate result = w;
do {
result = result.plusDays(1);
} while (result.getDayOfWeek().getValue() >= 6);
return result;
});
4.本地時間
LocalTime表示當日時刻,例如15:30:00。可以用now或of方法創建其實例:
LocalTime rightNow = LocalTime.now();
LocalTime bedtime = LocalTime.of(22, 30);
// plus和minus按照一天24小時操作
LocalTime wakeup = bedtime.plusHours(8);
還有一個表示日期和時間的LocalDateTime類。這個類適合存儲固定時區的時間點。例如,排課或排程。但是如果需要處理不同時區的用戶,那麼就應該使用ZonedDateTime類。
5.時區時間
互聯網編碼分配管理機構(Internet Assigned Numbers Authority,IANA)保存着一個數據庫,裏面存儲着世界上已知的時區。Java使用了IANA數據庫。
每個時區都有一個ID,例如America/New_York。ZoneId.getAvailableZoneIds()查看所有可用的時區。
給定一個時區ID,靜態方法ZonedId.of(id)可以產生一個ZonedId對象。可以調用local.atZone(zoneId)用找個對象將LocalDateTime對象轉換爲ZonedDateTime對象,或者:
ZonedDateTime zonedDateTime = ZonedDateTime.of(2020, 1, 2, 14, 39, 0, 0, ZoneId.of("Asia/Shanghai"));
// 獲得Instant對象
System.out.println(zonedDateTime.toInstant());
// 2020-01-02T14:39+08:00[Asia/Shanghai]
// 2020-01-02T06:39:00Z
反過來,如果有一個時刻對象,調用instant.atZone(ZoneId.of(“UTC”))可以獲得格林尼治的ZonedDateTime對象,或者使用其他的ZoneId獲得地球上其他地方的ZoneId。
UTC代表協調世界時,這是英語“Coordinated Universal Time”和法文“Temps Universal Coordine”首字母縮寫的折中。
如果將時間設置在下個星期,不要直接加上一個7天的Duration,而是使用Period類。
public class ZonedTimes {
public static void main(String[] args) {
ZonedDateTime apollo11launch = ZonedDateTime.of(2020, 1, 2, 14, 39, 0, 0, ZoneId.of("Asia/Shanghai"));
System.out.println("apollo11launch: " + apollo11launch);
Instant instant = apollo11launch.toInstant();
System.out.println("instant: " + instant);
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.of("UTC"));
System.out.println("zonedDateTime: " + zonedDateTime);
// 中歐地區在3月31日2:00切換夏令時,試圖構建2:30,實際得到的是3:30
ZonedDateTime skipped = ZonedDateTime.of(LocalDate.of(2013, 3, 31),
LocalTime.of(2, 30), ZoneId.of("Europe/Berlin"));
System.out.println("skipped: " + skipped);
// 反過來,時鐘會回撥慢一個小時,同一個本地時間就會出現兩次
ZonedDateTime ambiguous = ZonedDateTime.of(LocalDate.of(2013, 10, 27),
LocalTime.of(2, 30), ZoneId.of("Europe/Berlin"));
ZonedDateTime anHourLater = ambiguous.plusHours(1);
System.out.println("ambiguous: " + ambiguous);
System.out.println("anHourLater: " + anHourLater);
ZonedDateTime meeting = ZonedDateTime.of(LocalDate.of(2013, 10, 31),
LocalTime.of(14, 30), ZoneId.of("America/Los_Angeles"));
System.out.println("meeting: " + meeting);
// 小心!夏令時不起作用
ZonedDateTime nextMeeting = meeting.plus(Duration.ofDays(7));
System.out.println("nextMeeting: " + nextMeeting);
nextMeeting = meeting.plus(Period.ofDays(7));
System.out.println("nextMeeting: " + nextMeeting);
}
}
6.格式化和解析
DataTimeFormatter類提供了用三種用於打印日期/時間值的格式器:
1.預定義的格式器
2.Locale相關的格式器
3.帶有定製模式的格式器
要使用標準的格式器,可以直接調用其format方法:
String formatted = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(apollo11launch);
標準格式器只要是爲了機器刻度的時間戳設計的。爲了向人類讀者表示日期和時間,可以使用Locale相關的格式器。
靜態方法ofLocalizedDate、ofLocalizedTime和ofLocalizedDateTime可以創建這種格式器。
這些方法使用了默認的Locale。爲了切換到不同的Locale,可以直接使用withLocale方法。
DayofWeek和Month枚舉都有getDisplayName方法,可以按照不同的Locale和格式給出星期日期和月份的名字。
java.time.format.DateTimeFormatter類被設置用來替代java.util.DateFormat。如果爲了向後兼容性而需要使用後者,那麼可以調用formatter.toFormat()。
可以指定模式,來定製自己的日期格式:
DateTimeFormatter.ofPattern(“E yyyy-MM-dd HH:mm”);
每個字母表示一個不同的時間域,而字母重複的次數對應於所選擇的特定格式。
爲了解析字符串中的日期/時間值,可以使用衆多的靜態parse方法之一。
public class Formatting {
public static void main(String[] args) {
ZonedDateTime apollo11launch = ZonedDateTime.of(1969, 7, 16, 9, 32, 0, 0,
ZoneId.of("America/New_York"));
String formatted = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(apollo11launch);
System.out.println(formatted);
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
formatted = formatter.format(apollo11launch);
System.out.println(formatted);
formatted = formatter.withLocale(Locale.FRENCH).format(apollo11launch);
System.out.println(formatted);
formatter = DateTimeFormatter.ofPattern("E yyyy-MM-dd HH:mm");
formatted = formatter.format(apollo11launch);
System.out.println(formatted);
// 調用使用了標準的ISO_LOCAL_DATE格式器
LocalDate churchsBirthday = LocalDate.parse("1903-06-14");
System.out.println("churchsBirthday: " + churchsBirthday);
// 使用一個定製的格式器
apollo11launch = ZonedDateTime.parse("1969-07-16 03:32:00-0400",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssxx"));
System.out.println("apollo11launch: " + apollo11launch);
for (DayOfWeek w : DayOfWeek.values()) {
System.out.print(w.getDisplayName(TextStyle.SHORT, Locale.ENGLISH) + " ");
}
}
}
7.與遺留代碼的互操作
Instant類近似於java.util.Date。Java SE 8中,Date有兩個額外方法:將Date轉換爲Instant的toInstant方法,以及反方向的轉換的靜態的from方法。
類似地,ZonedDateTime近似於java.util.GregorianCalendar,在Java SE 8中,這個類有細粒度的轉換方法。toZonedDateTime方法可以將GregorianCalendar轉換爲ZonedDateTime,而靜態的from方法可以執行方向轉換。
另一個可以用於日期和時間類的轉換集位於java.sql包中。還可以傳遞一個DateTimeFotmatter給使用java.text.Format的遺留代碼。