Report this

What is the reason for this report?

Java SimpleDateFormat & DateFormat: Complete Guide

Updated on April 24, 2026
Java SimpleDateFormat & DateFormat: Complete Guide

Introduction

SimpleDateFormat formats and parses java.util.Date values using custom pattern strings, and DateFormat provides the abstract base API for locale-aware date and time formatting. Use these APIs when you maintain legacy Java code that still depends on java.util.Date; for new code, prefer DateTimeFormatter from the Java 8+ date/time API because SimpleDateFormat is not thread-safe.

This tutorial shows how to use DateFormat and SimpleDateFormat for formatting, parsing, timezone conversion, and locale-specific output. It also covers strict validation with setLenient(false), thread safety workarounds, and a practical migration path to DateTimeFormatter.

Key Takeaways

  • DateFormat is the abstract formatting API, and SimpleDateFormat is the concrete implementation for pattern-based formatting and parsing
  • SimpleDateFormat supports rich pattern tokens for years, months, days, time fields, and timezone output
  • A shared SimpleDateFormat instance is unsafe in multithreaded code because its internal state is mutable
  • ThreadLocal<SimpleDateFormat> isolates formatter instances per thread when legacy code cannot migrate immediately
  • DateTimeFormatter is immutable and thread-safe, so it is the preferred replacement for new Java code
  • .parse() can throw ParseException, so parsing code should use try-catch and useful error messages
  • setLenient(false) prevents silently adjusted invalid dates, such as 2024-13-45
  • Always pass an explicit Locale to avoid environment-dependent output in server deployments

What is SimpleDateFormat in Java?

SimpleDateFormat is the DateFormat subclass that lets you define explicit date/time patterns for formatting and parsing in Java.

Use DateFormat when you want style-based, locale-aware output through factory methods, and use SimpleDateFormat when you need exact pattern control such as "dd-MM-yyyy" or "yyyy-MM-dd'T'HH:mm:ssZ". The DateFormat style constants (SHORT, MEDIUM, LONG, FULL) produce locale-appropriate output without requiring you to know the exact regional date order, which is useful for user-facing display strings where the format should follow the user’s locale conventions. Use SimpleDateFormat when the output format is fixed by an external requirement such as an API contract, a log format, or a file naming convention, because those cases require an exact pattern regardless of locale.

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

Locale locale = Locale.US;
DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
String styleBased = dateFormat.format(new Date());

SimpleDateFormat patternBased = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", locale);
String patternOutput = patternBased.format(new Date());

System.out.println(styleBased);
System.out.println(patternOutput);
Jan 5, 2026
05-01-2026 14:30:22

new Locale(String, String) is deprecated as of Java 19. Use Locale.Builder instead.

Locale locale = new Locale.Builder()
    .setLanguage("en")
    .setRegion("US")
    .build();

To format time instead of a date, use DateFormat.getTimeInstance() with the same locale and style parameters.

Locale locale = Locale.US;
DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale);
System.out.println(timeFormat.format(new Date()));
Output depends on the current time and timezone, for example:
2:30:45 PM

For a broader overview of modern Java date/time APIs, see Java 8 Date, LocalDate, LocalDateTime, Instant and Java 8 Features with Examples.

SimpleDateFormat pattern syntax reference

SimpleDateFormat pattern letters map directly to date/time components, and correct pattern selection controls both output format and parse behavior.

Symbol Meaning Example Pattern Example Output
G Era designator G AD
y Year yyyy 2026
M Month in year MMMM January
w Week in year w 3
W Week in month W 1
D Day in year D 15
d Day in month dd 05
F Day of week in month F 1
E Day name in week EEE Mon
u Day number in week (1 = Monday) u 1
a AM/PM marker a PM
H Hour in day (0-23) HH 14
k Hour in day (1-24) k 14
K Hour in am/pm (0-11) K 2
h Hour in am/pm (1-12) hh 02
m Minute in hour mm 30
s Second in minute ss 45
S Millisecond SSS 123
z General timezone z IST
Z RFC 822 timezone offset Z +0530
X ISO 8601 timezone offset XXX +05:30

Pattern repetition and output width

Repeated pattern letters change width, numeric padding, or text style.

Field Pattern Example Output
Month M 1
Month MM 01
Month MMM Jan
Month MMMM January
Day of month d 5
Day of month dd 05
Hour (0-23) H 7
Hour (0-23) HH 07
Year yy 26
Year yyyy 2026

Common pattern examples with expected output

These common patterns are useful for logs, APIs, and user-facing dates.

