Java 8 Time 关于java.time包中你可能不知道的使用细节

目录

  • 前言
  • 一、时区与时间
    • 1. 世界标准时:UTC、GMT、UT
    • 2. 地区时:Asia/Shanghai、UTC+8
    • 3. 时区:ZoneId、TimeZone
    • 4. 时间偏移量:ZoneOffset
    • 5. 时区简称:CTT、PRC
  • 二、主要时间类
    • 1. 重要时间接口:Temporal
    • 2. 时间单位、时间字段、时长:ChronoUnit、ChronoField、Duration
      • A .ChronoUnit
      • B .ChronoField
      • C .Duration
    • 3. 时间点:Instant
    • 4. 本地时间:LocalTime、LocalDate、LocalDateTime
    • 5. 偏移量时间:OffsetTime、OffsetDateTime
    • 6. 时区时间:ZonedDateTime
    • 7. 动态时间(时钟):Clock——SystemClock、FixedClock、OffsetClock、TickClock
    • 8. 时间属性查询:TemporalQuery
    • 9. 时间属性调整:TemporalAdjuster
    • 10. 时间转换(含Date)
  • 总结
    • 参考

前言

鉴于每次用到时间的相关方法时,都是通过谷歌搜索如何实现,自己对这些实现背后隐藏的内容知之甚少。所以,这次打算开一篇文章,学习JDK中主要的时间类,了解之前用到时间类、参数和方法代表的含义。



Java8 掌握Date与Java.time转换的核心思路,轻松解决各种时间转换问题




一、时区与时间

1. 世界标准时:UTC、GMT、UT

UTC(Coordinated Universal Time,世界标准时,世界协调时)是最主要的世界时间标准。UTC基于国际原子时,并通过不规则的加入闰秒来抵消地球自转变慢的影响。鉴于UTC已经是Java中主要的世界时表示方式,除非特殊提示否则下文中都UTC来表示世界标准时时间。


GMT(Greenwich Mean Time,格林尼治时间),由于地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林尼治平时基于天文观测本身的缺陷,目前已经被原子钟报时的协调世界时(UTC)所取代。


UT(Universal Time,世界时),是一种以格林威治子夜起算的平太阳时。世界时是以地球自转为基准得到的时间尺度,其精度受到地球自转不均匀变化和极移的影响。

Instant(1.8)是jdk8时间框架的基础类,它以世界标准时UTC作为基础时区构建,基本上jdk8中的时间类都会于它打交道,支持以GMT、UT作为基础时区。
TimeZone(1.7前)它以世界标准时GMT作为基础时区构建,不支持UTC、UT时区。

用Instant来直接输出UTC当前的时间:

// Instant.now()代表UTC当前时间的秒 + 纳秒(从1970-1-1 00:00:00 开始所经过秒-纳秒数)
Instant instant = Instant.now(); // UTC(Z) ~ GMT ~ UT
System.out.println(instant);

输出如下时间:

2023-03-02T07:53:02.377Z



2. 地区时:Asia/Shanghai、UTC+8

地区时:是适用相同时区规则的地理区域,是UTC偏移量位置处的时间。在同一时刻,地区时根据UTC距当地的偏移量来计算其本地时间。


中国占据东五区、东六区、东七区、东八区、东九区5个时区,为了统一时间,1949年中华人民共和国成立后,中国大陆全境统一划为东八区,同时以北京时间作为全国唯一的标准时间。北京时间,又名中国标准时间(China Standard Time,CST,UTC+8),是我国的标准时间,比世界协调时快八小时(UTC+8)。注意CST并不能作为北京时间的时区ID来生成ZoneId,原因见时区简称。

对比一下世界标准时、其它地区时和北京时间的时间差异:

Instant instant = Instant.now();
System.out.println("UTC:" + instant);
System.out.println("纽约 UTC-5:" + instant.atZone(ZoneId.of("America/New_York")));
System.out.println("东京 UTC+9:" + instant.atZone(ZoneId.of("Asia/Tokyo")));
System.out.println("上海 UTC+8:" + instant.atZone(ZoneId.of("Asia/Shanghai")));

输出内容如下:

UTC:2023-03-02T08:37:10.736Z
纽约 UTC-5:2023-03-02T03:37:10.736-05:00[America/New_York]
东京 UTC+9:2023-03-02T17:37:10.736+09:00[Asia/Tokyo]
上海 UTC+8:2023-03-02T16:37:10.736+08:00[Asia/Shanghai]

可以通过这些网站查询各个时区以及主要城市的时区:timeanddate,时区列表



3. 时区:ZoneId、TimeZone

时区:是地球上使用的同一时间定义的国家或区域,它描述了这个国家或区域内的偏移量范围。

  • ZoneId代表时区。它的内部描述了这个国家或区域的整体时间规则。 即ZoneId = ZoneOffset+ ZoneRules
  • ZoneRules代表这个时区的时区规则细则,包含这个国家或区域的时区偏移量的变动和这个变动的时间,不同的时间这个时区可能会有不同的时间偏移量(ZoneOffset)情况。比如Asia/ShanghaiAsia/Singapore,虽然当前它们都使用的UTC+8这个时区的区时,但是不代表这些国家或地区的曾经或者未来还会使用这个区时,我国就在1986年至1991年使用过六年夏令时,即每年在4月中旬的第一个星期日2时整(北京时间)到9月中旬第一个星期日的凌晨2时整(北京夏令时),夏令时内时间将会向后调快一小时,使用+9:00时区的区时。ZoneRules中就完整的记录了这些变动。

时区和区时:时区是一个国家或区域,区时是这个国家或区域的本地时间

在Java中,以ZoneId(1.8)和TimeZone(1.1)来表示时区概念。
ZoneId:

当前默认时区

ZoneId zoneId = ZoneId.systemDefault(); // 本质是调用TimeZone.getDefault().toZoneId()
TimeZone timeZone = TimeZone.getDefault();
// 可以设置程序全局的默认时区
TimeZone.setDefault(timeZone);

ZoneId(1.8):
如下程序,展示了通过时区ID获取该时区的本地时间:

System.out.println(ZonedDateTime.now(ZoneId.of("UTC"))); // 2023-03-22T07:22:03.133Z[UTC]
System.out.println(ZonedDateTime.now(ZoneId.of("GMT"))); // 2023-03-22T07:22:03.133Z[GMT]
System.out.println(ZonedDateTime.now(ZoneId.of("Asia/Shanghai"))); // 2023-03-22T15:22:03.133+08:00[Asia/Shanghai]

TimeZone(1.1):
Date类中没有时区概念 可以借助SimpleDateFormat实现:

TimeZone utc = TimeZone.getTimeZone("UTC+8");
TimeZone gmt = TimeZone.getTimeZone("GMT+8");
TimeZone ut = TimeZone.getTimeZone("UT+8");
Date date = new Date(); // 2023-04-14 11:04:17
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(utc);
System.out.println(sdf.format(date)); // 2023-04-14 03:04:17
sdf.setTimeZone(gmt);
System.out.println(sdf.format(date)); // 2023-04-14 11:04:17
sdf.setTimeZone(ut);
System.out.println(sdf.format(date)); // 2023-04-14 03:04:17

上面的程序中,UTC+8、GMT+8、UT+8三个时区并没有输出相同的时间,只有GMT+8正确的输出了当前时间,原因是TimeZone是基于GMT构建的,它并不支持UTC+8、UT+8这两种时间标准时的偏移设置。此外以UTC作为时区ID时可以被识别成功,UT则不行。

刚开始用的时候,由于使用UTC与UT没报错,我还以为TimeZone支持这两种世界标准时,但是通过分析源码才发现TimeZone会将所有没有解析成功的时区ID转为GMT。

private static TimeZone getTimeZone(String ID, boolean fallback) {
    TimeZone tz = ZoneInfo.getTimeZone(ID); // 时区ID中仅存在UTC,不存在其偏移设置即UTC+1这种格式;不存在UT。
    if (tz == null) {
        tz = parseCustomTimeZone(ID); // 解析GMT的自定义偏移时间设置,如GMT+14:17
        if (tz == null && fallback) {
            tz = new ZoneInfo(GMT_ID, 0); // 所有未被识别的时区都会被设置为“GMT+0”
.....
}

TimeZone中可以正确使用的ID


ZoneId与TimeZone兼容性
为了兼容ZoneId带来的改变,1.8版本中为TimeZone类添加了ZoneId与TimeZone互转的方法:

ZoneId zone = ZoneId.of("Asia/Shanghai");
TimeZone timeZone = TimeZone.getTimeZone(zone);
ZoneId toZoneId = timeZone.toZoneId();

查看可用时区
如上所示,通过ZoneId和TimeZone都是通过时区ID来创建对应时区,那么你要怎么知道有哪些时区ID可用呢?
可以通过如下方式查看可用ID:

// ZoneId可用的时区ID
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
// TimeZone可用的时区ID
String[] availableIds = TimeZone.getAvailableIDs();
// 或通过ZoneInfoFile查看时区ID
String[] zoneIds = ZoneInfoFile.getZoneIds();

查看指定时区
如果要查找指定偏移量位置的时区可以通过如下方式实现(单位毫秒):

// 查找位于+8时区的所有时区ID
String[] availableIDs = ZoneInfo.getAvailableIDs(8 * 60 * 60 * 1000);
String[] availableIDs1= TimeZone.getAvailableIDs(8 * 60 * 60 * 1000);

查看时区的历史区时
我国就在1986年至1991年使用过六年夏令时(每年在4月中旬的第一个星期日2时整(北京时间)到9月中旬第一个星期日的凌晨2时整(北京夏令时),时间调快一个小时)。ZoneId由于记录了这些时区的历史变动,所以它可以很轻易的得到我国实行夏令时当时的本地时间:

// 输出曾经的夏令时时间
ZoneId ctt = ZoneId.of("Asia/Shanghai");
Instant parse = Instant.parse("1986-06-01T00:00:00.000Z");
LocalDateTime localDateTime = LocalDateTime.ofInstant(parse, ctt); // 1986-06-01T09:00
ZoneOffset offset = ctt.getRules().getOffset(parse); // +09:00



4. 时间偏移量:ZoneOffset

时间偏移量,是时区和特定地点的时间差异,通常以小时和分钟为单位。由于世界各国位于地球不同位置上,因此不同国家,特别是东西跨度大的国家日出、日落时间必定有所偏差。这些偏差就是所谓的时差,或者时间偏移量、时区偏移量。


ZoneOffset支持的偏移量范围(-18:00 to +18:00),它表示的是从UTC开始的偏移量范围,相对于世界标准时所在的0°(经度)来说,每相隔一个时区(15°)即相差一小时,往东经一个时区,时间快1h,偏移量+1,反之相反。


ZoneOffset与ZoneId

  • ZoneId内部包含了一系列的偏移量,它表示的是这个国家和区域的历史偏移量变动情况,而且这个时区的偏移量存在持续变动的可能。
  • ZoneOffset表示的是固定偏移量,这些偏移量永远是固定的。(ZoneId和ZoneOffset的区别即是ZonedDateTime和OffsetDateTime的区别)

时间偏移量通常以如下的格式显示:

  • ±[hh]:[mm]
  • ±[hh][mm]
  • ±[hh]
  • ±[h]

在Java中,表示时间偏移量的类为:ZoneOffset(1.8)、TimeZone(1.1)

  1. ZoneId可以通过当前时间获取这个时区的当前偏移量ZoneOffset。
  2. ZoneOffset表明有指定数量的时差,类中的时间偏移量以秒或毫秒数存储,如偏移量为:+02:30,那么其存储的值为(2 * 60 + 30) * 60 /s。

ZoneIdZoneOffset

Instant instant = Instant.now();
// 由于时区的偏移量存在变动可能,因此需要根据当前时间来获取这个时区当前的时间偏移量
ZoneOffset offset = ZoneId.of("Asia/Shanghai").getRules().getOffset(instant);
ZoneOffset zoneOffset = ZoneOffset.of("+02:30"); // ZoneOffset.ofHoursMinutes(2, 30);

TimeZone

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT+12:24"));



5. 时区简称:CTT、PRC

时区ID是代表一系列偏移量的特定区域。时区简称则是时区ID的缩写或简称。

时区简称对照表

// ZoneId时区ID简称对照表
Map<String, String> shortIds = ZoneId.SHORT_IDS;

// ZoneInfoFile时区ID简称对照表,这也是TimeZone内置的简称表
Map<String, String> aliasMap = ZoneInfoFile.getAliasMap();

ZoneId(1.8)
上海时间(“CTT”, “Asia/Shanghai”)。

// 使用时区简称
ZoneId ctt = ZoneId.of("CTT", ZoneId.SHORT_IDS);
// 或者使用TimeZone版本的简称来获取时区ID,该表中也有("PRC", "Asia/Shanghai")
ZoneId zoneCst = ZoneId.of("CTT", ZoneInfoFile.getAliasMap());
// 或者自定义简称ID也行……

TimeZone(1.1)

// 使用时区简称 
// TimeZone做了一个兼容处理,如果找不到对应的时区ID,则尝试寻找简称对照表
TimeZone timeZoneCtt = TimeZone.getTimeZone("CTT");

注意

  1. ZoneId要使用时区简称,必须搭配时区简称表使用,而TimeZone则不用。
  2. ZoneId的简称更新后,与TimeZone中的简称存在不匹配的情况。因此在TimeZone使用一些简称时,转ZoneId可能会出现时区错乱,反之同理。
  3. CST(中国标准时间),在ZoneId和TimeZone中都无法正确识别,因为CST已经被美国的地方时区占用了:CST -> Central Standard Time (North America) America/Chicago。
  4. 一般我们要表达中国所在时区时,Asia/Shanghai、Asia/Chongqing、UTC+8、GMT+8、UT+8等时区ID、PRC、CTT等时区简称、+8、+08:00等偏移量设置都可以。





二、主要时间类

1. 重要时间接口:Temporal

Temporal:框架级接口,定义对时间对象的读写访问,比如对日期、时间、偏移量或这些对象的一些组合的读写访问。这个接口对实现的可变性没有限制,但是强烈建议将该类的实现类设为不可变(final)。
Temporal实例支持读写操作,它同样存在线程安全问题。同StringTemporal的所有实现类的写操作都会生产新的对象而非修改原对象,所以它们都是线程安全的。

主要实现类
在这里插入图片描述


主要方法

TemporalAccessor

  1. 判断当前时间类是否支持这个时间字段TemporalAccessor.isSupported(TemporalField field),如果不支持的话,那么执行参数中带TemporalField的方法都会报错。
  2. 获取当前时间类指定时间字段的值范围TemporalAccessor.range(TemporalField field)
  3. 获取当前时间类指定时间字段的值,精度为intTemporalAccessor.get(TemporalField field)
  4. 获取当前时间类指定时间字段的值,精度为longTemporalAccessor.getLong(TemporalField field)
  5. 获取当前时间类的指定属性值TemporalAccessor.query(TemporalQuery<R> query)

Temporal

  1. 判断当前时间类是否支持这个时间单位Temporal.isSupported(TemporalUnit unit),如果不支持的话,那么执行参数中带TemporalUnit的方法都会报错。
  2. 调整当前时间类的局部时间Temporal.with(TemporalField field, long newValue)
  3. 调整时间类Temporal.with(TemporalAdjuster adjuster)
  4. 时间类增加N个单位的时间Temporal.plus(long amountToAdd, TemporalUnit unit)
  5. 时间类减少N个单位的时间Temporal.minus(long amountToSubtract, TemporalUnit unit)
  6. 比较两个时间相差多少个时间单位Temporal.until(Temporal endExclusive, TemporalUnit unit)
  7. 时间类增加N个时间长度Temporal.plus(TemporalAmount amount)
  8. 时间类减少N个时间长度Temporal.minus(TemporalAmount amount)
    在这里插入图片描述



2. 时间单位、时间字段、时长:ChronoUnit、ChronoField、Duration

由于这三个类中有很多相同的方法,也有一些方法是基于TemporalAccessor和Temporal类的,所以可能会省略这一部分内容。

A .ChronoUnit

ChronoUnit(1.8)|TimeUnit(1.5):枚举类,代表一段日期周期的时间单位。

ChronoUnit枚举的时间单位

NANOS("Nanos", Duration.ofNanos(1)),
MICROS("Micros", Duration.ofNanos(1000)),
MILLIS("Millis", Duration.ofNanos(1000_000)),
SECONDS("Seconds", Duration.ofSeconds(1)),
MINUTES("Minutes", Duration.ofSeconds(60)),
HOURS("Hours", Duration.ofSeconds(3600)),
HALF_DAYS("HalfDays", Duration.ofSeconds(43200)),
DAYS("Days", Duration.ofSeconds(86400)),
// ……

ChronoUnit方法使用

ChronoUnit的使用比较简单:

  • 比较两个时间类相差多少个单位TemporalUnit.between(Temporal temporal1Inclusive, Temporal temporal2Exclusive),要求temporal2Exclusive参数能够转为temporal1Inclusive这个时间类,否则将会抛java.time.DateTimeException,它实际上调用的是Temporal.until方法。
Instant instant = Instant.now();
ChronoUnit hours = ChronoUnit.HOURS;
// 时间差值计算
System.out.println("不同时间类的相差时间:" + hours.between(instant.atOffset(ZoneOffset.ofHours(-5)), instant.atOffset(ZoneOffset.ofHours(8))));
System.out.println("不同时间类的相差时间:" + hours.between(instant.atZone(ZoneId.of("-5")), instant.atZone(ZoneId.of("+8"))));
System.out.println("不同时间类的相差时间:" + hours.between(LocalDateTime.ofInstant(instant, ZoneId.of("-5")), LocalDateTime.ofInstant(instant, ZoneId.of("+8"))));

计算结果如下:

不同时间类的相差时间:0
不同时间类的相差时间:0
不同时间类的相差时间:13

ChronoUnit.between计算结果差异原因:
OffsetDateTime、ZonedDateTime在计算差值的时候,会将两个类的偏移量和时区通过OffsetDateTime.withOffsetSameInstant(ZoneOffset offset)ZonedDateTime.withZoneSameInstant(ZoneId zone)方法转为相同的偏移量和时区,而后计算它们在相同时间线上的时间差值。而LocalDateTime则不然,由于它不具备任何时区和偏移量的性质,导致它就是一个固定的模糊时间(无法用于形容任何时间点上的时间),它的计算也只是比较两个时间的差值。

不同时间类的between计算:

long d1 = ChronoUnit.DAYS.between(LocalDateTime.now(), ZonedDateTime.now());
long d2 = ChronoUnit.DAYS.between(OffsetDateTime.now(), ZonedDateTime.now());
long d3 = ChronoUnit.DAYS.between(ZonedDateTime.now(), OffsetDateTime.now());
long d4 = ChronoUnit.DAYS.between(ZonedDateTime.now(), LocalDateTime.now()); // 抛出异常

在Temporal实现类中的运用

ChronoUnit在源码里的其它时间类中用处不小,一般用在如下方法中:

  1. 当前时间类是否支持这个时间单位Temporal.isSupported(TemporalUnit unit),如果不支持的话,那么执行下面几个方法都会报错。
  2. 时间类增加N个单位的时间Temporal.plus(long amountToAdd, TemporalUnit unit)
  3. 时间类减少N个单位的时间Temporal.minus(long amountToSubtract, TemporalUnit unit)
  4. 比较两个时间相差多少个时间单位Temporal.until(Temporal endExclusive, TemporalUnit unit)
LocalDate localDate = LocalDate.parse("2023-03-21");
System.out.println(localDate.isSupported(ChronoUnit.DAYS) && localDate.isSupported(ChronoUnit.MONTHS) 
	&& localDate.isSupported(ChronoUnit.YEARS)); // true
System.out.println(localDate.plus(10, ChronoUnit.DAYS)); // 2023-03-31
System.out.println(localDate.minus(1, ChronoUnit.MONTHS)); // 2023-02-21
System.out.println(localDate.until(localDate.minus(10, ChronoUnit.YEARS), ChronoUnit.YEARS)); // -10

B .ChronoField

ChronoUnit表示的是时间单位,那么ChronoField表示什么呢?

你可以把它理解为更易于人类查看和使用的友好时间字段

我们可以通过日历知道当前是今年的几月几号星期几,通过时钟知道当前是多少时分秒,通过太阳来判断一天中的上午和下午,那么Java时间类要怎么知道并将其转换表达给我们呢?在Java8中可以通过ChronoField时间字段类实现。


通过这个枚举,我们可以知道当前时钟的秒SECOND_OF_MINUTE、分MINUTE_OF_HOUR、小时HOUR_OF_DAY,一天什么时间处于上午或下午AMPM_OF_DAY,一年中当前时间是几月MONTH_OF_YEAR几号DAY_OF_MONTH星期几DAY_OF_WEEK。java通过取模求余等操作计算时间戳处于某个ChronoField枚举的哪一个时间范围内,从而计算出更直观的时间表现形式。

ChronoField部分枚举

//……
SECOND_OF_MINUTE("SecondOfMinute", SECONDS, MINUTES, ValueRange.of(0, 59), "second"),
MINUTE_OF_HOUR("MinuteOfHour", MINUTES, HOURS, ValueRange.of(0, 59), "minute"),
HOUR_OF_DAY("HourOfDay", HOURS, DAYS, ValueRange.of(0, 23), "hour"),
DAY_OF_WEEK("DayOfWeek", DAYS, WEEKS, ValueRange.of(1, 7), "weekday"),
DAY_OF_MONTH("DayOfMonth", DAYS, MONTHS, ValueRange.of(1, 28, 31), "day"),
MONTH_OF_YEAR("MonthOfYear", MONTHS, YEARS, ValueRange.of(1, 12), "month"),
// ……

简单提一下ValueRange.of(long minSmallest, long minLargest, long maxSmallest, long maxLargest)的四个参数:

  • minSmallest minLargest:这两个值是限定最小值范围的,表示的是最小值处于这个区间内[minSmallest, minLargest] 。
  • maxSmallest maxLargest:这两个值是限定最大值范围,表示的是最大值处于这个区间内[maxSmallest, maxLargest]。如上面的月份ValueRange.of(1, 28, 31),月份的最大值就位于[28, 31]这个区间内。

获取时间字段值
通过getFrom方法可以获取指定时间的某个时间字段,它实际调用的是TemporalAccessor.get或TemporalAccessor.getLong方法。

LocalDateTime now = LocalDateTime.now();
ChronoField.DAY_OF_YEAR.getFrom(now);
ChronoField.DAY_OF_MONTH.getFrom(now);
ChronoField.DAY_OF_WEEK.getFrom(now);

在TemporalAccessor实现类中的运用

ChronoField是TemporalField的实现类,广泛用于各个时间类中。一般ChronoField有如下用途:

  1. 判断当前时间类是否支持这个时间字段TemporalAccessor.isSupported(TemporalField field),如果不支持的话,那么执行参数中带TemporalField的方法都会报错。
  2. 获取当前时间类指定时间字段的值范围TemporalAccessor.range(TemporalField field)
  3. 获取当前时间类指定时间字段的值,精度为intTemporalAccessor.get(TemporalField field)
  4. 获取当前时间类指定时间字段的值,精度为longTemporalAccessor.getLong(TemporalField field)

使用起来都比较简单,需要注意:

  • get方法是获取now这个时间点的某个时间字段的,譬如是几点钟
  • range则是获取处于now这个时间点下的某个时间字段的范围,譬如这个时间点所在的月有多少天。
LocalDateTime now = LocalDateTime.now(); // 2023-04-21T18:34:57.537
System.out.println(now.isSupported(ChronoField.AMPM_OF_DAY) && now.isSupported(ChronoField.DAY_OF_MONTH)); // true
// 0代表am 1代表pm
System.out.println(now.get(ChronoField.AMPM_OF_DAY)); // 1
System.out.println(now.getLong(ChronoField.MILLI_OF_SECOND)); // 537
System.out.println(now.range(ChronoField.DAY_OF_MONTH)); // 1-30 月份范围是在1——28~31,4月只有30天,所以这里是1-30

C .Duration

Duration:代表时间的持续的长度,也就是时长,存储时间字段为seconds)和毫秒nanoseconds),在Duration中,1秒、1分30秒、2天10小时表示的都是时长。
Duration实例支持修改其单位和具体长度,它的所有修改操作都同String一样,会生产新的对象而非修改原对象,所以它是线程安全的。

