preface

The book picks up where it left off, after learning about GMT, UTC, DAYLIGHT saving time, time stamps, and more. As Java developers, of course, our primary concern is how to solve the big issues of time zones, daylight saving time, and so on when we encounter date and time requirements.

Prior to Java8, we used Date, Calender, and SimpleDateFormat to handle dates and format and parse dates and times when dealing with date-time requirements. However, the apis of these classes have obvious drawbacks, such as poor readability, ease of use, redundancy, and thread safety.

As a result, Java8 introduced new datetime classes. Each class has clear functions, easy collaboration between classes, clear API definition and no pit, powerful API can complete operations without the aid of external utility classes, and thread safety.

An overview of date and time classes in Java8

Java8 introduces a new set of date and time apis. The classes in the java.time package are immutable and thread-safe. The new time and date API is in java.time, and here are some of the key classes.

Instant

The current time stored in a computer is essentially just an increasing integer.

The Java-provided System.CurrentTimemillis () returns the current timestamp in milliseconds.

This current timestamp is represented as Instant in java.time, and we use Instant. Now () to get the current timestamp, similar to System.currentTimemillis ().

Val now = Instant. Now () println(now.epochsecond) // 1648027759 println(now.toepochmilli ()) // milliseconds 1648027759732Copy the code

There are two core fields inside the Instant class:

public final class Instant implements Temporal, TemporalAdjuster, Comparable<Instant>, Serializable {/** * The number of seconds from The epoch of 1970-01-0t00:00:00 Z. * The number of seconds experienced since 00:00.0 (UTC time) on January 1, 1970. */ 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. * The number of nanoseconds in the second field. It's always a positive number and it doesn't exceed 999,999,999. */ private final int nanos; }Copy the code

One is the time in seconds and the other is the more accurate nanosecond (note not the time in nanoseconds).

Val now = Instant. Now () println(now.toepochmilli ()) // 1648027840587 println(now.nano) // 587945000Copy the code

Since Instant is a timestamp, attaching a time zone to it creates a ZonedDateTime.

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

It can be seen that for a certain timestamp, associate it with the specified ZoneId to obtain ZonedDateTime, and then obtain the LocalDateTime of the corresponding time zone.

Here are two practical methods:

  1. How many hours, days, months are the two timestamps apart?
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.hours)) // 0Copy the code
  1. Count the elapsed time of a program.
Val start = instant.now () doSomething() // Simulation takes 2 seconds val end = instant.now () // Use Duration to handle timestamp difference val nanos = Duration.between(start, End) println("nanos = ${nanos.tonanos ()}") // 2004176000 ns println("mills = ${nanos.tomillis ()}") // 2004176000 nsCopy the code

LocalDate

Provide simple dates without time information. Such as the 2018-06-28. It can be used to store birthdays, anniversaries, entry dates, etc.

You can either get a current LocalDate object with localdate.now () or create a LocalDate object with the static factory method of.

Val oldDate = localdate.now () val oldDate = localdate.of (2020, 8, 26) println(now) // 2022-03-25 println(oldDate) // 2020-08-26Copy the code

Dates can be added and subtracted directly using various minus and plus methods, such as the following code that implements subtracting and adding a day, and subtracting and adding a month.

val now = LocalDate.now()
    .minus(Period.ofDays(1))
    .plus(1, ChronoUnit.DAYS)
    .minusMonths(1)
    .plusMonths(1)
println(now) // 2022-03-25
Copy the code

Ponder: Add a month to date 2022-01-31, what is the return date?

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
Copy the code

As you can see, add a month to the date 2022-01-31 and return the date 2022-02-28. Notice the difference between adding a month to this API and adding 30 days.

You can also use the with method for quick time adjustment, such as:

  • Gets the first day of the current month
  • Gets the first day of the current year
  • Get last Saturday
  • Get the last Friday of the month
/ / this month on the first day of the val date1 = LocalDate. Now () with (TemporalAdjusters. FirstDayOfMonth ()) println (date1) / / / / the first day of the year 2022-03-01 Val date2 = LocalDate. Now () with (TemporalAdjusters. FirstDayOfYear ()) println (date2) / / 2022-01-01 / / val today before on a Saturday Date3 = LocalDate. Now () with (TemporalAdjusters. Previous (DayOfWeek. SATURDAY)) println (date3) / / 2022-03-19 / / the last business day of this month val date4 = LocalDate.now().with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)) println(date4) // 2022-03-25Copy the code