Pattern Example Output
MM/dd/yyyy 01/05/2026
dd-M-yyyy hh:mm:ss 05-1-2026 02:30:45
dd MMMM yyyy 05 January 2026
dd MMMM yyyy zzzz 05 January 2026 India Standard Time
E, dd MMM yyyy HH:mm:ss z Mon, 05 Jan 2026 14:30:45 IST

How to format a date using SimpleDateFormat

To format a Date, create a SimpleDateFormat with a pattern and call .format(date).

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

Date now = new Date();

SimpleDateFormat basic = new SimpleDateFormat("MM-dd-yyyy", Locale.US);
SimpleDateFormat withTime = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.US);
SimpleDateFormat isoLike = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);

System.out.println(basic.format(now));
System.out.println(withTime.format(now));
System.out.println(isoLike.format(now));
01-05-2026
05-01-2026 14:30:45
2026-01-05T14:30:45+0000

SimpleDateFormat applies timezone and locale rules from the formatter instance, so set those values explicitly when output must be consistent.

How to parse a string to a date using SimpleDateFormat

To parse a string into Date, create a formatter with the exact input pattern and call .parse(input).

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

try {
    SimpleDateFormat dateParser = new SimpleDateFormat("dd-MM-yyyy", Locale.US);
    Date parsedDate = dateParser.parse("05-01-2026");
    System.out.println(parsedDate);

    SimpleDateFormat timeParser = new SimpleDateFormat("HH:mm:ss", Locale.US);
    Date parsedTime = timeParser.parse("22:15:09");
    System.out.println(parsedTime);
} catch (ParseException e) {
    e.printStackTrace();
}
Mon Jan 05 00:00:00 UTC 2026
Thu Jan 01 22:15:09 UTC 1970

If only time is provided, Java uses the epoch date as the date portion.

Handling ParseException correctly

parse() throws ParseException when input does not match the configured pattern, so production code should catch it and log both the raw input and the expected pattern.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Locale;

String input = "31/01/2026";
String pattern = "dd-MM-yyyy";
SimpleDateFormat parser = new SimpleDateFormat(pattern, Locale.US);

try {
    parser.parse(input);
    System.out.println("Parsed successfully");
} catch (ParseException e) {
    // Include both the input value and pattern for easier debugging.
    System.err.println("Failed to parse date string: " + input);
    System.err.println("Expected pattern: " + pattern);
    System.err.println("Parser error: " + e.getMessage());
}
Failed to parse date string: 31/01/2026
Expected pattern: dd-MM-yyyy
Parser error: Unparseable date: "31/01/2026"

For exception handling patterns you can reuse across services, see Exception Handling in Java and Java 8 Date, LocalDate, LocalDateTime, Instant.

Validating date strings before parsing

SimpleDateFormat is lenient by default, so invalid values can roll over instead of failing; call setLenient(false) to enforce strict validation.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Locale;

String invalid = "2024-13-45";

SimpleDateFormat lenientParser = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
SimpleDateFormat strictParser = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
strictParser.setLenient(false);

System.out.println("Lenient parse result:");
try {
    // Lenient mode silently rolls over invalid values instead of throwing.
    System.out.println(lenientParser.parse(invalid));
} catch (ParseException e) {
    System.out.println("ParseException: " + e.getMessage());
}

System.out.println("Strict parse result:");
try {
    System.out.println(strictParser.parse(invalid));
} catch (ParseException e) {
    System.out.println("ParseException: " + e.getMessage());
}
Lenient parse result:
Fri Feb 14 00:00:00 UTC 2025
Strict parse result:
ParseException: Unparseable date: "2024-13-45"

Timezone handling in SimpleDateFormat

SimpleDateFormat can render the same Date in different timezones by setting a timezone on the formatter before output. A Date object stores only a UTC millisecond count and carries no timezone of its own. Timezone is a formatting concern, not a storage concern; the same Date value produces different human-readable output depending on which timezone the formatter applies. This means setTimeZone() must be called on the formatter before format() is called; setting it afterward has no effect on output already produced.

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

Date timestamp = new Date(1700000000000L); // Fixed instant for repeatable output
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US);

formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
System.out.println("UTC: " + formatter.format(timestamp));

formatter.setTimeZone(TimeZone.getTimeZone("America/New_York"));
System.out.println("New York: " + formatter.format(timestamp));

formatter.setTimeZone(TimeZone.getTimeZone("Asia/Kolkata"));
System.out.println("Kolkata: " + formatter.format(timestamp));
UTC: 2023-11-14 22:13:20 +0000
New York: 2023-11-14 17:13:20 -0500
Kolkata: 2023-11-15 03:43:20 +0530

