Konverter java.util.Date til java.time.LocalDate

Hva er den beste måten å konvertere et java.util.Date-objekt til det nye JDK 8/JSR-310 java.time.LocalDate?

Date input = new Date();
LocalDate date = ???
Løsning

Kort svar.

Date input = new Date();
LocalDate date = input.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

Forklaring

Til tross for navnet representerer java.util.Date et øyeblikk på tidslinjen, ikke en dato. De faktiske dataene som er lagret i objektet, er en "lang" telling av millisekunder siden 1970-01-01T00:00Z (midnatt ved begynnelsen av 1970 GMT/UTC).

Den tilsvarende klassen til java.util.Date i JSR-310 er Instant, og det finnes derfor en praktisk metode toInstant() for å foreta konverteringen:

Date input = new Date();
Instant instant = input.toInstant();

En java.util.Date-instans har ikke noe begrep om tidssone. Dette kan virke rart hvis du kaller toString() på en java.util.Date, fordi toString er relativ til en tidssone. Men den metoden bruker faktisk Javas standard tidssone i farten for å gi strengen. Tidssonen er ikke en del av den faktiske tilstanden til java.util.Date.

En Instant inneholder heller ingen informasjon om tidssonen. For å konvertere fra en Instant til en lokal dato er det derfor nødvendig å angi en tidssone. Dette kan være standardsonen - ZoneId.systemDefault() - eller det kan være en tidssone som applikasjonen din kontrollerer, for eksempel en tidssone fra brukerinnstillingene. Bruk atZone()-metoden for å bruke tidssonen:

Date input = new Date();
Instant instant = input.toInstant();
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());

En ZonedDateTime inneholder tilstand bestående av lokal dato og tid, tidssone og forskyvning fra GMT/UTC. Som sådan kan datoen - LocalDate - enkelt hentes ut ved hjelp av toLocalDate():

Date input = new Date();
Instant instant = input.toInstant();
ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
LocalDate date = zdt.toLocalDate();

Java 9-svar

I Java SE 9 er det lagt til en ny metode som forenkler denne oppgaven noe:

Date input = new Date();
LocalDate date = LocalDate.ofInstant(input.toInstant(), ZoneId.systemDefault());

Dette nye alternativet er mer direkte, skaper mindre søppel, og bør derfor fungere bedre.

Kommentarer (18)

En bedre måte er:

Date date = ...;
Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDate()

Fordeler med denne versjonen:

  • Fungerer uansett om input er en forekomst av java.util.Date eller dens underklasse java.sql.Date (i motsetning til @JodaStephens måte). Dette er vanlig med JDBC-opprinnede data. java.sql.Date.toInstant() kaster alltid et unntak.

  • det' er det samme for JDK8 og JDK7 med JSR-310 backport

Jeg personlig bruker en verktøyklasse (men dette er ikke backport-kompatibelt):

/**
 * Utilities for conversion between the old and new JDK date types 
 * (between {@code java.util.Date} and {@code java.time.*}).
 * 
 * <p>
 * All methods are null-safe.
 */
public class DateConvertUtils {

    /**
     * Calls {@link #asLocalDate(Date, ZoneId)} with the system default time zone.
     */
    public static LocalDate asLocalDate(java.util.Date date) {
        return asLocalDate(date, ZoneId.systemDefault());
    }

    /**
     * Creates {@link LocalDate} from {@code java.util.Date} or it's subclasses. Null-safe.
     */
    public static LocalDate asLocalDate(java.util.Date date, ZoneId zone) {
        if (date == null)
            return null;

        if (date instanceof java.sql.Date)
            return ((java.sql.Date) date).toLocalDate();
        else
            return Instant.ofEpochMilli(date.getTime()).atZone(zone).toLocalDate();
    }

    /**
     * Calls {@link #asLocalDateTime(Date, ZoneId)} with the system default time zone.
     */
    public static LocalDateTime asLocalDateTime(java.util.Date date) {
        return asLocalDateTime(date, ZoneId.systemDefault());
    }

