时间线
时间单位是以秒为单位,是从地球的自转中推导出来的。地球自转一周需要24个小时,即24 x 60 x 60 = 86400秒。但是地球有轻微的颤动,所以需要更加精确的定义。
1976年,人们根据铯133原子内的特性推导出了与其历史定义相匹配的秒的新的精确定义。
Java Date和Time API 规范要求Java使用的时间尺度为:
- 每天86400秒
- 每天正午与官方时间精确匹配
- 在其他时间点上,以精确定义的方式与官方时间接近匹配。
在Java中,Instant表示时间线上的某个点。
被称为“新纪元”的时间线原点被设置为穿过格林威治皇家天文台的本初子午线所处时区的1970年1月1日的午夜。这与UNIX/POSIX时间中使用的惯例相同。 从该原点开始,时间按照每天86400秒向前或向回度量,精确到纳秒。
Instance的值可向回追溯到10亿年(Instant.MIN)。最大值Instant.MAX是公元1000 000 000年的12月31日。
Instant.now()会给出当前的时刻。 可以按照常用的方式,用equals和compareTo方法来比较两个Instatnt的对象,因此可以将Instant对象用作时间戳。
使用静态方法计算两个时间的时间差:Duration.between.
代码语言:javascript复制Instant start = Instant.now();
runAlgorithm(); // 中间计算过程
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();
Duration是两个时刻之间的时间量。 通过调用toNanos、toMIllis、getSeconds、toMinutes、toHours和toDays来获得Duration按照传统单位度量的时间长度。
Duration对象的内部存储所需的空间超过了一个long值,因此秒数存储在一个long中,而纳秒数存储在一个额外的int中。如果想要让计算精确到纳秒级,那么就需要整个Duration的存储内容。 如果不要求那么高的精度,可以用long值来执行计算,然后直接调用toNanos。
注意:大约300年时间对应的纳秒数才会溢出long的范围。
例如:检测某个算法是否比另一个算法快10倍。
代码语言:javascript复制Duration timeElasped2 = Duration.between(start2, end2);
boolean overTenTimesFaster =
timeElasped.multipliedBy(10)*minus(timeElapsed2).isNegative();
// or timeElasped.toNanos() * 10 < timeElapsed2.toNanos();
用于时间的Instant和Duration的算术运算
方法 | 描述 |
---|---|
plus、minus | 在当前的Instant或Duration上加上或减去一个Duration |
plusNanos、plusMillis、plusSeconds、minusNanos、minusMillis、minusSeconds | 在当前的Instant或Duration上加上或减去给定时间单位的数值 |
plusMinutes、plusHours、plusDays、minusMinutes、minusHours、minusDays | 在当前Duration上加上或减去给定时间单位的数值 |
multipliedBy、dividedBy、negated | 返回由当前Duration乘以或除以给定long或-1而得到的Duration。注意,可以缩放Duration,但是不能缩放Instant |
isZero、isNegative | 检查当前的Duration是否是0或负值 |
注意:Instant和Duration类都是不可修改的类,所以诸如multipliedBy和minus这样的方法都会返回一个新的实例。
本地日期
Java API包含两种人类时间, 本地日期/时间 和时区时间。
本地日期/时间包含日期和当天的时间,但是与时区信息没有任何关联。
例如:2023年3月13日 就是一个本地日期。 因为这个日期既没有当前的时间,也没有时区信息,因此不对应精确的时刻。
例如:2023年3月13日 17:09:00 Asia/Shanghai 是一个时区日期/时间,表示的是时间线上的一个精确的时刻。
某些情况下,时区甚至是一个障碍。例如安排每周10:00开一次会议。 如果加7天(即7×24×60×60秒)到最后一次会议的时区时间上,可能会碰巧跨越夏令时的时间调整边界,这次会议可能会早一个小时或晚一个小时。
除非确实想要表示绝对时间的实例,不推荐使用时区时间。 生日、假日、计划时间等通常最好都表示成本地日期和时间。
LocalDate是带有年、月、日的日期。
代码语言:javascript复制LocalDate today = LocalDate.now();
LocalDate alonzosBirthday = LocalDate.of(1903, 6, 14);
alonzosBirthday = LocalDate.of(1903, Month.JUNE, 14);
// 可以使用Month的枚举类型
LocalDate的方法
方法 | 描述 |
---|---|
now, of | 构建一个LocalDate,要么从当前时间构建,要么从给定的年月日构建。 |
plusDays,plusWeeks,plusMonths,plusYears | 在当前的LocalDate上加上一定量的天、星期、月或年 |
minusDays,minusWeeks,minusMonths,minusYears | 在当前的LocalDate上减去一定量的天、星期、月或年 |
plus、minus | 加上或减去一个Duration或Period |
withDaysOfMonth,withDayOfYear,withMonth,withYear | 返回一个新的LocalDate,其月的日期、年的日期、月或年修改为给定的值 |
getDayOfMonth | 获取月的日期(在1到31之间) |
getDayOfYear | 获取年的日期(在1到366之间) |
getDayOfWeek | 获取星期日期,返回DayOfWeek枚举值 |
getMonth,getMonthValue | 获取月份的Month枚举值,或者是1 ~ 12之间的数字 |
getYear | 获取年份,在-999 999 999到999 999 999之间 |
until | 获取Period,或者两个日期之间按照给定的ChronoUnits计算的数值 |
isBefore,isAfter | 将当前的LocalDate与另一个LocalDate进行比较 |
isLeapYear | 如果当前是闰年,则返回true。即,该年份能够被4整除,但是不能被100整除,或者能够被400整除。该算法可以应用于已经过去的年份,尽管在历史上并不准确。 |
例如每年的第256天使程序员日。
代码语言:javascript复制LocalDate day = LocalDate.of(2023, 1, 1).plusDays(255);
两个Instant之间的时长是Duration,用于本地日期的等价物是Period,它表示的是流逝的年、月或日的数量。可以调用birthday.plus(Period.ofYears(1))获取下一年的生日。 也可以直接调用birthday.plusYears(1)。但是birthday.plus(Duration.ofDays(365))在闰年是错误的结果。
util方法:产生两个本地日期之间的时长: independenceDay.util(christmas),可以产生5个月21天的一段时长。确定一共有多少天,可以使用:independenceDay.util(christmas, ChronoUnit.DAYS);
警告:上述表中有些方法可能会创建并不存在的日期。例如:在1月31日加上1个月,不应该产生2月31日。这些方法并不会抛出异常,而是会返回该月有效的最后一天。
getDayOfWeek方法:产生星期日期,即DayOfWeek枚举的某个值。DayOfWeek.MONDAY的枚举值是1。
例如:LocalDate.of(1900, 1, 1).getDayOfWeek().getValue()返回1.
DayOfWeek枚举具有便捷方法plus和minus,以7为模型计算星期日期。
例如,DayOfWeek.SATURDAY.plus(3)会产生DayOfWeek.TUESDAY。
注意:周末实际上在每周的末尾。这与java.util.Calendar有所差异,在后者,星期六的值为1,而星期天的值为7.
日期调整器 - TmeporalAdjusters
对于日程安排应用来说,需要计算诸如“每个月的第一个星期二”日期。
TmeporalAdjusters类提供了大量用于常见调整的静态方法。 可以将调整方法的结果传递给with方法。
例如:计算某个月的第一个星期二
代码语言:javascript复制LocalDate firstTuesday = LocalDate.of(year, month, 1)
.with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));
with会返回一个新的LocalDate对象,不会修改原来的对象。
TemporalAdjusters类中的日期调整器
方法 | 描述 |
---|---|
next(weekday),previous(weekday) | 下一个或上一个给定的星期日期 |
nextOrSame(weekday),previousOrSame(weekday) | 从给定的日期开始的下一个或上一个给定的星期日期 |
dayOfWeekInMonth(n, weekday) | 月份中的n个weekday |
lastInMonth(weekday) | 月份中的最后一个weekday |
firstDayOfMonth(), firstDayOfNextMonth(), firstDayOfNextYear(), lastDayOfMonth(), lastDayOfYear() | 方法名描述的日期 |
也可以自定义创建自己的调整期。
例如:计算下一个工作日的调整器
代码语言:javascript复制TemporalAdjuster NEXT_WORKEDAY = w -> {
LocalDate result = (LocalDate)w;
do{
result = result.plusDays(1);
}while(result.getDayOfWeek().getValue() >= 6);
return result;
}
LocalDate backToWork = today.with(NEXT_WORKEDAY);
注意,lambda表达式的参数类型为Temporal,必须被强制转换成LocalDate。可以用ofDateAdjuster方法来避免强制类型转换,方法期望得到的参数是类型为Unary Operator<LocalDate>的lambda表达式。
代码语言:javascript复制TemporalAdjuster NEXT_WORKDAY = TemporalAdjusters.ofDateAdjuster(w -> {
LocalDate result = w;
do{
result = result.plusDays(1);
}while(result.getDayOfWeek().getValue() >= 6);
return result;
})
本地时间
LocalTime表示当日时刻,例如15:30:00。可以用now或of方法创建实例:
代码语言:javascript复制LocalTime now = LocalTime.now();
LocalTime bedTime = LocalTime.of(22, 30); // or LocalTime.of(22, 30, 0);
方法:
- plus
- minus
上述方法都是按照一天24小时循环操作的。
注意:LocalTime自身并不关心AM/PM.
LocalTime方法
方法 | 描述 |
---|---|
now,of | 静态方法构建一个LocalTime,要么从当前时间构建,要么从给定的小时和分钟,以及可选的秒和纳秒构建 |
plusHours,plusMinutes,plusSeconds,plusNanos | 在当前的LocalTime上加上一定量的小时、分钟、秒或纳秒 |
minusHours,minusMinutes,minusSeconds,minusNanos | 在当前的LocalTime上减去一定量的小时、分钟、秒或纳秒 |
plus,minus | 加上或减去一个Duration |
withHours,withMinute,withSecond,withNano | 返回一个新的LocalTime,其小时、分钟、秒和纳秒修改为给定值 |
getHour,getMinute,getSecond,getNano | 获取当前LocalTime的小时、分钟、秒或纳秒 |
toSecondOfDay,toNanoOfDay | 返回午夜到当前LocalTime的秒或纳秒的数量 |
isBefore,isAfter | 将当前的LocalTime与另一个LocalTime进行比较 |
表示日期和时间的LocalDateTime类。这个类适合存储固定时区的时间点。例如:排课或排程。
如果计算需要跨越夏令时,或者需要处理不同时区的用户,需要使用ZonedDateTime类。
时区时间
互联网编码分配管理机构(Internet Assigned Numbers Authority,IANA)保存着一个数据库,里面存储着世界上所有已知的时区(www.iana.org/time-zones),每年都会更新数次,而批量更新会处理夏令时的变更规则。
每个时区都有一个ID,例如America/New_York和Europe/Berlin。获取所有可用的时区,调用ZonedId.getAvailableZoneIds。
给定一个时区ID,通过静态方法ZoneId.of(id)产生一个ZoneId对象。调用local.atZone(zoneId)用这个对象将LocalDateTime对象转换成ZonedDateTime对象,或者通过调用静态方法ZonedDateTime.of(year, month, day, hour, minutes, second, nano, zoneId)构建一个ZonedDateTime对象。
代码语言:javascript复制ZonedDateTime nowTime = ZonedDateTime.of(2023, 3, 14, 10, 23 ,0, 0, ZonedId.of("Asia/Shanghai"));
这时一个具体的时刻,调用nowTime.toInstant可以获得对应的Instant对象。反过来,如果你有一个时刻对象,调用instant.toZone(ZonedId.of("UTC"))可以获得格林威治皇家天文台的ZonedDateTime对象,或者使用其他的ZonedId获得地球上其他地方的ZonedId。
注意:UTC代表“协调世界时”这是英文“Coordinated Universal Time”和法文“Temps Universel Coordiné”首字母缩写的折中,它与两种语言的缩写都不一致。UTC是不考虑夏令时的格林威治皇家天文台时间。
ZonedDateTime的许多方法和LocalDateTime的方法相同,它们大多数都很直接,但是夏令时带来了一些复杂性。
ZonedDateTime的方法
方法 | 描述 |
---|---|
now,of,ofInstant | 构建一个ZonedDateTIme,从当前时间构建,或从一个LocalDateTime、一个LocalDate、与ZoneId一起的年/月/日/分钟/秒/纳秒,或从一个Instant和ZoneId中创建。这些都是静态方法 |
plusDays,plusWeeks,plusMonths,plusYears,plusHours,plusMinutes,plusSeconds,plusNanos | 在当前的ZonedDateTime上加上一定量的时间单位 |
minusDays,minusWeeks,minusMonths,minusYears,minusHours,minusMinutes,minusSeconds,minusNanos | 在当前的ZonedDateTime上减去一定量的时间单位 |
plus,minus | 加上或减去一个Duration或Period |
withDayOfMonth,withDayOfYear,withMonth,withYear,withHour,withMinute,withSecond,withNano | 返回一个新的ZonedDateTime,其某个时间单位被修改为给定的值 |
withZoneSameInstant,withZoneSameLocal | 返回一个给定时区的新的ZonedDateTime,要么表示同一时刻,要么表示同一本地时间 |
getDayOfMonth | 获取月的日期 |
getDayOfYear | 获取年的日期 |
getDayOfweek | 获取星期日期,返回DayOfWeek枚举的某个值 |
getMonth,getMonthValue | 获取月份的Month枚举值 |
getYear | 获取年份 |
getHour,getMinute,getSecond,getNano | 获取当前的ZonedDateTime的小时、分钟、秒和纳秒 |
getOffset | 获取作为ZoneOffset实例的距离UTC的偏移量。偏移量位于-12:00 ~ 14:00之间变化。有些时区有小数偏移量。偏移量会随夏令时而发生变化 |
toLocalDate,toLocalTime,toInstant | 产生本地日期或本地时间,或者对应的Instant对象 |
isBefore,isAfter | 将当前的ZonedDateTime与另一个ZonedDateTime进行比较 |
当夏令时开始时,时钟要向前拨快一个小时。
如果构建的时间刚好落入了跳过去的一个小时内,会发生什么?
例如,2013年,中欧地区在3月31日2:00切换到夏令时,试图构建的时间是不存在的3月31日2:30,实际上得到的是3:30
代码语言:javascript复制ZonedDateTime skipped = ZonedDateTime.of(
LocalDate.of(2013, 3, 31),
LocalTime.of(2, 30),
ZonedId.of("Europe/Berlin")
);
// 实际得到的是 March 31 3:30
反过来,夏令时结束时,时钟要向回拨慢一个小时,同样一个本地时间就会出现两次。构建位于这个时间段内的时间对象时,会得到这两个时刻中较早的一个
代码语言:javascript复制ZonedDateTime ambiguous = ZonedDateTime.of(
LocalDate.of(2013, 10, 27),
LocalTime.of(2, 30),
ZonedId.of("Europe/Berlin")
);// 2013-10-27T02:30 02:00[Europe/Berlin]
ZonedDateTime anHourLater = ambiguous.plusHours(1);
// 2013-10-27T02:30 01:00[Europe/Berlin]
一个小时后的时间回具有相同的小时和分钟,但是时区的偏移量会发生变化。
在调整跨越夏令时边界的日期时特别注意。不要直接加上一个Duration。
代码语言:javascript复制ZonedDateTime nextTime = time.plus(Duration.ofDays(7));//error
// 错误的 不能准确到到下一个时间点
// 应该使用Peroid
ZonedDateTime nextTime = time.plus(Period.ofDays(7)); // ok
警告:OffSetDateTime类,它表示UTC具有偏移量的时间,但是没有时区规则的束缚。这个类被设计用于专用应用,这些应用特别需要提出这些规则的约束。例如某些网络协议。对于人类时间,还是应该使用ZonedDateTime
格式化和解析
DateTimeFormatter类提供了三种用于打印日期/时间值的格式器
- 预定义的格式器
- Locale相关的格式器
- 带有定制模式的格式器
预定义的格式器
格式器 | 描述 | 示例 |
---|---|---|
BASIC_ISO_DATE | 年、月、日时区偏移量,中间没有分隔符 | 19890716-0500 |
ISO_LOCAL_DATE, ISO_LOCAL_TIME, ISO_LOCAL_DATE_TIME | 分隔符为-、:、T | 1969-07-16,09:32:00, 1969-07-16T09:32:00 |
ISO_OFFSET_DATE, ISO_OFFSET_TIME, ISO_OFFSET_DATE_TIME | 类似ISO_LOCAL_XXX,但是有时区偏移量 | 1969-07-16-05:00, 09:32:00-05:00, 1969-07-16T09:32:00-05:00 |
ISO_ZONED_DATE_TIME | 有时区偏移量的时区ID | 1969-07-16T09:32:00-05:00[America/New_York] |
ISO_INSTANT | 在UTC中,用Z时区ID来表示 | 1969-07-16T14:32:00Z |
ISO_DATE,ISO_TIME, ISO_DATE_TIME | 类似ISO_OFFSET_xxx,但是时区信息时可选的 | 1969-07-16-05:00, 09:32:00-05:00, 1969-07-16T09:32:00-05:00[America/New_York] |
ISO_ORDINAL_DATE | LocalDate的年和日期 | 1969-197 |
ISO_WEEK_DATE | LocalDate的年、星期和星期日期 | 1969-W29-3 |
RFC_1123_DATE_TIME | 用于邮件时间戳的标准,编纂于RFC822,并在RFC1123中将年份更新到4位 | Wed, 16 Jul 1969 09:32:00 -0500 |
使用标准的格式器,可以直接调用其format方法:
代码语言:javascript复制String formatted = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(apollo11launch);// 1969-07-16T09:32:00-04:00
标准格式器主要时为了机器刻度的时间戳而设计的。
面向人类可读的日期和时间可以使用Locale相关的格式器。 对于日期和时间而言,有4中Locale相关的格式化风格,即SHORT、MEDIUM、LONG和FULL。
风格 | 日期 | 时间 |
---|---|---|
SHORT | 7/16/69 | 9:32 AM |
MEDIUM | Jul 16, 1969 | 9:32:00 AM |
LONG | July 16, 1969 | 9:32:00 AM EDT |
FULL | Wednesday, July 16, 1969 | 9:32:00 AM EDT |
静态方法ofLocalizedDate、ofLocalizedTime和ofLocalizedDateTime可以创建这中格式。
代码语言:javascript复制DateTimeFormatter formatter =
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
String formatted = formatter.format(apollo11launch);
// July 16, 1969 9:32:00 AM EDT
这些方法使用了默认的Locale,为了切换到不同的Locale,可以直接使用withLocale方法。
代码语言:javascript复制formatted =
formatter.withLocale(Locale.FRENCH).format(apollo11launch);
// 16 juillet 1969 9:32:00 EDT
DayOfWeek和Month枚举都有getDisplayName方法,可以按照不同的Locale和格式给出星期日期和月份的名字
代码语言:javascript复制for(DayOfWeek w : DayOfWeek.values()){
System.out.print(w.getDisplayName(TextStyle.SHORT, Locale.ENGLISH) " ")
}
注意:java.time.format.DateTimeFormatter类被设计用来替代java.util.DateFormat。如果为了向后兼容性而需要后者的示例,可以调用formatter.toFormat()
可以通过指定模式来定制自己的日期格式:
代码语言:javascript复制formatter = DateTimeFormatter.ofPattern("E yyyy-MM-dd HH:mm");
会将日期格式化为Wed 1969-07-16 09:32形式。
每个字母都表示一个不同的时间域,而字母重复的次数对应于所选择的特定格式。
常用的日期/时间格式的格式化符号
时间域或目的 | 示例 |
---|---|
ERA YEAR_OF_ERA MOTH_OF_YEAR DAY_OF_MONTH DAY_OF_WEEK HOUR_OF_DAY CLOCK_HOUR_OF_AM_PM AMPM_OF_DAY MINUTE_OF_HOUR SECOND_OF_MINUTE NANO_OF_SECOND 时区ID 时区名 时区偏移量 本地化的时区偏移量 | G: AD, GGGG: Anno Domini , GGGGG: A yy: 69 , yyyy: 1969 M: 7 , MM: 07, MMM: Jul , MMMM:July, MMMMM:J d: 6, dd: 06 e: 3 ,E: Wed , EEEE: Wednesday, EEEEE: W H: 9 , HH: 09 K: 9, KK: 09 a: AM mm: 02 ss: 00 nnnnnn: 000000 VV: America/New_York z: EDT, zzzz: Eastern Daylight Time x: -04, xx: -0400, xxx: -04:00, XXX: 与xxx相同,但是Z表示0 O: GMT-4, OOOO: GMT-04:00 |
为了解析字符串中的日期/时间值,可以使用静态parse方法。
代码语言:javascript复制LocalDate churchBirthday = LocalDate.parse("1903-06-14");
ZonedDateTime apollo11launch =
ZonedDateTime
.parse("1969-07-16 03:32:00-0400",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssxx"));
与遗留代码的互操作
全新的Java Date和Time API 必须能够与已有类之间进行互操作,特别是java.util.Date、java.util.GregorianCalendar和java.sql.Date/Time/Timestamp。
Instant类似于java.util.Date。Java SE8中有额外的方法:将Date转换为Instant的toInstant方法,以及反方向转换的静态的from方法。
ZonedDateTime近似于java.util.GregorianCalendar,Java SE8中,有更细粒度的转换。toZonedDateTime方法可以将GregorianCalendar转换为ZonedDateTime,而静态的from方法可以执行反方向的转换。
还有一类java.sql包中的日期和时间类。
可以传递一个DateTimeFormatter给使用java.text.Format的遗留代码。
java.time类与遗留代码的转换
类 | 转换到遗留类 | 转换自遗留类 |
---|---|---|
Instant ↔java.util.Date | Date.from(instant) | date.toInstant() |
ZonedDateTime ↔ java.util.GreGorianCalendar | GreGorianCalendar .from(zonedDateTime) | cal.toZonedDateTime() |
Instant ↔ java.sql.Timestamp | TimeStamp.from(instant) | timestamp.toInstant() |
LocalDateTime ↔ java.sql.Timestamp | Timestamp.valueOf(localDateTime) | timestamp.toLocalDateTime() |
LocalDate ↔ java.sql.Date | Date.valueOf(localDate) | date.toLocalDate() |
LocalTime ↔ java.sql.Time | Time.valueOf(localTime) | time.toLocalTime() |
DateTimeFormatter ↔ java.text.DateFormat | formatter.toFormat() | 无 |
java.util.TimeZone ↔ ZonedId | TimeZone.getTimeZone(id) | timeZone.toZoneId() |
java.nio,file.attribute.FileTime ↔ Instant | FileTime.from(instant) | fileTime.toInstant() |