See How to Convert Java Date into Specific TimeZone format for deeper timezone conversion patterns.

Common timezone mistakes and how to avoid them

Timezone bugs often come from confusing stored instants with rendered local time.

  • Date stores UTC milliseconds, and timezone is applied only when formatting
  • Calling setTimeZone() on a shared formatter instance in a multithreaded environment can change the timezone mid-operation for another thread’s in-flight call, producing output in the wrong timezone with no exception thrown
  • Reusing a single SimpleDateFormat instance across HTTP requests in a servlet or Spring controller can cause one request’s setTimeZone() call to affect another request’s output; create a new instance per request or use ThreadLocal<SimpleDateFormat> to isolate state
  • TimeZone.getTimeZone("Invalid/Zone") falls back to GMT silently because the method contract specifies GMT as the default for unrecognized IDs rather than throwing an exception
  • Validate zone IDs with ZoneId.of() from java.time before passing them to TimeZone.getTimeZone(); ZoneId.of() throws ZoneRulesException on an invalid ID, which makes the problem explicit

Locale-aware date formatting

To produce locale-specific date output such as French month names or German day names, pass a Locale to the SimpleDateFormat constructor. Use the built-in constants, Locale.US, Locale.UK, Locale.FRENCH, Locale.GERMAN, for common regions, or construct a custom locale with Locale.Builder when you need a region not covered by the constants. Locale controls which language is used for text fields such as month names, day names, and AM/PM markers.

Specifying Locale in the SimpleDateFormat constructor

Pass a Locale as the second constructor argument. If you omit it, SimpleDateFormat calls Locale.getDefault() internally, which binds the formatter to the JVM’s runtime locale, a value that differs between developer machines, CI environments, and production servers running under different OS locale configurations.

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

Date sample = new Date(1700000000000L);
String pattern = "EEEE, dd MMMM yyyy";

SimpleDateFormat usFormatter = new SimpleDateFormat(pattern, Locale.US);
SimpleDateFormat frFormatter = new SimpleDateFormat(pattern, Locale.FRENCH);

String usDate = usFormatter.format(sample);
String frDate = frFormatter.format(sample);

System.out.println("US: " + usDate);
System.out.println("French: " + frDate);
US: Tuesday, 14 November 2023
French: mardi, 14 novembre 2023

Formatting month and day names by region

The same pattern produces different text output by locale.

Locale Pattern Output
Locale.US EEEE, dd MMMM yyyy Tuesday, 14 November 2023
Locale.FRENCH EEEE, dd MMMM yyyy mardi, 14 novembre 2023
Locale.GERMAN EEEE, dd MMMM yyyy Dienstag, 14 November 2023

Relying on Locale.getDefault() in a JVM running in a server environment can produce inconsistent output because OS locale settings differ across hosts. Always pass an explicit Locale.

Thread safety problems with SimpleDateFormat

Why SimpleDateFormat is not thread-safe

SimpleDateFormat mutates internal Calendar and NumberFormat state during parse/format operations. If multiple threads use one instance, these mutable fields can be overwritten mid-operation, which causes race conditions and unpredictable output. In practice this surfaces as dates from one thread appearing in another thread’s result, .parse() returning a completely wrong date with no exception, or an ArrayIndexOutOfBoundsException thrown from inside the formatter itself. These failures are intermittent and load-dependent, which makes them difficult to reproduce in a single-threaded test environment.

When you need concurrency-safe formatting in legacy code, isolate instances by thread or request scope. For background on Java threading behavior, see Java Thread Example.

Using ThreadLocal to isolate SimpleDateFormat instances

ThreadLocal<SimpleDateFormat> gives each thread its own formatter instance and avoids shared mutable state. In thread-pool environments such as servlet containers and Spring applications, threads are reused across requests and are never terminated, which means ThreadLocal values are never automatically removed. Always call FORMATTER.remove() after the operation completes, or the formatter instance will persist for the lifetime of the thread and accumulate as a memory leak under sustained load.

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class ThreadSafeLegacyFormatter {
    // Each thread gets one dedicated formatter instance.
    private static final ThreadLocal<SimpleDateFormat> FORMATTER =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US));

    public static String format(Date date) {
        try {
            // No cross-thread mutation because each thread reads its own formatter.
            return FORMATTER.get().format(date);
        } finally {
            // Remove the instance after use to prevent memory leaks in thread pools.
            FORMATTER.remove();
        }
    }
}

