日期时间API

为什么需要新的日期时间API

一句话理解:Java 8之前的 DateCalendar 简直是反人类设计,新API(java.time包)终于让日期操作变得正常了

老API有多坑?

Date 的月份从0开始(0=一月,11=十二月),年份要减1900

Calendar 操作繁琐,一个简单的日期加减要写一堆代码

Date 是可变的(mutable),多线程不安全

SimpleDateFormat 线程不安全

新API的优点:不可变、线程安全、API设计合理、方法命名清晰

LocalDate(只有日期,没有时间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void testLocalDate() {
System.out.println("=== LocalDate ===");

// 创建
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(2000, 6, 15);
LocalDate parsed = LocalDate.parse("2024-03-15");

System.out.println("今天:" + today); // 2024-xx-xx
System.out.println("生日:" + birthday); // 2000-06-15
System.out.println("解析:" + parsed); // 2024-03-15

// 获取信息
System.out.println("年:" + today.getYear());
System.out.println("月:" + today.getMonthValue()); // 1-12,终于正常了!
System.out.println("日:" + today.getDayOfMonth());
System.out.println("星期:" + today.getDayOfWeek()); // MONDAY等
System.out.println("今年第几天:" + today.getDayOfYear());

// 判断
System.out.println("是闰年?" + today.isLeapYear());
System.out.println("之前?" + birthday.isBefore(today)); // true
System.out.println("之后?" + birthday.isAfter(today)); // false
}

LocalTime(只有时间,没有日期)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testLocalTime() {
System.out.println("=== LocalTime ===");

LocalTime now = LocalTime.now();
LocalTime lunch = LocalTime.of(12, 0, 0);
LocalTime parsed = LocalTime.parse("14:30:00");

System.out.println("现在:" + now);
System.out.println("午饭:" + lunch); // 12:00
System.out.println("解析:" + parsed); // 14:30

System.out.println("时:" + now.getHour());
System.out.println("分:" + now.getMinute());
System.out.println("秒:" + now.getSecond());
}

LocalDateTime(日期+时间,最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testLocalDateTime() {
System.out.println("=== LocalDateTime ===");

// 创建
LocalDateTime now = LocalDateTime.now();
LocalDateTime specific = LocalDateTime.of(2024, 12, 25, 20, 30, 0);
LocalDateTime combined = LocalDateTime.of(
LocalDate.of(2024, 12, 25),
LocalTime.of(20, 30)
);

System.out.println("现在:" + now);
System.out.println("圣诞:" + specific); // 2024-12-25T20:30

// LocalDate 和 LocalTime 互相转换
LocalDate date = now.toLocalDate();
LocalTime time = now.toLocalTime();
System.out.println("日期部分:" + date);
System.out.println("时间部分:" + time);
}

日期时间的加减操作

新API的操作方法非常直观,而且都是不可变的——每次操作返回新对象,不修改原对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
public void testDateTimeOperations() {
System.out.println("=== 日期时间操作 ===");

LocalDate today = LocalDate.now();

// plus系列:加
LocalDate nextWeek = today.plusDays(7);
LocalDate nextMonth = today.plusMonths(1);
LocalDate nextYear = today.plusYears(1);
System.out.println("一周后:" + nextWeek);
System.out.println("一月后:" + nextMonth);
System.out.println("一年后:" + nextYear);

// minus系列:减
LocalDate lastWeek = today.minusDays(7);
LocalDate lastMonth = today.minusMonths(1);
System.out.println("一周前:" + lastWeek);
System.out.println("一月前:" + lastMonth);

// with系列:直接设置某个字段
LocalDate firstDayOfMonth = today.withDayOfMonth(1);
LocalDate inYear2000 = today.withYear(2000);
System.out.println("本月第一天:" + firstDayOfMonth);
System.out.println("换成2000年:" + inYear2000);

// ⚠️ 原对象不会变!
System.out.println("today还是:" + today); // 没变
}
方法 作用 示例
plusDays(n) 加n天 today.plusDays(7)
plusMonths(n) 加n月 today.plusMonths(1)
plusYears(n) 加n年 today.plusYears(1)
minusDays(n) 减n天 today.minusDays(7)
minusMonths(n) 减n月 today.minusMonths(1)
withDayOfMonth(n) 设置为n号 today.withDayOfMonth(1)
withMonth(n) 设置为n月 today.withMonth(6)
withYear(n) 设置年份 today.withYear(2000)

Duration 和 Period(时间间隔)

Duration:时间维度的间隔(时分秒),用于 LocalTime / LocalDateTime

Period:日期维度的间隔(年月日),用于 LocalDate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void testDurationAndPeriod() {
System.out.println("=== Duration 和 Period ===");

// Duration:计算两个时间的间隔
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 30);
Duration duration = Duration.between(start, end);
System.out.println("工作时长:" + duration); // PT8H30M
System.out.println("工作了" + duration.toHours() + "小时"); // 8
System.out.println("工作了" + duration.toMinutes() + "分钟"); // 510

// Period:计算两个日期的间隔
LocalDate born = LocalDate.of(2000, 1, 1);
LocalDate now = LocalDate.now();
Period period = Period.between(born, now);
System.out.println("年龄:" + period.getYears() + "岁"
+ period.getMonths() + "月" + period.getDays() + "天");

// 也可以直接创建
Duration twoHours = Duration.ofHours(2);
Period threeMonths = Period.ofMonths(3);

LocalDateTime meeting = LocalDateTime.now().plus(twoHours);
LocalDate deadline = LocalDate.now().plus(threeMonths);
System.out.println("会议时间:" + meeting);
System.out.println("截止日期:" + deadline);
}
维度 适用于 示例
Duration 时分秒纳秒 LocalTime, LocalDateTime Duration.between(t1, t2)
Period 年月日 LocalDate Period.between(d1, d2)

DateTimeFormatter(格式化和解析)

线程安全的格式化器,替代老的 SimpleDateFormat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void testDateTimeFormatter() {
System.out.println("=== DateTimeFormatter ===");

LocalDateTime now = LocalDateTime.now();

// 预定义格式
String iso = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println("ISO格式:" + iso); // 2024-03-15T14:30:00.123

// 自定义格式(最常用)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(formatter);
System.out.println("自定义格式:" + formatted); // 2024-03-15 14:30:00

DateTimeFormatter cnFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒");
System.out.println("中文格式:" + now.format(cnFormatter)); // 2024年03月15日 14时30分00秒

// 解析字符串为日期
LocalDateTime parsed = LocalDateTime.parse("2024-12-25 20:00:00", formatter);
System.out.println("解析结果:" + parsed); // 2024-12-25T20:00

LocalDate dateParsed = LocalDate.parse("2024-03-15"); // 默认ISO格式直接解析
System.out.println("日期解析:" + dateParsed);
}

常用格式符号

符号 含义 示例
yyyy 四位年份 2024
MM 两位月份 03
dd 两位日期 15
HH 24小时制时 14
hh 12小时制时 02
mm 分钟 30
ss 00
E 星期 周五
a 上午/下午 PM

新旧API对比

对比项 旧API (Date/Calendar) 新API (java.time)
所在包 java.util java.time
可变性 可变(mutable) 不可变(immutable)
线程安全 不安全 安全
月份 0-11(反人类) 1-12(正常)
日期和时间 混在一起 LocalDate/LocalTime/LocalDateTime分开
格式化 SimpleDateFormat(线程不安全) DateTimeFormatter(线程安全)
时区 TimeZone ZoneId/ZonedDateTime
日期计算 Calendar操作繁琐 plus/minus直观简洁
API设计 混乱 清晰、一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void testOldVsNew() {
System.out.println("=== 新旧API对比 ===");

// ===== 旧API:创建2024年3月15日 =====
// Date的构造方法已经废弃了,只能用Calendar
Calendar cal = Calendar.getInstance();
cal.set(2024, 2, 15); // 注意:月份是2,表示三月!
Date oldDate = cal.getTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
System.out.println("旧API:" + sdf.format(oldDate));

// ===== 新API:创建2024年3月15日 =====
LocalDate newDate = LocalDate.of(2024, 3, 15); // 3就是3月,正常!
System.out.println("新API:" + newDate);

// ===== 新旧转换 =====
// Date → LocalDateTime
Date date = new Date();
LocalDateTime ldt = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();

// LocalDateTime → Date
Date back = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
}

时区处理 ZonedDateTime(了解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testZonedDateTime() {
System.out.println("=== 时区处理 ===");

// 带时区的日期时间
ZonedDateTime shanghaiNow = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime tokyoNow = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
ZonedDateTime londonNow = ZonedDateTime.now(ZoneId.of("Europe/London"));
ZonedDateTime newYorkNow = ZonedDateTime.now(ZoneId.of("America/New_York"));

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
System.out.println("上海:" + shanghaiNow.format(fmt));
System.out.println("东京:" + tokyoNow.format(fmt));
System.out.println("伦敦:" + londonNow.format(fmt));
System.out.println("纽约:" + newYorkNow.format(fmt));

// 时区转换
ZonedDateTime converted = shanghaiNow.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("上海时间转纽约:" + converted.format(fmt));

// 查看所有可用时区
// ZoneId.getAvailableZoneIds().forEach(System.out::println);
}

日常开发如果不涉及跨时区,用 LocalDateTime 就够了

如果需要处理时区(比如国际化项目),才需要用 ZonedDateTime

常见坑

坑1:新API的对象是不可变的,操作后要接收返回值

1
2
3
LocalDate today = LocalDate.now();
today.plusDays(7); // ❌ 这行没有意义!today没有变
LocalDate nextWeek = today.plusDays(7); // ✅ 要用新变量接收

坑2:DateTimeFormatter的大小写有含义

MM 是月份,mm 是分钟

HH 是24小时制,hh 是12小时制

写错了不会报错,但结果会很离谱

坑3:parse字符串格式必须严格匹配

1
2
3
4
5
6
// ❌ 格式不匹配会抛 DateTimeParseException
// LocalDate.parse("2024/03/15"); // 默认格式是 yyyy-MM-dd

// ✅ 自定义格式
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate date = LocalDate.parse("2024/03/15", fmt);

坑4:Period.between的参数顺序

1
2
3
// 如果start在end之后,得到的是负数
Period p = Period.between(LocalDate.of(2025, 1, 1), LocalDate.of(2024, 1, 1));
System.out.println(p.getYears()); // -1

练习题

题1:计算你的年龄(精确到年月日),用 Period.between

题2:写一个方法,接收一个日期字符串(格式:yyyy/MM/dd),返回这个日期是星期几

题3:计算两个日期之间相差多少天(提示:用 ChronoUnit.DAYS.between()

题4:写一个方法,输出本月的第一天和最后一天

提示:today.withDayOfMonth(1)today.withDayOfMonth(today.lengthOfMonth())

题5:把字符串 "2024-12-25 20:00:00" 转成 "2024年12月25日 晚上8点整" 格式输出


上一章 目录 下一章
方法引用与函数式接口 java基础 反射机制