Java编程的逻辑 (95) - Java 8的日期和时间API

时间:2023-12-20 15:57:32

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接http://item.jd.com/12299018.html

Java编程的逻辑 (95) - Java 8的日期和时间API


本节继续探讨Java 8的新特性,主要是介绍Java 8对日期和时间API的增强,关于日期和时间,我们在之前已经介绍过两节了,32节介绍了Java 1.8以前的日期和时间API,主要的类是Date和Calendar,由于它的设计有一些不足,业界广泛使用的是一个第三方的类库Joda-Time,关于Joda-time,我们在33节进行了介绍。Java 1.8学习了Joda-time,引入了一套新的API,位于包java.time下,本节,我们就来简要介绍这套新的API。

我们先从日期和时间的表示开始。

表示日期和时间

基本概念

我们在32节介绍过日期和时间的几个基本概念,这里简要回顾下。

  • 时刻:所有计算机系统内部都用一个整数表示时刻,这个整数是距离格林尼治标准时间1970年1月1日0时0分0秒的毫秒数,可以理解时刻就是绝对时间,它与时区无关,不同时区对同一时刻的解读,即年月日时分秒是不一样的;
  • 时区:同一时刻,世界上各个地区的时间可能是不一样的,具体时间与时区有关,一共有24个时区,英国格林尼治是0时区,北京是东八区,也就是说格林尼治凌晨1点,北京是早上9点;
  • 年历:我们都知道,中国有公历和农历之分,公历和农历都是年历,不同的年历,一年有多少月,每月有多少天,甚至一天有多少小时,这些可能都是不一样的,我们主要讨论公历。

Java 8中表示日期和时间的类有多个,主要的有:

  • Instant:表示时刻,不直接对应年月日信息,需要通过时区转换
  • LocalDateTime: 表示与时区无关的日期和时间信息,不直接对应时刻,需要通过时区转换
  • LocalDate:表示与时区无关的日期,与LocalDateTime相比,只有日期信息,没有时间信息
  • LocalTime:表示与时区无关的时间,与LocalDateTime相比,只有时间信息,没有日期信息
  • ZonedDateTime: 表示特定时区的日期和时间
  • ZoneId/ZoneOffset:表示时区

类比较多,但概念更为清晰了,下面我们逐个来看下。

Instant

Instant表示时刻,获取当前时刻,代码为:

Instant now = Instant.now();

可以根据Epoch Time (纪元时)创建Instant,比如,另一种获取当前时刻的代码可以为:

Instant now = Instant.ofEpochMilli(System.currentTimeMillis());

我们知道,Date也表示时刻,Instant和Date可以通过纪元时相互转换,比如,转换Date为Instant,代码为:

public static Instant toInstant(Date date) {
return Instant.ofEpochMilli(date.getTime());
}

转换Instant为Date,代码为:

public static Date toDate(Instant instant) {
return new Date(instant.toEpochMilli());
}

Instant有很多基于时刻的比较和计算方法,大多比较直观,我们就不列举了。

LocalDateTime

LocalDateTime表示与时区无关的日期和时间信息,获取系统默认时区的当前日期和时间,代码为:

LocalDateTime ldt = LocalDateTime.now();

还可以直接用年月日等信息构建LocalDateTime,比如,表示2017年7月11日20点45分5秒,代码可以为:

LocalDateTime ldt = LocalDateTime.of(2017, 7, 11, 20, 45, 5);

LocalDateTime有很多方法,可以获取年月日时分秒等日历信息,比如:

public int getYear()
public int getMonthValue()
public int getDayOfMonth()
public int getHour()
public int getMinute()
public int getSecond()

还可以获取星期几等信息,比如:

public DayOfWeek getDayOfWeek() 

DayOfWeek是一个枚举,有七个取值,从DayOfWeek.MONDAY到DayOfWeek.SUNDAY。

LocalDateTime不能直接转为时刻Instant,转换需要一个参数ZoneOffset,ZoneOffset表示相对于格林尼治的时区差,北京是+08:00,比如,转换一个LocalDateTime为北京的时刻,方法为:

public static Instant toBeijingInstant(LocalDateTime ldt) {
return ldt.toInstant(ZoneOffset.of("+08:00"));
}

给定一个时刻,使用不同时区解读,日历信息是不同的,Instant有方法根据时区返回一个ZonedDateTime:

public ZonedDateTime atZone(ZoneId zone)

默认时区是ZoneId.systemDefault(),可以这样构建ZoneId:

//北京时区
ZoneId bjZone = ZoneId.of("GMT+08:00")

ZoneOffset是ZoneId的子类,可以根据时区差构造。

LocalDate/LocalTime

可以认为,LocalDateTime由两部分组成,一部分是日期LocalDate,另一部分是时间LocalTime,它们的用法也很直观,比如:

//表示2017年7月11日
LocalDate ld = LocalDate.of(2017, 7, 11); //当前时刻按系统默认时区解读的日期
LocalDate now = LocalDate.now(); //表示21点10分34秒
LocalTime lt = LocalTime.of(21, 10, 34); //当前时刻按系统默认时区解读的时间
LocalTime time = LocalTime.now();

