java8日期屌炸天特性以及实战

对很多应用来说,时间和日期的概念都是必须的。像生日,租赁期,事件的时间戳和商店营业时长,等等,都是基于时间和日期的;然而,Java却没有好的API来处理它们。在Java SE 8中,添加了一个新包:java.time,它提供了结构良好的API来处理时间和日期。

历史

在Java刚刚发布,也就是版本1.0的时候,对时间和日期仅有的支持就是java.util.Date类。大多数开发者对它的第一印象就是,它根本不代表一个“日期”。实际上,它只是简单的表示一个,从1970-01-01Z开始计时的,精确到毫秒的瞬时点。由于标准的toString()方法,按照JVM的默认时区输出时间和日期,有些开发人员把它误认为是时区敏感的。

在升级Java到1.1期间,Date类被认为是无法修复的。由于这个原因,java.util.Calendar类被添加了进来。悲剧的是,Calendar类并不比java.util.Date好多少。它们面临的部分问题是:

  • 可变性。像时间和日期这样的类应该是不可变的。
  • 偏移性。Date中的年份是从1900开始的,而月份都是从0开始的。
  • 命名。Date不是“日期”,而Calendar也不真实“日历”。
  • 格式化。格式化只对Date有用,Calendar则不行。另外,它也不是线程安全的。

大约在2001年,Joda-Time项目开始了。它的目的很简单,就是给Java提供一个高质量的时间和日期类库。尽管被耽搁了一段时间,它的1.0版还是被发布。很快,它就成为了广泛使用和流行的类库。随着时间的推移,有越来越多的需求,要在JDK中拥有一个像Joda-Time的这样类库。在来自巴西的Michael Nascimento Santos的帮助下,官方为JDK开发新的时间/日期API的进程:JSR-310,启动了。

综述

新的API:java.time,由5个包组成:

大多数开发者只会用到基础和format包,也可能会用到temporal包。因此,尽管有68个新的公开类型,大多数开发者,大概,将只会用到其中的三分之一。

日期

在新的API中,LocalDate是其中最重要的类之一。它是表示日期的不可变类型,不包含时间和时区。

“本地”,这个术语,我们对它的熟悉来自于Joda-Time。它原本出自ISO-8061的时间和日期标准,它和时区无关。实际上,本地日期只是日期的描述,例如“2014年4月5日”。特定的本地时间,因你在地球上的不同位置,开始于不同的时间线。所以,澳大利亚的本地时间开始的比伦敦早10小时,比旧金山早18小时。

LocalDate被设计成,它的所有方法,都是常用方法:

LocalDate date = LocalDate.of(2014, Month.JUNE, 10);
int year = date.getYear(); // 2014
Month month = date.getMonth(); // 6月
int dom = date.getDayOfMonth(); // 10
DayOfWeek dow = date.getDayOfWeek(); // 星期二
int len = date.lengthOfMonth(); // 30 (6月份的天数)
boolean leap = date.isLeapYear(); // false (不是闰年)

在上面的例子中,我们看到日期使用工厂方法(所有的构造方法都是私有的)创建。然后用它查询了部分基本信息。注意:枚举类型,MonthDayOfWeek,被设计用来增强代码的可读性和可靠性。

在下面的例子中,我们来看看如何操作LocalDate的实例。由于它是不可变类型,每次操作都会产生一个新的实例,而原有实例不收任何影响。

LocalDate date = LocalDate.of(2014, Month.JUNE, 10);
date = date.withYear(2015); // 2015-06-10
date = date.plusMonths(2); // 2015-08-10
date = date.minusDays(1); // 2015-08-09

上面这些都很简单,但有时我们需要对日期进行更复杂的修改。java.time包含了对此的处理机制:TemporalAdjuster类。时间修改器背后的设计思想是,提供一个预装包的、能操纵日期的功能,比如,根据月份的最后一天获取日期的对象。API提供了一些通用的功能,你可以新增你自己的。修改器的使用很简单,但使用静态导入的方式,会让你更方便:

import static java.time.DayOfWeek.*
import static java.time.temporal.TemporalAdjusters.*
 
LocalDate date = LocalDate.of(2014, Month.JUNE, 10);
date = date.with(lastDayOfMonth());
date = date.with(nextOrSame(WEDNESDAY));

java.time包中,像所有主要的时间/日期类一样,LocalDate类是固定于单个历法系统的:ISO-8601标准定义的历法。
看见修改器的瞬间反应就是,这代码跟业务逻辑长的差不多啊!这对时间/日期类业务逻辑是很重的。我们最后想看的是多次手动修改日期。如果你的代码库里,有一个将会使用很多次的,对日期的通用操作,考虑把它做成修改器,然后告诉你的小组成员,把它当做一个写好的,测试过的组件,直接拿来用吧。

时间/日期值对象

值得花点时间弄清楚,是什么导致了LocalDate类成为值类型。值类型是这样一种简单的数据类型:2个实例,只要内容相同,就该是可以相互替换的,是不是同一个实例,并不重要。String类就是一个标准的值类型的例子,只要字面值一样,我们就认为它们相等,而不关心它们是不是同一个String对象的不同引用。

