π LocalDate and LocalTime Operations (Date-Time API)
Rule: LocalDate/LocalTime/LocalDateTime are immutable - all methods return new instances.
- LocalDate: Date only (year, month, day)
- LocalTime: Time only (hour, minute, second, nanosecond)
- LocalDateTime: Date and time combined
import java.time.*;
// Creating date/time objects
LocalDate today = LocalDate.now(); // Current date
LocalDate birthday = LocalDate.of(2000, 5, 15); // May 15, 2000
LocalTime now = LocalTime.now(); // Current time
LocalTime lunch = LocalTime.of(12, 30); // 12:30 PM
LocalDateTime meeting = LocalDateTime.of(2024, 3, 20, 14, 30); // Mar 20, 2024 at 2:30 PM
// All methods return NEW instances (immutable)
LocalDate tomorrow = today.plusDays(1); // Add 1 day
LocalDate nextMonth = today.plusMonths(1); // Add 1 month
LocalTime later = lunch.plusHours(2); // Add 2 hours
LocalDate earlier = birthday.minusYears(5); // Subtract 5 years
// Original objects unchanged
System.out.println(today); // Still original date
System.out.println(lunch); // Still 12:30
Period and Duration:
// Period - date-based amounts (years, months, days)
Period age = Period.between(birthday, today);
System.out.println("Age: " + age.getYears() + " years");
Period twoWeeks = Period.ofDays(14);
LocalDate vacation = today.plus(twoWeeks);
// Duration - time-based amounts (hours, minutes, seconds)
Duration workDay = Duration.ofHours(8);
LocalTime endWork = LocalTime.of(9, 0).plus(workDay); // 9:00 AM + 8 hours = 5:00 PM
Duration between = Duration.between(lunch, now);
System.out.println("Hours since lunch: " + between.toHours());
π‘ Learning Tip: Remember βIMMUTABLE TIMEβ - LocalDate/Time classes never change, they always return new instances. Period for dates, Duration for time.
Q: If you call birthday.plusYears(10)
without assigning the result, does birthday change?
A: No β LocalDate is immutable. The method returns a new LocalDate instance, but birthday remains unchanged.
π ZonedDateTime and Time Zones
Rule: ZonedDateTime handles time zones and daylight saving time transitions automatically.
- ZoneId: Represents a time zone (e.g., βAmerica/New_Yorkβ)
- ZoneOffset: Fixed offset from UTC (e.g., β+05:30β)
- ZonedDateTime: LocalDateTime + ZoneId with DST handling
import java.time.*;
// Creating ZonedDateTime
ZonedDateTime nyTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
ZonedDateTime utcTime = ZonedDateTime.now(ZoneId.of("UTC"));
// Convert between time zones
ZonedDateTime meeting = ZonedDateTime.of(2024, 3, 15, 14, 30, 0, 0,
ZoneId.of("America/Los_Angeles"));
ZonedDateTime meetingInNY = meeting.withZoneSameInstant(ZoneId.of("America/New_York"));
ZonedDateTime meetingInTokyo = meeting.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("LA: " + meeting); // 2024-03-15T14:30-08:00[America/Los_Angeles]
System.out.println("NY: " + meetingInNY); // 2024-03-15T17:30-04:00[America/New_York]
System.out.println("Tokyo: " + meetingInTokyo); // 2024-03-16T06:30+09:00[Asia/Tokyo]
// DST handling
ZoneId nyZone = ZoneId.of("America/New_York");
ZonedDateTime beforeDST = ZonedDateTime.of(2024, 3, 10, 1, 30, 0, 0, nyZone);
ZonedDateTime afterDST = beforeDST.plusHours(2); // Automatically handles "spring forward"
System.out.println("Before: " + beforeDST); // 2024-03-10T01:30-05:00
System.out.println("After: " + afterDST); // 2024-03-10T04:30-04:00 (skips 2:30-3:30)
OffsetDateTime vs ZonedDateTime:
// OffsetDateTime - fixed offset, no DST awareness
OffsetDateTime fixedOffset = OffsetDateTime.of(2024, 6, 15, 10, 30, 0, 0,
ZoneOffset.of("+05:30"));
// ZonedDateTime - full time zone with DST rules
ZonedDateTime zonedTime = ZonedDateTime.of(2024, 6, 15, 10, 30, 0, 0,
ZoneId.of("America/New_York"));
// Convert between them
ZonedDateTime fromOffset = fixedOffset.atZoneSameInstant(ZoneId.of("UTC"));
OffsetDateTime fromZoned = zonedTime.toOffsetDateTime();
Time zone operations:
// Available time zones
Set<String> allZones = ZoneId.getAvailableZoneIds();
allZones.stream()
.filter(zone -> zone.contains("America"))
.sorted()
.forEach(System.out::println);
// System default zone
ZoneId systemZone = ZoneId.systemDefault();
System.out.println("System zone: " + systemZone);
// UTC and GMT
ZoneId utc = ZoneId.of("UTC");
ZoneId gmt = ZoneId.of("GMT");
ZoneId zulu = ZoneId.of("Z"); // Same as UTC
// Custom offsets
ZoneOffset plus5_30 = ZoneOffset.of("+05:30"); // India Standard Time
ZoneOffset minus8 = ZoneOffset.ofHours(-8); // PST
π‘ Learning Tip: Remember βZONED = SMART, OFFSET = SIMPLEβ - ZonedDateTime handles DST automatically, OffsetDateTime uses fixed offsets.
π Date and Time Formatting
Rule: DateTimeFormatter provides flexible formatting and parsing for date/time objects.
- Predefined formatters: ISO_LOCAL_DATE, ISO_LOCAL_TIME, etc.
- Pattern-based formatters: Custom patterns using letters (yyyy, MM, dd)
- Localized formatters: Locale-specific formatting
import java.time.*;
import java.time.format.*;
import java.util.Locale;
LocalDateTime dateTime = LocalDateTime.of(2024, 3, 15, 14, 30, 45);
// Predefined formatters
System.out.println(dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE)); // 2024-03-15
System.out.println(dateTime.format(DateTimeFormatter.ISO_LOCAL_TIME)); // 14:30:45
System.out.println(dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); // 2024-03-15T14:30:45
// Custom patterns
DateTimeFormatter custom1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter custom2 = DateTimeFormatter.ofPattern("MMM dd, yyyy 'at' h:mm a");
DateTimeFormatter custom3 = DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy");
System.out.println(dateTime.format(custom1)); // 2024-03-15 14:30:45
System.out.println(dateTime.format(custom2)); // Mar 15, 2024 at 2:30 PM
System.out.println(dateTime.format(custom3)); // Friday, March 15, 2024
// Localized formatting
DateTimeFormatter usFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(Locale.US);
DateTimeFormatter frenchFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
.withLocale(Locale.FRANCE);
System.out.println(dateTime.format(usFormat)); // Mar 15, 2024, 2:30:45 PM
System.out.println(dateTime.format(frenchFormat)); // 15 mars 2024, 14:30:45
Parsing strings to date/time:
// Parse with predefined formatters
LocalDate date1 = LocalDate.parse("2024-03-15", DateTimeFormatter.ISO_LOCAL_DATE);
LocalTime time1 = LocalTime.parse("14:30:45", DateTimeFormatter.ISO_LOCAL_TIME);
// Parse with custom patterns
DateTimeFormatter parser = DateTimeFormatter.ofPattern("MM/dd/yyyy h:mm a");
LocalDateTime parsed = LocalDateTime.parse("03/15/2024 2:30 PM", parser);
// Case-insensitive parsing
DateTimeFormatter flexible = DateTimeFormatter.ofPattern("MMM dd, yyyy")
.withResolverStyle(ResolverStyle.LENIENT)
.withLocale(Locale.ENGLISH);
LocalDate flexible_date = LocalDate.parse("mar 15, 2024", flexible);
// Handling parse errors
try {
LocalDate invalid = LocalDate.parse("invalid-date");
} catch (DateTimeParseException e) {
System.out.println("Parse error: " + e.getMessage());
System.out.println("Error index: " + e.getErrorIndex());
}
Pattern symbols:
// Common pattern symbols
// y - year (yyyy = 2024, yy = 24)
// M - month (MM = 03, MMM = Mar, MMMM = March)
// d - day of month (dd = 15)
// H - hour 24-hour (HH = 14)
// h - hour 12-hour (hh = 02)
// m - minute (mm = 30)
// s - second (ss = 45)
// a - AM/PM marker
// E - day of week (EEE = Fri, EEEE = Friday)
// z - time zone name (PST, Pacific Standard Time)
// Z - time zone offset (-0800)
DateTimeFormatter complex = DateTimeFormatter.ofPattern(
"EEEE, MMMM d, yyyy 'at' h:mm:ss a z"
);
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("America/Los_Angeles"));
System.out.println(zdt.format(complex));
// Friday, March 15, 2024 at 2:30:45 PM PDT
Thread safety and performance:
// DateTimeFormatter is immutable and thread-safe
public class DateFormatter {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public String format(LocalDateTime dateTime) {
return dateTime.format(FORMATTER); // Safe to share
}
}
// Reuse formatters for better performance
DateTimeFormatter reusable = DateTimeFormatter.ofPattern("yyyy-MM-dd");
List<LocalDate> dates = Arrays.asList(/* many dates */);
// β
Efficient - reuse formatter
dates.stream()
.map(date -> date.format(reusable))
.forEach(System.out::println);
// β Inefficient - create new formatter each time
dates.stream()
.map(date -> date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
.forEach(System.out::println);
π‘ Learning Tip: Remember βPATTERN = PICTUREβ - yyyy-MM-dd shows exactly how the output looks. DateTimeFormatter is thread-safe and should be reused.
π Daylight Saving Time βFall Backβ Calculations
Rule: When DST ends (βfall backβ), the same local time occurs twice, creating a 25-hour day where time calculations must account for the repeated hour.
Timeline Visualization for November 5, 2023 DST End in America/Chicago:
Regular Timeline: DST "Fall Back" Timeline:
12:00 AM βββββββββββββββΊ 12:00 AM βββββββββββββββΊ
01:00 AM βββββββββββββββΊ 01:00 AM βββββββββββββββΊ
02:00 AM βββββββββββββββΊ 02:00 AM (first occurrence) βββ
03:00 AM βββββββββββββββΊ β Clock jumps back
04:00 AM βββββββββββββββΊ 01:00 AM (after fall back) βββ
02:00 AM (second occurrence) βββΊ
03:00 AM βββββββββββββββΊ
The Problem: When calculating time between 3:00 AM and 1:00 AM on DST end day:
import java.time.*;
import java.time.temporal.ChronoUnit;
// DST ends November 5, 2023 at 2:00 AM in America/Chicago
// Clock "falls back" from 2:00 AM to 1:00 AM
LocalDateTime morning = LocalDateTime.of(2023, Month.NOVEMBER, 5, 3, 0); // 3:00 AM
ZonedDateTime chicagoMorning = ZonedDateTime.of(morning, ZoneId.of("America/Chicago"));
LocalDateTime early = LocalDateTime.of(2023, Month.NOVEMBER, 5, 1, 0); // 1:00 AM
ZonedDateTime chicagoEarly = ZonedDateTime.of(early, ZoneId.of("America/Chicago"));
long hoursBetween = ChronoUnit.HOURS.between(chicagoMorning, chicagoEarly);
System.out.println("Hours from 3:00 AM to 1:00 AM: " + hoursBetween); // -2
Why the result is -2:
Step-by-step timeline:
- 3:00 AM (starting point)
- Go back 1 hour β 2:00 AM (second occurrence, after fall back)
- Go back 1 more hour β 1:00 AM (after the repeated hour)
Total: 2 hours backward = -2
The Key Insight:
- From 3:00 AM to 1:00 AM normally would be -2 hours
- On DST βfall backβ day, thereβs an extra hour (1:00-2:00 AM occurs twice)
- ZonedDateTime automatically picks the later occurrence (after fall back)
- So we still need -2 hours to go from 3:00 AM to the 1:00 AM that comes after the time change
Extended example with different times:
// DST ends March 26, 2023 at 3:00 AM in Europe/Berlin (falls back to 2:00 AM)
LocalDateTime breakfast = LocalDateTime.of(2023, Month.MARCH, 26, 4, 30); // 4:30 AM
ZonedDateTime berlinBreakfast = ZonedDateTime.of(breakfast, ZoneId.of("Europe/Berlin"));
LocalDateTime midnight = LocalDateTime.of(2023, Month.MARCH, 26, 2, 15); // 2:15 AM
ZonedDateTime berlinMidnight = ZonedDateTime.of(midnight, ZoneId.of("Europe/Berlin"));
long minutesDiff = ChronoUnit.MINUTES.between(berlinBreakfast, berlinMidnight);
System.out.println("Minutes from 4:30 AM to 2:15 AM: " + minutesDiff); // -195
// Breakdown: 4:30 AM β 2:15 AM = -2 hours 15 minutes = -135 minutes
// But DST adds an extra hour, so -135 - 60 = -195 minutes
Visual Timeline for the Calculation:
DST End Day (25-hour day):
ββ 12:00 AM
ββ 01:00 AM
ββ 02:00 AM (first time) βββ
ββ 02:00 AM (repeated) βββ β Extra hour here!
ββ 02:15 AM (our target) βββββββββ
ββ 03:00 AM β
ββ 04:00 AM β 195 minutes difference
ββ 04:30 AM (our start) βββββββββ
π‘ Learning Tip: Remember βFALL BACK = EXTRA HOURβ - When DST ends, one hour is repeated (1:00-2:00 AM happens twice), making time calculations longer than expected.
Q: On DST end day, if you calculate hours between 4:00 AM and 2:00 AM, why might the result be -3 instead of -2?
A: Because the 2:00 AM hour occurs twice on DST end day. Going from 4:00 AM to 2:00 AM crosses the repeated hour, requiring an extra hour: 4β3β2(first)β2(repeated) = 3 hours backward = -3.
π Localization - Locale and Resource Bundles
Rule: Localization uses Locale for region/language and ResourceBundle for externalized text.
- Locale: Represents language and country (e.g., en_US, fr_FR)
- ResourceBundle: Loads localized text from properties files
- Fallback mechanism: Searches for most specific to most general
import java.util.*;
import java.text.NumberFormat;
import java.time.format.DateTimeFormatter;
// Creating Locales
Locale english = Locale.ENGLISH; // en
Locale french = Locale.FRENCH; // fr
Locale usEnglish = Locale.US; // en_US
Locale canadianFrench = Locale.CANADA_FRENCH; // fr_CA
Locale custom = new Locale("es", "MX"); // es_MX (Spanish Mexico)
// Resource bundles (properties files)
// messages_en.properties: greeting=Hello
// messages_fr.properties: greeting=Bonjour
// messages.properties: greeting=Hi (default fallback)
ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.FRENCH);
String greeting = bundle.getString("greeting"); // "Bonjour"
// Fallback search order for Locale("fr", "CA"):
// 1. messages_fr_CA.properties
// 2. messages_fr.properties
// 3. messages.properties (default)
Number and currency formatting:
double amount = 1234.56;
// Number formatting per locale
NumberFormat usNumber = NumberFormat.getNumberInstance(Locale.US);
NumberFormat frenchNumber = NumberFormat.getNumberInstance(Locale.FRANCE);
System.out.println(usNumber.format(amount)); // 1,234.56
System.out.println(frenchNumber.format(amount)); // 1 234,56
// Currency formatting
NumberFormat usCurrency = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat euroCurrency = NumberFormat.getCurrencyInstance(Locale.FRANCE);
System.out.println(usCurrency.format(amount)); // $1,234.56
System.out.println(euroCurrency.format(amount)); // 1 234,56 β¬
// Percentage formatting
NumberFormat percent = NumberFormat.getPercentInstance(Locale.US);
System.out.println(percent.format(0.75)); // 75%
Date/time formatting:
LocalDateTime now = LocalDateTime.now();
// US format: MM/dd/yyyy
DateTimeFormatter usFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Locale.US);
// French format: dd/MM/yyyy
DateTimeFormatter frenchFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Locale.FRANCE);
System.out.println(now.format(usFormat)); // 03/20/2024
System.out.println(now.format(frenchFormat)); // 20/03/2024
π‘ Learning Tip: Think βLOCALE = WHERE, BUNDLE = WHATβ - Locale specifies location/language, ResourceBundle provides localized content with automatic fallback.
Q: If you request a ResourceBundle for Locale(βdeβ, βCHβ) but only have messages_de.properties and messages.properties, which file is used?
A: messages_de.properties β the search falls back from de_CH to de to default, using the most specific match found.