
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.
DateFormat is the abstract formatting API, and SimpleDateFormat is the concrete implementation for pattern-based formatting and parsingSimpleDateFormat supports rich pattern tokens for years, months, days, time fields, and timezone outputSimpleDateFormat instance is unsafe in multithreaded code because its internal state is mutableThreadLocal<SimpleDateFormat> isolates formatter instances per thread when legacy code cannot migrate immediatelyDateTimeFormatter 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 messagessetLenient(false) prevents silently adjusted invalid dates, such as 2024-13-45Locale to avoid environment-dependent output in server deploymentsSimpleDateFormat 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 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 |
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 |
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 |
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.
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.
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.
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"
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.
Timezone bugs often come from confusing stored instants with rendered local time.
Date stores UTC milliseconds, and timezone is applied only when formattingsetTimeZone() 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 thrownSimpleDateFormat 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 stateTimeZone.getTimeZone("Invalid/Zone") falls back to GMT silently because the method contract specifies GMT as the default for unrecognized IDs rather than throwing an exceptionZoneId.of() from java.time before passing them to TimeZone.getTimeZone(); ZoneId.of() throws ZoneRulesException on an invalid ID, which makes the problem explicitTo 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.
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
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.
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.
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();
}
}
}
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
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.
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
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 |
Use this checklist to convert one formatter at a time in existing codebases.
import java.text.SimpleDateFormat; with import java.time.format.DateTimeFormatter;new SimpleDateFormat("yyyy-MM-dd") with DateTimeFormatter.ofPattern("yyyy-MM-dd").parse() with LocalDate.parse() or LocalDateTime.parse() depending on input precision.format(date) with formatter.format(localDate) or formatter.format(localDateTime)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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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
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.
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
You Should Append About Calendar Class because Date is deprecated
- John Snow
Wrong, “whereas DateFormat allows only formatting of Date.” it can be for parse string to Date
- sda
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.