I know this may sound awkward after 3 years of software development, but I'm trying to better understand the java.time classes and how to use them to avoid errors and bad practice.
On the first project where I was involved, the java version used was 8, but date values was stored on legacy class java.util.Date
Two years later, new company, new project with java version 17, but still date values are stored into legacy class java.util.Date
The confusion started when I see that there was a DateUtils on the project with methods like:
public static String toString_ddMMyyyy_dotted(Date date){
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefualt()).toLocalDateTime();
DateTimeFormatter.ofPattern("dd.MM.yyyy").format(localDateTime);
}
public static Date getBeginOfTheYear(Date date){
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefualt()).toLocalDate();
LocalDate beginOfTheYear = localDate.withMonth(1).withDayOfMonth(1);
return Date.from(beginOfTheYear.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
}
So my question was, why not using directly the java.time classes which have a really good APIs instead of converting a variable into LocalDate/LocalDateTime whenever an operation on the Date is required?
The idea is to refactor the project as soon as possible to store Date/DateTime/Time values into java.time classes. I started from a simple module, that saves some data into MongoDB. I applied the following field type conversion:
- Date values into LocalDate
- Date and time values into LocalDateTime
During test I immediately find an issue, a localDateTime variable with value of 2023-10-07 12:00:00 was saved on MongoDB as 2023-10-07 10:00:00
First of all, does this happen only when using LocalDateTime, or does it also happen with LocalDate?
What are the best practices to store date/date-time values on entity/document classes? Should I use ZonedDateTime instead of LocalDateTime?
Conversion rather than revamp
Depending on the size and complexity of your codebase, this may be quite risky.
A more conservative approach is to do all new programming in java.time. To interoperate with old code not yet updated to java.time, convert. You will find new conversion methods on the old classes. Look for
to…&from…methods. These conversion methods provide complete coverage, allowing you to go back and forth.Another word of caution when revamping code: Many/most programmers have a poor understanding of date-time handling. The topic is surprisingly tricky and complicated, the concepts slippery when first encountered. Our quotidian understanding of date-time actually works against us when dealing with the strictness of programming and databases. So beware of poor code, buggy code, that mishandles date-time. Each time you discover such faulty code, you will be opening up a barrel of trouble as reports may have been produced showing incorrect results, databases may store invalid data, etc.
You can search existing Stack Overflow questions and answers to learn more. Below are a few brief points to help guide you.
Moment versus Not-a-moment
Incorrect. Or incomplete, I should say.
I recommend you frame your thinking this way… There are two kinds of time tracking:
Moment
A moment is a specific point on the timeline. Moments are tracked in java.time as a count of whole seconds, plus a fractional second as a count of nanoseconds, since the epoch reference of first moment of 1970 in UTC. "in UTC" is short for "an offset from the temporal meridian of UTC of zero hours-minutes-seconds".
The basic class for representing a moment is
java.time.Instant. Objects of this class represent a moment as seen in UTC, always in UTC. This class is the basic building-block of the jav.time framework. This class should be your first and last thought when dealing with moments. UseInstantto track a moment unless your business rules specifically involve a particular zone or offset. Andjava.util.Dateis specifically replaced byjava.time.Instant, with both representing a moment in UTC but withInstanthaving a finer resolution of nanoseconds over thejava.util.Dateresolution of milliseconds.Two other classes track moments in java.time:
OffsetDateTime&ZonedDateTime.OffsetDateTimerepresents a moment as seen with an offset-from-UTC other than zero. An offset is merely a number of hours-minutes-seconds ahead or behind UTC. So people in Paris set the clocks on their wall one or two hours ahead of UTC, while people in Tokyo set their clocks nine hours ahead, whereas people in Nova Scotia Canada set their clocks three or four hours behind UTC.A time zone is much more than a mere offset. A time zone is a named history of the past, present, and future changes to the offset used by the people of a particular region as decided by their politicians. A time zone has a name in the format of
Continent/Regionsuch asEurope/Paris&Asia/Tokyo.To view a date and time-of-day through the wall-clock time of a particular region, use
ZonedDateTime.Note that you can easily and cleanly move back and forth between these three classes,
Instant,OffsetDateTime, andZonedDateTimevia theirto…,at…, andwith…methods. They provide three ways of looking at the very same moment. Generally you useInstantin your business logic and data storage and data exchange, while usingZonedDateTimefor presentation to the user.Not-a-moment
On the other hand, we have "not a moment" tracking. These are the indefinite types.
The not-a-moment class causing the most confusion is
LocalDateTime. This class represents a date with a time-of-day but lacking any concept of offset or time zone. So if I say noon next January 23rd 2024,LocalDateTime.of( 2024 , Month.JANUARY , 23 , 12 , 0 , 0 , 0 ), you have no way of knowing if I mean noon in Tokyo, noon in Toulouse, or noon in Toledo Ohio US — three very different moments several hours apart.When in doubt, do not use
LocalDateTime. Generally in programming business apps we care about moments, and so rarely useLocalDateTime.The big exception, when we most often need
LocalDateTimein business apps, is booking appointments, such as for the dentist. There we need to hold aLocalDateTimeand time zone (ZoneId) separately rather than combining them into aZonedDateTime. The reason is crucial: Politicians frequently change time zone rules, and they do so unpredictably and often with little forewarning. So that dentist appoint at 3 PM may occur an hour earlier, or an hour later, if the politicians there decide:All of these happen much more often than most people realize. Such changes break naïvely-written apps needlessly.
The other indefinite types include:
LocalDatefor a date-only value without a time-of-day and without any zone/offset.LocalTimefor a time-only value without any date and without any zone/offset.The
LocalDatecan be tricky in that many people are unaware that, for any given moment, the date varies around the globe by time zone. Right now is "tomorrow" in Tokyo Japan while simultaneously "yesterday" in Edmonton Canada.Your code
That code has two problems.
LocalDateTime. Thejava.util.Dateclass behinddatevar represents a moment with an offset of zero. You discard the crucial piece of information, the offset, when converting toLocalDateTime.ZoneId.systemDefualt(). This means the results vary depending on the JVM’s current default time zone. As discussed above, this means the date may vary, not just the time-of-day. The same code running on two different machines may produce two different dates. This may be what you want in your app, or this may be a rude awakening to an unaware programmer.Your other piece of code also has problems.
First of all, be aware there are unfortunately two
Dateclasses amongst the legacy date-time classes:java.util.Datefor a moment, andjava.sql.Datethat pretends to represent a date-only but is actually a moment (in a horrendously poor class-design decision). I will assume your meansjava.util.Date.Secondly, avoid these kinds of call-chains with this kind of code. Readability, debugging, and logging are all made more difficult. Write simple short lines instead when doing a series of conversions. Use comments on each line to justify the operation, and explain your motivation. And, avoid the use of
varfor the same reason; use explicit return types when doing such conversions.The
date.toInstant()is good. When encountering ajava.util.Date, immediately convert to aInstant.Again, the use of
ZoneId.systemDefualt()means the results of your code vary by the whim of any sys-admin or user who is changing the default time zone. This might be the intent of the author of this code, but I doubt it.The part
beginOfTheYear.atStartOfDay()produces aLocalDateTimewhen you pass no arguments. So you are again discarding valuable info (offset) without gaining anything in return.Another problem: Your code will not even compile. The
java.util.Date.frommethod takes anInstant, not aLocalDateTimereturned by your callbeginOfTheYear.atStartOfDay().To correctly get the first moment of the first of the year of a particular moment, you almost certainly would want the people deciding business rules to dictate a particular time zone.
You should indeed convert the incoming
java.util.Dateto aInstant.Then apply the zone.
Extract year portion. As discussed above, the date may vary by time zone. Therefore, the year may vary depending on the zone you provided in the previous line of code above!
Get first day of that year.
Let java.time determine the first moment of that day in that zone. Do not assume the start time is 00:00. Some dates in some zones start at another time such as 01:00.
Notice that in determining the first moment of the year, we pass the argument for
ZoneIdtoatStartOfDay, in contrast to your code. The result here is aZonedDateTimerather than theLocalDateTimein your code.Lastly, we convert to
java.util.Date. In greenfield code, we would avoid this class like the Plague. But in your existing codebase, we must convert to interoperate with the parts of your old code not yet updated to java.time.MongoDB
You said:
Too much to unpack there, with too little detail from you.
I suggest you post another Question specifically on that issue. Provide sufficient detail, along with example code, to make a diagnosis.