Switching to DateTimeFormatter as the thread-safe alternative

DateTimeFormatter is immutable and thread-safe, so one shared static instance is safe.

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Locale;

// Not thread-safe when shared across threads
SimpleDateFormat unsafe = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
System.out.println(unsafe.format(new Date()));

// Thread-safe: DateTimeFormatter is immutable and can be stored as a static field
DateTimeFormatter safe = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.US);
System.out.println(safe.format(LocalDateTime.now()));
2026-01-05 14:30:22
2026-01-05 14:30:22

Migrating from SimpleDateFormat to DateTimeFormatter

Migration to DateTimeFormatter removes thread-safety risks, eliminates ThreadLocal workarounds, and integrates cleanly with java.time types. For most codebases the migration is mechanical: pattern syntax is nearly identical, the two APIs can coexist during an incremental rollout, and you do not need to convert everything at once. The main risk is the u symbol, which means day-of-week in SimpleDateFormat and year in DateTimeFormatter; patterns copied directly between APIs without checking this will silently produce wrong output.

Side-by-side code comparison

This side-by-side snippet shows equivalent formatting and parsing behavior in legacy and modern APIs.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Locale;

String pattern = "yyyy-MM-dd";
String input = "2026-01-05";

// SimpleDateFormat (legacy java.util.Date API)
SimpleDateFormat legacyFormatter = new SimpleDateFormat(pattern, Locale.US);
Date legacyParsed = legacyFormatter.parse(input);
String legacyFormatted = legacyFormatter.format(new Date());

// DateTimeFormatter (modern java.time API)
DateTimeFormatter modernFormatter = DateTimeFormatter.ofPattern(pattern, Locale.US);
LocalDate modernParsed = LocalDate.parse(input, modernFormatter);
String modernFormatted = modernFormatter.format(LocalDate.now());

System.out.println(legacyParsed);
System.out.println(legacyFormatted);
System.out.println(modernParsed);
System.out.println(modernFormatted);
Mon Jan 05 00:00:00 UTC 2026
2026-01-05
2026-01-05
2026-01-05

Pattern syntax differences between the two APIs

Most pattern letters are similar, but some symbols differ and can break migrations if copied directly.

Task SimpleDateFormat Pattern DateTimeFormatter Pattern Note
Year yyyy uuuu or yyyy Prefer uuuu in java.time for proleptic year consistency
Timezone offset/name z, Z, X z, Z, x, X java.time distinguishes more offset variants (x and X)
Literal text 'T' 'T' Both APIs use single quotes for literals
u symbol Day-of-week number Year Known gotcha: u changes meaning between APIs

Step-by-step migration checklist

Use this checklist to convert one formatter at a time in existing codebases.

  1. Replace import java.text.SimpleDateFormat; with import java.time.format.DateTimeFormatter;
  2. Replace new SimpleDateFormat("yyyy-MM-dd") with DateTimeFormatter.ofPattern("yyyy-MM-dd")
  3. Replace .parse() with LocalDate.parse() or LocalDateTime.parse() depending on input precision
  4. Replace .format(date) with formatter.format(localDate) or formatter.format(localDateTime)
  5. Remove new Date() where possible and use LocalDate.now() or Instant.now()

DateTimeFormatter works exclusively with java.time types (LocalDate, LocalDateTime, ZonedDateTime, Instant). It does not accept java.util.Date directly. If you have code that still produces java.util.Date values, for example from a legacy ORM or JDBC driver, convert first with date.toInstant() before passing to DateTimeFormatter. The two APIs can coexist in the same codebase during an incremental migration; you do not need to convert everything at once. DateTimeFormatter also replaces ParseException with java.time.format.DateTimeParseException, which is an unchecked exception, so try-catch blocks become optional but are still recommended in production parsing code.

For migration context and type choices, refer to Java 8 Date, LocalDate, LocalDateTime, Instant.

Frequently Asked Questions

What is the difference between SimpleDateFormat and DateFormat in Java?

DateFormat is an abstract class that defines the contract for date/time formatting. SimpleDateFormat is its concrete subclass that allows custom pattern-based formatting. You use SimpleDateFormat directly in code; you use DateFormat as a reference type when you want to accept any implementation.

Is SimpleDateFormat thread-safe?

No. SimpleDateFormat instances are not thread-safe. Sharing a single instance across threads without synchronization causes unpredictable parse and format results. Use ThreadLocal<SimpleDateFormat> or replace it with java.time.format.DateTimeFormatter, which is immutable and thread-safe.

