Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Migrating from java.util.Date to Java 8 Date-Time API

Tech 1

LocalDate, LocalTime, LocalDateTime

The java.time package introduced in Java 8 provides three main classes for handling date and time components:

  • LocalDate represents a calendar date without time information
  • LocalTime represents a time of day, such as 11:23
  • LocalDateTime combines 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

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.