大部分时间/日期类都应该是值类型的,java.time开发包印证了这一点。因此,我们没有理由去用==来判断2个LocalDate是不是相等,实际上,Javadoc也反对这样做。

对于值类型,想要更多了解的同学,可以参考我最近的文章,VALJOs:在Java中,它对值类型,定义了严格的规则集合,包括不可变性、工厂方法和良好定义的equals()hashCode()toString()compareTo()方法。

不同的历法系统

java.time包中,像所有主要的时间/日期类一样,LocalDate是固定于单个历法系统的:由ISO-8601标准定义。

ISO-8601历法系统是事实上的世界民用历法系统,也就是公历。平年有365天,闰年是366天。闰年的定义是:非世纪年,能被4整除;世纪年能被400整除。为了计算的一致性,公元1年的前一年被当做公元0年,以此类推。

采用这套历法,第一个影响就是,ISO-8601的日期不必跟GregorianCalendar一致。在GregorianCalendar中,凯撒历格里高利历之间有一个转换日,一般默认在1582年10月15日。那天之前,用凯撒历:每4年一个闰年,没有例外。那天之后,用格里高利历,也就是公历,拥有稍微复杂点的闰年计算方式。

既然凯撒历和格里高利历之间的转换是个历史事实,那为什么新的java.time开发包不参照它呢?原因就是,现在使用历史日期的大部分Java应用程序,都是不正确的,继续下去,是个错误。这是为什么呢?当年,罗马的梵蒂冈,把历法从凯撒历改换成格里高利历的时候,世界上大部分其他地区并没有更换历法。比如大英帝国,包括早期的美国,直到大约200后的1752年9月14日才换历法,沙俄直到1918年2月14日,而瑞典的历法转换更是一团糟。因此,实际上,对1918之前的日期,解释是相当多的;仅相信拥有单一转换日的GregorianCalendar,是不靠谱的。所以LocalDate中没有这种转换,就是一个合理的选择了。应用程序需要额外的上下文信息,才能在凯撒历和格里高利历间,精确的解释特定的历史日期。

第二个影响是,我们需要额外的一组类来帮助处理其他历法系统。Chronology接口,是其他历法的主要入口点,它允许通过所属的语言环境查找对应的历法系统。Java 8支持额外的4个历法系统:泰国佛教历,中华民国历,日本历(沿袭中国古代帝位纪年),伊斯兰历。如有需要,应用程序也可以实现自己的历法系统。

每个历法系统都有自己的日期类,有ThaiBuddhistDateMinguoDateJapaneseDateHijrahDate。它们应在对本地化有严重需求的应用中使用,比如为日本政府开发的系统。它们4个都继承了另外一个接口,ChronoLocalDate,可以让代码在不知道历法系统的情况下,去操作它们。虽然如此,但还是希望少用这个接口。

理解为什么少用ChronoLocalDate,对正确的使用整个java.time开发包很关键。真实情况是,当我们检视当前的应用,尽量以历法无关的方式来操作日期的大部分代码,都有问题。例如,你不能假定一年有12个月,而开发者都是这样认为的,并且增加12个月,他们就认为是增加了一年。你不能认为所有的月份都有相同的天数,比如,科普特人的历法,包括12个30天的月份,还有一个月仅有5天,或者6天。你也不能认为,下一年的年份就比现在的年份大了1年,比如日本历,在天皇换代时,会重新纪年,此时,还在那一年的年中(你甚至不能认为在同一个月的两天,是属于同一年的)。

在一个大型的系统中,唯一的以历法无关的方式开发的方法是:形成严格的代码审查制度,对日期和时间相关的每行代码都要做双重检查,以防偏向ISO历法系统。因此,推荐的做法是,在系统中,全部使用LocalDate,包括存储,操作和解释业务规则。仅有的,使用ChronoLocalDate的时候是,本地化的输入/输出,典型的做法是使用用户配置中首选的历法;即使如此,大多数应用并不需要那样的本地化级别。

如需更全面的了解,查看ChronoLocalDate的Javadoc

时间

日期之后,下一个考虑的概念就是本地时间,LocalTime。典型的例子就是便利店的营业时间,例如从07:00到23:00(早上7点到晚上11点)。可能,在这个时间段营业的便利店,遍布整个美利坚,但是这个时间是本地化的,跟时区无关。

LocalTime是值类型,且跟日期和时区没有关联。当我们对时间进行加减操作时,以午夜基准,24小时一个周期。因此,20:00加上6小时,结果就是02:00。

LocalTime的用法跟LocalDate相似:

LocalTime time = LocalTime.of(20, 30);
int hour = date.getHour(); // 20
int minute = date.getMinute(); // 30
time = time.withSecond(6); // 20:30:06
time = time.plusMinutes(3); // 20:33:06

修改器机制同样适用于LocalTime,只是对它的复杂操作比较少。

时间和日期组合

下一个要考察的是LocalDateTime类。这个值类型只是LocalDateLocalTime的简单组合。它表示一个跟时区无关的日期和时间。

LocalDateTime可以直接创建,或者组合时间和日期:

LocalDateTime dt1 = LocalDateTime.of(2014, Month.JUNE, 10, 20, 30);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(20, 30);
LocalDateTime dt4 = date.atTime(time);