    /**
     * Creates {@link LocalDateTime} from {@code java.util.Date} or it's subclasses. Null-safe.
     */
    public static LocalDateTime asLocalDateTime(java.util.Date date, ZoneId zone) {
        if (date == null)
            return null;

        if (date instanceof java.sql.Timestamp)
            return ((java.sql.Timestamp) date).toLocalDateTime();
        else
            return Instant.ofEpochMilli(date.getTime()).atZone(zone).toLocalDateTime();
    }

    /**
     * Calls {@link #asUtilDate(Object, ZoneId)} with the system default time zone.
     */
    public static java.util.Date asUtilDate(Object date) {
        return asUtilDate(date, ZoneId.systemDefault());
    }

    /**
     * Creates a {@link java.util.Date} from various date objects. Is null-safe. Currently supports:<ul>
     * <li>{@link java.util.Date}
     * <li>{@link java.sql.Date}
     * <li>{@link java.sql.Timestamp}
     * <li>{@link java.time.LocalDate}
     * <li>{@link java.time.LocalDateTime}
     * <li>{@link java.time.ZonedDateTime}
     * <li>{@link java.time.Instant}
     * </ul>
     * 
     * @param zone Time zone, used only if the input object is LocalDate or LocalDateTime.
     * 
     * @return {@link java.util.Date} (exactly this class, not a subclass, such as java.sql.Date)
     */
    public static java.util.Date asUtilDate(Object date, ZoneId zone) {
        if (date == null)
            return null;

        if (date instanceof java.sql.Date || date instanceof java.sql.Timestamp)
            return new java.util.Date(((java.util.Date) date).getTime());
        if (date instanceof java.util.Date)
            return (java.util.Date) date;
        if (date instanceof LocalDate)
            return java.util.Date.from(((LocalDate) date).atStartOfDay(zone).toInstant());
        if (date instanceof LocalDateTime)
            return java.util.Date.from(((LocalDateTime) date).atZone(zone).toInstant());
        if (date instanceof ZonedDateTime)
            return java.util.Date.from(((ZonedDateTime) date).toInstant());
        if (date instanceof Instant)
            return java.util.Date.from((Instant) date);

        throw new UnsupportedOperationException("Don't know hot to convert " + date.getClass().getName() + " to java.util.Date");
    }

    /**
     * Creates an {@link Instant} from {@code java.util.Date} or it's subclasses. Null-safe.
     */
    public static Instant asInstant(Date date) {
        if (date == null)
            return null;
        else
            return Instant.ofEpochMilli(date.getTime());
    }

    /**
     * Calls {@link #asZonedDateTime(Date, ZoneId)} with the system default time zone.
     */
    public static ZonedDateTime asZonedDateTime(Date date) {
        return asZonedDateTime(date, ZoneId.systemDefault());
    }

    /**
     * Creates {@link ZonedDateTime} from {@code java.util.Date} or it's subclasses. Null-safe.
     */
    public static ZonedDateTime asZonedDateTime(Date date, ZoneId zone) {
        if (date == null)
            return null;
        else
            return asInstant(date).atZone(zone);
    }

}

Metoden asLocalDate() her er nullsikker, bruker toLocalDate(), hvis input er java.sql.Date (den kan overstyres av JDBC-driveren for å unngå tidssoneproblemer eller unødvendige beregninger), ellers brukes metoden ovenfor.

Kommentarer (8)

Hvis du bruker Java 8, er @JodaStephens svar åpenbart det beste. Men hvis du jobber med JSR-310 backport, må du dessverre gjøre noe som dette:

Date input = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(input);
LocalDate date = LocalDate.of(cal.get(Calendar.YEAR),
        cal.get(Calendar.MONTH) + 1,
        cal.get(Calendar.DAY_OF_MONTH));
Kommentarer (2)