怎么处理日期中的时区与夏令时问题

前言

书接上篇,在了解了 GMT、UTC、夏令时、时间戳等诸多概念后。作为一名 Java 开发者,我们最关心的当然是遇到日期时间需求时,如何解决跨时区、夏令时等等头大问题。

在 Java8 之前,我们处理日期时间需求时,使用 DateCalenderSimpleDateFormat,来处理日期和格式化解析日期时间。但是,这些类的 API 的缺点比较明显,比如可读性、易用性差,使用起来冗余繁琐,还有线程安全问题。

因此,Java8 推出了新的日期时间类。每一个类功能明确、类之间协作简单、API 定义清晰不踩坑,API 功能强大无需借助外部工具类即可完成操作,并且线程安全。

Java8 中日期、时间类的概述

java8 引入了一套全新的时间日期 API。java.time 包中的是类是不可变且线程安全的。新的时间及日期 API 位于 java.time 中,下面是一些关键类。

image.png

Instant

计算机存储的当前时间,本质上只是一个不断递增的整数。

Java 提供的 System.currentTimeMillis() 返回的就是以毫秒为单位的当前时间戳。

这个当前时间戳在 java.time 中以 Instant 类型表示,我们用 Instant.now() 获取当前时间戳,效果和 System.currentTimeMillis() 类似。

val now = Instant.now()
println(now.epochSecond) // 秒 1648027759
println(now.toEpochMilli()) // 毫秒 1648027759732
复制代码

Instant 类内部有两个核心字段:

public final class Instant
        implements Temporal, TemporalAdjuster, Comparable<Instant>, Serializable {
        
    /**
     * The number of seconds from the epoch of 1970-01-01T00:00:00Z.
     * 以1970年1月1日0时0分0秒(UTC时间)开始所经历的秒数。
     */
    private final long seconds;

    /**
     * The number of nanoseconds, later along the time-line, from the seconds field.
     * This is always positive, and never exceeds 999,999,999.
     * 秒字段中的纳秒数。始终是一个正数且不会超过 999,999,999。
     */
    private final int nanos;
}
复制代码

一个是以秒为单位的时间,一个是更精确的纳秒精度(注意不是以纳秒为单位的时间)。

val now = Instant.now()
println(now.toEpochMilli()) // 1648027840587 (毫秒)
println(now.nano) // 587945000 (纳秒精度)
复制代码

既然 Instant 就是时间戳,那么,给它附加上一个时区,就可以创建出 ZonedDateTime。

val ins = Instant.ofEpochSecond(1648028393)
val zdt = ins.atZone(ZoneId.systemDefault())
println(zdt) // 2022-03-23T17:39:53+08:00[Asia/Shanghai]
复制代码

可见,对于某一个时间戳,给它关联上指定的 ZoneId,就得到了 ZonedDateTime,继而获得了对应时区的 LocalDateTime

下面介绍两个比较实用的方法:

  1. 判断两个时间戳相隔了几个小时、几天、几个月?
val ins1 = Instant.parse("2022-03-23T17:39:53Z")
val ins2 = ins1.plusSeconds(TimeUnit.HOURS.toSeconds(23))

println(ins1.until(ins2, ChronoUnit.HOURS)) // 23
println(ins1.until(ins2, ChronoUnit.DAYS)) // 0(这两个时间间隔不满 24 小时)
复制代码
  1. 统计一段程序的运行时间。
val start = Instant.now()
doSomething() // 模拟耗时2秒
val end = Instant.now()
//采用Duration来处理时间戳的差
val nanos = Duration.between(start, end)
println("nanos = ${nanos.toNanos()}") // 2004176000 纳秒
println("mills = ${nanos.toMillis()}") // 2004 毫秒
复制代码

LocalDate

只提供简单的日期,并不包含时间信息。比如 2018-06-28。它可以用来存储生日,周年纪念日,入职日期等。

可以通过 LocalDate.now() 获取一个当前的 LocalDate 对象,也可以通过静态工厂方法 of 创建一个 LocalDate 对象。

// 获取当前日期
val now = LocalDate.now()

// 2020-08-26
val oldDate = LocalDate.of(2020, 8, 26)

println(now) // 2022-03-25
println(oldDate) // 2020-08-26
复制代码

可以使用各种 minusplus 方法直接对日期进行加减操作,比如以下代码实现了减一天和加一天,以及减一个月和加一个月。