LocalDateTime由LocalDate和LocalTime构成,LocalDate加上时间可以构成LocalDateTime,LocalTime加上日期可以构成LocalDateTime,比如:

LocalDateTime ldt = LocalDateTime.of(2017, 7, 11, 20, 45, 5);
LocalDate ld = ldt.toLocalDate(); //2017-07-11
LocalTime lt = ldt.toLocalTime(); // 20:45:05 //LocalDate加上时间,结果为2017-07-11 21:18:39
LocalDateTime ldt2 = ld.atTime(21, 18, 39); //LocalTime加上日期,结果为2016-03-24 20:45:05
LocalDateTime ldt3 = lt.atDate(LocalDate.of(2016, 3, 24));

ZonedDateTime

ZonedDateTime表示特定时区的日期和时间,获取系统默认时区的当前日期和时间,代码为:

ZonedDateTime zdt = ZonedDateTime.now();

LocalDateTime.now()也是获取默认时区的当前日期和时间,有什么区别呢?LocalDateTime内部不会记录时区信息,只会单纯记录年月日时分秒等信息,而ZonedDateTime除了记录日历信息,还会记录时区,它的其他大部分构建方法都需要显式传递时区,比如:

//根据Instant和时区构建ZonedDateTime
public static ZonedDateTime ofInstant(Instant instant, ZoneId zone) //根据LocalDate, LocalTime和ZoneId构造
public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone)

ZonedDateTime可以直接转换为Instant,比如:

ZonedDateTime ldt = ZonedDateTime.now();
Instant now = ldt.toInstant();

格式化/解析字符串

Java 8中,主要的格式化类是java.time.format.DateTimeFormatter,它是线程安全的,看个例子:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.of(2016,8,18,14,20,45);
System.out.println(formatter.format(ldt));

输出为:

2016-08-18 14:20:45

将字符串转化为日期和时间对象,可以使用对应类的parse方法,比如:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str = "2016-08-18 14:20:45";
LocalDateTime ldt = LocalDateTime.parse(str, formatter);

设置和修改时间

修改时期和时间有两种方式,一种是直接设置绝对值,另一种是在现有值的基础上进行相对增减操作,Java 8的大部分类都支持这两种方式,另外,与Joda-Time一样,Java 8的大部分类都是不可变类,修改操作是通过创建并返回新对象来实现的,原对象本身不会变。

我们来看一些例子。

调整时间为下午3点20

代码示例为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.withHour(15).withMinute(20).withSecond(0).withNano(0);

还可以为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.toLocalDate().atTime(15, 20);

三小时五分钟后

示例代码为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.plusHours(3).plusMinutes(5);

LocalDateTime有很多plusXXX和minusXXX方法,用于相对增加和减少时间。

今天0点

可以为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.with(ChronoField.MILLI_OF_DAY, 0);

ChronoField是一个枚举,里面定义了很多表示日历的字段,MILLI_OF_DAY表示在一天中的毫秒数,值从0到(24 * 60 * 60 * 1,000) - 1。

还可以为:

LocalDateTime ldt = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);

LocalTime.MIN表示"00:00"

也可以为:

LocalDateTime ldt = LocalDate.now().atTime(0, 0);

下周二上午10点整

可以为:

LocalDateTime ldt = LocalDateTime.now();
ldt = ldt.plusWeeks(1).with(ChronoField.DAY_OF_WEEK, 2)
.with(ChronoField.MILLI_OF_DAY, 0).withHour(10);

下一个周二上午10点整

上面下周二指定是下周,如果是下一个周二呢?这与当前是周几有关,如果当前是周一,则下一个周二就是明天,而其他情况则是下周,代码可以为:

LocalDate ld = LocalDate.now();
if(!ld.getDayOfWeek().equals(DayOfWeek.MONDAY)){
ld = ld.plusWeeks(1);
}
LocalDateTime ldt = ld.with(ChronoField.DAY_OF_WEEK, 2).atTime(10, 0);

针对这种复杂一点的调整,Java 8有一个专门的接口TemporalAdjuster,这是一个函数式接口,定义为:

public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}

Temporal是一个接口,表示日期或时间对象,Instant,LocalDateTime,LocalDate等都实现了它,这个接口就是对日期或时间进行调整,还有一个专门的类TemporalAdjusters,里面提供了很多TemporalAdjuster的实现,比如,针对下一个周几的调整,方法是:

public static TemporalAdjuster next(DayOfWeek dayOfWeek)

针对上面的例子,代码可以为:

LocalDate ld = LocalDate.now();
LocalDateTime ldt = ld.with(TemporalAdjusters.next(DayOfWeek.TUESDAY)).atTime(10, 0);

这个next方法是怎么实现的呢?看代码:

public static TemporalAdjuster next(DayOfWeek dayOfWeek) {
int dowValue = dayOfWeek.getValue();
return (temporal) -> {
int calDow = temporal.get(DAY_OF_WEEK);
int daysDiff = calDow - dowValue;
return temporal.plus(daysDiff >= 0 ? 7 - daysDiff : -daysDiff, DAYS);
};
}

