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

news2024/7/4 4:47:42

目录

  • 前言
  • 一、时区与时间
    • 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?

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【CocosCreator入门】CocosCreator组件 | Collider(碰撞)组件

Cocos Creator是一款流行的游戏开发引擎&#xff0c;具有丰富的组件和工具&#xff0c;其中碰撞系统组件是该引擎的重要组成部分。该组件可用于检测游戏中各个元素之间的碰撞&#xff0c;例如玩家角色与敌人、子弹与障碍物等。 目录 一、组件介绍 二、组件属性 2.1BoxCollid…

基于SpringBoot+微信小程序的农产品销售平台

基于SpringBoot微信小程序的农产品销售平台 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目…

Test Doubles测试替身: Testing in Distributed Systems and Real World

什么是Test Doubles In software testing, we developed unit tests and integration tests to test the codes functionality. However, in the real world, it is very common for a piece of code to interact with external components, for example, databases, public A…

【人工智能概论】pyplot作图中文显示、逐点坐标显示、保存图像

【人工智能概论】pyplot作图中文显示、逐点标记、保存图像 文章目录 【人工智能概论】pyplot作图中文显示、逐点标记、保存图像一. 简单的绘图二. 逐点坐标显示三. 中文显示四. 中文显示可能遇到的问题——缺少字体4.1 下载 SimHei.ttf4.2 复制 SimHei.ttf 到 Matplotlib 的 fo…

好的Robots.txt设计对Google收录有很大的帮助

Robots.txt 文件是用于指导搜索引擎爬虫在网站上爬行的标准。正确地设计 Robots.txt 文件可以帮助 Google 爬虫更好地理解您的网站结构&#xff0c;从而提高您的网站在 Google 搜索引擎上的收录率。 以下是一些设计 Robots.txt 文件的技巧&#xff0c;可以帮助 Google 爬虫更好…

security 报错:There is no PasswordEncoder mapped for the id “null“

security在登录的时候 无法登录成功 首先解读错误 下面百度翻译 安全框架设置了登录验证 说你没有密码编辑器 解决方法 一: 往容器中注册一个PasswordEncoder 解决方法二: 设置用户权限和角色的时候添加方法,加进去一个PasswordEncoder 只需要解决方案的话 下面的内容…

K8S系列之污点和容忍度详细分析

架构图 本篇文档主要介绍污点和容忍度的关系。 污点和容忍度 污点顾名思义就是脏的东西&#xff0c;给节点添加污点来限制pod调度到该节点上&#xff0c;如果pod可以容忍这种污点就可以被调度到有污点的节点上&#xff0c;如果不能容忍就不能被调度到该节点上。 污点作用于节…

排队领奖模式吸引新消费者,电商平台如何创新引流拓客?

在当前的电商市场中&#xff0c;由于竞争日趋激烈&#xff0c;很多电商平台产生了引流拓客缺乏新意的难题&#xff0c;即很难找到新的流量&#xff0c;并且难以把这些流量转化为消费者。在这个瞬息万变的时代&#xff0c;当然是谁有创意谁能吸引消费者&#xff0c;谁才能当道。…

Sequence-to-Sequence Knowledge Graph Completion and Question Answering

[2203.10321] Sequence-to-Sequence Knowledge Graph Completion and Question Answering (arxiv.org) 目录 1 Abstract 2 Introduction 3 KGT5 Model 3.1 Textual Representations & Verbalization 3.2 Training KGT5 for Link Prediction 3.3 Link Prediction Inf…

Inception Network

文章目录 一、Inception Network简介二、CNN的痛点三、Inception Network1. 1x1卷积核1.1 升维/降维&#xff1a;1.2. 调节参数数量&#xff1a;1.3. 增加非线性特性&#xff1a; 2. Inception原始模型3. Inception Module4. Inception Network 四、代码示例 一、Inception Net…

接口自动化测试 vs. UI自动化测试:为什么前者更快,更省力,更稳定?

从入门到精通&#xff01;企业级接口自动化测试实战&#xff0c;详细教学&#xff01;&#xff08;自学必备视频&#xff09; 目录 前言&#xff1a; 一、什么是接口自动化测试和 UI 自动化测试 二、为什么接口自动化测试效率比 UI 自动化测试高 1.执行速度 2.维护成本 3.…

AI人工智能与机器人的探索和应用1.1

文章来源于&#xff1a;https://mp.weixin.qq.com/s/fqivYVdakVKG-zDVfD4Qzg 研究机器人和人工智能的技术已有多年了&#xff0c;想来想去&#xff0c;觉得还是有必要对过往的技术做一些凝练和总结。在此过程中&#xff0c;除了能够将知识系统化&#xff0c;构建自己的知识体系…

三次输错密码后,系统是怎么做到不让我继续尝试的?

故事背景 忘记密码这件事&#xff0c;相信绝大多数人都遇到过&#xff0c;输一次错一次&#xff0c;错到几次以上&#xff0c;就不允许你继续尝试了。 但当你尝试重置密码&#xff0c;又发现新密码不能和原密码重复&#xff1a; 相信此刻心情只能用一张图形容&#xff1a; 虽…

python二次加工标准类型 | 包装与授权

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

洽洽离年轻人更近了,陈先保离百亿KPI呢?

文|螳螂观察 作者|图霖 留给陈先保实现百亿营收的时间不多了。 过去几年&#xff0c;洽洽凭借着在产品端、市场端、供应端的绝对实力&#xff0c;守住了休闲零食行业龙头的地位。但最新发布的2023年第一季度业绩报告&#xff0c;却出现了6年来首次营收、净利双双下降。 报告…

Excel大数据量单元格快速填充

个人简介&#xff1a;一个从会计转行数据分析师的三旬老汉 擅长领域&#xff1a;数据分析、数据仓库、大数据 博客内容&#xff1a;平时会将自己工作中遇到的问题进行归纳总结&#xff0c;分享给各位小伙伴&#xff0c;意在帮助大家少加班、不掉发&#xff0c;让我们相互学习&a…

const/static修饰成员函数+初始化列表

一、const修饰成员函数 首先&#xff0c;我们知道&#xff0c;所有的成员函数&#xff08;除static修饰的&#xff09;&#xff0c;编译器都会隐式传递一个this指针。 它的默认类型为 Type* const this&#xff0c;即this指针只能指向最左边的第一个传入的对象。 const修饰成…

Docker容器体系结构及特点

Docker容器体系结构及特点 Docker是一个应用容器引擎&#xff0c;通过Docker&#xff0c;管理员可以非常方便地对容器进行管理。Docker基于Go语言开发&#xff0c;并且遵从Apache 2.0开源协议。 Docker提供了对容器镜像的打包封装功能。利用Docker&#xff0c;开发者可以将他…

2023 最新版IntelliJ IDEA 2023.1创建Java Web 项目详细步骤(图文详解)

文章目录 &#x1f9ed; 版本情况JavaIDEATomcatmaven &#x1f30f; 创建步骤&#x1f697; 1、依次点击File >> New >> Project&#x1f693; 2、选择New Project 输入自己的项目名&#xff0c;选择JDK版本&#xff0c;而后点击create进行创建&#x1f695; 3、鼠…

开新能源汽车有充电焦虑吗?2022年国内充电桩数量已达521万台

哈喽大家好&#xff0c;新能源汽车的不断普及使充电桩市场快速发展起来&#xff0c;同时充电桩的技术正在不断改进&#xff0c;包括充电速度、安全性、互联网功能等多个方面。相比与日渐壮大的新能源汽车用户规模&#xff0c;充电桩的建设相对发展速度较慢&#xff0c;近几年来…