java.time包使用指南

news2024/10/5 15:31:31

目录

  • 前言
  • 一、时区与时间
    • 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/588737.html

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

相关文章

Uni-app学习从0到1开发一个app——(1)初步了解各种小程序开发框架

文章目录 0 引入1、小程序常用框架1.1、 mpvue1.2、 mpvue1.3、 Tina.js1.4、 WePY1.5 微信官方1.6 TouchUI WX 2、uin-app3、引用 0 引入 uin-app官网地址&#xff1a;https://uniapp.dcloud.net.cn/ 最近对于小程序莫名的感兴趣起来&#xff0c;索性就从uni-app开始吧 1、小…

spring boot使用elasticsearch分词,排序,分页,高亮简单示例

目录 1. 创建ES实体2. 创建查询实体3. 查询方法实现3.1 核心代码3.2 构建查询条件3.2.1 关键词分词 3.3 高亮处理 4.完整查询代码展示 记&#xff0c;写一个简单的es分词demo,es版本6.8.12 如果使用es7有些方法可能会有所改变&#xff0c;请参考7的文档 es安装教程&#xff1a;…

PMP-识别相关方的重要性

一、为什么要识别相关方 每个项目都有相关方&#xff0c;他们会受项目的积极或消极影响&#xff0c;或者能对项目施加积极或消极的影响。有些相关方影响项目工作或成果的能力有限&#xff0c;而有些相关方可能对项目及其期望成果有重大影响。项目经理和团队正确识别并合理引导所…

Apache的日志分割

一、日志分割的作用 我们知道当服务安装完成以后&#xff0c;都会在相关服务下有一个logs文件里面有着访问日志和错误日志。访问日志可以帮我们记录访问者的信息&#xff0c;错误日志一般在服务搭建&#xff0c;服务配置&#xff0c;启动时能够快速寻找错误原因的日志&#xf…

堆的应用(堆排序、TOP - K问题)

前言 &#x1f34e; 时间复杂度&#xff1a; &#x1f95d; 堆排序的最坏时间复杂度为 &#xff1a;O(n*lg(n)) &#x1f95d; TOP - K问题的最坏时间复杂度为&#xff1a;O(n*lg(k)) &#x1f341;前面我们学习了二叉树、以及堆的结构&#xff0c;也用顺序表的结构成功的把堆的…

【uni-app】使用外部组件不显示报错组件未找到解决方案

文章目录 前言一、问题描述二、温馨提示总结 前言 大家好&#xff0c;今天和大家分享一下uni-app使用外部组件不显示报错的一个解决方案&#xff0c;希望能够帮助到大家。 一、问题描述 我在使用uni-app在开发项目时引入了一个外部的组件库&#xff0c;刚开始引入的时候还是…

【复习笔记】FreeRTOS(二)创建和删除任务

本文是FreeRTOS复习笔记的第二节&#xff0c;创建和删除任务&#xff0c;使用的开发板是stm32f407VET6&#xff0c;创建两个任务&#xff0c;task1负责闪烁LED&#xff0c;task2负责按键控制&#xff0c;当按键按下时task1停止执行&#xff0c;任务的状态显示到TFT显示屏和串口…

【数据库系统及应用】— 日志、故障恢复、事务、并发控制、调度、检查点

日志文件是用于记录__________。 A、数据操作 B、程序运行过程 C、程序执行结果 D、对数据的所有更新操作 下列说法正确的是__________。 A、事务故障可以通过运行日志进行恢复 B、介质故障只需将备份恢复到系统中即可实现正确性 C、检查点是指检查发生故障并进行恢复的时刻点…

MySQL 对日期使用 DATE_FORMAT()函数

文章目录 DATE_FORMAT()函数显示今天是星期几只显示年月显示当前时间的分钟数和秒数 DATE_FORMAT()函数 前面使用日期时间函数&#xff0c;获取到的要么是 yyyy-mm-dd 形式的日期&#xff0c;要么是 hh:MM:ss 形式的时间&#xff0c;或者是 yyyy-mm-dd hh:mm:ss 形式的日期及时…

论文解读 | ICRA2022:用深度贝叶斯算法来估计ICP的协方差

原创 | 文 BFT机器人 01 研究背景 在点云处理中&#xff0c;ICP算法是一种常用的点云配准方法&#xff0c;通过将两个或多个点云对齐&#xff0c;可以进行后续的建模、识别和跟踪等处理。 然而&#xff0c;在ICP算法中&#xff0c;协方差估计起着非常重要的作用&#xff0c;它…

