16 Use Java 8's Date and Time Classes to Avoid Some Old Pitfalls

16 Use Java 8’s Date and Time Classes To Avoid Some Old Pitfalls #

Today, let’s talk about the frustrating issue of time inconsistency.

Prior to Java 8, when dealing with date and time requirements, we would use Date, Calendar, and SimpleDateFormat to declare timestamps, handle dates using calendars, and format and parse date and time. However, these classes have noticeable drawbacks in terms of poor readability, usability, redundancy, complexity, and thread safety issues.

Therefore, Java 8 introduced new date and time classes. Each class has clear and well-defined functionalities, simple collaboration between classes, clear API definitions, and avoids common pitfalls. These APIs are powerful enough to complete operations without relying on external utility classes, and they are also thread-safe.

However, when Java 8 was first introduced, libraries such as serialization and data access did not support Java 8’s date and time types, requiring constant conversion between new and old classes. For example, using LocalDateTime in the business logic layer but converting it back to Date when storing it in a database or returning it to the frontend. As a result, many developers still chose to use the old date and time classes.

Now, several years have passed, and almost all libraries support the new date and time types. There is no longer a need for constant conversion. However, many codebases still use the legacy date and time classes, leading to numerous issues with time inconsistency. For example, attempting to match retrieved data to the current clock by arbitrarily modifying time zones, or directly manipulating retrieved data by adding or subtracting several hours to “adjust the data.”

Today, I will specifically analyze the reasons behind time inconsistency issues and explore the problems one might encounter when using legacy date and time classes for date and time initialization, formatting, parsing, and calculation. I will also discuss how to solve these issues by utilizing the new date and time classes.

Initializing Date and Time #

Let’s start by looking at the initialization of date and time. If we want to initialize a time like “December 31, 2019, 11:12:13”, can we use the following two lines of code?

Date date = new Date(2019, 12, 31, 11, 12, 13);
System.out.println(date);

As you can see, the output is “January 31, 3029, 11:12:13 CST”.

Sat Jan 31 11:12:13 CST 3920

You might say that this is a beginner’s mistake: the year should be the difference from 1900, and the month should be from 0 to 11 instead of 1 to 12.

Date date = new Date(2019 - 1900, 11, 31, 11, 12, 13);

You’re right, but the more important issue is that when there is an internationalization requirement, you need to use the Calendar class to initialize the date and time.

After using Calendar for transformation, you can use the current year as the year parameter for initialization. However, you need to pay attention that the month should be from 0 to 11. Of course, you can also use Calendar.DECEMBER directly to initialize the month, which is less error-prone. To illustrate the issue of time zone, I initialized the same date twice, once with the current time zone and once with the New York time zone.

Calendar calendar = Calendar.getInstance();
calendar.set(2019, 11, 31, 11, 12, 13);
System.out.println(calendar.getTime());

Calendar calendar2 = Calendar.getInstance(TimeZone.getTimeZone("America/New_York"));
calendar2.set(2019, Calendar.DECEMBER, 31, 11, 12, 13);
System.out.println(calendar2.getTime());

The output shows two different times, indicating that the time zone has an effect. However, we are more used to the format of “year/month/day hour:minute:second” and are not satisfied with the current output format.

Tue Dec 31 11:12:13 CST 2019
Wed Jan 01 00:12:13 CST 2020

So, what is the issue with time zones and how can we format the date and time for output? Next, I will analyze these two issues with you step by step.

The Annoyance of Time Zones #

We know that there are 24 time zones in the world, and the time is different in different time zones at the same moment (e.g. Shanghai, China and New York, USA). For projects that require globalization, if the time zone is not provided when initializing the time, it is not a true representation of time, but only a representation of the current time as seen by me.

Regarding the Date class, we need to understand two points:

First, Date does not have a time zone issue. The time obtained by initializing a new Date object is the same on any computer in the world. This is because Date stores UTC time, which is a standardized time based on atomic clocks and is not based on solar time or divided into time zones.

Second, Date stores a timestamp that represents the number of milliseconds from January 1, 1970 (Epoch time) to the present. Let’s try outputting Date(0):

System.out.println(new Date(0));
System.out.println(TimeZone.getDefault().getID() + ":" + TimeZone.getDefault().getRawOffset()/3600000);