val now = LocalDate.now()
    .minus(Period.ofDays(1))
    .plus(1, ChronoUnit.DAYS)
    .minusMonths(1)
    .plusMonths(1)
println(now) // 2022-03-25
复制代码

思考:对日期 2022-01-31 增加一个月,返回的日期是多少?

val date1 = LocalDate.of(2022, 1, 31).plus(1, ChronoUnit.MONTHS)
println(date1) // 2022-02-28

val date2 = LocalDate.of(2022, 1, 31).plus(30, ChronoUnit.DAYS)
println(date2) // 2022-03-02
复制代码

可以看到,对日期 2022-01-31 增加一个月,返回的日期的是 2022-02-28。 注意此 API 加一个月与加 30 天的区别。

还可以通过 with 方法进行快捷时间调节,比如:

  • 得到当前月的第一天
  • 得到当前年的第一天
  • 得到上一个周六
  • 得到本月最后一个周五
// 本月的第一天
val date1 = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth())
println(date1) // 2022-03-01

// 本年的第一天
val date2 = LocalDate.now().with(TemporalAdjusters.firstDayOfYear())
println(date2) // 2022-01-01

// 今天之前的上一个周六
val date3 = LocalDate.now().with(TemporalAdjusters.previous(DayOfWeek.SATURDAY))
println(date3) // 2022-03-19

// 本月的最后一个工作日
val date4 = LocalDate.now().with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY))
println(date4) // 2022-03-25
复制代码

还可以直接使用 lambda 表达式自定义时间调整。比如,为当前时间增加 100 天以内的随机数。

LocalDate.now().with { temporal ->
    temporal.plus(ThreadLocalRandom.current().nextLong(100), ChronoUnit.DAYS)
}
复制代码

除了计算外,还可以判断日期是否符合某个条件。比如,自定义函数,判断指定日期是否是家庭成员的生日。


fun isFamilyBirthday(date: TemporalAccessor): Boolean {
    val month = date.get(ChronoField.MONTH_OF_YEAR)
    val day = date.get(ChronoField.DAY_OF_MONTH)

    if (month == Month.FEBRUARY.value && day == 17)
        return true
    if (month == Month.SEPTEMBER.value && day == 21)
        return true
    if (month == Month.MAY.value && day == 22)
        return true

    return false
}
复制代码

然后,使用 query 方法查询是否匹配条件:

val isFamilyBirthday = LocalDate.now().query { temporal ->
    isFamilyBirthday(temporal)
}
复制代码

下面介绍一个比较实用的方法:计算两个日期差。

// 比如计算 2021年10月26日 和 2022年1月10日 的日期间隔
val date = LocalDate.of(2021, 10, 26)
val specifyDate = LocalDate.of(2022, 1, 10)

println(ChronoUnit.DAYS.between(date, specifyDate)) // 76
println(Period.between(specifyDate, date)) // P-2M-15D
println(Period.between(date, specifyDate).days) // 15
复制代码

注意:PeriodgetDays() 方法得到的只是最后的“零几天”,而不是算总的间隔天数。

LocalTime

只提供时间信息,不包含日期。

可以通过 LocalTime.now() 获取一个当前的 LocalTime 对象,也可以通过静态工厂方法 of 创建一个 LocalTime 对象。

// 获取当前时间
val now = LocalTime.now()
// 13:45:20
val time = LocalTime.of(13, 45, 20)

println(now) // 15:24:04.211591
println(time) // 13:45:20
复制代码

LocalDateLocalTime 都可以通过解析代表它们的字符串创建,使用静态方法 parse

val date = LocalDate.parse("2022-03-20")
val time = LocalTime.parse("15:26:36")

println(date) // 2022-03-20
println(time) // 15:26:36
复制代码

可以使用各种 minusplus 方法直接对时间进行加减操作,比如以下代码实现了减一小时和加一小时,以及减一分钟和加一分钟。

val now = LocalTime.now()
    .minus(Duration.ofHours(1))
    .plus(1, ChronoUnit.HOURS)
    .minusMinutes(1)
    .plusMinutes(1)

println(now) // 15:49:23.473146
复制代码

可以直接使用 lambda 表达式自定义时间调整。

LocalTime.now().with { temporal ->
    temporal.plus(1, ChronoUnit.HOURS)
}
复制代码

我们还可以判断一个时间是否在另一个时间之前还是之后。

val time1 = LocalTime.of(13, 56, 12)
val time2 = LocalTime.of(20, 1, 52)

println(time1.isBefore(time2)) // true
println(time1.isAfter(time2)) // false
复制代码