网络安全基础免杀

1. 会话提升的几种方式2. armitage的熟悉3. handler生成监听器的方法4. 防止会话假死5. 控制台设置编码6. upx加壳7. msfvenom木马payload持久化8. msfvenom木马编码 正文 免杀1 1. 会话提升的几种方式 python -c "import pty;pty.spawn(/bin/bash);" 会话提升 se…

linuxOps基础_linux文件打包压缩与解压缩

linux打包压缩概念 默认情况下&#xff0c;Linux的压缩概念一次只能压缩一个文件。针对多文件或文件夹无法进行直接压缩。所以需要提前对多个文件或文件夹进行打包&#xff0c;这样才可以进行压缩操作。 打包 1.txt 5MB 2.txt 10MB 3.txt 15MB1.txt 2.txt 3.txt 打包…

为kong网关添加key-auth插件实现安全认证

官方指导文档&#xff1a;https://docs.konghq.com/gateway/latest/get-started/key-authentication/ 一、新建一个用户 这里我们新建一个usernameluka的用户 [rootmin ~]# curl -i -X POST http://localhost:8001/consumers/ \ > --data usernameluka HTTP/1.1 201 Cr…

JavaEE HTTP状态码 HTTP数据报的构造

HTTP状态码HTTP数据报的构造 文章目录 JavaEE & HTTP状态码 & HTTP数据报的构造1. HTTP状态码1.1 200 - OK1.2 404 - Not Found1.3 403 - Forbidden1.4 500 - Internal Server Error1.5 504 - Gateway Timeout1.6 302/301 重定向 2. 构造HTTP请求2.1 浏览器搜索栏输入u…

聚焦2023北京安博会,超高清安防应用将成潮流

&#xff08;1&#xff09;2023北京安博会 中国安全防范产品行业协会主办并承办的第十六届&#xff08;2023&#xff09;中国国际社会公共安全产品博览会&#xff08;Security China 2023&#xff09;&#xff0c;将于2023年6月7&#xff5e;10日在北京首钢会展中心开幕。安博…

面试阿里测开岗失败后,被面试官在朋友圈吐槽了......

前一阵子有个徒弟向我诉苦&#xff0c;说自己在参加某大厂测试面试的时候被面试官怼得哑口无言&#xff0c;场面让他一度十分尴尬 印象最深的就是下面几个问题&#xff1a; 根据你以前的工作经验和学习到的测试技术&#xff0c;说说你对质量保证的理解&#xff1f; 非关系型…

Jenkins+JMeter实现自动化,性能压测玩转CICD!

目录 前言&#xff1a; 准备工作 编写Jenkinsfile 编写JMeter脚本 编写 Dockerfile 总结 前言&#xff1a; 性能压测是应用程序开发中不可或缺的一环。它通过模拟应用程序在真实环境下的负载情况&#xff0c;从而检测系统在高负载下的性能表现。而随着依托云架构部署业务的不…

2022年长三角高校数学建模竞赛B题齿轮箱故障诊断解题全过程文档及程序

2022年长三角高校数学建模竞赛 B题 齿轮箱故障诊断 原题再现&#xff1a; 齿轮箱是用于增加输出扭矩或改变电机速度的机械装置&#xff0c;被广泛应用于如汽车、输送机、风机等机械设备中。它由两个或多个齿轮组成&#xff0c;其中一个齿轮由电机驱动。电机的轴连接到齿轮箱的…

大数据 Ranger2.1.0 适配 Kafka3.4.0

Ranger2.1.0 适配 Kafka3.4.0 官方说明POM代码说明 根据官方说明Kafka3.0以上版本将会被替换权限认证方式&#xff0c;包括 类和方法 的变换&#xff0c;所以需要对ranger中继承 kafka 的实现中&#xff0c;修改相应的逻辑 官方说明 Kafka3.0以上版本将会被替换权限认证方式&a…

vue项目瘦身

如图 项目中node_modules包已占用十几G&#xff0c;我也是发现我的磁盘空间缩小的超级快&#xff0c;因为好几个项目&#xff0c;甚至有的项目包已经占了50多G&#xff0c;这都得益于上一个刚走了的laji npm install -g depcheck 一旦安装了depCheck&#xff0c;您可以在命令行…