I get January 1, 1970, 8:00 AM. This is because the time zone of my machine is Shanghai, China, which is 8 hours ahead of UTC:

Thu Jan 01 08:00:00 CST 1970
Asia/Shanghai:8

For international projects (used by people in different countries), the first thing to consider in dealing with time and time zones is to correctly store the date and time. There are two ways to do this:

Option 1 is to store the time in UTC, without the time zone property, which represents a unified time across the world without considering time zone differences. This is commonly referred to as a timestamp, or the Date class in Java, and is the recommended approach.

Option 2 is to store the time literally, for example, as year/month/day hour:minute:second, and it is necessary to also store the time zone information. Only with the time zone information can we know the true time represented by this literal time. Otherwise, it is just a representation of time for viewing purposes and only meaningful in the current time zone. The Calendar class has a concept of time zones, so by initializing the Calendar with different time zones, we get different times.

After correctly storing the date and time, the next step is to display it correctly, which means using the correct time zone to represent the time point in accordance with the current time zone. At this point, we can understand why there is a so-called “time mismatch” problem. Next, I will analyze two types of problems: parsing literals into time and formatting time as literals, using actual examples.

The first type of problem is that for the same time representation, such as “2020-01-02 22:00:00”, different people in different time zones will get different times (timestamps) when converted to Date objects:

String stringDate = "2020-01-02 22:00:00";
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// Parsing time representation in the default time zone
Date date1 = inputFormat.parse(stringDate);
System.out.println(date1 + ":" + date1.getTime());

// Parsing time representation in the New York time zone
inputFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
Date date2 = inputFormat.parse(stringDate);
System.out.println(date2 + ":" + date2.getTime());

As you can see, when converting a time representation like “2020-01-02 22:00:00” to UTC timestamps, the results differ in the current Shanghai time zone and the New York time zone:

Thu Jan 02 22:00:00 CST 2020:1577973600000
Fri Jan 03 11:00:00 CST 2020:1578020400000

This is the significance of UTC, not a time mismatch. For a representation of the same local time, people in different time zones will always parse it into different UTC times, while different local times may correspond to the same UTC time.

The second type of problem is when formatting a Date object, the resulting time representation differs in different time zones. For example, formatting “2020-01-02 22:00:00” in my current time zone and the New York time zone:

String stringDate = "2020-01-02 22:00:00";
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// Same Date
Date date = inputFormat.parse(stringDate);
// Format output with default time zone
System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));
// Format output with New York time zone
TimeZone.setDefault(TimeZone.getTimeZone("America/New_York"));
System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));

The output is as follows. The offset (time difference) of my current time zone is +8 hours, and for New York, which is -5 hours, 10 PM corresponds to 9 AM:

[2020-01-02 22:00:00 +0800]
[2020-01-02 09:00:00 -0500]

Therefore, sometimes the same time in the database is represented differently due to differences in the time zone settings of the servers. This is not a time disorder, but the result of the time zone being applied, because UTC time needs to be parsed into the correct local time according to the current time zone.

So, to correctly handle time zones, it is necessary to consider both storing and retrieving data: when storing data, use the correct current time zone to ensure that UTC time is stored correctly; when retrieving data, only by correctly setting the local time zone can UTC time be converted to the correct local time.

Java 8 introduced new date and time classes such as ZoneId, ZoneOffset, LocalDateTime, ZonedDateTime, and DateTimeFormatter, which make it easier to handle time zone issues. Let’s understand time parsing and formatting using these classes with a complete example:

First, initialize three time zones: Shanghai, New York, and Tokyo. We can use ZoneId.of to initialize a standard time zone, or use ZoneOffset.ofHours to initialize a custom time zone with a specified time difference offset.

In terms of date and time representation, LocalDateTime does not have a time zone property, so it represents a date and time in the local time zone. On the other hand, ZonedDateTime is made up of LocalDateTime and ZoneId and has a time zone property. Therefore, LocalDateTime can only be considered as a time representation, whereas ZonedDateTime is a valid time representation. Here, we parse the time “2020-01-02 22:00:00” using the Tokyo time zone and obtain a ZonedDateTime object.

When formatting time using DateTimeFormatter, we can directly set the time zone to be used for formatting by using the withZone method. Finally, we format the time output using Shanghai, New York, and Tokyo time zones respectively:

// A time representation
String stringDate = "2020-01-02 22:00:00";

// Initialize three time zones
ZoneId timeZoneSH = ZoneId.of("Asia/Shanghai");
ZoneId timeZoneNY = ZoneId.of("America/New_York");
ZoneId timeZoneJST = ZoneOffset.ofHours(9);

// Formatter
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
ZonedDateTime date = ZonedDateTime.of(LocalDateTime.parse(stringDate, dateTimeFormatter), timeZoneJST);

// Use DateTimeFormatter to format time, and set the time zone for formatting using withZone method
DateTimeFormatter outputFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");
System.out.println(timeZoneSH.getId() + outputFormat.withZone(timeZoneSH).format(date));
System.out.println(timeZoneNY.getId() + outputFormat.withZone(timeZoneNY).format(date));
System.out.println(timeZoneJST.getId() + outputFormat.withZone(timeZoneJST).format(date));

As you can see, the time representations stored and retrieved in the same time zone are the same (e.g., the last line). However, for different time zones, such as Shanghai and New York, the resulting local times are different. The +9 hours time zone at 10 PM is +8 hours in Shanghai, so the local time in Shanghai is 9 PM. On the other hand, for New York, it is -5 hours, which is a 14-hour difference, so the local time is 8 AM:

Asia/Shanghai 2020-01-02 21:00:00 +0800
America/New_York 2020-01-02 08:00:00 -0500
+09:00 2020-01-02 22:00:00 +0900

To summarize, I recommend using Java 8’s date and time classes to correctly handle international time issues. Use ZonedDateTime to store time and use DateTimeFormatter with a ZoneId set for formatting to obtain the local time representation. This division is clear, refined, and less prone to errors.

Next, let’s continue to see what issues we might encounter when formatting and parsing date and time using the legacy SimpleDateFormat.

Date and Time Formatting and Parsing #

At the end of each year, many developers fall into the pit of time formatting, such as “This is clearly a date in the year 2019, why does it show the next year when formatted using SimpleDateFormat?” Let’s reproduce this problem.

Initialize a Calendar, set the date and time to December 29th, 2019, and use the uppercase YYYY to initialize SimpleDateFormat:

Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
System.out.println("defaultLocale:" + Locale.getDefault());
Calendar calendar = Calendar.getInstance();
calendar.set(2019, Calendar.DECEMBER, 29, 0, 0, 0);
SimpleDateFormat YYYY = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("Formatted: " + YYYY.format(calendar.getTime()));
System.out.println("weekYear:" + calendar.getWeekYear());
System.out.println("firstDayOfWeek:" + calendar.getFirstDayOfWeek());
System.out.println("minimalDaysInFirstWeek:" + calendar.getMinimalDaysInFirstWeek());

The output obtained is December 29th, 2020:

defaultLocale: zh_CN
Formatted: 2020-12-29
weekYear: 2020
firstDayOfWeek: 1
minimalDaysInFirstWeek: 1

The reason for this problem is that the developer confused the different formatting patterns of SimpleDateFormat. The JDK documentation explains that lowercase ‘y’ represents the year, while uppercase ‘Y’ represents the week year, which refers to the year to which the week belongs.

The determination of the first week of the year is based on the getFirstDayOfWeek() method, which requires a full 7 days starting from that day, and contains at least getMinimalDaysInFirstWeek() days of the year. This calculation depends on the region. For the zh_CN region, the first week of 2020 requires a full 7 days starting from Sunday and only needs to include 1 day of 2020. Obviously, from Sunday, December 29, 2019 to Saturday, January 4, 2020 is the first week of 2020, so the week year obtained is 2020.

If the region is changed to France:

Locale.setDefault(Locale.FRANCE);

The week year will still be 2019 because the first day of the week is counted from Monday. The first week of 2020 starts on Monday, December 30th, 2019, and December 29th still belongs to the previous year:

defaultLocale: fr_FR
Formatted: 2019-12-29
weekYear: 2019
firstDayOfWeek: 2
minimalDaysInFirstWeek: 4

This example tells us that, unless there are special requirements, the formatting of the year should always use “y” instead of “Y”.

