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.