时长计算

Instant now = Instant.now();
Duration days = Duration.ofDays(1);
System.out.printf("时间长度:%s,增加后:%s,倍数计算:%s \n", days, 
	days.plus(Duration.ofHours(6)), days.multipliedBy(10));
// 时间长度:PT24H,增加后:PT30H,倍数计算:PT240H

时间计算
Duration通过addTosubtractFrom来增加或减少时长,它实际调用的是Temporal.plus和Temporal.minus方法。

System.out.printf("时间加减:%s,增加后:%s,减少后:%s \n", now, days.addTo(now), days.subtractFrom(now));

// 时间加减:2023-03-10T09:44:20.420Z,增加后:2023-03-11T09:44:20.420Z,减少后:2023-03-09T09:44:20.420Z 

在TemporalAccessor实现类中的运用

Duration是TemporalAmount的实现类。一般Duration有如下用途:

  1. 时间类增加N个时间长度Temporal.plus(TemporalAmount amount)
  2. 时间类减少N个时间长度Temporal.minus(TemporalAmount amount)

Duration的使用与ChronoUnit有所不同,Duration是支持计算的时长,所以它不拘泥于某个具体的时间单位,你可以在一次时间加减中,添加任意的年月日时分秒日期

Instant now = Instant.now(); // 2023-04-23T03:53:40.525Z
Duration plus = Duration.ofDays(10).plus(Duration.ofHours(10)).plus(Duration.ofMinutes(10));
System.out.println(plus); // PT250H10M
System.out.println(now.plus(plus)); // 2023-05-03T14:03:40.525Z



