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

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


 

本节继续探讨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。java

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

表示日期和时间github

基本概念编程

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

  • 时刻:全部计算机系统内部都用一个整数表示时刻,这个整数是距离格林尼治标准时间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编程及计算机技术的本质。用心原创,保留全部版权。

相关文章
相关标签/搜索