Migrating from java.util.Date to Java 8 Date-Time API
LocalDate, LocalTime, LocalDateTime
The java.time package introduced in Java 8 provides three main classes for handling date and time components:
LocalDaterepresents a calendar date without time informationLocalTimerepresents a time of day, such as11:23LocalDateTimecombines both date and time components
The LocalDateTime class stores the date and time as separate immutable objects internally:
@Override
public String toString() {
return date.toString() + 'T' + time.toString();
}
The implementation shows this composition clearly:
private final LocalDate date;
private final LocalTime time;
Creating Current Time Instances
LocalDateTime current = LocalDateTime.now();
Date legacyDate = new Date();
LocalDateTime is designed specifically for time manipulation. Its constructor is private, encouraging the use of factory methods.
Parsing from String
To parse a date string like 2019-01-11:
String input = "2019-01-11";
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate parsed = LocalDate.parse(input, pattern);
Compare this with the legacy approach:
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
try {
Date parsed = format.parse(input);
} catch (ParseException e) {
e.printStackTrace();
}
Key difference: DateTimeFormatter is located in java.time.format, the same package as LocalDate. SimpleDateFormat belongs to java.text alongside Date.
When parsing fails, exception handling differs significantly. The new API throws DateTimeParseException, which extends RuntimeException. The legacy ParseException extends the checked Exception. This design choice reflects a philosophy that parsing failures often indicate programming errors rather than expected runtime conditions—similar to NumberFormatException.
Direct Initialization
Creating a specific date with LocalDate:
LocalDate target = LocalDate.of(2019, 1, 12);
The Date class requires either milliseconds since epoch, Calendar manipulation, or string parsing.
Timestamp Conversions
Converting a timestamp to LocalDateTime requires an intermediate Instant and a ZoneId:
long epochMillis = System.currentTimeMillis();
Instant inst = Instant.ofEpochMilli(epochMillis);
LocalDateTime converted = LocalDateTime.ofInstant(inst, ZoneId.systemDefault());
Converting back to a timestamp:
LocalDateTime dt = LocalDateTime.now();
dt.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
dt.toInstant(ZoneOffset.of("+08:00")).toEpochMilli();
dt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
Adding hours manually also works for timezone adjustments, useful when working with systems like Elasticsearch that store timestamps in UTC:
LocalDateTime adjusted = LocalDateTime.parse(json.getString("@timestamp"),
DateTimeFormatter.ISO_DATE_TIME).plusHours(8L);
Interoperability with Date
Date legacy = new Date();
Instant intermediary = legacy.toInstant();
Date reconstructed = Date.from(intermediary);
Improved API Design
Working with Date and Calendar introduces cognitive overhead. Month indexing starts at 0, and day-of-week definitions vary. LocalDate uses DayOfWeek as an enum, eliminating ambiguity:
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MONTH, cal.get(Calendar.MONTH) + 1);
The modern equivalent:
LocalDate.of(2019, 1, 12);
Thread Safety Comparison
All java.time classes use final fields, ensuring immutability. Operations return new instances rather than modifying state. Date, however, is mutable through its getter and setter methods.
The Calendar.set() method was deprecated precisely because better alternatives exist, not primarily for thread safety reasons:
Calendar.set(Calendar.DAY_OF_MONTH, int date)
SimpleDateFormat Thread Safety
SimpleDateFormat is notorious thread-unsafe. Under concurrent load, it produces incorrect results without throwing exceptions, making bugs harder to detect:
public class FormatThreadTest {
private static final String DATE_STRING = "2019-01-11 11:11:11";
private static final long EXPECTED_MS = 1547176271000L;
private static final SimpleDateFormat LEGACY_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter MODERN_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
concurrentTest(LEGACY_FORMAT, DATE_STRING, EXPECTED_MS);
concurrentTest(MODERN_FORMAT, DATE_STRING);
}
private static void concurrentTest(Object formatter, String input, long expectedMs) {
CountDownLatch latch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
if (formatter instanceof SimpleDateFormat) {
try {
Date result = ((SimpleDateFormat) formatter).parse(input);
if (result.getTime() != expectedMs) {
System.out.println("Mismatch: " + result);
}
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
latch.countDown();
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void concurrentTest(DateTimeFormatter formatter, String input) {
CountDownLatch latch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
LocalDateTime parsed = LocalDateTime.parse(input, formatter);
} catch (Exception e) {
System.out.println("Parse error: " + e.getMessage());
}
latch.countDown();
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Typical output from the SimpleDateFormat test includes malformed dates and parsing errrors. The DateTimeFormatter test completes without issues.
Workarounds like creating a new SimpleDateFormat instance per thread add garbage collection pressure under high load. For thread-safe formatting, ThreadLocal<SimpleDateFormat> or switching to DateTimeFormatter are preferred.
Additional Classses in java.time
The java.time package contains several other useful types:
- Instant: Point on the timeline, similar to epoch milliseconds
- Duration: Measures time between two instants
- Period: Date-based amount of time in units like days or months
- ZoneOffset: Fixed offset from UTC, such as
+08:00 - ZonedDateTime: Date-time with explicit time zone context
- Clock: Abstraction for accessing current moments, useful for testing