使用 until() 方法,计算两个时间差。

val now = LocalTime.parse("19:45:30.221287")
val time = LocalTime.parse("19:55:30.121563")

println(now)
println(now.until(time, ChronoUnit.HOURS)) // 0
println(now.until(time, ChronoUnit.MINUTES)) // 9
println(now.until(time, ChronoUnit.SECONDS)) // 599
复制代码

一天中的最大、最小和中午时间都可以在 LocalTime 类中的常量获得。当在数据库中查询给定时间段内的记录时,非常有用。

val maxTime = LocalTime.MAX
val minTime = LocalTime.MIN
val noonTime = LocalTime.NOON
复制代码

LocalDateTime

它包含了日期及时间。

可以通过 LocalDateTime.now() 获取一个当前的 LocalDateTime 对象,也可以通过静态工厂方法 of 创建一个 LocalDateTime 对象,还可以通过合并 LocalDateLocalTime 对象构造。

val now = LocalDateTime.now()

val nowDate = now.toLocalDate()
val nowTime = now.toLocalTime()
val now2 = LocalDateTime.of(nowDate, nowTime)

val now3 = LocalDateTime.of(2022, Month.MARCH, 18, 13, 15, 20)

println(now) // 2022-03-25T19:58:46.707791
println(now2) // 2022-03-25T19:58:46.707791
println(now3) // 2022-03-18T13:15:20
复制代码

LocalDateLocalTimeLocalDateTime 默认严格按照 ISO8601 规定的日期和时间格式进行打印,因为,将字符串转换为 LocalDateTime 就可以传入标准格式:

val localDateTime = LocalDateTime.parse("2022-01-22T15:16:17")
println(localDateTime) // 2022-01-22T15:16:17
复制代码

如果要自定义输出格式,或者要把一个非 ISO8601 格式的字符串解析成 LocalDateTime,可以使用新的 DateTimeFormatter

val dateTimeFormatter = DateTimeFormatterBuilder()
    .appendValue(ChronoField.YEAR) // 年
    .appendLiteral("/")
    .appendValue(ChronoField.MONTH_OF_YEAR) // 月
    .appendLiteral("/")
    .appendValue(ChronoField.DAY_OF_MONTH) // 日
    .appendLiteral(" ")
    .appendValue(ChronoField.HOUR_OF_DAY) // 时
    .appendLiteral(":")
    .appendValue(ChronoField.MINUTE_OF_HOUR) // 分
    .appendLiteral(":")
    .appendValue(ChronoField.SECOND_OF_MINUTE) // 秒
    .appendLiteral(".")
    .appendValue(ChronoField.MICRO_OF_SECOND) // 毫秒
    .toFormatter()

val localDateTime = LocalDateTime.parse("2020/01/02 12:34:56.789", dateTimeFormatter)
println(localDateTime) // 2020-01-02T12:34:56.000789
复制代码

可以使用各种 minusplus 方法直接对日期和时间进行加减操作,比如一下代码实现了减一天和加一天,减一个月和加一个月,减一小时和加一小时以及减一分钟和加一分钟。

val now = LocalDateTime.now()
    .minusMonths(1)
    .plus(1, ChronoUnit.MONTHS)
    .minusDays(1)
    .plusDays(1)
    .minusHours(1)
    .plus(1, ChronoUnit.HOURS)
    .minusMinutes(1)
    .plusMinutes(1)

println(now) // 2022-03-25T20:54:35.623162
复制代码

注意:月份加减会自动调整日期,例如从 2021-10-31 减去一个月得到的结果是 2021-09-30,因为 9 月没有 31 日。

val localDateTime = LocalDateTime.of(2022, 10, 31, 21, 26, 15)
println(localDateTime.minusMonths(1)) // 2022-09-30T21:26:15
复制代码

对日期和时间进行调整则使用 with 方法,例如 withHour(15) 会把 10:50:23 变为 15:50:23。

  • 调整年 withYear()
  • 调整月 withMonth()
  • 调整日 withDayOfMonth()
  • 调整时 withHour()
  • 调整分 withMinute()
  • 调整秒 withSecond()
val localDateTime = LocalDateTime.of(2022, 3, 25, 10, 50, 23)
val newLocalDateTime = localDateTime
    .withYear(2023)
    .withMonth(1)
    .withDayOfMonth(2)
    .withHour(15)
    .withMinute(8)
    .withSecond(10)

