计算机程序的思维逻辑 (95)

?本节继续探讨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-11LocalTime lt = ldt.toLocalTime(); // 20:45:05//LocalDate加上时间,结果为2017-07-11 21:18:39LocalDateTime ldt2 = ld.atTime(21, 18, 39);//LocalTime加上日期,结果为2016-03-24 20:45:05LocalDateTime ldt3 = lt.atDate(LocalDate.of(2016, 3, 24));

ZonedDateTime

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

ZonedDateTime zdt = ZonedDateTime.now();

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

//根据Instant和时区构建ZonedDateTimepublic 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编程及计算机技术的本质。用心原创,保留所有版权。

我们摇摇头说,困难其实没什么大不了。

计算机程序的思维逻辑 (95)

相关文章:

你感兴趣的文章:

标签云: