上篇文章Java中的时间和日期(上)简单介绍了Java中的Date类,Calendar类以及用于格式化的SimpleDateFormater类。使用这些的时候我们会明显地感受到其中的不便之处,比如,Calendar类的月份是从0开始计数的;日期格式输出不够友好,很多情况下都需要使用SimpleDateFormater类来格式化;一些简单得日期计算也比较麻烦等等。所以就有了joda-time
这种第三方库来简化java对于时间和日期的操作。为了改变这种情况,java 8中对时间和日期对处理就吸收了joda-time
库的特性。那么新的时间日期处理会带来怎样的便捷呢?这便是本篇文章所要聊的内容。
月份和星期的枚举类
Month
在以前使用Java的时候,你一定痛恨了月份的表示和计算,最主要的原因就是因为一月份是从0开始计数的。而在Java 8中为了改变这一现状,增加了一个Month
枚举类来表示月份。使用这个枚举类甚至还可以直接进行月份的加减运算!
of(int month)
这是一个静态方法,用于创建一个Month
对象。传入的参数当然是从1开始计数啦,1表示一月,12表示十二月。当传入的参数小于1或者大于12时,就会抛出异常。getValue()
返回该Month
对象当前的值。一月份返回1,二月份返回2,依次类推。minus(long months)
这个是用来做月份的减法计算的。传入的参数表示你想在该Month
对象的基础上减去几个月。如果是1月份减去2个月,返回的当然是11月份。plus(long months)
用来计算月份的加法。传入的参数表示你想在该Month
对象的基础上增加几个月。比如12月加2个月就变成了二月份。maxLength(), minLength()和length(boolean leapYear)
用来获取Month
对象表示的该月的日期数。其中,length(boolean leapYear)
中的参数表示是否为闰年。其实这三个方法返回的结果在很多情况下都是一样的,返回的都是当月的日期数,30或者31。只有二月份除外,当Month
对象表示二月份时,maxLength()
和length(true)
返回29,minLength()
和length(false)
返回28。
下面用代码来说明上述方法的使用:
1 | public static void main(String[] args) { |
有时候我们希望返回月份是中文,而不是英文。毕竟程序员大多都比较懒,能少转化一次自然是很好的。又或者你需要显示的是月份的英文缩写?Java 8都为你想到了。只要调用getDisplayName(TextStyle, Locale)
方法就行,该方法第一个参数是文本类型,也就是说你想显示完整的名称还是缩写;第二个参数表示地区,如果没有特殊要求,传入Locale.getDefault()
就行。就像下面的代码演示的那样:
1 | public static void main(String[] args) { |
DayOfWeek
DayOfWeek枚举类用来表示一个周的七天。常用的方法和Month
枚举类的几乎一致,包括of(int dayOfWeek)
静态方法用于创建DayOfWeek
对象;getValue()
方法用来获取该对象的值;plus(long days)
和minus(long days)
方法用来进行加减法计算。也可以使用getDisplayName(TextStyle style, Locale locale)
来格式化输出。代码演示如下:
1 | public static void main(String[] args) { |
但是呢,在DayOfWeek
枚举类中,是没有maxLength(), minLength()和length(boolean leapYear)
这三个方法的,相信你们也知道是为什么。
最后说一句,由于Month
和DayOfWeek
只是枚举类,它们并不持有当前时间信息,所以就别妄想使用这两个枚举类来解决”今天是星期几”,”明天是几号”等问题了。
源码中的加减法计算
刚开始学Java的时候,计算月份/星期几乎是必备作业,不过当时用的是Date/Calendar类来计算,相当麻烦,Java 8使用枚举来表示月份和星期之后,进行相应的加减法计算就变的相对简单了,我们可以看一下是怎么实现的。
由于月份的计算和星期的计算原理是一样的,我们就只看Month
的加减法计算。
1 | private static final Month[] ENUMS = Month.values(); |
这里的处理方法很巧妙,减法直接调用加法的处理逻辑,当年我就没想到过,哈哈,值得学习。
LocalDate和LocalTime
重头戏来了,现在开始隆重介绍Java 8的常用的时间日期类:LocalDate
和LocalTime
。使用LocalDate
可以获取当前日期(注意只是日期,不包含时间),并可以进行相应处理。使用LocalTime
可以获取当前时间(注意只是时间,不包含日期)并进行相应处理。这样就更好的符合“单一职责原则”。
构造方法
根据不同的需求,提供了不同的创建方式,主要包括两个静态方法now()
和of()
方法。其实,在后面我们会看到,在Java 8中,创建时间和日期几乎都会用的这两个方法。
1 | public static void main(String[] args) { |
withNano()
方法会在后面提及,主要是修改当前对象表示的纳秒数的值。在上面的代码中,有几点需要注意的地方:
ofEpochDay(long epochDay)
方法中的参数,指的是距1970年1月1日那天的时间间隔。- 在Java 8中,时间和日期的格式是按照ISO-8061的时间和日期标准来显示的。年份为4位数,月日时分秒都是2位数,不足两位用0补齐。
常用方法
LocalDate
还记得之前说过的,DayOfWeek
枚举类不持有当前时间信息,所以你无法单独使用它来得到今天是星期几
这种信息。然而如果获取到了当前日期的LocalDate
对象后,问题就迎刃而解了。
LocalDate
提供了大量的方法来进行日期信息的获取和计算。有了这一个LocalDate
对象,你不仅可以知道这个对象是哪年几月几日星期几,还能够对于年月日进行加减法计算,甚至你可以以周为单位进行日期的加减法计算,比如,你可以轻松解决两个周前的今天是几月几日
这类问题。
下面,我就将常用的方法以表格的形式列举出来,注意列举的只是常用方法,并不是所有方法。想知道所有方法,请自行查阅API文档
方法名 | 返回值类型 | 对该方法的解释 |
---|---|---|
getYear() | int | 获取当前日期的年份 |
getMonth() | Month | 获取当前日期的月份对象 |
getMonthValue() | int | 获取当前日期是第几月 |
getDayOfWeek() | DayOfWeek | 表示该对象表示的日期是星期几 |
getDayOfMonth() | int | 表示该对象表示的日期是这个月第几天 |
getDayOfYear() | int | 表示该对象表示的日期是今年第几天 |
withYear(int year) | LocalDate | 修改当前对象的年份 |
withMonth(int month) | LocalDate | 修改当前对象的月份 |
withDayOfMonth(int dayOfMonth) | LocalDate | 修改当前对象在当月的日期 |
isLeapYear() | boolean | 是否是闰年 |
lengthOfMonth() | int | 这个月有多少天 |
lengthOfYear() | int | 该对象表示的年份有多少天(365或者366) |
plusYears(long yearsToAdd) | LocalDate | 当前对象增加指定的年份数 |
plusMonths(long monthsToAdd) | LocalDate | 当前对象增加指定的月份数 |
plusWeeks(long weeksToAdd) | LocalDate | 当前对象增加指定的周数 |
plusDays(long daysToAdd) | LocalDate | 当前对象增加指定的天数 |
minusYears(long yearsToSubtract) | LocalDate | 当前对象减去指定的年数 |
minusMonths(long monthsToSubtract) | LocalDate | 当前对象减去注定的月数 |
minusWeeks(long weeksToSubtract) | LocalDate | 当前对象减去指定的周数 |
minusDays(long daysToSubtract) | LocalDate | 当前对象减去指定的天数 |
compareTo(ChronoLocalDate other) | int | 比较当前对象和other对象在时间上的大小,返回值如果为正,则当前对象时间较晚, |
isBefore(ChronoLocalDate other) | boolean | 比较当前对象日期是否在other对象日期之前 |
isAfter(ChronoLocalDate other) | boolean | 比较当前对象日期是否在other对象日期之后 |
isEqual(ChronoLocalDate other) | boolean | 比较两个日期对象是否相等 |
列出这么多方法,不是要你死记硬背记住它们,而是要在脑海有个印象,知道有哪些常用方法,可以做什么。概括起来,LocalDate
类中常用的方法有四种:获取日期信息,修改日期信息,加减法运算和日期对象间的比较。记住了这些,以后在工作中就可以查阅使用,而不用自己在造一遍轮子。
有几点需要注意的地方:
- 上面列表里面有一个
ChronoLocalDate
,它是一个接口,LocalDate
类实现了这个接口,所以直接传一个LocalDate
类对象即可。 - isEqual(ChronoLocalDate other)这个方法,如果两个对象是同一个对象,或者这两个对象的值相等(同年同月同日),则返回true,否则返回false。
- 当一个方法返回的是
LocalDate
对象时,便可以使用链式调用
。举个例子,获取昨天的日期,我们可以直接这样写:LocalDate.now().minusDays(1)
。
下面用代码演示几个方法:
1 | public static void main(String[] args) { |
LocalTime
LocalDate
类中的方法和LocalDate
中的类似,同样可以分为:获取时间信息,修改时间信息,加减法运算和时间对象间的比较。方法的具体描述我就不写了。根据LocalDate
类中列举的常用方法,你也能猜得出在LocalTime
类中有哪些对应的常用方法。下面还是用代码演示几个方法:
1 | public static void main(String[] args) { |
不过有几点需要说明:
LocalTime
中没有isEqual()
方法。- 在
getNano()
中,nano指的是纳秒(毫微秒),1秒等于1亿纳秒。
LocalDateTime
或许有人觉得,将日期和时间分开处理有些不方便。我想将时间和日期一起处理怎么办?当然可以,Java 8中还提供了LocalDateTime
来满足你的这个需求。
构造方法
和前面的类似,可以使用静态方法now()
和静态方法of()
来创建一个LocalDateTime
对象。比如:
1 | public static void main(String[] args) { |
通常,你需要在of()
方法中传入6个参数,表示年月日时分秒。关于月份,既可以传入Month对象,也可以传入int值(当然1表示一月份)。也可以将秒这个参数省略了,传入5个参数。也可以增加一个纳秒参数,变为7个参数。
常用方法
这个不想再说了,和LocalDate
及LocalTime
类似。
和LocalDate、LocalTime之间的转化
LocalDateTime既然是“集LocalDate和LocalTime的大成者”,自然能将LocalDateTime转化位LocalDate或者LocalTime,而且方法很简单,只需要调用toLocalDate()
或者toLocalTime()
方法,就像下面演示的那样:
1 | public static void main(String[] args) { |
解析和格式化
在Date和Calendar统治的时代,想要格式化一个日期,只能用Date来格式化,并且SimpleDateFormat还有线程安全隐患,无疑很麻烦。而现在,在Java 8中,这些问题都不复存在了。
有一点需要再次强调,再Java 8中,时间日期的格式是按照ISO-8061的时间和日期标准来显示的。年份为4位数,月日时分秒都是2位数,不足两位用0补齐,日期之间需要用短横线连接,时间之间要用:
连接。必须按照此规则来进行解析,比如:
1 | public static void main(String[] args) { |
当然,Java是宽容的,如果你不按照ISO-8061
的格式传入,也有解决办法,这个可以使用parse(CharSequence text, DateTimeFormatter formatter)
这个方法,第二个参数传入你所想要的格式类型。
或者,你也可以使用如下的方法,来解析一个日期字符串
1 | public static void main(String[] args) { |
然后,格式化一个时间日期对象也很简单,就想下面一样:
1 | public static void main(String[] args) { |
调节器(Temporal Adjuster)
如果说,新版的时间日期处理方法,和我们以前使用的Date和Calendar类有什么使用上的区别的话,最明显的使用区别就是调节器的使用了。调节器有什么用呢?比如要获取下周星期一的日期,用之前的方法不容易获得,而这时使用调节器就能轻松解决。
先看一下使用方法:
1 | public static void main(String[] args) { |
很简单,得到LocalDate对象后,调用with()
方法,传入一个TemporalAdjusters对象即可。TemporalAdjusters类有许多静态工厂方法来创建该对象,比如:
- firstDayOfMonth()
- lastDayOfMonth()
- firstDayOfNextMonth()
- firstDayOfYear()
- lastDayOfYear()
- firstDayOfNextYear()
- firstInMonth(DayOfWeek dayOfWeek)
- lastInMonth(DayOfWeek dayOfWeek)
- dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek)
- next(DayOfWeek dayOfWeek)
- nextOrSame(DayOfWeek dayOfWeek)
- previous(DayOfWeek dayOfWeek)
- previousOrSame(DayOfWeek dayOfWeek)
这些方法都见名知意,但是有可能这些方法并不能满足你的需求,这时,就需要自定义TemporalAdjusters了。
自定义调节器
自定义一个调节器很简单,创建一个类,实现TemporalAdjuster
接口,重写adjustInto(Temporal input)
方法,将你需要的逻辑都在里面实现。
假设一个场景,一个商店每个月进货两次,上半月一次,下半月一次,上半月的那次进货是在15号,如果实在下半月,则是该月的最后一天进货。如果进货的那天恰逢周六周日,则提前到该周周五进货(货车司机也要双休嘛),那么如何自定义一个调节器,计算下一次的进货时间呢?
我们可以使用下面的代码实现一个自定义调节器:
1 | public class PurchaseAdjuster implements TemporalAdjuster { |
这里面使用到了LocalDate.from(TemporalAccessor temporal)
方法,该方法获取一个Temporal
对象,返回一个LocalDate
对象(LocalTime
和LocalDateTime
都有此静态方法),然后使用内置的TemporalAdjuster
静态工厂方法完成逻辑处理,最后返回修改之后的Temporal对象。
让我们看看效果如何:
1 | public static void main(String[] args) { |
7月29日是星期五,嗯,看来效果不错哈?
最后,其实还有很多没说完
聊了这么多,但是仍然有很多没聊完。Java 8中对时间和日期的处理比较复杂,涉及的东西比较广泛,本篇只说了一些常用的类和方法,希望通过这些方法,让你能熟悉使用Java 8中的时间和日期的处理。如果需要更多的信息,可以去查阅官方文档。