println(localDateTime) // 2022-03-25T10:50:23
println(newLocalDateTime) // 2023-01-02T15:08:10
复制代码

同样注意到调整月份时,会相应地调整日期,即把 2021-10-31 的月份调整为 9 时,日期也自动变为 30。

val localDateTime = LocalDateTime.of(2022, 10, 31, 21, 26, 15)
val newLocalDateTime = localDateTime.minusMonths(1)

println(localDateTime) // 2022-10-31T21:26:15
println(newLocalDateTime) // 2022-09-30T21:26:15
复制代码

ZoneOffset

距离 格林威治/UTC 的时区偏移量,例如 +02:00。

时区偏移量是时区与 格林威治/UTC 之间的时间差。这通常是固定的小时数和分钟数。世界不同的地区有不同的时区偏移量。

  1. 最小/最大偏移量。
val min = ZoneOffset.MAX
val max = ZoneOffset.MIN

println(min) // +18:00
println(max) // -18:00
复制代码
  1. 通过时分秒构造偏移量。
val zoneOffset1 = ZoneOffset.ofHours(8)
val zoneOffset2 = ZoneOffset.ofHoursMinutes(8, 10)
val zoneOffset3 = ZoneOffset.ofHoursMinutesSeconds(8, 10, 30)

println(zoneOffset1) // +08:00
println(zoneOffset2) // +08:10
println(zoneOffset3) // +08:10:30
复制代码

因为有夏令时规则的存在,让操作日期/时间的复杂度大大增加。但还好 JDK 尽量的屏蔽了这些规则对使用者的影响。因此:推荐使用时区 ZoneId 转换日期/时间,一般情况下不建议使用偏移量 ZoneOffset 去搞。

ZoneId

在 Java8 之前,Java 使用 TimeZone 来表示时区。而在 Java8 里使用了 ZoneId 表示时区,它代表了一个时区 ID,如如 Europe/Paris。它规定了一些规则可用于将一个 Instant 时间戳转换为本地日期时间LocalDateTime

时区 ZoneId 是包含有规则的,实际上描述偏移量何时以及如何变化的实际规则由 ZoneRules 定义。ZoneId 则只是一个用于获取底层规则的 ID。之所以采用这种方法,是因为规则是由政府定义的,并且经常变化,而ID是稳定的

对于 API 调用者来说只需要使用这个ID(也就是 ZoneId )即可,而无需关心更为底层的时区规则 ZoneRules。如:夏令时这条规则是由各国政府制定的,而且不同国家不同年一般都不一样,这个事就交由 JDK 底层的 ZoneRules 机制自行 sync,使用者无需关心。

  1. 获取系统默认的 ZoneId
// 获取所有可用的时区 size=600 这个数字不是固定的
val allZones = ZoneId.getAvailableZoneIds()

// 获取系统默认的时区
val zone = ZoneId.systemDefault()

println(allZones.size) // 600
println(zone) // Asia/Shanghai
复制代码

IANA(Internet Assigned Numbers Authority,因特网拨号管理局) 维护着一份全球所有已知的时区数据库,每年会更新几次,主要处理夏令时规则的改变。Java 使用了 IANA 的数据库。

  1. 指定字符串获取一个 ZoneId
val timeZoneSH = ZoneId.of("Asia/Shanghai")
val timeZoneNY = ZoneId.of("America/New_York")

println(timeZoneSH) // Asia/Shanghai
println(timeZoneNY) // America/New_York
复制代码
  1. 根据偏移量的到一个 ZoneId
val timeZone = ZoneId.ofOffset("UTC", ZoneOffset.of("+8"))
println(timeZone) // UTC+08:00
复制代码

这里第一个参数传的前缀,可用值为:"GMT", "UTC", or "UT"。当然还可以传空串,那就直接返回第二个参数ZoneOffset。

注意:根据偏移量得到的 ZoneId 内部并无现成时区规则可用,因此对于有夏令营的国家转换可能出问题,一般不建议这么去做。

  1. ZonedDateTime 中获得时区:
val timeZone = ZoneId.from(ZonedDateTime.now())
println(timeZone) // Asia/Shanghai
复制代码

值得强调的是,ZoneId(时区)和 ZoneOffset(偏移量)在概念和实际作用是有较大区别的,主要体现在:

  1. UTC 偏移量仅仅记录了偏移的小时分钟而已,除此之外无任何其它信息。举个例子:+08:00 的意思是比UTC时间早 8 小时,没有地理/时区含义,相应的 -03:30 代表的意思仅仅是比 UTC 时间晚 3 个半小时。
  2. 时区是特定于地区而言的,它和地理上的地区(包括规则)强绑定在一起。比如整个中国都叫东八区,纽约在西五区等等。

