Skip to content

Java 日期与时间

在许多应用程序中,处理日期和时间是一项常见需求。Java 为此提供了强大的 API,但使用 Java 8 中引入的现代 API 非常重要。

java.time 包在 Java 8 中引入,提供了一个全面、不可变且大大改进的 API,用于处理日期、时间、时区、持续时间和时间段。这是所有新开发的推荐 API。

  • 不可变性 (Immutability): java.time 中的对象(如 LocalDate, LocalTime)是不可变的,这意味着它们的状态一旦创建后就无法更改。这使得它们本质上是线程安全的,并且更容易推理。
  • 清晰性 (Clarity): 日期、时间、日期-时间、时间戳、持续时间、时间段和时区等概念之间有明确的区分。
  • 流畅 API (Fluent API): 方法设计支持清晰易读的链式调用(例如,dateTime.plusDays(5).minusHours(2))。
  • 更好的时区处理: 对时区提供强大的支持(ZoneId, ZonedDateTime)。
  • ISO 8601 标准: 基于 ISO 日期和时间表示标准。
  • LocalDate:表示一个没有时间或时区的日期(例如,2023-10-27)。
  • LocalTime:表示一个没有日期或时区的时间(例如,14:30:55.123)。
  • LocalDateTime:表示日期和时间,但没有时区(例如,2023-10-27T14:30:55)。
  • ZonedDateTime:表示带有特定时区的日期和时间(例如,2023-10-27T14:30:55+01:00[Europe/London])。
  • Instant:表示时间线上的特定点(纳秒精度),通常相对于 UTC epoch(1970 年 1 月 1 日)。适用于时间戳。
  • Duration:表示基于时间的时间量(秒,纳秒)。适用于测量机器时间(例如,两个 Instant 之间的差值)。
  • Period:表示基于日期的时间量(年,月,日)。适用于人类可读的时间差(例如,年龄)。
  • DateTimeFormatter:用于将日期/时间对象格式化为字符串以及将字符串解析为日期/时间对象。
  • ZoneId:表示时区标识符(例如,Europe/Paris, Asia/Tokyo)。
  • ZoneOffset:表示与 UTC 的固定偏移量(例如,+02:00)。

使用静态方法 now():

import java.time.*;
public class CurrentDateTime {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
System.out.println("Today's Date: " + today); // 例如,2023-10-27
LocalTime currentTime = LocalTime.now();
System.out.println("Current Time: " + currentTime); // 例如,10:15:30.123456789
LocalDateTime currentDateTime = LocalDateTime.now();
System.out.println("Current Date and Time: " + currentDateTime);
// 获取特定时区的当前时间
ZoneId londonZone = ZoneId.of("Europe/London");
ZonedDateTime londonTime = ZonedDateTime.now(londonZone);
System.out.println("Current London Time: " + londonTime);
Instant timestamp = Instant.now();
System.out.println("Current Instant (UTC): " + timestamp);
}
}

使用静态工厂方法 of():

import java.time.*;
public class SpecificDateTime {
public static void main(String[] args) {
LocalDate specificDate = LocalDate.of(2024, Month.JANUARY, 15); // 年,月,日
System.out.println("Specific Date: " + specificDate);
LocalDate dateFromYMD = LocalDate.of(2024, 1, 15); // 年,月(1-12),日
System.out.println("Date from YMD: " + dateFromYMD);
LocalTime specificTime = LocalTime.of(18, 30, 0); // 时,分,秒
System.out.println("Specific Time: " + specificTime);
LocalDateTime specificDateTime = LocalDateTime.of(2024, 1, 15, 18, 30);
System.out.println("Specific DateTime: " + specificDateTime);
ZoneId newYorkZone = ZoneId.of("America/New_York");
ZonedDateTime newYorkMeeting = ZonedDateTime.of(specificDateTime, newYorkZone);
System.out.println("New York Meeting: " + newYorkMeeting);
}
}