第三和第四行使用atTime()方法,平滑地构造一个LocalDateTime实例。大部分的时间和日期类都有“at“方法:以这样的方式,把当前对象和其他对象组合,生成更复杂的对象。

LocalDateTime的其他方法跟LocalDateLocalTime相似。这种相似的方法模式非常有利于API的学习。下面总结了用到的方法前缀:

  • of: 静态工厂方法,从组成部分中创建实例
  • from: 静态工厂方法,尝试从相似对象中提取实例。from()方法没有of()方法类型安全
  • now: 静态工厂方法,用当前时间创建实例
  • parse: 静态工厂方法,总字符串解析得到对象实例
  • get: 获取时间日期对象的部分状态
  • is: 检查关于时间日期对象的描述是否正确
  • with: 返回一个部分状态改变了的时间日期对象拷贝
  • plus: 返回一个时间增加了的、时间日期对象拷贝
  • minus: 返回一个时间减少了的、时间日期对象拷贝
  • to: 把当前时间日期对象转换成另外一个,可能会损失部分状态
  • at: 用当前时间日期对象组合另外一个,创建一个更大或更复杂的时间日期对象
  • format: 提供格式化时间日期对象的能力

时间点

在处理时间和日期的时候,我们通常会想到年,月,日,时,分,秒。然而,这只是时间的一个模型,是面向人类的。第二种通用模型是面向机器的,或者说是连续的。在此模型中,时间线中的一个点表示为一个很大的数。这有利于计算机处理。在UNIX中,这个数从1970年开始,以秒为的单位;同样的,在Java中,也是从1970年开始,但以毫秒为单位。

java.time包通过值类型Instant提供机器视图。Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。因为java.time包是基于纳秒计算的,所以Instant的精度可以达到纳秒级。

Instant start = Instant.now();
// perform some calculation
Instant end = Instant.now();
assert end.isAfter(start);

Instant典型的用法是,当你需要记录事件的发生时间,而不需要记录任何有关时区信息时,存储和比较时间戳。它额很多有趣的地方在于,你不能对它做什么,而不是你能做什么。例如,下面的几行代码会抛出异常:

instant.get(ChronoField.MONTH_OF_YEAR);
instant.plus(6, ChronoUnit.YEARS);

抛出这些异常是因为,Instant只包含秒数和纳秒数,不提供处理人类意义上的时间单位。如果确实有需要,你需要额外提供时区信息。

时区

时区的概念由大英帝国开始采用。铁路的发明和通讯工具的改进,突然意味着人们的活动范围,大到跟太阳时的改变有很大的关系。在此之前,每个城镇和村庄,都通过太阳和日晷规定自己的时间。

下面的英国布鲁斯托交易所的时钟照片,显示了时区导致的最初混乱的一个例子。红色的指针显示格林威治时间,而黑色指针显示布鲁斯托时间,它们相差10分钟。

 

在技术的推动下,标准的时区系统慢慢演进,最终替代了老旧的本地太阳法计时。然而,关键的事实是,时区也是政治的产物。它们被用来显示对一个地区的政治控制,例如,最近的克里米亚时改为莫斯科时。一旦和政治挂钩,相关的规则常常不合逻辑。

时区规则,由一个发布IANA时区数据库的国际组织搜集和汇总。这些数据,包含地球上每个地区的标识和时区的历史变化。标识的格式类似”欧洲/伦敦“,或者”美洲/纽约“。

java.time之前,我们用TimeZone表示时区,而现在,用ZoneId。它们有2个主要的不同。第一,ZoneId是不可变的,它里面保存时区缩写的静态变量也是不可变的;第二,实际的规则集在ZoneRules里,不在ZoneId中,通过getRules()方法可以获得。

时区的常见情况,是从UTC/格林威治开始的一个固定偏移。我们通常在说时差的时候,会遇到它,例如,我们说纽约比伦敦晚5个小时。ZoneId的子类,ZoneOffset,代表了这种从伦敦格林威治零度子午线开始的时间偏移。

作为一个开发者,如果不用去处理时区和它带来的复杂性,将会是非常棒的。java.time开发包尽最大努力的帮助你那样做。只要有可能,尽量使用LocalDateLocalTimeLocalDateInstant。当你不能回避时区时,ZonedDateTime可以满足你的需求。

ZoneDateTime负责处理面向人类的(台历和挂钟上看到的)时间和面向机器的时间(始终连续增长的秒数)之间的转换。因此,你可以通过本地时间或时间点来创建ZoneDateTime实例:

ZoneId zone = ZoneId.of("Europe/Paris");
 
LocalDate date = LocalDate.of(2014, Month.JUNE, 10);
ZonedDateTime zdt1 = date.atStartOfDay(zone);
 
Instant instant = Instant.now();
ZonedDateTime zdt2 = instant.atZone(zone);

最恼人的时区问题之一就是夏令时。在夏令时中,从格林威治的偏移每年要调整两次(也许更多);典型的做法是,春天调快时间,秋天再调回来。夏令时开始时,我们都需要手动调整家里的挂钟时间。这些调整,在java.time包中,叫偏移过渡。春天时,跟本地时间相比,缺了一段时间;相反,秋天时,有的时间会出现两次。

