La Classe Date Legacy di Java
La classe legacy java.util.Date — perché è stata sostituita da java.time e come convertire tra i due.
java.util.Date era la classe originale di Java per data e ora, presente fin da Java 1.0 nel 1995. È ancora nel JDK; il codice nuovo non dovrebbe utilizzarla, ma la incontrerai in librerie, database (java.sql.Date la estende) e in qualsiasi codice precedente al 2014 circa. Questo capitolo serve a collegarlo a java.time.
La versione breve: Date è internamente un wrapper attorno a un long di millisecondi dall'epoca, proprio come Instant è internamente una coppia (seconds, nanos). Quindi la conversione naturale è Date ↔ Instant. Per qualsiasi altra cosa (anno, mese, giorno, aritmetica del calendario), converti prima in java.time.
Cosa è realmente Date
public class Date implements Cloneable, Comparable<Date>, Serializable {
private long fastTime; // milliseconds since 1970-01-01T00:00:00Z
}Questo è l'intero stato reale. Un Date è un punto nel tempo, misurato in millisecondi dall'epoca Unix, in UTC. Nonostante il nome, non porta una data di calendario — getYear() e simili calcolano dal valore in millisecondi nel fuso orario predefinito della JVM, che è la fonte dei famosi problemi dell'API.
Date now = new Date(); // current moment
Date epoch = new Date(0); // 1970-01-01T00:00:00Z
Date fromMs = new Date(1_700_000_000_000L);
long ms = now.getTime(); // milliseconds since epochnew Date() e Date.getTime() sono i due metodi che hanno resistito bene al tempo. Tutto il resto è stato deprecato o porta con sé una trappola.
Gli accessor di calendario deprecati
Questi metodi sono stati deprecati in Java 1.1 (1997) quando è stato aggiunto Calendar:
date.getYear(); // year - 1900 (deprecated)
date.getMonth(); // 0-11 (deprecated)
date.getDate(); // 1-31, day of month (deprecated)
date.getDay(); // 0-6, day of week (deprecated)
date.getHours(); date.getMinutes(); date.getSeconds(); // local-zone reads (deprecated)Le deprecazioni esistono da 28 anni. Funzionano ancora. Le trappole:
getYear()restituisceyear - 1900. Per il 2025, restituisce125. Questo è un candidato indiscusso alla "decisione API più strana del JDK."getMonth()restituisce 0-11. Gennaio è 0, Dicembre è 11. I bug off-by-one sono garantiti se scrivigetMonth() + 1e te ne dimentichi anche solo una volta.- Ogni accessor legge nel fuso orario predefinito della JVM. Lo stesso
Datesu due macchine in fusi orari diversi produce risultati diversi congetDate().
Non chiamare questi metodi. Nel momento in cui ti ritrovi a voler usare date.getYear(), converti in Instant/ZonedDateTime e usa gli accessor moderni.
Il bridge Date ↔ Instant
Date legacy = new Date();
Instant inst = legacy.toInstant(); // since Java 8
Instant other = Instant.parse("2025-11-04T19:30:00Z");
Date back = Date.from(other); // since Java 8toInstant() e Date.from(...) sono i metodi di conversione moderni, aggiunti con java.time. Sono le uniche due chiamate a java.util.Date che dovresti scrivere in codice nuovo.
La conversione è con perdita di dati in una direzione: Date ha precisione al millisecondo, Instant ha precisione al nanosecondo. Il round-trip Instant → Date → Instant tronca i nanos sub-millisecondo:
Instant high = Instant.parse("2025-11-04T19:30:00.123456789Z");
Instant low = Date.from(high).toInstant();
// low = 2025-11-04T19:30:00.123Z — the 456789 nanos are gonePer i timestamp server questo va bene; per la cattura di eventi ad alta risoluzione, rimani in Instant dall'inizio alla fine.
java.sql.Date e java.sql.Timestamp
I tipi JDBC sono sottoclassi di java.util.Date:
java.sql.Date— una data senza ora (la componente temporale è forzata a 00:00:00 in qualche fuso). Nome fuorviante; è ancora un wrapper di millisecondi-dall'epoca internamente.java.sql.Time— un'ora senza data.java.sql.Timestamp— comeDatema con precisione al nanosecondo.
Tutti hanno metodi di conversione introdotti con JDK 8:
java.sql.Date sqlDate = java.sql.Date.valueOf(LocalDate.of(2025, 11, 4));
LocalDate localDate = sqlDate.toLocalDate();
java.sql.Timestamp ts = java.sql.Timestamp.from(Instant.now());
Instant inst = ts.toInstant();I driver JDBC moderni accettano anche i tipi java.time direttamente tramite setObject/getObject — per il codice nuovo, salta i tipi java.sql.* e usa LocalDate/Instant. Le conversioni esistono per il codice che deve interoperare con un driver o framework non ancora migrato.
Confronto e ordinamento
Date implementa Comparable<Date>. L'ordine è per millisecondi dall'epoca — uguale a Instant. Quindi ordinare List<Date> funziona come ordinare List<Instant>.
equals confronta il long sottostante. Due valori Date sono uguali se e solo se hanno lo stesso valore in millisecondi. L'hashing funziona ((int)(time ^ (time >>> 32))), quindi Date va bene come chiave HashMap — anche se, ancora una volta, Instant è la scelta moderna.
Mutabilità
Il trabocchetto nascosto più grande: Date è mutabile.
Date d = new Date();
d.setTime(0); // mutates d in placeCiò significa che qualsiasi metodo che accetta o restituisce un Date è pericoloso — il chiamante può modificare il valore dopo averlo passato; il destinatario può modificare il valore che il chiamante detiene. Il codice di libreria copia difensivamente (new Date(d.getTime())) ogni Date ricevuto. Questo è il tipo di contabilità che java.time ha eliminato del tutto rendendo ogni tipo immutabile.
Nel codice legacy, tratta qualsiasi campo Date come se potesse cambiare sotto di te. Converti in Instant al confine dell'API se hai bisogno di uno snapshot stabile.
SimpleDateFormat: l'altra cosa deprecata
Accoppiato con Date nel codice vecchio c'è java.text.SimpleDateFormat:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String s = sdf.format(new Date());
Date d = sdf.parse("2025-11-04");SimpleDateFormat non è thread-safe. Condividerlo tra thread produrrà output errati, eccezioni, o entrambi, in modo intermittente. Il workaround standard nel codice legacy è ThreadLocal<SimpleDateFormat>; il sostituto moderno è DateTimeFormatter, che è thread-safe e memorizzabile nella cache.
Se mantieni codice con un static SimpleDateFormat, trattalo come un bug noto indipendentemente dal fatto che qualcuno abbia segnalato un guasto.
Un esempio pratico: interoperabilità legacy
Il programma seguente usa Date come lo troveresti in un'API legacy e mostra il percorso di conversione verso java.time per ogni operazione. Leggilo come "ecco la ricetta di migrazione": ogni chiamata legacy ha un equivalente di una riga con Instant o ZonedDateTime.
Cosa ricavare dall'esecuzione:
Date.toString()viene stampato nel fuso orario predefinito della JVM. Lo stesso valoreDateappare diversamente su un server UTC rispetto a un laptop in America/New_York. Questo è il difetto di progettazione centrale che ha guidato la riprogettazione dijava.time— il valore è UTC, la visualizzazione è locale, e l'API non ti dà un modo semplice per capire quale vista stai guardando. Se ti importa quale fuso intendi, converti inZonedDateTimee fornisci esplicitamente il fuso.now.getYear()ha restituito125per il 2025. La convenzioneyear - 1900era un errore di Java 1.0 mai corretto; il metodo è stato deprecato in 1.1 ed è ancora lì. Qualsiasi chiamata agetYear()su unDateche legge "125" o "85" è un bug in attesa di mostrarsi all'utente.Instant.parse("...nanoseconds")ha stampato nove cifre di precisione; lo stesso valore attraversoDate.frome di ritorno ha perso le ultime sei. Per i log server (la precisione al millisecondo è più che sufficiente), il troncamento non ha importanza. Per "ho catturato questo evento con temporizzazione ad alta precisione," non fare il round-trip attraversoDate.- Il +1 giorno legacy era
now.getTime() + 24L * 60 * 60 * 1000— aritmetica manuale, facile da sbagliare (dimenticare laLe andare in overflow). Il modernoinst.plus(Duration.ofDays(1))è type-safe e si legge ad alta voce. Quando stai migrando, sostituire ogni calcolotime + N * msconDurationè il frutto più a portata di mano. - La demo sulla mutabilità alla fine ha mostrato
shared.setTime(0)che modifica il valore visibile attraverso entrambi i riferimenti. In un codebase multi-thread questo è una race condition; nel codice single-thread è comunque un rituale di copia difensiva che il JDK ha imposto a ogni libreria. L'API moderna non chiede mai quel rituale.
Cosa viene dopo
java.util.Date è una metà dell'API legacy. L'altra metà è java.util.Calendar — la classe aggiunta in Java 1.1 per dare a Date gli accessor di calendario che Date stessa non avrebbe dovuto avere. Il prossimo capitolo, Java Calendar Class, è l'ultimo in questa parte e la tratta con la stessa forma da ricetta di migrazione: ogni chiamata legacy ha un sostituto in java.time.