DateTimeFormatter 用于将日期/时间对象转换为字符串(格式化)以及将字符串解析回日期/时间对象(解析)。

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
public class PredefinedFormatting {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
// 预定义的 ISO 格式(toString() 的默认格式)
System.out.println("ISO DATE TIME: " + now.format(DateTimeFormatter.ISO_DATE_TIME));
System.out.println("ISO DATE: " + now.format(DateTimeFormatter.ISO_DATE));
// 特定于语言环境的格式
DateTimeFormatter shortFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
DateTimeFormatter mediumFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
System.out.println("Short format: " + now.format(shortFormat)); // 例如,10/27/23, 11:30 AM (US 语言环境)
System.out.println("Medium format: " + now.format(mediumFormat)); // 例如,Oct 27, 2023, 11:30:00 AM
}
}
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public class CustomFormattingParsing {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
// 定义自定义模式
DateTimeFormatter customFormatter = DateTimeFormatter.ofPattern("EEEE, MMMM dd, yyyy 'at' hh:mm a");
// 格式化
String formattedString = now.format(customFormatter);
System.out.println("Custom format: " + formattedString);
// 例如,Friday, October 27, 2023 at 11:35 AM
// 解析
String dateString = "Monday, June 05, 2023 at 02:30 PM";
try {
LocalDateTime parsedDateTime = LocalDateTime.parse(dateString, customFormatter);
System.out.println("Parsed DateTime: " + parsedDateTime);
} catch (DateTimeParseException e) {
System.err.println("Error parsing date string: " + e.getMessage()); // 解析日期字符串出错:
}
// 解析更简单的格式
String simpleDate = "2023-01-20";
try {
// 这里适合使用 ISO_DATE 格式化器
LocalDate parsedDate = LocalDate.parse(simpleDate, DateTimeFormatter.ISO_DATE);
System.out.println("Parsed simple date: " + parsedDate);
} catch (DateTimeParseException e) {
System.err.println("Error parsing simple date: " + e.getMessage()); // 解析简单日期出错:
}
}
}
字母组件示例
y年份yy (03), yyyy (2003)
M月份M (7), MM (07), MMM (Jul), MMMM (July)
d月份中的天数d (5), dd (05)
E星期中的天数E/EE/EEE (Tue), EEEE (Tuesday)
H小时 (0-23)H (9), HH (09)
h小时 (1-12)h (9), hh (09)
m分钟m (5), mm (05)
s秒s (8), ss (08)
aAM/PM 标记a (AM/PM)
z时区名称z (PDT)
Z / O / X时区偏移Z (+0100), O (GMT+1), X / XX / XXX (-08 / -0800 / -08:00)
'文本转义'Date:' yyyy

由于 java.time 对象是不可变的,操作方法会返回一个带有修改值的新对象。

import java.time.*;
public class DateTimeManipulation {
public static void main(String[] args) {
LocalDateTime meetingTime = LocalDateTime.of(2023, 11, 10, 14, 0); // 2023 年 11 月 10 日下午 2 点
System.out.println("Original Meeting Time: " + meetingTime);
// 添加/减去时间
LocalDateTime rescheduledTime = meetingTime.plusDays(3).minusHours(1);
System.out.println("Rescheduled Time: " + rescheduledTime); // 2023 年 11 月 13 日下午 1 点
LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plusWeeks(1);
System.out.println("Next Week: " + nextWeek);
// 获取日期/时间的部分
int year = meetingTime.getYear(); // 2023
Month month = meetingTime.getMonth(); // NOVEMBER
int day = meetingTime.getDayOfMonth(); // 10
DayOfWeek dayOfWeek = meetingTime.getDayOfWeek(); // FRIDAY
int hour = meetingTime.getHour(); // 14
System.out.println("Meeting details: " + dayOfWeek + ", " + month + " " + day + ", " + year);
// 使用 'with' 修改特定字段
LocalDateTime startOfDay = meetingTime.withHour(9).withMinute(0).withSecond(0);
System.out.println("Start of Business Day: " + startOfDay);
}
}

时间段 (Periods) 和持续时间 (Durations)

Section titled “时间段 (Periods) 和持续时间 (Durations)”

Period 用于基于日期的差异,Duration 用于基于时间的差异。