3. 时间点:Instant

Instant:该类表示的是UTC时间线上的某个瞬时时间点的纳秒值,也因此它能很容易的转为各种时间类。

  • 同除了时钟类外的其它Temporal实现类一样,表示的是一个静态时间实例。
  • Instant实现了TemporalTemporalAccessor接口的所有方法,这些方法描述和使用见ChronoUnit、ChronoField、Duration。

主要属性

private final long seconds; // 秒
private final int nanos; // 纳秒

创建实例
Instant无法通过构造函数初始化,可以通过如下方式创建实例(后续的时间类创建方式大同小异,后面的类省略)

  • now:生成当前时间线上的时间实例
  • of:从一个时间实例或时间戳转换为当前类的时间实例
  • parse:根据时间格式从字符串解析时间
// Instant EPOCH = new Instant(0, 0); // EPOCH为Instant内部属性,表示时间1970-01-01 00:00:00
Instant instant = Instant.now();
Instant now = Instant.now(Clock.systemUTC());
Instant ofEpochMilli = Instant.ofEpochMilli(System.currentTimeMillis());
Instant parse = Instant.parse("2023-04-14T08:22:28.987Z");

时间对比
时间是用秒seconds和毫秒nanos存储,谁的值小谁就越早,反之越晚。其它时间类的比较也类似。

boolean after = instant.isAfter(minus); // true
boolean before = instant.isBefore(plus); // true



4. 本地时间:LocalTime、LocalDate、LocalDateTime

基于ISO-8601日历系统,它不属于UTC时间线上的任意一个时间点,仅仅表示一个含义较为模糊的本地时间。
前面多次提到它表述的是模糊时间,是因为它由Instant.getEpochSecond()+ZoneOffset.getTotalSeconds()转化而来,且转化后并没有保存额外的偏移量时间,导致它无法直接还原为UTC时间点。通俗来讲,给你一个LocalDateTime且不告诉你这个地区的时区或偏移量,你就不可能知道这个时间描述的是哪个时区或地点的时间。在国际化项目中一般不推荐使用本地时间,如果确实需要使用本地时间的话,需要确保你知道转换成这个本地时间的时差或时区值。

  • LocalDate:存储年月日数据,如2023-04-19。
  • LocalTime:存储时分秒纳秒数据,如13:45:30.123456789。
  • LocalDateTime:存储LocalTime、LocalDate数据,它集成了前两个类,后文方法都是以LocalDateTime为示例。
  • 这三个类都实现了TemporalTemporalAccessor接口的所有方法,方法描述和使用见ChronoUnit、ChronoField、Duration。

主要属性

// LocalTime
private final byte hour;
private final byte minute;
private final byte second;
private final int nano;
// LocalDate
private final int year;
private final short month;
private final short day;
// LocalDateTime
private final LocalDate date;
private final LocalTime time;

三者转换

// 转LocalDateTime
LocalDateTime of = LocalDateTime.of(localDate, localTime);
localDate.atTime(localTime);
localTime.atDate(localDate);
// 转LocalDate、LocalTime
of.toLocalDate();
of.toLocalTime();

获取时间字段
由于LocalDate、LocalTime中就保存时间的年 月 日时 分 秒 纳秒属性,所以可以轻易拿到它们。根据这七个基础属性,还可以延伸出其它内容:获取当前是星期几、几月几号等。

int year = now.getYear();
int monthValue = now.getMonthValue();
Month month = now.getMonth();
DayOfWeek dayOfWeek = now.getDayOfWeek();
int dayOfMonth = now.getDayOfMonth();

