Avoid Common SimpleDateFormat Pitfalls and Embrace Thread‑Safe DateTimeFormatter in Java
This article explains the hidden traps of Java's SimpleDateFormat—such as confusing 'y' and 'Y', week‑year calculations, and thread‑safety issues—demonstrates failures with multithreaded parsing, and shows how to replace it with the modern, thread‑safe DateTimeFormatter, including practical code examples and conversion tips.
SimpleDateFormat Pitfalls
Formatting with week‑year
When formatting a Calendar set to 2020‑12‑29, using the pattern YYYY‑MM‑dd prints a week‑year of 2021 instead of the calendar year. The lower‑case y formats the calendar year, while upper‑case Y formats the week‑year, which depends on the locale’s first‑day‑of‑week and minimal days in the first week. In the zh_CN locale the first week of 2020 starts on Sunday 2020‑12‑27, so the week‑year is 2021. Switching to Locale.FRANCE (Monday as first day) makes the week‑year remain 2020.
Locale.setDefault(Locale.FRANCE);Guideline: Use y for the calendar year unless the week‑year is explicitly required.
Thread‑safety
SimpleDateFormat is not thread‑safe because it holds a mutable Calendar . In a test with a 100‑thread pool each thread repeatedly parsed the string "2020-01-01 11:12:13", many threads produced corrupted results (e.g., year 57728) or threw exceptions. The root cause is that SimpleDateFormat#parse invokes CalendarBuilder#establish , which clears and rebuilds the calendar without synchronization, so concurrent threads interfere with each other’s calendar state.
Solution: store a separate SimpleDateFormat instance per thread, e.g. via ThreadLocal , or switch to the immutable DateTimeFormatter introduced in Java 8.
Mismatched pattern
Parsing the string "20160901" with the pattern yyyyMM incorrectly yields the year 2112 because the parser interprets the trailing "1111" as a month value. This demonstrates that SimpleDateFormat can produce nonsensical results when the input does not match the pattern.
Using Java 8’s DateTimeFormatter , which is immutable and strict, avoids these pitfalls.
Java 8 DateTimeFormatter
Defining formatters
Construct formatters with DateTimeFormatterBuilder to avoid memorising case‑sensitive symbols. The builder lets you specify year, month, day, hour, minute, second, etc., in a fluent way.
Thread‑safety
A DateTimeFormatter instance is immutable; it can be declared static and safely shared across threads without additional synchronization.
Strict parsing
If the input string does not match the formatter pattern, parsing throws a DateTimeParseException instead of silently producing a wrong date.
2020/11/11 11:11:11.789
Exception in thread "main" java.time.format.DateTimeParseException: Text '20201111' could not be parsed at index 0
at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1777)
at org.example.CommonMistakesApplication.main(CommonMistakesApplication.java:47)Java 8 Date‑Time Calculations
Adding and subtracting days
Use LocalDate.plusDays(long) / minusDays(long) or the generic plus / minus methods on LocalDateTime to shift dates.
TemporalAdjusters
TemporalAdjusters.firstDayOfMonth()– first day of the current month TemporalAdjusters.firstDayOfYear() – first day of the current year TemporalAdjusters.previous(DayOfWeek.SATURDAY) – previous Saturday TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY) – last Friday of the month
Custom adjustments with lambda
Example: add a random number of days (0‑100) to the current date.
Checking date conditions
Use predicates to test whether a date satisfies a condition (e.g., is after today, falls on a weekend, etc.).
Period vs. ChronoUnit
Period.between(start, end) returns a period expressed in years, months and days. Calling getDays() yields only the remaining days, not the total number of days between the two dates. To obtain the exact day count, use ChronoUnit.DAYS.between(start, end) .
Conversion between java.util.Date and LocalDateTime
java.util.Date represents an instant in UTC (a timestamp). LocalDateTime represents a date‑time without a time‑zone. Converting requires an explicit ZoneId :
Date date = new Date();
LocalDateTime ldt = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
Date back = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());This conversion ensures that the UTC instant is correctly interpreted in the desired time‑zone and vice‑versa.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
JavaEdge
First‑line development experience at multiple leading tech firms; now a software architect at a Shanghai state‑owned enterprise and founder of Programming Yanxuan. Nearly 300k followers online; expertise in distributed system design, AIGC application development, and quantitative finance investing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