ZonedDateTime在它的工厂方法和控制方法中处理了这些。例如,在夏令时切换的那天,增加一天会增加逻辑上的一天:可能多于24小时,也可能少于24小时。同样的,方法atStartOfDay()之所以这样命名,是因为你不能假定它的处理结果就一定是午夜零点,夏令时开始的那天,一天是从午夜1点开始的。

下面是关于夏令时的最后一个小提示。如果你想证明,在夏令时结束那天的重叠时段,你有考虑过什么情况会发生,你可以用这两个专门处理重叠时段的方法之一:

zdt = zdt.withEarlierOffsetAtOverlap();
zdt = zdt.withLaterOffsetAtOverlap();

处于时间重叠时段时,使用这两个方法之一,你可以得到调整之前或调整之后的时间。在其他情况下,这两个方法是无效的。

时间长度

到目前为止,我们讨论的时间/日期类以多种不同的方式表示时间线上的一个点。java.time还为时间长度额外提供了两个值类型。

Duration表示以秒和纳秒为基准的时长。例如,“23.6秒”。

Period表示以年、月、日衡量的时长。例如,“3年2个月零6天”。

它们可以作为参数,传给主要的时间/日期类的增加或减少时间的方法:

Period sixMonths = Period.ofMonths(6);
LocalDate date = LocalDate.now();
LocalDate future = date.plus(sixMonths);

解析和格式化

java.time.format包是专门用来格式化输出时间/日期的。这个包围绕DateTimeFormatter类和它的辅助创建类DateTimeFormatterBuilder展开。

静态方法加上DateTimeFormatter中的常量,是最通用的创建格式化器的方式。包括:

  • 常用ISO格式常量,如ISO_LOCAL_DATE
  • 字母模式,如ofPattern(“dd/MM/uuuu”)
  • 本地化样式,如ofLocalizedDate(FormatStyle.MEDIUM)

很典型的,一旦有了格式化器,你可以把它传递给主要的时间/日期类的相关方法:

DateTimeFormatter f = DateTimeFormatter.ofPattern("dd/MM/uuuu");
LocalDate date = LocalDate.parse("24/06/2014", f);
String str = date.format(f);

这把你从格式化器自己的格式化和解析方法中隔离开来。

如果你想控制格式化的语言环境,调用格式化器的withLocale(Locale)方法。相似的方式可以允许你控制格式化的历法系统、时区、十进制数和解析度。

如果你需要更多的控制权,查看DateTimeFormatterBuilder类吧,它允许你一步一步的构造更复杂的格式化器。它还提供大小写不敏感的解析,松散的解析,字符填充和可选的格式。

总结

Java 8中的java.time是一个新的、复杂的时间/日期API。它把Joda-Time中的设计思想和实现推向了更高的层次,让开发人员把java.util.DateCalendar抛在了身后。是时候重新享受时间/日期编程的乐趣了。

 

示例1 如何 在Java 8中获取当天的日期

Java 8中有一个叫LocalDate的类,它能用来表示今天的日期。这个类与java.util.Date略有不同,因为它只包含日期,没有时间。因此,如果你只需要表示日期而不包含时间,就可以使用它。

LocalDate today = LocalDate.now();
System.out.println("Today's Local date : " + today);


Output
Today's Local date : 2014-01-14
示例2 如何在Java 8中获取当前的年月日

LocalDate类中提供了一些很方便的方法可以用于提取出年月日以及其它的日期属性。使用这些方法,你可以获取到任何你所需要的日期属性,而不再需要使用java.util.Calendar这样的类了:

LocalDate today = LocalDate.now();
int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();
System.out.printf("Year : %d Month : %d day : %d \t %n", year, month, day);
 

Output
Today's Local date : 2014-01-14
Year : 2014 Month : 1 day : 14

可以看到,在Java 8中获取年月信息非常简单,只需使用对应的getter方法就好了,无需记忆,非常直观。你可以拿它和Java中老的获取当前年月日的写法进行一下比较。

示例3 在Java 8中如何获取某个特定的日期

在第一个例子中,我们看到通过静态方法now()来生成当天日期是非常简单的,不过通过另一个十分有用的工厂方法LocalDate.of(),则可以创建出任意一个日期,它接受年月日的参数,然后返回一个等价的LocalDate实例。关于这个方法还有一个好消息就是它没有再犯之前API中的错,比方说,年只能从1900年开始,月必须从0开始,等等。这里的日期你写什么就是什么,比如说,下面这个例子中它代表的就是1月14日,没有什么隐藏逻辑。

LocalDate dateOfBirth = LocalDate.of(2010, 01, 14);
System.out.println("Your Date of birth is : " + dateOfBirth);
 

Output : Your Date of birth is : 2010-01-14

可以看出,创建出来的日期就是我们所写的那样,2014年1月14日。

示例4 在Java 8中如何检查两个日期是否相等

如果说起现实中实际的处理时间及日期的任务,有一个常见的就是要检查两个日期是否相等。你可能经常会碰到要判断今天是不是某个特殊的日子,比如生日啊,周年纪念日啊,或者假期之类。有的时候,会给你一个日期,让你检查它是不是某个日子比方说假日。下面这个例子将会帮助你在Java 8中完成这类任务。正如你所想的那样,LocalDate重写了equals方法来进行日期的比较,如下所示:

LocalDate date1 = LocalDate.of(2014, 01, 14); if(date1.equals(today)){

    System.out.printf("Today %s and date1 %s are same date %n", today, date1);

}

 

Output
today 2014-01-14 and date1 2014-01-14 are same date

在本例中我们比较的两个日期是相等的。同时,如果在代码中你拿到了一个格式化好的日期串,你得先将它解析成日期然后才能比较。你可以将这个例子与Java之前比较日期的方式进行下比较,你会发现它真是爽多了。

示例5 在Java 8中如何检查重复事件,比如说生日

在Java中还有一个与时间日期相关的实际任务就是检查重复事件,比如说每月的帐单日,结婚纪念日,每月还款日或者是每年交保险费的日子。如果你在一家电商公司工作的话,那么肯定会有这么一个模块,会去给用户发送生日祝福并且在每一个重要的假日给他们捎去问候,比如说圣诞节,感恩节,在印度则可能是万灯节(Deepawali)。如何在Java中判断是否是某个节日或者重复事件?使用MonthDay类。这个类由月日组合,不包含年信息,也就是说你可以用它来代表每年重复出现的一些日子。当然也有一些别的组合,比如说YearMonth类。它和新的时间日期库中的其它类一样也都是不可变线程安全的,并且它还是一个值类(value class)。我们通过一个例子来看下如何使用MonthDay来检查某个重复的日期:

LocalDate dateOfBirth = LocalDate.of(2010, 01, 14);
MonthDay birthday = MonthDay.of(dateOfBirth.getMonth(), dateOfBirth.getDayOfMonth());
MonthDay currentMonthDay = MonthDay.from(today);
if(currentMonthDay.equals(birthday)){
    System.out.println("Many Many happy returns of the day !!");
}else{
    System.out.println("Sorry, today is not your birthday");
}
     

Output: Many Many happy returns of the day !!

虽然年不同,但今天就是生日的那天,所以在输出那里你会看到一条生日祝福。你可以调整下系统的时间再运行下这个程序看看它是否能提醒你下一个生日是什么时候,你还可以试着用你的下一个生日来编写一个JUnit单元测试看看代码能否正确运行。

示例6 如何在Java 8中获取当前时间

这与第一个例子中获取当前日期非常相似。这次我们用的是一个叫LocalTime的类,它是没有日期的时间,与LocalDate是近亲。这里你也可以用静态工厂方法now()来获取当前时间。默认的格式是hh:mm:ss:nnn,这里的nnn是纳秒。可以和Java 8以前如何获取当前时间做一下比较。

LocalTime time = LocalTime.now(); System.out.println("local time now : " + time);
 
Output
local time now : 16:33:33.369 // in hour, minutes, seconds, nano seconds

可以看到,当前时间是不包含日期的,因为LocalTime只有时间,没有日期。

示例7 如何增加时间里面的小时数

很多时候我们需要增加小时,分或者秒来计算出将来的时间。Java 8不仅提供了不可变且线程安全的类,它还提供了一些更方便的方法譬如plusHours()来替换原来的add()方法。顺便说一下,这些方法返回的是一个新的LocalTime实例的引用,因为LocalTime是不可变的,可别忘了存储好这个新的引用。

LocalTime time = LocalTime.now();
LocalTime newTime = time.plusHours(2); // adding two hours
System.out.println("Time after 2 hours : " + newTime);
 
Output :
Time after 2 hours : 18:33:33.369

可以看到当前时间2小时后是16:33:33.369。现在你可以将它和Java中增加或者减少小时的老的方式进行下比较。一看便知哪种方式更好。

示例8 如何获取1周后的日期

这与前一个获取2小时后的时间的例子类似,这里我们将学会如何获取到1周后的日期。LocalDate是用来表示无时间的日期的,它有一个plus()方法可以用来增加日,星期,或者月,ChronoUnit则用来表示这个时间单位。由于LocalDate也是不可变的,因此任何修改操作都会返回一个新的实例,因此别忘了保存起来。

LocalDate nextWeek = today.plus(1, ChronoUnit.WEEKS);
System.out.println("Today is : " + today);
System.out.println("Date after 1 week : " + nextWeek);
 
 
Output:
Today is : 2014-01-14
Date after 1 week : 2014-01-21

可以看到7天也就是一周后的日期是什么。你可以用这个方法来增加一个月,一年,一小时,一分钟,甚至是十年,查看下Java API中的ChronoUnit类来获取更多选项。

示例9 一年前后的日期

这是上个例子的续集。上例中,我们学习了如何使用LocalDate的plus()方法来给日期增加日,周或者月,现在我们来学习下如何用minus()方法来找出一年前的那天。

LocalDate previousYear = today.minus(1, ChronoUnit.YEARS);
System.out.println("Date before 1 year : " + previousYear);
LocalDate nextYear = today.plus(1, YEARS);
System.out.println("Date after 1 year : " + nextYear);
 
Output:
Date before 1 year : 2013-01-14
Date after 1 year : 2015-01-14

可以看到现在一共有两年,一个是2013年,一个是2015年,分别是2014的前后那年。

示例10 在Java 8中使用时钟