import java.time.*;
public class PeriodDurationExample {
public static void main(String[] args) {
// 时间段 (年, 月, 日)
LocalDate startDate = LocalDate.of(2023, 5, 15);
LocalDate endDate = LocalDate.of(2024, 7, 20);
Period periodBetween = Period.between(startDate, endDate);
System.out.println("Period: " + periodBetween); // P1Y2M5D (1 年, 2 个月, 5 天)
System.out.printf("Difference: %d years, %d months, %d days%n",
periodBetween.getYears(), periodBetween.getMonths(), periodBetween.getDays());
Period twoWeeks = Period.ofWeeks(2);
LocalDate futureDate = startDate.plus(twoWeeks);
System.out.println("Two weeks later: " + futureDate);
// 持续时间 (天, 时, 分, 秒, 纳秒)
Instant startInstant = Instant.now();
// 模拟一些工作
try { Thread.sleep(1500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
Instant endInstant = Instant.now();
Duration timeElapsed = Duration.between(startInstant, endInstant);
System.out.println("\nTime Elapsed (Duration): " + timeElapsed); // 例如,PT1.500123456S
System.out.println("Time Elapsed (Millis): " + timeElapsed.toMillis()); // 经过的时间 (毫秒):
Duration twoHours = Duration.ofHours(2);
LocalTime deadline = LocalTime.of(15, 0).plus(twoHours);
System.out.println("Deadline Time: " + deadline);
}
}

使用 Instant 和 Duration 进行精确的时间测量。

import java.time.Duration;
import java.time.Instant;
public class MeasureTime {
public static void main(String[] args) {
Instant start = Instant.now();
System.out.println("Starting operation at: " + start);
// 模拟一个任务
try {
Thread.sleep(2000); // 暂停 2 秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Sleep interrupted"); // 暂停被中断
}
Instant end = Instant.now();
System.out.println("Finished operation at: " + end);
Duration duration = Duration.between(start, end);
System.out.println("Duration: " + duration); // 例如,PT2.001S
System.out.println("Duration in milliseconds: " + duration.toMillis()); // 持续时间(毫秒):
}
}

遗留日期/时间 API (java.util.Date, Calendar)

Section titled “遗留日期/时间 API (java.util.Date, Calendar)”

在 Java 8 之前,开发者使用 java.util.Date 和 java.util.Calendar (通常是 java.util.GregorianCalendar)。这些类有几个缺点:

  • 可变性 (Mutability): Date 对象是可变的,这可能导致在并发环境或对象传递时出现 bug。
  • 令人困惑的 API: Date 表示一个时间点,但其 toString() 方法包含基于默认时区的日期/时间组件。Date 和 Calendar 中的月份索引基于 0(一月=0),这是一个常见的错误源。
  • 糟糕的区分: Date 将时间点概念与日期/时间字段混合在一起。
  • 有限的时区支持: Calendar 的时区处理很麻烦。

在新代码中避免使用 java.util.Date 和 java.util.Calendar。 如果需要与使用它们的旧 API 交互,请在遗留 API 和现代 API 之间进行转换:

import java.time.*;
import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
public class LegacyConversion {
public static void main(String[] args) {
// 遗留 Date 转 Instant
Date legacyDate = new Date();
Instant instantFromDate = legacyDate.toInstant();
ZonedDateTime zdtFromDate = instantFromDate.atZone(ZoneId.systemDefault());
System.out.println("From legacy Date: " + zdtFromDate);
// Instant 转 遗留 Date
Instant nowInstant = Instant.now();
Date dateFromInstant = Date.from(nowInstant);
System.out.println("To legacy Date: " + dateFromInstant);
// 遗留 Calendar 转 ZonedDateTime
Calendar legacyCalendar = new GregorianCalendar(); // 表示当前时间
ZonedDateTime zdtFromCalendar = ((GregorianCalendar) legacyCalendar).toZonedDateTime();
System.out.println("From legacy Calendar: " + zdtFromCalendar);
// ZonedDateTime 转 遗留 Calendar
ZonedDateTime currentZdt = ZonedDateTime.now();
GregorianCalendar calendarFromZdt = GregorianCalendar.from(currentZdt);
System.out.println("To legacy Calendar (month is 0-based): " + calendarFromZdt.get(Calendar.MONTH)); // 转遗留 Calendar (月份基于 0 开始):
}
}

掌握 java.time API 对于现代 Java 开发至关重要。接下来,我们将探讨另一个常见的数据操作任务:使用 String 类和相关概念(如正则表达式)处理字符序列。