中国没有夏令时,所有东八区对应的偏移量永远是 +8;纽约有夏令时,因此它的偏移量可能是 -4 也可能是 -5:

val zone = ZoneId.of("America/New_York")
val localDateTime1 = LocalDateTime.of(2022, 3, 28, 10, 26, 38)
val localDateTime2 = LocalDateTime.of(2022, 11, 28, 10, 26, 38)

println(zone.rules.getOffset(localDateTime1).id) // -04:00
println(zone.rules.getOffset(localDateTime2).id) // -05:00
复制代码

中国在东 8 区,美国纽约在西 5 区,我们之间的时间差可能为 12 小时(夏令时),也可能为 13 小时:

val localDateTime = LocalDateTime.of(2022, 3, 25, 18, 10, 26)

val zonedDateTimeSH = localDateTime.atZone(ZoneId.of("Asia/Shanghai"))
val zonedDateTimeNY = zonedDateTimeSH.withZoneSameInstant(ZoneId.of("America/New_York"))

// 时间相差 12 小时(因为3月25日正处于美国夏令时的时间段内)
println(zonedDateTimeSH) // 2022-03-25T18:10:26+08:00[Asia/Shanghai]
println(zonedDateTimeNY) // 2022-03-25T06:10:26-04:00[America/New_York]

val localDateTime2 = LocalDateTime.of(2022, 1, 25, 18, 10, 26)

val zonedDateTimeSH1 = localDateTime2.atZone(ZoneId.of("Asia/Shanghai"))
val zonedDateTimeNY1 = zonedDateTimeSH1.withZoneSameInstant(ZoneId.of("America/New_York"))

// 时间相差 13 小时
println(zonedDateTimeSH1) // 2022-01-25T18:10:26+08:00[Asia/Shanghai]
println(zonedDateTimeNY1) // 2022-01-25T05:10:26-05:00[America/New_York]
复制代码

综合来看,ZoneIdZoneOffset 更好用。若你使用 UTC 偏移量去表示那么就很麻烦,因为夏令时的存在,它是可变的。但若你使用 ZoneId 时区去表示就很方便,比如纽约是西五区,你在任何时候获取的当地时间都是能得到正确答案的,因为它内置了对夏令时规则的处理,不需要 API 调用者关心。

ZonedDateTime

LocalDateTime 不带有时区属性,所以命名为本地时区的日期时间。要表示一个带时区的日期和时间,我们就需要 ZonedDateTime

ZonedDateTime = LocalDateTime + ZoneId,具有时区属性。ZoneIdjava.time 引入的新的时区类,注意和旧的 java.util.TimeZone 的区别。

要创建一个 ZonedDateTime 对象,有以下几种方法。

  1. 通过 now() 方法返回当前时间。
val zonedDateTime1 = ZonedDateTime.now() // 默认时区
val zonedDateTime2 = ZonedDateTime.now(ZoneId.of("America/New_York"))

// 它们时区不同,但表示的时间都是同一时刻,也就是常说的时间戳值是相等的
println(zonedDateTime1) // 2022-03-25T17:36:46.556709+08:00[Asia/Shanghai]
println(zonedDateTime2) // 2022-03-25T05:36:46.558299-04:00[America/New_York]
复制代码
  1. 通过给一个 LocalDateTime 附加一个 ZoneId
val localDateTime = LocalDateTime.of(2022, 3, 20, 15, 16, 36)
val zonedDateTime1 = localDateTime.atZone(ZoneId.systemDefault())
val zonedDateTime2 = localDateTime.atZone(ZoneId.of("America/New_York"))

// 它们的日期和时间与 LocalDateTime 相同,但是附加的时区不同,因此是两个不同的时刻
println(localDateTime) // 2022-03-20T15:16:36
println(zonedDateTime1) // 2022-03-20T15:16:36+08:00[Asia/Shanghai]
println(zonedDateTime2) // 2022-03-20T15:16:36-04:00[America/New_York]
复制代码
  1. 通过给一个 Instant 附加一个 ZoneId
val ins = Instant.now()

val zonedDateTime1 = ZonedDateTime.ofInstant(ins, ZoneId.systemDefault())
val zonedDateTime2 = ZonedDateTime.ofInstant(ins, ZoneId.of("America/New_York"))