Java 8中自带了一个Clock类,你可以用它来获取某个时区下当前的瞬时时间,日期或者时间。可以用Clock来替代System.currentTimeInMillis()与 TimeZone.getDefault()方法。

// Returns the current time based on your system clock and set to UTC.
Clock clock = Clock.systemUTC();
System.out.println("Clock : " + clock);
 
// Returns time based on system clock zone Clock defaultClock =
Clock.systemDefaultZone();
System.out.println("Clock : " + clock);
 
Output:
Clock : SystemClock[Z]
Clock : SystemClock[Z]

你可以用指定的日期来和这个时钟进行比较,比如下面这样:

public class MyClass {
    private Clock clock; // dependency inject ...
     
    public void process(LocalDate eventDate) {
     
        if(eventDate.isBefore(LocalDate.now(clock)) {
            ...
        }
    }
}

如果你需要对不同时区的日期进行处理的话这是相当方便的。

示例11 在Java中如何判断某个日期是在另一个日期的前面还是后面

这也是实际项目中常见的一个任务。你怎么判断某个日期是在另一个日期的前面还是后面,或者正好相等呢?在Java 8中,LocalDate类有一个isBefore()和isAfter()方法可以用来比较两个日期。如果调用方法的那个日期比给定的日期要早的话,isBefore()方法会返回true。

LocalDate tomorrow = LocalDate.of(2014, 1, 15); 、if(tommorow.isAfter(today)){
    System.out.println("Tomorrow comes after today");
}
LocalDate yesterday = today.minus(1, DAYS);
if(yesterday.isBefore(today)){
    System.out.println("Yesterday is day before today");
}
 
Output:
Tomorrow comes after today
Yesterday is day before today

可以看到在Java 8中进行日期比较非常简单。不需要再用像Calendar这样的另一个类来完成类似的任务了。

示例12 在Java 8中处理不同的时区

Java 8不仅将日期和时间进行了分离,同时还有时区。现在已经有好几组与时区相关的类了,比如ZonId代表的是某个特定的时区,而ZonedDateTime代表的是带时区的时间。它等同于Java 8以前的GregorianCalendar类。使用这个类,你可以将本地时间转换成另一个时区中的对应时间,比如下面这个例子:

// Date and time with timezone in Java 8 ZoneId america = ZoneId.of("America/New_York"); 
LocalDateTime localtDateAndTime = LocalDateTime.now(); 
ZonedDateTime dateAndTimeInNewYork = ZonedDateTime.of(localtDateAndTime, america ); 
System.out.println("Current date and time in a particular timezone : " + dateAndTimeInNewYork); 

Output : 
Current date and time in a particular timezone : 2014-01-14T16:33:33.373-05:00[America/New_York]

可以拿它跟之前将本地时间转换成GMT时间的方式进行下比较。顺便说一下,正如Java 8以前那样,对应时区的那个文本可别弄错了,否则你会碰到这么一个异常:

Exception in thread "main" java.time.zone.ZoneRulesException: Unknown time-zone ID: ASIA/Tokyo
        at java.time.zone.ZoneRulesProvider.getProvider(ZoneRulesProvider.java:272)
        at java.time.zone.ZoneRulesProvider.getRules(ZoneRulesProvider.java:227)
        at java.time.ZoneRegion.ofId(ZoneRegion.java:120)
        at java.time.ZoneId.of(ZoneId.java:403)
        at java.time.ZoneId.of(ZoneId.java:351)  
示例13 如何表示固定的日期,比如信用卡过期时间

正如MonthDay表示的是某个重复出现的日子的,YearMonth又是另一个组合,它代表的是像信用卡还款日,定期存款到期日,options到期日这类的日期。你可以用这个类来找出那个月有多少天,lengthOfMonth()这个方法返回的是这个YearMonth实例有多少天,这对于检查2月到底是28天还是29天可是非常有用的。

YearMonth currentYearMonth = YearMonth.now(); System.out.printf("Days in month year %s: %d%n", currentYearMonth, currentYearMonth.lengthOfMonth()); 
YearMonth creditCardExpiry = YearMonth.of(2018, Month.FEBRUARY); 
System.out.printf("Your credit card expires on %s %n", creditCardExpiry); 

Output: 
Days in month year 2014-01: 31 
Your credit card expires on 2018-02   
示例14 如何在Java 8中检查闰年

这并没什么复杂的,LocalDate类有一个isLeapYear()的方法能够返回当前LocalDate对应的那年是否是闰年。如果你还想重复造轮子的话,可以看下这段代码,这是纯用Java编写的判断某年是否是闰年的逻辑

if(today.isLeapYear()){ 
	System.out.println("This year is Leap year"); 
}else { 
	System.out.println("2014 is not a Leap year"); 
} 


Output: 2014 is not a Leap year

你可以多检查几年看看结果是否正确,最好写一个单元测试来对正常年份和闰年进行下测试。

示例15 两个日期之间包含多少天,多少个月

还有一个常见的任务就是计算两个给定的日期之间包含多少天,多少周或者多少年。你可以用java.time.Period类来完成这个功能。在下面这个例子中,我们将计算当前日期与将来的一个日期之前一共隔着几个月。

LocalDate java8Release = LocalDate.of(2014, Month.MARCH, 14); 
Period periodToNextJavaRelease = 
Period.between(today, java8Release); 
System.out.println("Months left between today and Java 8 release : " + periodToNextJavaRelease.getMonths() ); 

Output: 
Months left between today and Java 8 release : 2

可以看到,本月是1月,而Java 8的发布日期是3月,因此中间隔着2个月。

示例16 带时区偏移量的日期与时间

在Java 8里面,你可以用ZoneOffset类来代表某个时区,比如印度是GMT或者UTC5:30,你可以使用它的静态方法ZoneOffset.of()方法来获取对应的时区。只要获取到了这个偏移量,你就可以拿LocalDateTime和这个偏移量创建出一个OffsetDateTime。

LocalDateTime datetime = LocalDateTime.of(2014, Month.JANUARY, 14, 19, 30); 
ZoneOffset offset = ZoneOffset.of("+05:30"); 
OffsetDateTime date = OffsetDateTime.of(datetime, offset); 
System.out.println("Date and Time with timezone offset in Java : " + date); 

Output : 
Date and Time with timezone offset in Java : 2014-01-14T19:30+05:30

可以看到现在时间日期与时区是关联上了。还有一点就是,OffSetDateTime主要是给机器来理解的,如果是给人看的,可以使用ZoneDateTime类。

示例17 在Java 8中如何获取当前时间戳

如果你还记得在Java 8前是如何获取当前时间戳的,那现在这简直就是小菜一碟了。Instant类有一个静态的工厂方法now()可以返回当前时间戳,如下:

Instant timestamp = Instant.now(); 
System.out.println("What is value of this instant " + timestamp); 

Output : 
What is value of this instant 2014-01-14T08:33:33.379Z

可以看出,当前时间戳是包含日期与时间的,与java.util.Date很类似,事实上Instant就是Java 8前的Date,你可以使用这两个类中的方法来在这两个类型之间进行转换,比如Date.from(Instant)是用来将Instant转换成java.util.Date的,而Date.toInstant()是将Date转换成Instant的。

示例18 如何在Java 8中使用预定义的格式器来对日期进行解析/格式化

在Java 8之前,时间日期的格式化可是个技术活,我们的好伙伴SimpleDateFormat并不是线程安全的,而如果用作本地变量来格式化的话又显得有些笨重。多亏了线程本地变量,这使得它在多线程环境下也算有了用武之地,但Java维持这一状态也有很长一段时间了。这次它引入了一个全新的线程安全的日期与时间格式器。它还自带了一些预定义好的格式器,包含了常用的日期格式。比如说,本例 中我们就用了预定义的BASIC_ISO_DATE格式,它会将2014年2月14日格式化成20140114。

String dayAfterTommorrow = "20140116"; 
LocalDate formatted = LocalDate.parse(dayAfterTommorrow, 
DateTimeFormatter.BASIC_ISO_DATE); 
System.out.printf("Date generated from String %s is %s %n", dayAfterTommorrow, formatted); 

Output : 
Date generated from String 20140116 is 2014-01-16

你可以看到生成的日期与指定字符串的值是匹配的,就是日期格式上略有不同。

示例19 如何在Java中使用自定义的格式器来解析日期

在上例中,我们使用了内建的时间日期格式器来解析日期字符串。当然了,预定义的格式器的确不错但有时候你可能还是需要使用自定义的日期格式,这个时候你就得自己去创建一个自定义的日期格式器实例了。下面这个例子中的日期格式是”MMM dd yyyy”。你可以给DateTimeFormatter的ofPattern静态方法()传入任何的模式,它会返回一个实例,这个模式的字面量与前例中是相同的。比如说M还是代表月,而m仍是分。无效的模式会抛出DateTimeParseException异常,但如果是逻辑上的错误比如说该用M的时候用成m,这样就没办法了。


String goodFriday = "Apr 18 2014"; 
try { 
	DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd yyyy"); 	
	LocalDate holiday = LocalDate.parse(goodFriday, formatter); 
	System.out.printf("Successfully parsed String %s, date is %s%n", goodFriday, holiday); 
} catch (DateTimeParseException ex) { 
	System.out.printf("%s is not parsable!%n", goodFriday); 
	ex.printStackTrace(); 
} 

Output : 
Successfully parsed String Apr 18 2014, date is 2014-04-18

可以看到日期的值与传入的字符串的确是相符的,只是格式不同。

示例20 如何在Java 8中对日期进行格式化,转换成字符串

在上两个例子中,尽管我们用到了DateTimeFormatter类但我们主要是进行日期字符串的解析。在这个例子中我们要做的事情正好相反。这里我们有一个LocalDateTime类的实例,我们要将它转换成一个格式化好的日期串。这是目前为止Java中将日期转换成字符串最简单便捷的方式了。下面这个例子将会返回一个格式化好的字符串。与前例相同的是,我们仍需使用指定的模式串去创建一个DateTimeFormatter类的实例,但调用的并不是LocalDate类的parse方法,而是它的format()方法。这个方法会返回一个代表当前日期的字符串,对应的模式就是传入的DateTimeFormatter实例中所定义好的。
 

LocalDateTime arrivalDate = LocalDateTime.now(); 
try { 
	DateTimeFormatter format = DateTimeFormatter.ofPattern("MMM dd yyyy hh:mm a"); 
	String landing = arrivalDate.format(format); 
	System.out.printf("Arriving at : %s %n", landing); 
	} catch (DateTimeException ex) { 
	System.out.printf("%s can't be formatted!%n", arrivalDate); 
	ex.printStackTrace(); 
} 

Output : Arriving at : Jan 14 2014 04:33 PM

可以看到,当前时间是用给定的”MMM dd yyyy hh:mm a”模式来表示的,它包含了三个字母表示的月份以及用AM及PM来表示的时间。

Java 8中日期与时间API的几个关键点

看完了这些例子后,我相信你已经对Java 8这套新的时间日期API有了一定的了解了。现在我们来回顾下关于这个新的API的一些关键的要素。

1. 它提供了javax.time.ZoneId用来处理时区。
2. 它提供了LocalDate与LocalTime类
3. Java 8中新的时间与日期API中的所有类都是不可变且线程安全的,这与之前的Date与Calendar API中的恰好相反,那里面像java.util.Date以及SimpleDateFormat这些关键的类都不是线程安全的。
4. 新的时间与日期API中很重要的一点是它定义清楚了基本的时间与日期的概念,比方说,瞬时时间,持续时间,日期,时间,时区以及时间段。它们都是基于ISO日历体系的。
5. 每个Java开发人员都应该至少了解这套新的API中的这五个类:
– Instant 它代表的是时间戳,比如2014-01-14T02:20:13.592Z,这可以从java.time.Clock类中获取,像这样:
Instant current = Clock.system(ZoneId.of(“Asia/Tokyo”)).instant();
– LocalDate 它表示的是不带时间的日期,比如2014-01-14。它可以用来存储生日,周年纪念日,入职日期等。
– LocalTime – 它表示的是不带日期的时间
– LocalDateTime – 它包含了时间与日期,不过没有带时区的偏移量
– ZonedDateTime – 这是一个带时区的完整时间,它根据UTC/格林威治时间来进行时区调整

6. 这个库的主包是java.time,里面包含了代表日期,时间,瞬时以及持续时间的类。它有两个子package,一个是java.time.foramt,这个是什么用途就很明显了,还有一个是java.time.temporal,它能从更低层面对各个字段进行访问。
7. 时区指的是地球上共享同一标准时间的地区。每个时区都有一个唯一标识符,同时还有一个地区/城市(Asia/Tokyo)的格式以及从格林威治时间开始的一个偏移时间。比如说,东京的偏移时间就是+09:00。
8. OffsetDateTime类实际上包含了LocalDateTime与ZoneOffset。它用来表示一个包含格林威治时间偏移量(+/-小时:分,比如+06:00或者 -08:00)的完整的日期(年月日)及时间(时分秒,纳秒)。
9. DateTimeFormatter类用于在Java中进行日期的格式化与解析。与SimpleDateFormat不同,它是不可变且线程安全的,如果需要的话,可以赋值给一个静态变量。DateTimeFormatter类提供了许多预定义的格式器,你也可以自定义自己想要的格式。当然了,根据约定,它还有一个parse()方法是用于将字符串转换成日期的,如果转换期间出现任何错误,它会抛出DateTimeParseException异常。类似的,DateFormatter类也有一个用于格式化日期的format()方法,它出错的话则会抛出DateTimeException异常。
10. 再说一句,“MMM d yyyy”与“MMm dd yyyy”这两个日期格式也略有不同,前者能识别出”Jan 2 2014″与”Jan 14 2014″这两个串,而后者如果传进来的是”Jan 2 2014″则会报错,因为它期望月份处传进来的是两个字符。为了解决这个问题,在天为个位数的情况下,你得在前面补0,比如”Jan 2 2014″应该改为”Jan 02 2014″。

关于Java 8这个新的时间日期API就讲到这了。这几个简短的示例 对于理解这套新的API中的一些新增类已经足够了。由于它是基于实际任务来讲解的,因此后面再遇到Java中要对时间与日期进行处理的工作时,就不用再四处寻找了。我们学习了如何创建与修改日期实例。我们还了解了纯日期,日期加时间,日期加时区的区别,知道如何比较两个日期,如何找到某天到指定日期比如说下一个生日,周年纪念日或者保险日还有多少天。我们还学习了如何在Java 8中用线程安全的方式对日期进行解析及格式化,而无需再使用线程本地变量或者第三方库这种取巧的方式。新的API能胜任任何与时间日期相关的任务。

附录:

其他的一些jdk8日期api示例:

	/**
	 * 判断两个日期相差多少天 (相同的2个日期相差0day) 如果start小于end,返回负数
	 */
	public Long diffDay(String start, String end) throws ParseException {
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
		// 反向解析时间
		Date startDate = simpleDateFormat.parse(start);
		// 转换成jdk8的日期时间格式
		LocalDate startLocalDate=startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
		// 反向解析时间
		Date endDate = simpleDateFormat.parse(end);
		// 转换成jdk8的日期时间格式
		LocalDate endLocalDate = endDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
		// 计算差值
		long dayDiff = (int) (endLocalDate.toEpochDay() - startLocalDate.toEpochDay());
		// 计算完毕
		return dayDiff;
	}

 

转载于:https://my.oschina.net/u/2338224/blog/1528751

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值