How do I parse a date string in the format dd-MM-yyyy using SimpleDateFormat?

Instantiate SimpleDateFormat with the pattern "dd-MM-yyyy", then call .parse(yourString). Wrap the call in a try-catch block for ParseException. Ensure the input string matches the pattern exactly, including separator characters.

How do I convert a Date to a different timezone using SimpleDateFormat?

Call .setTimeZone(TimeZone.getTimeZone("America/New_York")) on your SimpleDateFormat instance before calling .format(date). The underlying Date object holds UTC milliseconds; the formatter applies the timezone offset during output.

What does the pattern “yyyy-MM-dd’T’HH:mm:ssZ” produce?

It produces an ISO 8601-compatible timestamp, for example 2024-06-15T14:30:00+0530. The T is a literal character wrapped in single quotes. Z represents the RFC 822 timezone offset.

How do I migrate from SimpleDateFormat to DateTimeFormatter?

Replace new SimpleDateFormat("yyyy-MM-dd") with DateTimeFormatter.ofPattern("yyyy-MM-dd"). Replace .parse() calls with LocalDate.parse() or LocalDateTime.parse(). Replace .format(date) calls with formatter.format(localDate). Note that DateTimeFormatter uses u for year in some contexts instead of y.

Why does SimpleDateFormat parse an invalid date without throwing an exception?

By default, SimpleDateFormat uses lenient parsing mode, which adjusts out-of-range values rather than rejecting them. Call .setLenient(false) before parsing to enforce strict validation and throw ParseException on invalid input.

How do I format a date with a specific locale using SimpleDateFormat?

Pass the Locale as a second argument to the constructor: new SimpleDateFormat("dd MMMM yyyy", Locale.FRENCH). This renders month and day names in the specified language, which is critical for applications serving multiple regions.

Conclusion

This guide covered DateFormat and SimpleDateFormat fundamentals, pattern syntax, formatting and parsing workflows, timezone behavior, locale-aware rendering, thread safety issues, and migration to DateTimeFormatter.

You can now choose the right formatter for legacy and modern code, parse input defensively with strict validation, avoid locale and timezone drift in server environments, and remove concurrency risks from shared date formatters.

For next steps, review Java 8 Date, LocalDate, LocalDateTime, Instant, Java 8 Features with Examples, Exception Handling in Java, and How to Convert Java Date into Specific TimeZone format to continue modernizing date/time handling in Java applications.

References:

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the author(s)

Anish Singh Walia
Anish Singh Walia
Author
Sr Technical Content Strategist and Team Lead
See author profile

I help Businesses scale with AI x SEO x (authentic) Content that revives traffic and keeps leads flowing | 3,000,000+ Average monthly readers on Medium | Sr Technical Writer(Team Lead) @ DigitalOcean | Ex-Cloud Consultant @ AMEX | Ex-Site Reliability Engineer(DevOps)@Nutanix

Pankaj Kumar
Pankaj Kumar
Author
See author profile

Java and Python Developer for 20+ years, Open Source Enthusiast, Founder of https://www.askpython.com/, https://www.linuxfordevices.com/, and JournalDev.com (acquired by DigitalOcean). Passionate about writing technical articles and sharing knowledge with others. Love Java, Python, Unix and related technologies. Follow my X @PankajWebDev

Vinayak Baranwal
Vinayak Baranwal
Editor
Technical Writer II
See author profile

Building future-ready infrastructure with Linux, Cloud, and DevOps. Full Stack Developer & System Administrator. Technical Writer @ DigitalOcean | GitHub Contributor | Passionate about Docker, PostgreSQL, and Open Source | Exploring NLP & AI-TensorFlow | Nailed over 50+ deployments across production environments.

Category:
Tags:

Still looking for an answer?

Was this helpful?

You Should have addressed the important topic like Date conversion to different TimeZones. Anyway great tutorial.

- ManishS

Thank you for the article, it’s been very helpful. I believe the day of the week should be amended in this manner below: EEEE, E Day name in the week Tuesday(EEEE), Tue(E or EEE)

- Shane McCurdy

How can i set date format 20th may, 2010

- sridhar

You Should Append About Calendar Class because Date is deprecated

- John Snow

This is the best tutorial on dates I have ever seen

- Joe E

Wrong, “whereas DateFormat allows only formatting of Date.” it can be for parse string to Date

- sda

Creative CommonsThis work is licensed under a Creative Commons Attribution-NonCommercial- ShareAlike 4.0 International License.
Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Start building today

From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.

Dark mode is coming soon.