W3docs

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 epoch

new 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() restituisce year - 1900. Per il 2025, restituisce 125. 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 scrivi getMonth() + 1 e te ne dimentichi anche solo una volta.
  • Ogni accessor legge nel fuso orario predefinito della JVM. Lo stesso Date su due macchine in fusi orari diversi produce risultati diversi con getDate().

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 8

toInstant() 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 gone

Per 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 — come Date ma 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 place

Ciò 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.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Date.toString() viene stampato nel fuso orario predefinito della JVM. Lo stesso valore Date appare diversamente su un server UTC rispetto a un laptop in America/New_York. Questo è il difetto di progettazione centrale che ha guidato la riprogettazione di java.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 in ZonedDateTime e fornisci esplicitamente il fuso.
  • now.getYear() ha restituito 125 per il 2025. La convenzione year - 1900 era un errore di Java 1.0 mai corretto; il metodo è stato deprecato in 1.1 ed è ancora lì. Qualsiasi chiamata a getYear() su un Date che legge "125" o "85" è un bug in attesa di mostrarsi all'utente.
  • Instant.parse("...nanoseconds") ha stampato nove cifre di precisione; lo stesso valore attraverso Date.from e 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 attraverso Date.
  • Il +1 giorno legacy era now.getTime() + 24L * 60 * 60 * 1000 — aritmetica manuale, facile da sbagliare (dimenticare la L e andare in overflow). Il moderno inst.plus(Duration.ofDays(1)) è type-safe e si legge ad alta voce. Quando stai migrando, sostituire ogni calcolo time + N * ms con Duration è 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.

Pratica

Pratica
Una vecchia libreria restituisce `java.util.Date` da `getCreatedAt()`. Vuoi sapere 'questa data rientra nello stesso giorno di calendario di oggi a New York?'. Qual è il percorso corretto?
Una vecchia libreria restituisce `java.util.Date` da `getCreatedAt()`. Vuoi sapere 'questa data rientra nello stesso giorno di calendario di oggi a New York?'. Qual è il percorso corretto?
Was this page helpful?