时间加减
LocalDateTime对时间的加减方法进行了扩展,它支持通过方法来直接加减年 月 日时 分 秒 纳秒,这些方法所产生的日期也同样是一个新的实例(日期不变时不会生成新实例)。

  • 这些加减方法会自行处理加减溢出的情况,比如2023-04-30 + 1天,那么结果为2023-05-01。
  • OffsetDateTime和ZonedDateTime 同名方法直接调用LocalDateTime方法。
// 不推荐使用这种方式来多次操作改动一个LocalDateTime对象,这种方式会频繁创建实例。
LocalDateTime plusAndMinus = now.plusYears(1).plusMonths(12).plusWeeks(1).minusDays(40)
	.minusHours(1).minusMinutes(1).plusSeconds(120).plusNanos(10);

局部时间调整
与上面的时间加减不同,with方法会直接修改LocalDate、LocalTime的属性值,并且根据变动结果生成一个新的实例对象

  • 日期时间不变不会产生新对象
  • OffsetDateTime和ZonedDateTime 同名方法是基于LocalDateTime的。
LocalDateTime withLocal = now.withYear(2022).withMonth(12).withDayOfMonth(23)
	.withHour(10).withMinute(55).withSecond(12).withNano(123456);



5. 偏移量时间:OffsetTime、OffsetDateTime

基于ISO-8601日历系统,表示某个UTC瞬时时间点偏移后的偏移时间。
OffsetDateTime用法与LocalDateTime大致相当,可以理解为如下内容:

  • OffsetDateTime = ZoneOffset + LocalDateTime
  • OffsetTime= ZoneOffset + LocalTime
  • 这两者都实现了TemporalTemporalAccessor接口的所有方法,方法描述和使用见ChronoUnit、ChronoField、Duration。

主要属性

// OffsetTime
private final LocalTime time;
private final ZoneOffset offset;
// OffsetDateTime
private final LocalDateTime dateTime;
private final ZoneOffset offset;

调整偏移量

  1. withOffsetSameLocal:仅调整偏移量。
OffsetDateTime of = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(8));
System.out.println(of); // 2023-04-23T17:32:09.640+08:00
System.out.println(of.withOffsetSameLocal(ZoneOffset.ofHours(-5))); // 2023-04-23T17:32:09.640-05:00
  1. withOffsetSameInstant:调整偏移量时同时调整本地时间至该偏移量处。
System.out.println(of.withOffsetSameInstant(ZoneOffset.ofHours(8))); // 2023-04-23T17:33:31.002+08:00
System.out.println(of.withOffsetSameInstant(ZoneOffset.ofHours(-5))); // 2023-04-23T04:33:31.002-05:00



6. 时区时间:ZonedDateTime

基于ISO-8601日历系统,表示的是某个时区的瞬时时间点。
这个类涵盖了所有日期和时间的属性:存储精度为毫秒、有本地日期时间、时区和偏移量信息。所以它是java8time包中功能最丰富的类,上面提到的方法ZonedDateTime基本上都可以调用。
ZonedDateTime用法与LocalDateTime也差不多:

  • ZonedDateTime= ZoneId + ZoneOffset + LocalDateTime
  • ZonedDateTime实现了TemporalTemporalAccessor接口的所有方法,方法描述和使用见ChronoUnit、ChronoField、Duration。

主要属性

private final LocalDateTime dateTime;
private final ZoneOffset offset;
private final ZoneId zone;

调整时区

  1. withZoneSameLocal:仅调整时区。
OffsetDateTime of = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(8));
System.out.println(of); // 2023-04-23T17:32:09.640+08:00
System.out.println(of.withOffsetSameLocal(ZoneOffset.ofHours(-5))); // 2023-04-23T17:32:09.640-05:00
  1. withZoneSameInstant:调整时区时同时调整本地时间为该时区的区时。
System.out.println(of.withOffsetSameInstant(ZoneOffset.ofHours(8))); // 2023-04-23T17:33:31.002+08:00
System.out.println(of.withOffsetSameInstant(ZoneOffset.ofHours(-5))); // 2023-04-23T04:33:31.002-05:00



7. 动态时间(时钟):Clock——SystemClock、FixedClock、OffsetClock、TickClock

Clock时钟:与前面的几种静态时间不同,它能实时获取当前时间线的时间点,包括日期和时间。时钟类虽然有时区概念,但是它的内部不会用到时区,所有输出的时间瞬时点或时间戳都是UTC+0的时间,这个时区一般会用在实例化其它时间类。
Clock是一个抽象类,它有如下4种实现类(静态内部类,只能通过Clock的静态方法创建实例):

  • SystemClock:系统时钟,调用该时钟获取时间,将会实时返回**System.currentTimeMillis()**的值。
  • FixedClock:静态时钟,调用该时钟的时间,将会一直返回初始化该时钟的时间。这个类是静态时间可以用来测试其它时钟类。
  • OffsetClock:偏移时钟,该时钟存在一个基础时钟Clock和偏移时长Duration,每次调用该时钟的时间时,将会返回基础时钟时间±偏移时间。该时钟一般用来模拟过去或未来的时间,以便触发某种操作。
  • TickClock:滴答时钟,该时钟存在一个基础时钟Clock和间隔时长Duration,调用该时钟获取时间,将会返回已经过时间线上距离该时长的最近的时间。

实例化Clock

Clock systemClock = Clock.systemDefaultZone();
Clock.fixed(Instant.now(), ZoneId.systemDefault());
Clock.offset(systemClock, Duration.ofDays(1));
Clock.tick(systemClock, Duration.ofMinutes(10));

以Clock实例化其它时间类
你可能会注意到,前面提到的几种类的now(Clock clock)方法就是以Clock为参数的构造方法,并且无参方法now()都是通过Clock.systemUTC()now(Clock.systemDefaultZone())来获取当前时间戳和本地默认时区的:

Instant instant = Instant.now(Clock.systemUTC());
LocalDateTime localDateTime = LocalDateTime.now(Clock.systemUTC());
OffsetDateTime offsetDateTime = OffsetDateTime.now(Clock.systemUTC());
ZonedDateTime zonedDateTime = ZonedDateTime.now(Clock.systemDefaultZone());

