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


本节继续探讨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编程及计算机技术的本质。用心原创,保留所有版权。

计算机程序的思维逻辑 (95) - Java 8的日期和时间API的更多相关文章

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

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

  2. 计算机程序的思维逻辑 (8) - char的真正含义

    看似简单的char 通过前两节,我们应该对字符和文本的编码和乱码有了一个清晰的认识,但前两节都是与编程语言无关的,我们还是不知道怎么在程序中处理字符和文本. 本节讨论在Java中进行字符处理的基础 - ...

  3. 计算机程序的思维逻辑 (29) - 剖析String

    上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比 ...

  4. 计算机程序的思维逻辑 (64) - 常见文件类型处理: 属性文件/CSV/EXCEL/HTML/压缩文件

    对于处理文件,我们介绍了流的方式,57节介绍了字节流,58节介绍了字符流,同时,也介绍了比较底层的操作文件的方式,60节介绍了随机读写文件,61节介绍了内存映射文件,我们也介绍了对象的序列化/反序列化 ...

  5. 计算机程序的思维逻辑 (91) - Lambda表达式

    ​在之前的章节中,我们的讨论基本都是基于Java 7的,从本节开始,我们探讨Java 8的一些特性,主要内容包括: 传递行为代码 - Lambda表达式 函数式数据处理 - 流 组合式异步编程 - C ...

  6. 建议大家使用Java 8 的日期、时间,而非java.util.Date

    建议大家使用Java 8 的日期.时间,而非java.util.Date. 详细原因见:如何在Java 8中愉快地处理日期和时间 总结一下就是, java.util.Date 太乱,如 月份从0开始. ...

  7. Java中的日期和时间

    Java中的日期和时间 Java在java.util包中提供了Date类,这个类封装了当前的日期和时间. Date类支持两种构造函数.第一个构造函数初始化对象的当前日期和时间. Date() 下面的构 ...

  8. Java 8 (11) 新的日期和时间API

    在Java 1.0中,对日期和时间的支持只能依赖java.util.Date类.这个类只能以毫秒的精度表示时间.这个类还有很多糟糕的问题,比如年份的起始选择是1900年,月份的起始从0开始.这意味着你 ...

  9. java 8中新的日期和时间API

    java 8中新的日期和时间API 使用LocalDate和LocalTime LocalDate的实例是一个不可变对象,它只提供了简单的日期,并不含当天的时间信息.另外,它也不附带任何与时区相关的信 ...

随机推荐

  1. Kibana5 数据探索使用(Discover功能)

    认识Kibana Kibana 是一个为 Logstash 和 ElasticSearch 提供的日志分析的 Web 接口.可使用它对日志进行高效的搜索.可视化.分析等各种操作.Kibana的使用场景 ...

  2. [javascript] postmessage

    摘要 postmessage 作为 html5 跨域传值的解决方法,灰常好用啊..本次用的是页面a 用iframe 嵌入 页面b. 使用方法 postmessage 参数 otherWindow.po ...

  3. cesium根据经纬度计算距离

    var startLatitude = 36;var startLongitude = 120; var endLatitude=34; var endLongitude=121; var start ...

  4. java迭代器浅析

    简介 迭代器是遍历容器的一种常用方法,它屏蔽了容器的实现细节,无需暴露数据结构内部,就可以对容器进行遍历,迭代器本身也是一种设计模式,迭代是一种特殊的遍历方式 Iterator 在java中,迭代器接 ...

  5. Java 字符串截取问题

    编程:编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串. 但是要保证汉字不被截半个,如"我ABC"4 public class StringSplit  ...

  6. Android文件上传与下载

    文件上传与下载 文件上传 -- 服务端 以Tomcat为服务器,Android客服端访问Servlet,经Servlet处理逻辑,最终将文件上传,这里就是简单模拟该功能,就将文件上传到本机的D:\\u ...

  7. 47. leetcode 437. Path Sum III

    437. Path Sum III You are given a binary tree in which each node contains an integer value. Find the ...

  8. 6. leetcode 136. Single Number

    Given an array of integers, every element appears twice except for one. Find that single one. Note: ...

  9. HDU-2017-字符串统计

    /* Name: HDU-2017-字符串统计 Date: 18/04/17 20:19 Description: 水过 */ #include<bits/stdc++.h> using ...

  10. 只需要一点点C++基础,新手也可以制作单机游戏内存修改器

    声明:本文只是为了初学C++的,能够做出一些实用的东西,跳出管理系统的束缚,提升学习的兴趣,在这里选取了单机游戏,请不要尝试在线游戏,违发而已未必可行.序:首先我们需要一个Qt+VS环境Qt从http ...