You can also customize time adjustments directly using lambda expressions. For example, add a random number within 100 days to the current time.

LocalDate.now().with { temporal ->
    temporal.plus(ThreadLocalRandom.current().nextLong(100), ChronoUnit.DAYS)
}
Copy the code

In addition to the calculation, you can also determine whether a date meets a condition. For example, a custom function that determines whether a specified date is a family member’s birthday.

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 }Copy the code

Then, use the query method to check whether the condition is matched:

val isFamilyBirthday = LocalDate.now().query { temporal ->
    isFamilyBirthday(temporal)
}
Copy the code

Here is a more practical method: calculate two date differences.

/ / such as calculation on October 26, 2021, and the date of January 10, 2022 interval 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) // 15Copy the code

Note: the getDays() method of a Period only gets the last “zero days”, not the total number of days between.

LocalTime

Provides only time information, not date.

You can either get a current LocalTime object with localtime.now () or create one with the static factory method of.

Val now = localtime.now () // 13:45:20 val time = localtime.of (13, 45,) 20) println(now) // 15:24:04.211591 println(time) // 13:45:20Copy the code

Both LocalDate and LocalTime can be created by parsing the string representing them, using the static method 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
Copy the code

You can add and subtract time directly using various minus and plus methods. For example, the following code implements subtracting and adding an hour, and subtracting and adding a minute.

val now = LocalTime.now() .minus(Duration.ofHours(1)) .plus(1, HOURS).minusminutes (1).plusminutes (1) println(now) // 15:49:23.473146Copy the code

You can customize time adjustments directly using lambda expressions.

LocalTime.now().with { temporal ->
    temporal.plus(1, ChronoUnit.HOURS)
}
Copy the code

We can also tell if one time comes before or after another.

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

println(time1.isBefore(time2)) // true
println(time1.isAfter(time2)) // false
Copy the code

Use the until() method to calculate the two time differences.