使用时钟类
Clock类有两个常用方法,Instant instant()获取当前的瞬时时间点、long millis()获取当前的时间戳,方法的使用还是比较简单的,这里就主要说一下TickClock。

Clock clock = Clock.fixed(Instant.parse("2023-05-04T01:20:20Z"), ZoneId.systemDefault());
Clock tick = Clock.tick(clock, Duration.ofMinutes(3));
Clock.tickMinutes(ZoneId.systemDefault());
Clock.tickSeconds(ZoneId.systemDefault());
System.out.println(clock.instant()); // 2023-05-04T01:20:20Z
System.out.println(tick.instant()); // 2023-05-04T01:18:00Z

这里间隔时长设置为3分钟,从0分钟开始,每3分钟会滴答一次,已过时间中离20分钟最近的一次滴答是18分钟。

这里将间隔时长设置为0-59分钟内的任意时间,这个滴答时钟运行都是正常的,奇怪的是设置为61时,这里的值就变为了2023-05-04T00:40:00Z。源码中文档的表述是:“Obtains a clock that returns instants from the specified clock truncated to the nearest occurrence of the specified duration”,翻译过来就是:获得一个时钟,该时钟返回从指定时钟截断到最近出现的指定时长的实例。这个时钟在源码中的计算方式为:millis - Math.floorMod(millis, tickNanos / 1000_000L)。不是很清楚这里官网的表述到底是什么意思,可以看看这篇:Why TickClock out of control when the Duration value more than 60mins



8. 时间属性查询:TemporalQuery

函数式接口

@FunctionalInterface
public interface TemporalQuery<R> {
    R queryFrom(TemporalAccessor temporal);
}

作用和功能
TemporalQuery的作用是查询时间实例(Instant、LocaDateTime、Year等)拥有的某个属性对象,比如获取这个时间下某个时间单位的长度、最小时间精度、时区、偏移量、本地时间等。

  • 能查询所有实现TemporalTemporalAccessor接口的类
  • 查询某个类属性的前提是这个时间类支持这个属性,否则会抛出异常:java.time.DateTimeException
  • TemporalAccessor.query(TemporalQuery<R> query)方法就是基于TemporalQuery。
Integer nano = Instant.now().query(temporal -> temporal.get(ChronoField.NANO_OF_SECOND)); // 获取时间的纳秒数:nanos
Integer milli = Instant.now().query(temporal -> temporal.get(ChronoField.MILLI_OF_SECOND)); // 获取时间的毫秒数:nanos / 1000_000
ZoneId query = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).query(TemporalQueries.zoneId()); // 获取时间的时区
LocalDate localDate = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).query(TemporalQueries.localDate()); //获取本地日期



9. 时间属性调整:TemporalAdjuster

函数式接口

@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}

作用和功能
TemporalAdjuster主要作用是根据当前的时间类调整来目标时间类,调整结果的类型为目标时间类。

  • 适用于所有TemporalTemporalAdjuster接口的实现类
  • 为没有某个属性的时间类赋予这个属性就会抛出异常:java.time.DateTimeException
  • Temporal.with(TemporalAdjuster adjuster)方法就是基于TemporalAdjuster,很多时间类的源码都用到了with这个功能。

比如我们要将LocalDateTime的时间调整为当天的最后一刻或者变更年份:

LocalDateTime now = LocalDateTime.now();
LocalDateTime adjustInto = (LocalDateTime) LocalTime.MAX.adjustInto(now); // 2023-04-19T23:59:59.999999999
LocalDateTime adjustInto1 = (LocalDateTime) Year.of(2025).adjustInto(now); // 2025-04-19T13:46:45.908

10. 时间转换(含Date)

时间转换可以参考这里:Java8 掌握Date与Java.time转换的核心思路,轻松解决各种时间转换问题





总结

最近加班严重,花了近两个月时间才把这篇文章完善得差不多了。本来打算只对简单了解一下LocalDateTime、LocalDate等常用类,没想到看源码越看越懵,没法只有多花了很多时间把整个体系了解了一下。总的来说,java.time还有很多类都没深入过,一些内容了解得还不够深,期待后续补全吧,不过像Year,YearMonth这种简单的类就不会再写文章出来了。可能后续会单独写写TemporalQuery和TemporalAdjuster的使用方法,以及各个时间类的转换、常用时间方法等等。

如果看到结尾觉得这篇文章还有用处的话,🙏请大家多支持支持,多多点赞收藏关注,球球了(_)。





参考

java.time包源码
Clock
Time zone
Class ZoneId
How to set time zone of a java.util.Date?
What’s the difference between Instant and LocalDateTime?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/18913.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

腾讯云轻量16核32G28M带宽服务器CPU流量性能测评

腾讯云轻量16核32G28M服务器3468元15个月&#xff0c;折合每月231元&#xff0c;28M公网带宽下载速度峰值可达3584KB/s&#xff0c;折合3.5M/秒&#xff0c;系统盘为380GB SSD盘&#xff0c;6000GB月流量&#xff0c;折合每天200GB流量。腾讯云百科来详细说下腾讯云轻量应用服务…

13 | visual studio与Qt的结合

1 前提 Qt 5.15.2 visual studio 2019 vsaddin 2.8 2 具体操作 2.1 visual studio tool 2.1.1 下载 https://visualstudio.microsoft.com/zh-hans/downloads/2.1.2 安装 开发

10_Uboot启动流程_2

目录 _main函数详解 board_init_f函数详解 relocate_code函数详解 relocate_vectors函数详解 board_init_r 函数详解 _main函数详解 在上一章得知会执行_main函数_main函数定义在文件arch/arm/lib/crt0.S 中,函数内容如下: 第76行,设置sp指针为CONFIG_SYS_INIT_SP_ADDR,也…

信息抽取与命名实体识别:从原理到实现

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

君子签:助力高校毕业、就业协议电子签,打通就业最后一公里