In addition to the pitfalls in formatting expressions, SimpleDateFormat also has two well-known pitfalls.

The first pitfall is that a statically defined SimpleDateFormat may have thread safety issues. For example, using a thread pool with 100 threads and submitting 20 tasks to format the time 10 times in each task, which represents the time “2020-01-01 11:12:13”:

ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
    // Submit 20 tasks for concurrent time parsing to the thread pool
    threadPool.execute(() -> {
        for (int j = 0; j < 10; j++) {
            try {
                System.out.println(simpleDateFormat.parse("2020-01-01 11:12:13"));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    });
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);

After running the program, a large number of errors occur, and the output without errors is also incorrect, such as parsing 2020 as the year 1212:

img

The purpose of SimpleDateFormat is to define patterns for parsing and formatting date and time. This seems to be a one-time job that should be reused, but its parsing and formatting operations are not thread-safe. Let’s analyze the relevant source code:

SimpleDateFormat inherits DateFormat, which has a Calendar field;

The parse method of SimpleDateFormat calls the establish method of CalendarBuilder to build a Calendar;

The establish method first clears the Calendar and then constructs a new Calendar, without any locking.

Clearly, if multiple threads in a thread pool call the parse method, it means that multiple threads are operating on the same Calendar concurrently, which may result in one thread clearing the Calendar before another thread has a chance to process it:

public abstract class DateFormat extends Format {
    protected Calendar calendar;
}

public class SimpleDateFormat extends DateFormat {

    @Override
    public Date parse(String source) throws ParseException {
        CalendarBuilder calb = new CalendarBuilder();
        // ...
        calb.establish(calendar);

        return calb.getDate();
    }
}

In conclusion, SimpleDateFormat is not thread-safe and should not be shared across multiple threads. It should either be allocated locally or synchronized when used by multiple threads. public Date parse(String text, ParsePosition pos) { CalendarBuilder calb = new CalendarBuilder(); parsedDate = calb.establish(calendar).getTime(); return parsedDate; } }

class CalendarBuilder {

Calendar establish(Calendar cal) {

     ...
    cal.clear();//清空

    for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {

        for (int index = 0; index <= maxFieldIndex; index++) {
            if (field[index] == stamp) {
                cal.set(index, field[MAX_FIELD + index]);//构建
                break;
            }
        }
    }
    return cal;
}

}

The format method is similar. You can analyze it yourself. Therefore, the best solution is to reuse SimpleDateFormat in the same thread. You can store SimpleDateFormat in ThreadLocal:

private static ThreadLocal<SimpleDateFormat> threadSafeSimpleDateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

The second issue is that when the string to be parsed does not match the format, SimpleDateFormat is very tolerant and still returns a result. For example, if we expect to parse the string “20160901” using the format “yyyyMM”:

String dateString = "20160901";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMM");

System.out.println("result:" + dateFormat.parse(dateString));

It actually outputs January 1, 2091, because it treats 0901 as the month, which is equivalent to the year 75:

result:Mon Jan 01 00:00:00 CST 2091

With these three issues with SimpleDateFormat, we can avoid them by using DateTimeFormatter in Java 8. First, use DateTimeFormatterBuilder to define the formatting string, so you don’t have to remember whether to use uppercase Y or lowercase y, uppercase M or lowercase m:

private static DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR) //year
        .appendLiteral("/")
        .appendValue(ChronoField.MONTH_OF_YEAR) //month
        .appendLiteral("/")
        .appendValue(ChronoField.DAY_OF_MONTH) //day
        .appendLiteral(" ")
        .appendValue(ChronoField.HOUR_OF_DAY) //hour
        .appendLiteral(":")
        .appendValue(ChronoField.MINUTE_OF_HOUR) //minute
        .appendLiteral(":")
        .appendValue(ChronoField.SECOND_OF_MINUTE) //second
        .appendLiteral(".")
        .appendValue(ChronoField.MILLI_OF_SECOND) //millisecond
        .toFormatter();

Second, DateTimeFormatter is thread-safe and can be defined as static. Finally, DateTimeFormatter’s parsing is strict. If the string to be parsed does not match the format, it will throw an error instead of parsing 0901 as the month. Let’s test it:

//Use the DateTimeFormatterBuilder defined earlier to parse this time
LocalDateTime localDateTime = LocalDateTime.parse("2020/1/2 12:34:56.789", dateTimeFormatter);
//Parsing succeeded
System.out.println(localDateTime.format(dateTimeFormatter));
//Can we successfully parse 20160901 using the format yyyyMM?
String dt = "20160901";
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM");
System.out.println("result:" + dateTimeFormatter.parse(dt));

The output log is as follows:

2020/1/2 12:34:56.789
Exception in thread "main" java.time.format.DateTimeParseException: Text '20160901' could not be parsed at index 0
  at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
  at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1777)
  at org.geekbang.time.commonmistakes.datetime.dateformat.CommonMistakesApplication.better(CommonMistakesApplication.java:80)
  at org.geekbang.time.commonmistakes.datetime.dateformat.CommonMistakesApplication.main(CommonMistakesApplication.java:41)

Here we can see that using DateTimeFormatter in Java 8 for formatting and parsing date/time is more reliable. So, for date/time calculations, will using the date/time classes in Java 8 make it simpler?

Calculation of Date and Time #

When it comes to calculating date and time, let me first tell you about a common pitfall. Some students like to directly use timestamps for time calculations. For example, if they want to get the time after 30 days from the current time, they might write code like this: they directly add the number of milliseconds corresponding to 30 days to the timestamp obtained from the new Date().getTime() method, which is 30 days * 1000 milliseconds * 3600 seconds * 24 hours:

Date today = new Date();
Date nextMonth = new Date(today.getTime() + 30 * 1000 * 60 * 60 * 24);
System.out.println(today);
System.out.println(nextMonth);

The resulting date is actually earlier than the current date, not 30 days later:

Sat Feb 01 14:17:41 CST 2020
Sun Jan 12 21:14:54 CST 2020

The cause of this problem is integer overflow. The fix is to change 30 to 30L to make it a long:

Date today = new Date();
Date nextMonth = new Date(today.getTime() + 30L * 1000 * 60 * 60 * 24);
System.out.println(today);
System.out.println(nextMonth);

This way, you can get the correct result:

Sat Feb 01 14:17:41 CST 2020
Mon Mar 02 14:17:41 CST 2020

It is not difficult to see that manually performing calculations on timestamps is very error-prone. For code before Java 8, I suggest using Calendar:

Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.DAY_OF_MONTH, 30);
System.out.println(c.getTime());

Using the date and time types in Java 8, you can directly perform various calculations, which is more concise and convenient:

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime.plusDays(30));

Moreover, when it comes to calculating date and time, the Java 8 Date and Time API is much more powerful than Calendar.

Firstly, you can use various minus and plus methods to directly perform addition and subtraction operations on dates. For example, the following code subtracts one day, adds one day, subtracts one month, and adds one month:

System.out.println("//Test date manipulation");
System.out.println(LocalDate.now()
        .minus(Period.ofDays(1))
        .plus(1, ChronoUnit.DAYS)
        .minusMonths(1)
        .plus(Period.ofMonths(1)));

The result is:

//Test date manipulation
2020-02-01

Secondly, you can use the with method for quick date adjustment, such as:

  • Using TemporalAdjusters.firstDayOfMonth() to get the first day of the current month;
  • Using TemporalAdjusters.firstDayOfYear() to get the first day of the current year;
  • Using TemporalAdjusters.previous(DayOfWeek.SATURDAY) to get the previous Saturday;
  • Using TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY) to get the last Friday of the current month.
System.out.println("//First day of this month");
System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()));
System.out.println("//Programmer's Day this year");
System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfYear()).plusDays(255));
System.out.println("//Previous Saturday before today");
System.out.println(LocalDate.now().with(TemporalAdjusters.previous(DayOfWeek.SATURDAY)));
System.out.println("//Last working day of this month");
System.out.println(LocalDate.now().with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)));

The output is as follows:

//First day of this month
2020-02-01
//Programmer's Day this year
2020-09-12
//Previous Saturday before today
2020-01-25
//Last working day of this month
2020-02-28

Thirdly, you can directly use lambda expressions for custom time adjustment. For example, to add a random number of days within 100 days to the current date:

System.out.println(LocalDate.now().with(temporal -> temporal.plus(ThreadLocalRandom.current().nextInt(100), ChronoUnit.DAYS)));

The result is:

2020-03-15

In addition to calculations, you can also determine whether a date meets a certain condition. For example, you can define a custom function to determine if a specified date is a family member’s birthday:

public static Boolean isFamilyBirthday(TemporalAccessor date) {
    int month = date.get(MONTH_OF_YEAR);
    int day = date.get(DAY_OF_MONTH);
    if (month == Month.FEBRUARY.getValue() && day == 17)
        return Boolean.TRUE;
    if (month == Month.SEPTEMBER.getValue() && day == 21)
        return Boolean.TRUE;
    if (month == Month.MAY.getValue() && day == 22)
        return Boolean.TRUE;
    return Boolean.FALSE;
}

Then, use the query method to check if it matches the condition:

System.out.println("//Check if today is someone's birthday");
System.out.println(LocalDate.now().query(CommonMistakesApplication::isFamilyBirthday));

Using Java 8’s date and time operations and calculations is convenient, but there may be pitfalls when calculating the difference between two dates: in Java 8, there is a dedicated Period class that defines date intervals. By using Period.between, you get the difference between two LocalDate objects, which returns the number of years, months, and days between the two dates. If you want to know the number of days between two dates, calling the getDays() method of Period only gives you the final “number of days”, not the total number of days.

For example, when calculating the difference between December 12, 2019, and October 1, 2019, it is clear that the difference is 2 months and 11 days, but calling the getDays method gives you only 11 days, not 72 days:

System.out.println("//Calculating date difference");
LocalDate today = LocalDate.of(2019, 12, 12);
LocalDate specifyDate = LocalDate.of(2019, 10, 1);
System.out.println(Period.between(specifyDate, today).getDays());
System.out.println(Period.between(specifyDate, today));
System.out.println(ChronoUnit.DAYS.between(specifyDate, today));

You can use ChronoUnit.DAYS.between to solve this problem:

//Calculating date difference
11
P2M11D
72

From time zones to formatting to calculations, are you starting to appreciate the power of the Java 8 Date and Time classes?

Key Review #

Today, we looked at issues related to the initialization, time zone, formatting, parsing, and calculation of date and time. We have seen that using the classes in the Java 8 date and time package, Java.time, for various operations is easier, clearer, more feature-rich, and less error-prone compared to using legacy classes like Date, Calendar, and SimpleDateFormat.

If possible, I still recommend fully transitioning to using the Java 8 date and time types. I have summarized the date and time types before and after Java 8 on a mind map, where the arrows represent conceptually equivalent types between the old and new APIs:

img

There is a misconception that java.util.Date is similar to LocalDateTime in the new API. However, they are not. Although both of them do not have a time zone concept, java.util.Date does not have a time zone concept because it is represented in UTC and is essentially a timestamp. On the other hand, LocalDateTime can strictly be considered as a representation of date and time, rather than a specific point in time.

Therefore, when converting a Date to LocalDateTime, you need to obtain a UTC timestamp using the toInstant method of Date and provide the current time zone in order to convert the UTC time to a local date and time (representation). Conversely, when converting the time representation of LocalDateTime to a Date, you also need to provide the time zone to specify the time representation for which time zone, which means you need to first convert LocalDateTime to ZonedDateTime using the atZone method and then obtain the UTC timestamp:

Date in = new Date();
LocalDateTime ldt = LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());
Date out = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());

Many students say that using the new API is cumbersome and introduces the concept of time zones, which is not concise at all. But what I want to convey to you through this article is that it is not because the API is designed to be so cumbersome, but because when converting UTC time to local time, the time zone must be taken into account.

I have put all the code used today on GitHub, and you can click this link to view it.

Reflection and Discussion #

Today I have emphasized multiple times that Date is a timestamp, it represents UTC time and does not have a concept of time zone. So why does calling its toString method output time zone names like CST?

When it comes to storing date and time data in a database, there are two data types in MySQL: datetime and timestamp. Can you explain the differences between them and whether they contain time zone information?

Have you encountered any pitfalls when working with dates and times? I am Zhu Ye, and I welcome you to leave a comment in the comment section to share your thoughts. Feel free to share today’s content with your friends or colleagues for further discussion.