Truncating java.util.Date to LocalDate *without* toInstant() because java.sql.Date gives UnsupportedOperationException

Often the simplest solutions are the hardest to find:

public LocalDate convertDateObject(java.util.Date suspectDate) {

    try {
        // Don't do this if there is the smallest chance 
        // it could be a java.sql.Date!
        return suspectDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

    } catch (UnsupportedOperationException e) {
        // BOOM!!
    }

    // do this first:
    java.util.Date safeDate = new Date(suspectDate.getTime());

    return safeDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

}

If the input variable is known to be a java.sql.Date, then you can simply cast it and call the toLocalDate() method:

LocalDate date = ((java.sql.Date) input).toLocalDate();

Unfortunately, you can't call toInstant() on a java.sql.Date, because according to javadoc, it always throws an UnsupportedOperationException.

If you don't know the type (it can be either a java.util.Date or a java.sql.Date), you can use the value returned by getTime() method to build an Instant, then convert it to a timezone (below I'm using the JVM's default) and finally get the local date from it:

LocalDate date = Instant
    // get the millis value to build the Instant
    .ofEpochMilli(input.getTime())
    // convert to JVM default timezone
    .atZone(ZoneId.systemDefault())
    // convert to LocalDate
    .toLocalDate();

The toLocalDate() method gets the date part (day/month/year), ignoring the rest, so there's no need to truncate it: it doesn't matter if the time is midnight, 10 AM, or any other time of the day, toLocalDate() will ignore it and get just the date part.

If you really want to set the time to midnight, though, you can use the with method and pass a LocalTime to it:

LocalDate date = Instant
    // get the millis value to build the Instant
    .ofEpochMilli(input.getTime())
    // convert to JVM default timezone
    .atZone(ZoneId.systemDefault())
    // set time to midnight
    .with(LocalTime.MIDNIGHT)
    // convert to LocalDate
    .toLocalDate();

But as I said, the toLocalDate() method will just ignore the time part, so setting the time is not needed in this case (the LocalDate will be the same).


You could also check the date's type and choose the proper action accordingly, like this:

if (input instanceof java.sql.Date) {
    date = ((java.sql.Date) input).toLocalDate();
} else {
    date = input.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}

Instead of using the JVM default timezone (ZoneId.systemDefault()), you can use any other timezone, according to your needs, by calling ZoneId.of("zoneName"), where the zone name is any of the valid IANA timezones names (always in the format Region/City, like America/New_York or Europe/London). Avoid using the 3-letter abbreviations (like CET or PST) because they are ambiguous and not standard.

You can get a list of available timezones (and choose the one that fits best your system) by calling ZoneId.getAvailableZoneIds(). You can also keep using the JVM default timezone if you want, but remind that it can be changed without notice, even at runtime, so it's better to always make it explicit which one you're using.


tl;dr

myResultSet.getObject( … , LocalDate.class ) 

java.time

Your troubles started when using the legacy date-time classes such as java.sql.Date, java.util.Date, and Calendar. Avoid these classes entirely. They are now legacy, supplanted by the java.time classes.

You said the value began as a stored value in MySQL column of type DATE. That type is date-only, without a time-of-day. So you introduced a time-of-day value needlessly by using the wrong classes.

Use a JDBC driver that complies with JDBC 4.2 or later to exchange values with the database using the java.time classes.

LocalDate ld = myResultSet.getObject( … , LocalDate.class ) ;

And pass to a PreparedStatement.

myPstmt.setObject( … , myLocalDate ) ;