据介绍&#xff0c;2023届全国普通高校毕业生规模预计达1158万人&#xff0c;同比增加82万人。毕业季即将来临&#xff0c;全国各大高校毕业、就业材料签署压力大&#xff0c;盖章需求激增&#xff0c;如何快捷、高效地处理各类毕业、就业材料签署问题呢&#xff1f; 在教育部…

2023 年第八届数维杯数学建模挑战赛 赛题浅析

为了更好地让大家本次数维杯比赛选题&#xff0c;我将对本次比赛的题目进行简要浅析。本次比赛的选题中&#xff0c;研究生、本科组请从A、B题中任选一个 完成答卷&#xff0c;专科组请从B、C题中任选一个完成答卷。这也暗示了本次比赛的难度为A>B>C 选题人数初步估计也…

解决APP抓包问题「网络安全」

1.前言 在日常渗透过程中我们经常会遇到瓶颈无处下手&#xff0c;这个时候如果攻击者从APP进行突破&#xff0c;往往会有很多惊喜。但是目前市场上的APP都会为防止别人恶意盗取和恶意篡改进行一些保护措施&#xff0c;比如模拟器检测、root检测、APK加固、代码混淆、代码反调试…

程序员痛心流涕自述:“因为把自己代码给了别人,我亲手断送了自己的前程”

在求职的过程中&#xff0c;一般都会有投递简历、笔试、面试以及背调的环节&#xff0c;而在这几个环节中折戟沉沙的人也着实不少。 不少人觉得&#xff0c;在求职时简历需要优化&#xff0c;背调不能有瞒报、捏造的情况&#xff0c;而笔试面试则是纯纯的要靠硬实力。 虽然说…

Springboot +Flowable,服务任务ServiceTask执行的三种方式(二)

一.简介 ServiceTask 从名字上看就是服务任务&#xff0c;它的图标是像下面这样&#xff0c;截图如下&#xff1a; ServiceTask 一般由系统自动完成&#xff0c;当流程走到这一步的时候&#xff0c;不会自动停下来&#xff0c;而是会去执行我们提前在 ServiceTask 中配置好的…

Linux - 第12节 - 网络编程套接字

1.预备知识 1.1.理解源IP地址和目的IP地址 因特网上的每台计算机都有一个唯一的IP地址&#xff0c;如果一台主机上的数据要传输到另一台主机&#xff0c;那么对端主机的IP地址就应该作为该数据传输时的目的IP地址。但仅仅知道目的IP地址是不够的&#xff0c;当对端主机收到该数…

【新星计划-2023】什么是ARP?详解它的“解析过程”与“ARP表”。

一、什么是ARP ARP&#xff08;地址解析协议&#xff09;英文全称“Address Resolution Protocol”&#xff0c;是根据IP地址获取物理地址的一个TCP/IP协议。主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机&#xff0c;并接收返回消息&#xff0c;以此确…

Linux 中的文件锁定命令:flock、fcntl、lockfile、flockfile

在 Linux 系统中&#xff0c;文件锁定是一种对文件进行保护的方法&#xff0c;可以防止多个进程同时访问同一个文件&#xff0c;从而导致数据损坏或者冲突。文件锁定命令是一组用于在 Linux 系统中实现文件锁定操作的命令&#xff0c;它们可以用于对文件进行加锁或解锁&#xf…

Android UI深度理解:Activity UI视图结构

Activity UI视图结构 每个Activity都会获得一个窗口&#xff0c;那就是Window&#xff0c;它用于绘制用户的UI界面 Window是一个抽象类&#xff0c;提供了绘制窗口的一组通用API&#xff0c;PhoneWindow是它的唯一实现类 DecorView是所有应用窗口的根节点。是FrameLayout的子类…

windows下python下载及安装

下载python安装包 进入python官网&#xff1a;https://www.python.org/ 鼠标移动到“Downloads”->"Windows"上&#xff0c;可以看到最新版本是3.11.3版本 点击“Windows”按钮&#xff0c;可以去下载其他版本 标记为embeddable package的表示嵌入式版本&#x…

【C语言】通讯录(文件版)

前言 前面我们完成了通讯录的静态版本和动态版本&#xff0c;虽然功能已经比较完善了&#xff0c;但是前面的通讯录缺少了存储联系人的能力&#xff0c;所以我们学习了文件的操作管理&#xff0c;这里我们就用上一篇文章的知识来完成这次的文章吧。 关于通讯录的前两篇文章我放…

Windows无法完成格式化怎么办?正确的3个解决方法!

案例&#xff1a;Windows无法完成格式化怎么办 【由于我的U盘使用时间过长&#xff0c;很多文件都是不需要的&#xff0c;我想将其格式化&#xff0c;但插入电脑后&#xff0c;Windows根本无法完成格式化&#xff0c;这是为什么呢&#xff1f;我应该怎么做呢&#xff1f;求答案…

java版工程项目管理系统源码+系统管理+系统设置+项目管理+合同管理+二次开发

工程项目各模块及其功能点清单 一、系统管理 1、数据字典&#xff1a;实现对数据字典标签的增删改查操作 2、编码管理&#xff1a;实现对系统编码的增删改查操作 3、用户管理&#xff1a;管理和查看用户角色 4、菜单管理&#xff1a;实现对系统菜单的增删改查操…

【c语言小demo】登录demo | 账号密码验证功能

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ …

Linkage Mapper解密数字世界链接 专栏内容介绍

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: Linkage Mapper 解密数字世界链接 在数字时代&#xff0c;链接是信息的核心&#xff0c;链接地…

一文读懂ChatGPT(全文由ChatGPT撰写)

最近ChatGPT爆火&#xff0c;相信大家或多或少都听说过ChatGPT。到底ChatGPT是什么&#xff1f;有什么优缺点呢&#xff1f; 今天就由ChatGPT自己来给大家答疑解惑~ 全文文案来自ChatGPT&#xff01; 01 ChatGPT是什么 ChatGPT是一种基于人工智能技术的自然语言处理系统&…