Parse ("19:55:30.121563") println(now) val now = localtime. parse("19:55:30. println(now.until(time, ChronoUnit.HOURS)) // 0 println(now.until(time, ChronoUnit.MINUTES)) // 9 println(now.until(time, ChronoUnit.SECONDS)) // 599Copy the code

The maximum, minimum, and noon times of the day are all available in constants in the LocalTime class. Useful when querying records in a database for a given time period.

val maxTime = LocalTime.MAX
val minTime = LocalTime.MIN
val noonTime = LocalTime.NOON
Copy the code

LocalDateTime

It contains the date and time.

You can either get a current LocalDateTime object with localDatetime.now () or create a LocalDateTime object with the static factory method of, It can also be constructed by merging LocalDate and LocalTime objects.

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, 2022-03-25t19:58:46.707791 println(now2) // 2022-03-25t19:58:46.707791 println(now3) // 2022-03-25t19:58:46.707791 println(now3) // 2022-03-18T13:15:20Copy the code

By default, LocalDate, LocalTime, and LocalDateTime are printed in strict accordance with the date and time format specified in ISO8601. Because, converting a string to LocalDateTime can be passed into the standard format:

val localDateTime = LocalDateTime.parse("2022-01-22T15:16:17")
println(localDateTime) // 2022-01-22T15:16:17
Copy the code

If you want to customize the output format, or if you want to parse a non-ISO8601 string to LocalDateTime, you can use the new DateTimeFormatter.

Val dateTimeFormatter = DateTimeFormatterBuilder().appendValue(chronofield.year) // year.appendLiteral ("/") AppendValue (chronofield.month_of_year) // month.appendValue (chronofield.day_of_month) // day .appendliteral (" ").appendValue(chronofield.hour_of_day) // node.appendliteral (":") .appendValue(chronofield.minute_of_hour) // min.appendliteral (":"). AppendValue (chronofield.second_of_minute) // second .appendLiteral(".").appendValue(chronofield.micro_of_second) // millisecond.toformatter () val localDateTime = Parse ("2020/01/02 12:34:56.789", dateTimeFormatter) println(LocalDateTime) // 2020-01-02T12:34:56.000789Copy the code

Dates and times can be added and subtracted directly using various minus and plus methods. For example, the code below implements subtracting and adding a day, subtracting and adding a month, subtracting and adding an hour, and subtracting and adding a minute.

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
Copy the code

Note: the month plus or minus automatically adjusts the date, for example subtracting one month from 2021-10-31 gives 2021-09-30 because there is no 31 in September.

val localDateTime = LocalDateTime.of(2022, 10, 31, 21, 26, 15)
println(localDateTime.minusMonths(1)) // 2022-09-30T21:26:15
Copy the code

Adjusting dates and times using the with method, such as withHour(15) changes 10:50:23 to 15:50:23.

  • Adjust the yearswithYear()
  • Adjust the monthwithMonth()
  • regardwithDayOfMonth()
  • When the adjustmentwithHour()
  • Adjusted scorewithMinute()
  • Adjust the secondswithSecond()
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
Copy the code

Also notice that when the month is adjusted, the date will be adjusted accordingly, that is, when the month of 2021-10-31 is adjusted to 9, the date will automatically change to 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
Copy the code

ZoneOffset

The time zone offset from Greenwich /UTC, for example +02:00.

The time zone offset is the time difference between the time zone and Greenwich /UTC. This is usually a fixed number of hours and minutes. Different regions of the world have different time zone offsets.

  1. Minimum/maximum offset.
val min = ZoneOffset.MAX
val max = ZoneOffset.MIN

println(min) // +18:00
println(max) // -18:00
Copy the code
  1. The offset is constructed by time minute second.
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
Copy the code

Because of the daylight saving time rule, the complexity of operating date/time is greatly increased. Fortunately, the JDK does its best to shield users from these rules. Therefore, it is recommended to use the time zone ZoneId to convert the date/time. In general, it is not recommended to use the offset ZoneOffset.

ZoneId

Before Java8, Java used TimeZone to represent time zones. In Java8, ZoneId is used to represent the time zone, which represents a time ZoneId such as Europe/Paris. It specifies rules that can be used to convert an Instant timestamp to a local date-time LocalDateTime.

The time zone ZoneId contains rules, and the actual rules that actually describe when and how offsets change are defined by ZoneRules. ZoneId is just an ID to get the underlying rule. This approach is adopted because rules are defined by the government and change frequently, while ids are stable.

This ID (i.e., ZoneId) is all that API callers need to use, regardless of the more low-level ZoneRules for time ZoneRules. For example, the rules for daylight saving time are set by governments around the world and generally vary from country to country, leaving it to the underlying ZoneRules mechanism of the JDK to sync.

  1. Gets the system defaultZoneId
/ / get all of the available time zone size = 600 this figure is not constant val allZones = ZoneId. GetAvailableZoneIds () / / access to the system default time zone val zone = ZoneId.systemDefault() println(allZones.size) // 600 println(zone) // Asia/ShanghaiCopy the code

The Internet Assigned Numbers Authority (IANA) maintains a global database of all known time zones that is updated several times a year to deal with DST rule changes. Java uses IANA’s database.

  1. Gets one of the specified stringsZoneId
val timeZoneSH = ZoneId.of("Asia/Shanghai")
val timeZoneNY = ZoneId.of("America/New_York")

println(timeZoneSH) // Asia/Shanghai
println(timeZoneNY) // America/New_York
Copy the code
  1. Based on the offset to oneZoneId
val timeZone = ZoneId.ofOffset("UTC", ZoneOffset.of("+8"))
println(timeZone) // UTC+08:00
Copy the code

The prefix for the first parameter is “GMT”, “UTC”, or “UT”. You can also pass an empty string, which returns the second argument, ZoneOffset.

Note: There are no existing time zone rules within the ZoneId derived from the offset, so this is generally not recommended for countries with summer camps that may have problems switching.

  1. fromZonedDateTimeTo get the time zone:
val timeZone = ZoneId.from(ZonedDateTime.now())
println(timeZone) // Asia/Shanghai
Copy the code

It is worth emphasizing that the concept and actual functions of ZoneId and ZoneOffset are quite different, mainly reflected in:

  1. The UTC offset simply records the hour and minute of the offset, but nothing else. For example: +08:00 means 8 hours ahead of UTC, with no geographical/time zone implications, and the corresponding -03:30 means only 3 1/2 hours behind UTC.
  2. Time zones are region-specific and are strongly tied to geographical regions (including rules). For example, all of China is called East 8th, New York is in West 5th, etc.

There is no daylight saving time in China, and the offset for all east 8 zones is always +8; New York has daylight saving time, so its offset could be -4 or -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:00Copy the code

China is in the 8th east and New York is in the 5th West. The time difference between us could be 12 hours (DAYLIGHT saving Time) or 13 hours:

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 "is encouraged)) / / time difference 12 hours (since March 25, is in the United States daylight saving time) 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 "is encouraged)) / / time 13 hours difference println (zonedDateTimeSH1) / / 2022-01-25T18:10:26+08:00[Asia/Shanghai] println(zonedDateTimeNY1) // 2022-01-25T05:10:26-05:00[America/New_York]Copy the code

