With the introduction of Java 8, the modern date and time API (java.time) was added to the Java platform, largely inspired by Joda-Time but with significant improvements. While Joda-Time was the go-to library for date/time handling for years, the official recommendation is now to migrate to java.time. This article provides a complete guide for converting Joda-Time code to java.time, including equivalent classes, common patterns, and migration strategies.
Why Migrate from Joda-Time to java.time?
Joda-Time Status:
- Joda-Time is in maintenance mode
- The authors recommend migrating to java.time
- No new features are being added
- Future Java versions may not include Joda-Time
java.time Advantages:
- Part of the Java standard library (JDK 8+)
- Better performance in many cases
- Improved API design
- More comprehensive time zone handling
- Built-in support for modern standards
Core Class Equivalents
| Joda-Time | java.time | Purpose |
|---|---|---|
DateTime | ZonedDateTime | Date and time with time zone |
DateTime (UTC) | Instant | Instantaneous point on timeline |
LocalDateTime | LocalDateTime | Date and time without time zone |
LocalDate | LocalDate | Date without time and time zone |
LocalTime | LocalTime | Time without date and time zone |
DateTimeZone | ZoneId | Time zone identifier |
Period | Period | Date-based amount of time (years, months, days) |
Duration | Duration | Time-based amount of time (hours, minutes, seconds) |
Interval | No direct equivalent | Use Instant range or custom implementation |
Basic Conversion Examples
1. Current Date and Time
// Joda-Time DateTime jodaNow = new DateTime(); DateTime jodaUtc = new DateTime(DateTimeZone.UTC); // java.time ZonedDateTime javaNow = ZonedDateTime.now(); Instant javaUtc = Instant.now();
2. Specific Date and Time
// Joda-Time DateTime jodaDateTime = new DateTime(2023, 12, 25, 10, 30, 0, 0); LocalDate jodaDate = new LocalDate(2023, 12, 25); LocalTime jodaTime = new LocalTime(10, 30); // java.time LocalDateTime javaDateTime = LocalDateTime.of(2023, 12, 25, 10, 30); LocalDate javaDate = LocalDate.of(2023, 12, 25); LocalTime javaTime = LocalTime.of(10, 30);
3. With Time Zones
// Joda-Time DateTimeZone jodaZone = DateTimeZone.forID("America/New_York"); DateTime jodaWithZone = new DateTime(jodaZone); // java.time ZoneId javaZone = ZoneId.of("America/New_York"); ZonedDateTime javaWithZone = ZonedDateTime.now(javaZone); Detailed Conversion Patterns
1. Parsing and Formatting
// Joda-Time DateTimeFormatter jodaFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); DateTime jodaParsed = jodaFormatter.parseDateTime("2023-12-25 10:30:00"); String jodaFormatted = jodaFormatter.print(jodaParsed); // java.time DateTimeFormatter javaFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); LocalDateTime javaParsed = LocalDateTime.parse("2023-12-25 10:30:00", javaFormatter); String javaFormatted = javaFormatter.format(javaParsed); 2. Date Arithmetic
// Joda-Time DateTime jodaPlusDays = jodaDateTime.plusDays(7); DateTime jodaMinusMonths = jodaDateTime.minusMonths(1); DateTime jodaWithHour = jodaDateTime.withHourOfDay(15); // java.time LocalDateTime javaPlusDays = javaDateTime.plusDays(7); LocalDateTime javaMinusMonths = javaDateTime.minusMonths(1); LocalDateTime javaWithHour = javaDateTime.withHour(15);
3. Period and Duration
// Joda-Time Period jodaPeriod = new Period().withDays(5).withHours(3); Duration jodaDuration = new Duration(1000 * 60 * 60); // 1 hour in milliseconds DateTime jodaPlusPeriod = jodaDateTime.plus(jodaPeriod); // java.time Period javaPeriod = Period.ofDays(5); Duration javaDuration = Duration.ofHours(1); LocalDateTime javaPlusPeriod = javaDateTime.plus(javaPeriod);
Advanced Conversions
1. Interval Conversion (No Direct Equivalent)
// Joda-Time DateTime startJoda = new DateTime(2023, 1, 1, 0, 0); DateTime endJoda = new DateTime(2023, 12, 31, 23, 59); Interval jodaInterval = new Interval(startJoda, endJoda); // java.time - Custom implementation class DateTimeInterval { private final Instant start; private final Instant end; public DateTimeInterval(Instant start, Instant end) { this.start = start; this.end = end; } public boolean contains(Instant instant) { return !instant.isBefore(start) && !instant.isAfter(end); } public Duration toDuration() { return Duration.between(start, end); } } // Usage Instant startJava = Instant.parse("2023-01-01T00:00:00Z"); Instant endJava = Instant.parse("2023-12-31T23:59:59Z"); DateTimeInterval javaInterval = new DateTimeInterval(startJava, endJava); 2. Time Zone Conversions
// Joda-Time DateTimeZone jodaZone = DateTimeZone.forID("Europe/Paris"); DateTime jodaParis = jodaDateTime.withZone(jodaZone); DateTime jodaUtc = jodaParis.withZone(DateTimeZone.UTC); // java.time ZoneId javaZone = ZoneId.of("Europe/Paris"); ZonedDateTime javaParis = javaDateTime.atZone(javaZone); Instant javaUtc = javaParis.toInstant(); 3. Working with Instants
// Joda-Time DateTime jodaInstant = new DateTime(System.currentTimeMillis()); long jodaMillis = jodaInstant.getMillis(); DateTime jodaFromInstant = new DateTime(instant.getMillis()); // java.time Instant javaInstant = Instant.now(); long javaMillis = javaInstant.toEpochMilli(); Instant javaFromMillis = Instant.ofEpochMilli(javaMillis);
Migration Strategy: Gradual Approach
Step 1: Add java.time Dependency
<!-- If you need to support older Android or Java versions --> <dependency> <groupId>org.threeten</groupId> <artifactId>threetenbp</artifactId> <version>1.6.8</version> </dependency>
Step 2: Create Adapter Classes
/** * Adapter to gradually migrate from Joda-Time to java.time */ public class DateTimeAdapter { // Convert Joda-Time to java.time public static ZonedDateTime toZonedDateTime(DateTime jodaDateTime) { if (jodaDateTime == null) return null; return ZonedDateTime.ofInstant( Instant.ofEpochMilli(jodaDateTime.getMillis()), ZoneId.of(jodaDateTime.getZone().getID()) ); } public static LocalDate toLocalDate(org.joda.time.LocalDate jodaDate) { if (jodaDate == null) return null; return LocalDate.of(jodaDate.getYear(), jodaDate.getMonthOfYear(), jodaDate.getDayOfMonth()); } public static LocalTime toLocalTime(org.joda.time.LocalTime jodaTime) { if (jodaTime == null) return null; return LocalTime.of(jodaTime.getHourOfDay(), jodaTime.getMinuteOfHour(), jodaTime.getSecondOfMinute(), jodaTime.getMillisOfSecond() * 1_000_000); } // Convert java.time to Joda-Time (for backward compatibility) public static DateTime toDateTime(ZonedDateTime javaDateTime) { if (javaDateTime == null) return null; return new DateTime( javaDateTime.toInstant().toEpochMilli(), DateTimeZone.forID(javaDateTime.getZone().getId()) ); } public static org.joda.time.LocalDate toJodaLocalDate(LocalDate javaDate) { if (javaDate == null) return null; return new org.joda.time.LocalDate(javaDate.getYear(), javaDate.getMonthValue(), javaDate.getDayOfMonth()); } } Step 3: Mixed Usage During Transition
public class OrderService { // Gradually migrate method by method public void processOrderMixed(DateTime jodaOrderDate) { // Convert to java.time for new logic ZonedDateTime javaOrderDate = DateTimeAdapter.toZonedDateTime(jodaOrderDate); // New code uses java.time ZonedDateTime deliveryDate = calculateDeliveryDate(javaOrderDate); // Convert back if needed for existing code DateTime jodaDeliveryDate = DateTimeAdapter.toDateTime(deliveryDate); legacyProcessDelivery(jodaDeliveryDate); } private ZonedDateTime calculateDeliveryDate(ZonedDateTime orderDate) { // New implementation using java.time return orderDate.plusDays(3); } @Deprecated private void legacyProcessDelivery(DateTime deliveryDate) { // Old implementation using Joda-Time // TODO: Migrate this method later } } Common Pitfalls and Solutions
1. Null Handling
// Problem: Joda-Time allows null in some cases, java.time is stricter public static LocalDate safeConvert(org.joda.time.LocalDate jodaDate) { return jodaDate != null ? LocalDate.of(jodaDate.getYear(), jodaDate.getMonthOfYear(), jodaDate.getDayOfMonth()) : null; } 2. Different Default Time Zones
// Joda-Time uses system default time zone DateTime jodaDefault = new DateTime(); // Uses default time zone // java.time is more explicit ZonedDateTime javaDefault = ZonedDateTime.now(); // Uses system default ZonedDateTime javaExplicit = ZonedDateTime.now(ZoneId.systemDefault());
3. Week-based Differences
// Joda-Time week calculations int jodaWeek = jodaDateTime.getWeekOfWeekyear(); // java.time week calculations (ISO standard) int javaWeek = javaDateTime.get(WeekFields.ISO.weekOfWeekBasedYear());
Complete Migration Example
Before (Joda-Time):
public class JodaTimeExample { public boolean isOrderInShippingWindow(DateTime orderDate, DateTime shippingStart, DateTime shippingEnd) { Interval shippingInterval = new Interval(shippingStart, shippingEnd); return shippingInterval.contains(orderDate); } public String formatOrderDate(DateTime orderDate) { return orderDate.toString("EEE, MMM d, yyyy 'at' h:mm a"); } public DateTime getNextBusinessDay(DateTime date) { return date.plusDays(1) .withDayOfWeek(DateTimeConstants.MONDAY) .withTime(9, 0, 0, 0); } } After (java.time):
public class JavaTimeExample { public boolean isOrderInShippingWindow(ZonedDateTime orderDate, ZonedDateTime shippingStart, ZonedDateTime shippingEnd) { return !orderDate.isBefore(shippingStart) && !orderDate.isAfter(shippingEnd); } public String formatOrderDate(ZonedDateTime orderDate) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE, MMM d, yyyy 'at' h:mm a", Locale.US); return orderDate.format(formatter); } public ZonedDateTime getNextBusinessDay(ZonedDateTime date) { return date.plusDays(1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)) .withHour(9) .withMinute(0) .withSecond(0) .withNano(0); } } Testing the Migration
class MigrationTest { @Test void testDateTimeConversion() { // Test data DateTime jodaDateTime = new DateTime(2023, 12, 25, 10, 30, 0, 0); // Convert ZonedDateTime javaDateTime = DateTimeAdapter.toZonedDateTime(jodaDateTime); DateTime convertedBack = DateTimeAdapter.toDateTime(javaDateTime); // Verify assertEquals(jodaDateTime.getMillis(), convertedBack.getMillis()); assertEquals(jodaDateTime.getZone().getID(), convertedBack.getZone().getID()); } @Test void testEdgeCases() { // Test null safety assertNull(DateTimeAdapter.toZonedDateTime(null)); assertNull(DateTimeAdapter.toDateTime(null)); // Test UTC conversion DateTime jodaUtc = new DateTime(DateTimeZone.UTC); ZonedDateTime javaUtc = DateTimeAdapter.toZonedDateTime(jodaUtc); assertEquals(ZoneId.of("UTC"), javaUtc.getZone()); } } Performance Considerations
Benchmark Example:
@State(Scope.Benchmark) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class DateTimeBenchmark { private DateTime jodaDateTime; private ZonedDateTime javaDateTime; @Setup public void setup() { jodaDateTime = new DateTime(); javaDateTime = ZonedDateTime.now(); } @Benchmark public DateTime jodaPlusDays() { return jodaDateTime.plusDays(1); } @Benchmark public ZonedDateTime javaPlusDays() { return javaDateTime.plusDays(1); } } Typical Results:
- java.time often performs better for simple operations
- Joda-Time may be faster for some complex operations
- The difference is usually negligible for most applications
Migration Checklist
- [ ] Identify all Joda-Time imports in your codebase
- [ ] Create adapter classes for gradual migration
- [ ] Update build files to include java.time (if needed)
- [ ] Migrate simple cases first (LocalDate, LocalTime)
- [ ] Handle complex cases (Interval, custom formatters)
- [ ] Update tests to verify correctness
- [ ] Remove Joda-Time dependency once migration is complete
- [ ] Monitor performance in production
Conclusion
Migrating from Joda-Time to java.time is a worthwhile investment that provides:
- Future-Proofing: Using standard Java APIs
- Better Performance: Optimized implementations
- Improved Safety: More immutable and thread-safe classes
- Enhanced Features: Additional functionality not available in Joda-Time
Key Takeaways:
- Most Joda-Time concepts have direct equivalents in java.time
- Use gradual migration with adapter classes for large codebases
- Pay attention to time zone handling differences
- Test thoroughly, especially around edge cases and business logic
While the migration requires effort, the long-term benefits of using the standard Java date/time API make it a necessary step for modern Java applications.