日期专题
🕙 本文阅读时长:10分钟
本文将从以下几个方面,简介的解释 java 8 中新的日期组件以及常用的方法
- 为什么java需要新的日期和时间API
- 面向机器和人类的日期和时间
- 定义日期和时间
- 操作、分析和格式化日期
- 不同时区和日期的处理
一、早期版本中的日期问题
在 java 1.0 支持日期和时间仅仅是java.util.Date这个类,它有个很大的问题是它以1900为基准年份,在表示月份以0为开始。所以要用 Date 表示 2000-1-1,它的形式是new Date(100,0,1),看上去太奇怪了。并且在 java 1.0 中 Date 类中仅仅只有机器所在的默认时区。在 java 1.0 发布后,日期问题很快显露出来,并且在后续的 java 1.1 版本中试图修复一些问题,但新的java.util.Calendar类并没有解决之前的一些问题,比如月份还是以 0 表示首月。Date 中的很多方法被标志为已过期,DateFormat类又仅支持 Date 类,很多开发人员对此感到更加的困惑。
DateFormat 也有其自身问题。在单例模式多线程环境下,多个线程同时访问该实例很有可能出现解析的日期出错,主要的一个原因是 DateFormat 对象内部维护着一个Calenar实例,每次解析时会更改Calendar 实例的值,这是造成多线程问题的首要原因。
鉴于此,第三方的 Joda-Time 孕育而生,弥补 java 本身对日期 API 的不足。Oracle 在 JAVA 8 的时候借鉴了很多 Joda-Time 的特性,推出了新的日期时间 API。
本文会从最简单和常用的日期时间类用例开始,逐步的探索更高级的特性,比如日期时间操作、解析、打印不同的日期时间格式,以及不同的时区和历法。
二、LocalDate,LocalTime,LocalDateTime,Instant,Duration 和 Period
从两个人类和机器的两个维度,介绍 JAVA 新 API 提供的类和其他接口。其中LocalDate、LocalTime、LocalDateTime 和 Period 主要用于人类的日期时,其他的 Instant 和 Duration 用于机器的日期和时间。
2.1 LocalDate和LocalTime
LocalDate 和 LocalTime是见到就能大概猜到的两个类,分别表示日期和时间。它们都有类似的工厂方式去创建,并且都是不可变的,换言之,对它们的修改不会改变原来的值,而是会返回一个新的实例对象。下面就从 LocalDate 的创建开始吧
// 创建实例对象2017年9月21日
LocalDate date = LocalDate.of(2017, 9, 21);
// 得到2017
int year = date.getYear();
// 得到月份的英语表示
Month month = date.getMonth();
int day = date.getDayOfMonth();
// 星期的英语表示
DayOfWeek dow = date.getDayOfWeek();
// 月份的长度
int len = date.lengthOfMonth();
// 是否闰年
boolean leap = date.isLeapYear();
LocalTIme 的用法类似,也有一个和 LocalDate 对应的静态工厂方法。
LocalTime time = LocalTime.of(13, 45, 20);
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();
LocalDate 和 LocalTime 的组合形式是另外一个类,叫 LocalDateTime, 是我们日常开发中最常用的类,它的使用方式如下
LocalDateTime dt1 = LocalDateTime.of(2017, Month.SEPTEMBER, 21, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
2.3 用于表示机器时间的Instant类
JAVA 中的java.time.Instan类,和人类在使用日期时间会使用星期、月份、年份等概念不同,它是表示从 UTC 1970年1月1日开始,到特定时间的时间搓。和前面介绍的类一样,Instant 类也有相同的静态工厂方式来创建对象实例
// 从1970年1月1日后的3秒后
Instant.ofEpochSecond(3);
// 从1970年1月1日后的2秒后的1_000_000_000纳秒之后
Instant instant1 = Instant.ofEpochSecond(2, 2_000_000_000);
2.4 Duration和Period类
我们目前看到的所有类都是 java.time.temporal 接口的实现类,Temporal 接口定义如何定义读取和操作日期时间类的值,下面介绍的 Duration 和 Period 类则时间间隔相关的类。
Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);
System.out.println(d1.get(ChronoUnit.SECONDS));
System.out.println(d1.get(ChronoUnit.DAYS));
上面的三行代码获取的是第二个参数减去第一个参数的间隔值,Duration默认的两个日期的差值是秒,并且想获取其他形式的差值时会抛出异常。需要注意的是,between 两个参数不能一个是 Instant,另外一个事 LocalDateTime, 否则就会抛出异常。
如果需要两个日期间表示年数、月数、天数等的间隔,用Period会更加的便利,我们先看下面的例子
Period tenDays = Period.between(LocalDate.of(2020, 9, 11),
LocalDate.of(2025, 10, 21));
// 输出 1
System.out.println(tenDays.getMonths());
// 输出 5
System.out.println(tenDays.getYears());
// 输出 61
System.out.println(tenDays.toTotalMonths());
Period还有一个便利的方法,用月数来表示两个日期的间隔。除此之外,它们都还有各自的方式来创建一个相隔日期的实例对象。
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
三、操作、解析和格式化日期
对一个LocalDate对象实例而言,最通用的修改它的属性值大致就是一些列以 with 开头的方法了。这里再提一些,目前提到的所有新 API 的类,都是不可变更的,就是说说有在这些类上的更改操作,都是返回一个新的实例对象。下面的方法是常用的一些改变属性的方法
LocalDate date1 = LocalDate.of(2023, 9, 21);
LocalDate date2 = date1.withYear(2022);
LocalDate date3 = date2.withDayOfMonth(25);
// ChronoField.DAY_OF_YEAR 修改当年的天数
// ChronoField.DAY_OF_MONTH 修改当月的天数
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2);
上面提到的四个方法,本质上而言都是最后哪个方法的简化形式,我们还可以传ChronoField 的不同枚举值来实现修改当月和当年的天数值。
3.2 更直观的修改方式
除了上面介绍的以 with 开头的实例方式,还有其他更加语义化的方式修改属性值
LocalDate date1 = LocalDate.of(2024, 9, 21);
LocalDate date2 = date1.plusWeeks(1);
LocalDate date3 = date2.minusYears(6);
LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime localDateTime1 = localDateTime.plusHours(1);
3.3 TemporalAdjusters接口
上面提到的这些修改日期和时间的方式,都是比较直观的,也是日常开发中使用最频繁的使用方式了。但有的时候我们需要一些更加灵活的方式,我们先看下面的两个例子。
LocalDate date1 = LocalDate.of(2025, 1, 12);
// 1月19
LocalDate date2 = date1.with(next(DayOfWeek.SUNDAY));
// 1月31
LocalDate date3 = date2.with(lastDayOfMonth());
上面的例子中,with 方法在处理完上面调用后,一个返回下个周日,另外一个返回本月的最后一天。 with 方法接收一个java.time.temporal.TemporalAdjuster 函数接口,而java.time.temporal.TemporalAdjusters这个类定义了很多常用的静态方法,每个静态方式都返回一个 TemporalAdjuster 的实现。下面截图是所有 TemporalAdjusters 所有的方法,这些方法给开发很大的便利。
除了系统自带的 TemporalAdjusters 外,我们其实可以通过实现TemporalAdjuste接口来定义自己需要的功能,我们想获取下一个工作日。方然这里为了方便,只有所有的周六和周日才是假期,其他都是工作日。按照要求,可以自定
import java.time.DayOfWeek;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
public class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek currentDay = DayOfWeek.from(temporal);
int daysToAdd;
if (currentDay == DayOfWeek.FRIDAY) {
daysToAdd = 3;
} else if (currentDay == DayOfWeek.SATURDAY) {
daysToAdd = 2;
} else {
daysToAdd = 1;
}
return temporal.plus(daysToAdd, TemporalAdjusters.ofDateAdjuster(DayOfWeek::plus));
}
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate nextWorkingDay = today.with(new NextWorkingDay());
System.out.println("下一个工作日是: " + nextWorkingDay);
}
}
3.4 格式化和解析日期
格式化和解析日期也是开发中最常用的动作之一。在 java 新版本中使用DateTimeFormatter类处理这些操作,下面的例子简单的演示该类的使用方式。
LocalDate date = LocalDate.of(2014, 3, 18);
// 20140318
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
// 2014-03-18
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
除了系统自带的格式化形式,还可以自定义,比如中文里常用的
String s3 = date.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
四、时区(ZoneId和UTC/Greenwich)
ZoneId使用:到目前为止,我们使用的关于日期和时间的类都是不涉及到时区概念的。由于时区这个概念在一般性的开发中碰到的会比较少,这里就介绍下可能得使用场景把。假设开发了一个航班查询的应用,在北京的员工需要查看在纽约航班的一些信息,按照新的API提供的方式我们可以用下面的方式来查询
LocalDateTime eventDateTimeInBeijing = LocalDateTime.of(2025, 1, 12, 14, 0);
ZoneId beijingZoneId = ZoneId.of("Asia/Shanghai");
// 为纽约(美国东部时间,西五区)用户显示活动时间
ZoneId newYorkZoneId = ZoneId.of("America/New_York");
ZonedDateTime eventDateTimeInNewYork = zonedDateTime.withZoneSameInstant(newYorkZoneId);
通过这样的方式,在北京的人可以快速的吧北京的时间转化为纽约当前对应的时间,并且在碰到夏令时的情况会自动的帮我们把时间转化好。
ZoneOffset使用:除了ZoneId,还可以使用ZoneOffset来设置时区,剩余应用场景和ZoneId一样。
LocalDateTime localDateTime = LocalDateTime.now();
ZoneOffset offsetForUTCPlus8 = ZoneOffset.ofHours(8);
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, offsetForUTCPlus8);
System.out.println("使用ZoneOffset创建的ZonedDateTime: " + zonedDateTime);