Overall, ZoneId is better than ZoneOffset. If you use the UTC offset then it’s a bit of a hassle, because daylight saving time, it’s variable. But if you use the ZoneId time zone, such as New York’s West 5th Ward, you will get the correct local time at any time, because it has built-in daylight saving time rules and doesn’t need API callers to care.

ZonedDateTime

LocalDateTime does not have a time zone attribute, so it is named the date time of the local time zone. To represent a date and time with a time zone, we need ZonedDateTime.

ZonedDateTime = LocalDateTime + ZoneId, with the time zone attribute. ZoneId is a new time zone class introduced by java.time. Note the difference with the old java.util.TimeZone.

To create a ZonedDateTime object, there are several ways.

  1. throughnow()Method returns the current time.
Val zonedDateTime2 = zonedDatetime.now ("America/New_York")) // The time zone is different, but the time is the same. Println (zonedDateTime1) // 2022-03-25T17:36:46.556709+08:00[Asia/Shanghai] println(zonedDateTime2) // The 2022-03-25 T05:36:46. 558299-04:00 [America/New_York]Copy the code
  1. By giving aLocalDateTimeAttach aZoneId.
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")) // They have the same date and time as localDateTime, but the additional time zone is different, So two different times 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]Copy the code
  1. By giving aInstantAttach aZoneId.
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) // The 2022-03-25 T07: capacity. 845243-04:00 [America/New_York]Copy the code

To switch time zones, we first need to have a ZonedDateTime object, and then with withZoneSameInstan() we convert the associated time zone to another time zone, adjusting the date and time accordingly.

Val zonedDateTime1 = zonedDatetime.now (zoneid.of ("Asia/Shanghai")) // Convert to New York time val zonedDateTime2 = zonedDateTime1.withZoneSameInstant(ZoneId.of("America/New_York")) println(zonedDateTime1) // When it T19:2022-03-25. 766262 + 08:00 Asia/Shanghai println (zonedDateTime2) / / When it T07:2022-03-25. 766262-04:00 America/New_YorkCopy the code

The following figure illustrates the components of ZonedDateTime to help us understand the differences between LocalDate, LocalTime, LocalDateTime and ZonedDateTime.

Best practices

In the database, we need to store the most frequently used timestamps, because with the timestamp information, we can display the correct local time according to the time zone chosen by the user. So the best way is to write it as long and store it in the database as 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) }Copy the code

To filter the records in the database by date, the front end might pass in a start date (such as 2021-03-18) and an end date (2021-05-10), along with time zone information. We need to turn the date from the front end into a timestamp to query in the database.

fun main() { val (startTimestamp, endTimestamp) = dateStrToTimeStamp("2021-03-18", "2021-05-10", 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()) }Copy the code

Also, always explicitly specify the time zone you want, even if you want to get the default time zone.

Localdatetime.now (); Localdatetime.now (zoneid.systemdefault ());Copy the code

The code above is exactly the same. But approach two is best practice.

The reason: This gives the code a clear intent and eliminates the possibility of ambiguity, even if the default time zone is acquired. For one thing, the intent is unclear: Was the writer of the code thoughtless in forgetting to specify the time zone, or did he just want to use the default time zone? This answer cannot be determined without reading through the context, resulting in unnecessary communication maintenance costs. So even if you want to get the default time zone, use zoneid.systemDefault () to display it.

  • Use the JVM’s default time zone with caution, and it is recommended that time zones remain bound to the current session

The reason for this is that the JVM’s default time zone can be set globally using the static method TimeZone#setDefault(), so any thread of the JVM can change the default time zone at will. If time processing code is sensitive to time zones, it is best practice that you bind the time zone information to the current session so that it is not potentially affected by other threads, ensuring robustness.