它内部封装了一些条件判断和具体调整,提供了更为易用的接口。

TemporalAdjusters中还有很多方法,部分方法如下:

public static TemporalAdjuster firstDayOfMonth()
public static TemporalAdjuster lastDayOfMonth()
public static TemporalAdjuster firstInMonth(DayOfWeek dayOfWeek)
public static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek)
public static TemporalAdjuster previous(DayOfWeek dayOfWeek)
public static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek)

这些方法的含义比较直观,就不解释了,它们主要是封装了日期和时间调整的一些基本操作,更为易用。

明天最后一刻

代码可以为:

LocalDateTime ldt = LocalDateTime.of(LocalDate.now().plusDays(1), LocalTime.MAX);

或者为:

LocalDateTime ldt = LocalTime.MAX.atDate(LocalDate.now().plusDays(1));

本月最后一天最后一刻

代码可以为:

LocalDateTime ldt =  LocalDate.now()
.with(TemporalAdjusters.lastDayOfMonth())
.atTime(LocalTime.MAX);

lastDayOfMonth()是怎么实现的呢?看代码:

public static TemporalAdjuster lastDayOfMonth() {
return (temporal) -> temporal.with(DAY_OF_MONTH, temporal.range(DAY_OF_MONTH).getMaximum());
}

这里使用了range方法,从它的返回值可以获取对应日历单位的最大最小值,展开来,本月最后一天最后一刻的代码还可以为:

long maxDayOfMonth = LocalDate.now().range(ChronoField.DAY_OF_MONTH).getMaximum();
LocalDateTime ldt = LocalDate.now()
.withDayOfMonth((int)maxDayOfMonth)
.atTime(LocalTime.MAX);

下个月第一个周一的下午5点整

代码可以为:

LocalDateTime ldt = LocalDate.now()
.plusMonths(1)
.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))
.atTime(17, 0);      

时间段的计算

Java 8中表示时间段的类主要有两个,Period和Duration,Period表示日期之间的差,用年月日表示,不能表示时间,Duration表示时间差,用时分秒表等表示,也可以用天表示,一天严格等于24小时,不能用年月表示,下面看一些例子。

计算两个日期之间的差

看个Period的例子:

LocalDate ld1 = LocalDate.of(2016, 3, 24);
LocalDate ld2 = LocalDate.of(2017, 7, 12);
Period period = Period.between(ld1, ld2);
System.out.println(period.getYears() + "年"
+ period.getMonths() + "月" + period.getDays() + "天");

输出为:

1年3月18天

根据生日计算年龄

示例代码可以为:

LocalDate born = LocalDate.of(1990,06,20);
int year = Period.between(born, LocalDate.now()).getYears();

计算迟到分钟数

假定早上9点是上班时间,过了9点算迟到,迟到要统计迟到的分钟数,怎么计算呢?看代码:

long lateMinutes = Duration.between(
LocalTime.of(9,0),
LocalTime.now()).toMinutes();

与Date/Calendar对象的转换

Java 8的日期和时间API没有提供与老的Date/Calendar相互转换的方法,但在实际中,我们可能是需要的,前面介绍了,Date可以与Instant通过毫秒数相互转换,对于其他类型,也可以通过毫秒数/Instant相互转换。

比如,将LocalDateTime按默认时区转换为Date,代码可以为:

public static Date toDate(LocalDateTime ldt){
return new Date(ldt.atZone(ZoneId.systemDefault())
.toInstant().toEpochMilli());
}

将ZonedDateTime转换为Calendar,代码可以为:

public static Calendar toCalendar(ZonedDateTime zdt) {
TimeZone tz = TimeZone.getTimeZone(zdt.getZone());
Calendar calendar = Calendar.getInstance(tz);
calendar.setTimeInMillis(zdt.toInstant().toEpochMilli());
return calendar;
}

Calendar保持了ZonedDateTime的时区信息。

将Date按默认时区转为LocalDateTime,代码可以为:

public static LocalDateTime toLocalDateTime(Date date) {
return LocalDateTime.ofInstant(
Instant.ofEpochMilli(date.getTime()),
ZoneId.systemDefault());
}

将Calendar转为ZonedDateTime,代码可以为:

public static ZonedDateTime toZonedDateTime(Calendar calendar) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(
Instant.ofEpochMilli(calendar.getTimeInMillis()),
calendar.getTimeZone().toZoneId());
return zdt;
}

小结

本节简要介绍了Java 8中的日期和时间API,相比以前版本的Date和Calendar,它引入了更多的类,但概念更为清晰了,更为强大和易用了,Java 8学习了Joda-Time的很多概念和实现,与我们之前介绍的Joda-Time很像。

91节讨论Lambda表达式到本节,关于Java 8的主要内容,我们就介绍完了。

同时,关于整个Java编程的基础部分,通过共95节的内容,我们也基本探讨完了,下一节是本系列文章的最后一篇,我们对全部95节内容进行简要梳理。

(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic,位于包shuo.laoma.java8.c95下)

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

Java编程的逻辑 (95) - Java 8的日期和时间API