在Java8之前,在代码里处理日期和时间是一件让人头疼的事情。
java.util.Date和java.util.Calendar这对搭档存在着很多的问题:
Date类的月份从0开始(一月是0),星期从1开始(周日是1),很容易出错。Calendar的操作方式(add(), set())也很笨重。
Date和Calendar对象是可变的,可以被任意修改。在多线程环境下很容易出问题,出BUG了很难找。
SimpleDateFormat作为主要的格式化工具,不是线程安全的,程序员只能用ThreadLocal或同步锁来配合使用。
Date类本身不包含时区信息,但在toString()的时候又会使用JVM的默认时区,造成很多混乱。
后来就在Java8引入了全新的java.time包,基于Joda-Time库构建,给日期时间操作提供了很好的解决方案。
一、java.time的核心设计思想
所有java.time包里的核心类都是不可变的,任何修改操作(比如增加一天)都会返回一个新的实例,原始实例保持不变。这使得它们天然线程安全。
API把"人类可读的日期时间"和"机器使用的精确时间戳"明确分开,并清晰地处理有时区和无时区的场景。
像LocalDate、LocalTime、LocalDateTime这些类表示的时间就是我们日常说的时间:
今天是2025年7月16日下午3点半。
Instant是对时间轴上的一个点的抽象,更适合做时间差计算、系统时钟同步、日志排序等。
API的设计采用了链式调用风格,代码读起来更像自然语言。
现在的时间,往后加一天,把小时设置成10点,把分钟设成30分。
二、核心类
2.1 人类日期时间
Local系列处理的是不带时区的日期和时间,它代表的是我们日常所见的日历和时钟上的时间。
LocalDate: 只表示日期,如2025-07-15。
LocalTime: 只表示时间,如16:36:37。
LocalDateTime: 表示日期和时间的组合,如2025-07-15T16:36:37。
下面是一个简短的案例,包含了当前日期时间获取、创建指定日期时间、操作日期时间等方法。
package com.lazy.snail.day34;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
/**
* @ClassName Day34Demo
* @Description TODO
* @Author lazysnail
* @Date 2025/7/16 15:21
* @Version 1.0
*/
public class Day34Demo {
public static void main(String[] args) {
// --- 获取当前日期和时间 ---
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime currentDateTime = LocalDateTime.now();
System.out.println("今天日期: " + today);
System.out.println("当前时间: " + now);
System.out.println("当前日期时间: " + currentDateTime);
// --- 创建指定的日期和时间 ---
LocalDate birthday = LocalDate.of(2000, Month.JANUARY, 1);
LocalTime meetingTime = LocalTime.of(10, 30, 0);
System.out.println("生日: " + birthday);
// --- 操作日期和时间 ---
LocalDate nextWeek = today.plusWeeks(1);
System.out.println("一周后的今天: " + nextWeek);
LocalDateTime meetingDateTime = LocalDateTime.of(today, meetingTime);
LocalDateTime nextMeeting = meetingDateTime.plusDays(7).withHour(11);
System.out.println("下次会议时间: " + nextMeeting);
}
}
2.2 机器时间戳
Instant代表的是时间线上的一个精确的点,以UTC为基准,从1970年1月1日0时0分0秒开始计算的秒数和纳秒数。
适合用来记录事件发生的时间戳、进行时间戳计算等机器级的应用。
来看一个案例:
package com.lazy.snail.day34;
import java.time.Duration;
import java.time.Instant;
/**
* @ClassName Day34Demo2
* @Description TODO
* @Author lazysnail
* @Date 2025/7/16 15:38
* @Version 1.0
*/
public class Day34Demo2 {
public static void main(String[] args) {
Instant start = Instant.now();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Instant end = Instant.now();
System.out.println("操作开始时间戳: " + start);
System.out.println("操作结束时间戳: " + end);
Duration duration = Duration.between(start, end);
System.out.println("操作耗时: " + duration.toMillis() + " 毫秒");
}
}
案例里面用Thread.sleep模拟了一个耗时操作,使用Duration.between计算出耗时操作耗费的时间差值。
Thread.sleep(1000);就是让线程暂停了1秒,后面讲到线程时会详细讲。
2.3 带时区的日期和时间
如果你想精确处理特定时区的日期和时间时,就要使用ZonedDateTime。
它是由一个LocalDateTime和一个ZoneId(时区ID)组成的。
直接看案例:
package com.lazy.snail.day34;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
/**
* @ClassName Day34Demo3
* @Description TODO
* @Author lazysnail
* @Date 2025/7/16 15:42
* @Version 1.0
*/
public class Day34Demo3 {
public static void main(String[] args) {
ZoneId beijingZone = ZoneId.of("Asia/Shanghai");
ZoneId parisZone = ZoneId.of("Europe/Paris");
ZonedDateTime beijingTime = ZonedDateTime.now(beijingZone);
System.out.println("当前北京时间: " + beijingTime);
LocalDateTime localMeeting = LocalDateTime.of(2025, 7, 20, 14, 0);
ZonedDateTime parisMeeting = localMeeting.atZone(parisZone);
System.out.println("巴黎会议时间: " + parisMeeting);
ZonedDateTime beijingMeeting = parisMeeting.withZoneSameInstant(beijingZone);
System.out.println("对应的北京会议时间: " + beijingMeeting);
}
}
通过ZoneId指定时区,获取特定时区的当前时间。
把一个本地时间转换为特定时区的时间。
不同时区时间之间的转换。
为什么我们经常听到的是北京时间,但是代码里定义的时区是Asia/Shanghai? “北京时间”是官方规定的法定时间标准。 IANA时区数据库历史上使用的是Asia/Shanghai,定的都是东八区(UTC+8)时间。
2.4 时间段的度量
Duration: 用于度量基于秒和纳秒的时间差,适合机器时间(如Instant, LocalTime, LocalDateTime)。
Period: 用于度量基于年、月、日的时间差,适合人类日历时间(如LocalDate)。
package com.lazy.snail.day34;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
/**
* @ClassName Day34Demo4
* @Description TODO
* @Author lazysnail
* @Date 2025/7/16 16:13
* @Version 1.0
*/
public class Day34Demo4 {
public static void main(String[] args) {
LocalDate startDate = LocalDate.of(2025, 1, 20);
LocalDate endDate = LocalDate.of(2026, 3, 15);
Period period = Period.between(startDate, endDate);
System.out.printf("两个日期相差: %d年, %d月, %d天\n",
period.getYears(), period.getMonths(), period.getDays());
LocalTime startTime = LocalTime.of(9, 0, 0);
LocalTime endTime = LocalTime.of(10, 30, 30);
Duration duration = Duration.between(startTime, endTime);
System.out.println("两个时间相差: " + duration.toMinutes() + " 分钟");
}
}
上半段是Period示例,下半段是Duration示例。
2.5 格式化
DateTimeFormatter替代了以前的SimpleDateFormat。
package com.lazy.snail.day34;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @ClassName Day34Demo5
* @Description TODO
* @Author lazysnail
* @Date 2025/7/16 16:15
* @Version 1.0
*/
public class Day34Demo5 {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
String isoDateTime = now.format(DateTimeFormatter.ISO_DATE_TIME);
System.out.println("ISO格式: " + isoDateTime);
DateTimeFormatter customFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String customFormatted = now.format(customFormatter);
System.out.println("自定义格式: " + customFormatted);
String dateTimeStr = "2025年08月10日 20:30:00";
LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeStr, customFormatter);
System.out.println("解析后的日期时间: " + parsedDateTime);
}
}
可以使用预定义的格式化器,DateTimeFormatter里面预定义了很多的格式化器。
既然有预定义的,当然也可以自定义,按照自定义的格式进行格式化。
把字符串按照自定义的格式转换成日期时间对象也是常见的操作。
三、TemporalAdjusters
可以看成一个工具类,这个类里提供了一些静态方法,可以用他们进行复杂的日期调整。
package com.lazy.snail.day34;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
/**
* @ClassName Day34Demo6
* @Description TODO
* @Author lazysnail
* @Date 2025/7/16 16:31
* @Version 1.0
*/
public class Day34Demo6 {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth());
System.out.println("本月第一天: " + firstDayOfMonth);
LocalDate nextSunday = today.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
System.out.println("下一个周日: " + nextSunday);
}
}
获取本月的第一天,获取下一个周日。
结语
为什么我要单开一篇文章来讲一下Java里的日期时间的操作?
一是日期时间处理不管在什么系统中,都是绕不开的,在实际开发中确实很常用。
二是Java版本的迭代,确实对日期时间API进行了很大的改变。
下一篇预告
Day35 | Java多线程入门
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
更多文章请关注我的公众号《懒惰蜗牛工坊》