// 它们时区不同,但表示的时间都是同一时刻
println(zonedDateTime1) // 2022-03-25T19:19:35.845243+08:00[Asia/Shanghai]
println(zonedDateTime2) // 2022-03-25T07:19:35.845243-04:00[America/New_York]
复制代码

要转换时区,首先我们需要有一个 ZonedDateTime 对象,然后通过 withZoneSameInstan() 将关联时区转换到另一个时区,转换后日期和时间都会相应调整。

val zonedDateTime1 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"))
// 转换为纽约时间
val zonedDateTime2 = zonedDateTime1.withZoneSameInstant(ZoneId.of("America/New_York"))

println(zonedDateTime1) // 2022-03-25T19:27:57.766262+08:00[Asia/Shanghai]
println(zonedDateTime2) // 2022-03-25T07:27:57.766262-04:00[America/New_York]
复制代码

下面这张图对 ZonedDateTime 的组成部分进行了说明,可以帮助我们理解 LocalDateLocalTimeLocalDateTime 以及 ZonedDateTime 之间的差异。

image.png

最佳实践

在数据库中,我们需要存储最常用的时间戳,因为有了时间戳信息,就可以根据用户自己选择的时区,显示出正确的本地时间。所以最好的方法是直接用 long 表示,在数据库中存储为 BIGINT 类型。

fun main(){
    val currentTimeMillis = System.currentTimeMillis()
    // val currentTimeMillis = Instant.now().toEpochMilli()

    val date = timestampToDateStr(currentTimeMillis, "Asia/Shanghai")
    println(date) // 2022-03-25 20:38:54.846
}


fun timestampToDateStr(epochMilli: Long, zoneId: String): String {
    val ins = Instant.ofEpochMilli(epochMilli)
    val zonedDateTime = ZonedDateTime.ofInstant(ins, ZoneId.of(zoneId))
    val dataTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")

    return zonedDateTime.format(dataTimeFormatter)
}
复制代码

根据日期来筛选数据库中的记录,前端可能会传入一个开始日期(如 2021-03-18 )和一个结束日期( 2021-05-10 ),并附带时区信息。我们需要把前端传过来的日期转为时间戳去数据库中查询。

fun main() {
    val (startTimestamp, endTimestamp) = dateStrToTimeStamp("2021-03-18", "2021-05-10", "America/New_York")

    // 伪代码
    val records = queryFromDB(startTimestamp, endTimestamp)
}


fun dateStrToTimeStamp(startDateStr: String, endDateStr: String, zoneId: String): Pair<Long, Long> {
    val dataTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
    val startLocalDate = LocalDate.parse(startDateStr, dataTimeFormatter)
    val endLocalDate = LocalDate.parse(endDateStr, dataTimeFormatter)

    val startLocalDateTime = LocalDateTime.of(startLocalDate, LocalTime.MIN)
    val endLocalDateTime = LocalDateTime.of(endLocalDate, LocalTime.MAX)


    val startZonedDateTime = startLocalDateTime.atZone(ZoneId.of(zoneId))
    val endZonedDateTime = endLocalDateTime.atZone(ZoneId.of(zoneId))
    return Pair(startZonedDateTime.toInstant().toEpochMilli(), endZonedDateTime.toInstant().toEpochMilli())
}
复制代码

另外,请永远显式的指定你需要的时区,即使你要获取的是默认时区

// 方式一
LocalDateTime.now(); 

// 方式二
LocalDateTime.now(ZoneId.systemDefault());
复制代码

如上代码二者效果一模一样。但是方式二是最佳实践。

理由是:这样做能让代码带有明确的意图,消除模棱两可的可能性,即使获取的是默认时区。拿方式一来说吧,它就存在意图不明确的地方:到底是代码编写者忘记指定时区欠考虑了,还是就想用默认时区呢?这个答案如果不通读上下文是无法确定的,从而造成了不必要的沟通维护成本。因此即使你是要获取默认时区,也请显示的用ZoneId.systemDefault() 写上去。

  • 使用JVM的默认时区需当心,建议时区和当前会话保持绑定

这么做的理由是:JVM 的默认时区通过静态方法 TimeZone#setDefault() 可全局设置,因此 JVM 的任何一个线程都可以随意更改默认时区。若关于时间处理的代码对时区非常敏感的话,最佳实践是你把时区信息和当前会话绑定,这样就可以不用再受到其它线程潜在影响了,确保了健壮性。

猜你喜欢

转载自juejin.im/post/7080092826885292040