时间操作
Java 时间操作全解析:从传统类到 Java 8 新 API
在 Java 中,时间操作是日常开发的常见需求,包括获取当前时间、格式化时间、计算时间差等。随着 Java 版本的演进,时间 API 也从早期的 Date、Calendar 发展到 Java 8 引入的 java.time 系列(如 LocalDateTime、ZonedDateTime),后者解决了传统 API 的线程不安全、设计混乱等问题。本文将全面介绍 Java 中操作时间的主要方式及最佳实践。
传统时间类(Java 8 之前)
1. java.util.Date
Date 是最早期的时间类,存储自 1970 年 1 月 1 日 00:00:00(UTC)以来的毫秒数,但大部分方法已被废弃(如 getYear()、getMonth()),仅保留少数核心方法(如 getTime())。
示例:创建 Date 对象
import java.util.Date;
public class DateDemo {
public static void main(String[] args) {
Date now = new Date(); // 获取当前时间
System.out.println(now); // 输出:Thu Aug 14 15:30:45 CST 2025
System.out.println(now.getTime()); // 输出:1755166245000(毫秒时间戳)
}
}
- 缺点:
- 线程不安全(无同步机制)。
- 设计混乱(年从 1900 开始,月从 0 开始)。
- 不支持时区和历法扩展。
2. java.util.Calendar
Calendar 是为了弥补 Date 的缺陷而设计的抽象类,提供了更丰富的时间操作方法(如获取年月日、加减时间),但仍存在线程安全问题。
示例:使用 Calendar 获取年月日时分秒
import java.util.Calendar;
public class CalendarDemo {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance(); // 获取默认时区的Calendar实例
// 获取年、月、日(注意:月份从0开始,需+1)
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1; // 0~11 → 1~12
int day = cal.get(Calendar.DATE);
// 获取时、分、秒(24小时制用HOUR_OF_DAY,12小时制用HOUR)
int hour = cal.get(Calendar.HOUR_OF_DAY); // 24小时制
int minute = cal.get(Calendar.MINUTE);
int second = cal.get(Calendar.SECOND);
System.out.printf("%d-%02d-%02d %02d:%02d:%02d%n",
year, month, day, hour, minute, second);
// 输出:2025-08-14 15:35:22
}
}
- 常用字段:
Calendar.YEAR:年Calendar.MONTH:月(0~11)Calendar.DATE/DAY_OF_MONTH:日Calendar.HOUR_OF_DAY:24 小时制小时Calendar.HOUR:12 小时制小时Calendar.MINUTE:分钟Calendar.SECOND:秒Calendar.MILLISECOND:毫秒
3. java.text.SimpleDateFormat
SimpleDateFormat 用于时间的格式化(Date → 字符串)和解析(字符串 → Date),但线程不安全(多线程并发使用可能导致异常)。
示例:时间格式化与解析
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SimpleDateFormatDemo {
public static void main(String[] args) throws ParseException {
// 格式化:Date → String
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date now = new Date();
String formatted = sdf.format(now);
System.out.println("格式化后:" + formatted); // 输出:2025-08-14 15:40:15
// 解析:String → Date
String timeStr = "2025-12-31 23:59:59";
Date parsedDate = sdf.parse(timeStr);
System.out.println("解析后:" + parsedDate); // 输出:Wed Dec 31 23:59:59 CST 2025
}
}
格式化字符含义
| 字符 | 描述 | 示例 |
|---|---|---|
y |
年(4 位) | yyyy → 2025 |
M |
月(数字) | MM → 08 |
d |
日 | dd → 14 |
H |
24 小时制小时 | HH → 15 |
h |
12 小时制小时 | hh → 03 |
m |
分钟 | mm → 45 |
s |
秒 | ss → 30 |
S |
毫秒 | SSS → 123 |
E |
星期(中文) | E → 星期四 |
a |
上 / 下午(A.M./P.M.) | a → 下午 |
z |
时区 | z → CST |
D |
一年中的第几天 | D → 226 |
w |
一年中的第几周 | w → 33 |
Java 8 新时间 API(java.time)
Java 8 引入的 java.time 包(基于 JSR-310)是时间操作的推荐方案,解决了传统类的线程不安全、设计混乱等问题,核心类包括:
LocalDate:仅含日期(年、月、日)。LocalTime:仅含时间(时、分、秒、毫秒)。LocalDateTime:含日期和时间(无时区)。ZonedDateTime:含日期、时间和时区。DateTimeFormatter:线程安全的时间格式化器。
1. LocalDateTime:处理本地日期时间(无时区)
示例:获取年月日时分秒
import java.time.LocalDateTime;
public class LocalDateTimeDemo {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now(); // 获取当前日期时间
// 获取各字段
int year = now.getYear();
int month = now.getMonthValue(); // 1~12(无需+1)
int day = now.getDayOfMonth();
int hour = now.getHour(); // 24小时制
int minute = now.getMinute();
int second = now.getSecond();
int nano = now.getNano(); // 纳秒(1秒=1e9纳秒)
System.out.printf("%d-%02d-%02d %02d:%02d:%02d.%03d%n",
year, month, day, hour, minute, second, nano / 1_000_000);
// 输出:2025-08-14 15:50:30.123
}
}
时间增减与修改
LocalDateTime now = LocalDateTime.now();
// 增加1天,减少2小时
LocalDateTime modified = now.plusDays(1).minusHours(2);
// 修改年份为2026,月份为10
LocalDateTime updated = now.withYear(2026).withMonth(10);
System.out.println("原时间:" + now); // 2025-08-14T15:55:00
System.out.println("增减后:" + modified); // 2025-08-15T13:55:00
System.out.println("修改后:" + updated); // 2026-10-14T15:55:00
2. DateTimeFormatter:线程安全的格式化器
DateTimeFormatter 替代了 SimpleDateFormat,支持格式化和解析,且线程安全,可直接定义为静态变量复用。
示例:格式化与解析
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateTimeFormatterDemo {
// 定义静态格式化器(线程安全,可复用)
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
// 格式化:LocalDateTime → String
String formatted = FORMATTER.format(now);
System.out.println("格式化后:" + formatted); // 2025-08-14 16:00:45
// 解析:String → LocalDateTime
String timeStr = "2025-01-01 00:00:00";
LocalDateTime parsed = LocalDateTime.parse(timeStr, FORMATTER);
System.out.println("解析后:" + parsed); // 2025-01-01T00:00:00
}
}
3. ZonedDateTime:处理时区
ZonedDateTime 包含时区信息,可用于跨时区时间转换(如 UTC 与北京时间的转换)。
示例:时区转换
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class ZonedDateTimeDemo {
public static void main(String[] args) {
// 获取 UTC 时间
ZonedDateTime utcTime = ZonedDateTime.now(ZoneId.of("UTC"));
// 获取北京时区时间(Asia/Shanghai)
ZonedDateTime beijingTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println("UTC 时间:" + utcTime); // 2025-08-14T08:05:30Z[UTC]
System.out.println("北京时区:" + beijingTime); // 2025-08-14T16:05:30+08:00[Asia/Shanghai]
// 北京时区转纽约时区(America/New_York)
ZonedDateTime newYorkTime = beijingTime.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("纽约时区:" + newYorkTime); // 2025-08-14T04:05:30-04:00[America/New_York]
}
}
4. 时间间隔计算(Duration 与 Period)
Duration:计算两个时间(LocalTime/LocalDateTime)之间的间隔(秒、纳秒)。Period:计算两个日期(LocalDate)之间的间隔(年、月、日)。
示例:计算时间差
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.LocalDate;
public class DurationPeriodDemo {
public static void main(String[] args) {
// 计算时间差(Duration)
LocalDateTime start = LocalDateTime.of(2025, 8, 1, 10, 0, 0);
LocalDateTime end = LocalDateTime.of(2025, 8, 1, 12, 30, 0);
Duration duration = Duration.between(start, end);
System.out.println("相差小时:" + duration.toHours()); // 2
System.out.println("相差分钟:" + duration.toMinutes()); // 150
// 计算日期差(Period)
LocalDate date1 = LocalDate.of(2025, 1, 1);
LocalDate date2 = LocalDate.of(2025, 12, 31);
Period period = Period.between(date1, date2);
System.out.println("相差月数:" + period.getMonths()); // 11
System.out.println("相差天数:" + period.getDays()); // 30(实际总天数需计算)
}
}
传统类与 Java 8 新 API 的对比
| 特性 | 传统类(Date/Calendar) |
Java 8 新 API(java.time) |
|---|---|---|
| 线程安全 | 不安全(SimpleDateFormat 等) |
安全(所有类均为不可变对象) |
| 设计合理性 | 混乱(月从 0 开始,年偏移 1900) | 直观(月从 1 开始,无偏移) |
| 时区支持 | 弱(需手动处理) | 强(ZonedDateTime 原生支持) |
| 格式化 | SimpleDateFormat(线程不安全) |
DateTimeFormatter(线程安全) |
| 时间计算 | 繁琐(add() 方法) |
简洁(plusXxx()/minusXxx()) |
| 推荐使用 | 不推荐(仅兼容旧代码) | 推荐(新代码首选) |
最佳实践
- 优先使用 Java 8 新 API:
LocalDateTime、ZonedDateTime、DateTimeFormatter等,避免传统类的线程安全和设计问题。 - 线程安全:
SimpleDateFormat线程不安全,多线程环境下应使用DateTimeFormatter或ThreadLocal<SimpleDateFormat>(不推荐)。 - 时区处理:涉及跨时区时间时,使用
ZonedDateTime而非LocalDateTime,避免时区混淆。 - 格式化模式:日期用
yyyy(4 位年)而非yy(2 位年,易歧义);24 小时制用HH,12 小时制用hh并配合a(上 / 下午)。