上篇文章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。
下面用代码来说明上述方法的使用:
public static void main(String[] args) {
System.out.println(Month.DECEMBER); // DECEMBER
System.out.println(Month.of(2)); // FEBRUARY
Month month = Month.FEBRUARY;
System.out.println(month.getValue()); // 2
System.out.println(month.minus(3)); // NOVEMBER
System.out.println(month.plus(2)); // APRIL
System.out.println(month.length(false)); // 28
System.out.println(month.length(true)); // 29
}
有时候我们希望返回月份是中文,而不是英文。毕竟程序员大多都比较懒,能少转化一次自然是很好的。又或者你需要显示的是月份的英文缩写?Java 8都为你想到了。只要调用getDisplayName(TextStyle, Locale)
方法就行,该方法第一个参数是文本类型,也就是说你想显示完整的名称还是缩写;第二个参数表示地区,如果没有特殊要求,传入Locale.getDefault()
就行。就像下面的代码演示的那样:
public static void main(String[] args) {
Month month = Month.APRIL;
System.out.println(month.getDisplayName(TextStyle.FULL, Locale.getDefault())); // 四月
System.out.println(month.getDisplayName(TextStyle.SHORT, Locale.getDefault())); // 四月
System.out.println(month.getDisplayName(TextStyle.NARROW, Locale.getDefault())); // 4
System.out.println(month.getDisplayName(TextStyle.FULL, Locale.ENGLISH)); // April
System.out.println(month.getDisplayName(TextStyle.SHORT, Locale.ENGLISH)); // Apr
System.out.println(month.getDisplayName(TextStyle.NARROW, Locale.ENGLISH)); // A
}
DayOfWeek
DayOfWeek枚举类用来表示一个周的七天。常用的方法和Month
枚举类的几乎一致,包括of(int dayOfWeek)
静态方法用于创建DayOfWeek
对象;getValue()
方法用来获取该对象的值;plus(long days)
和minus(long days)
方法用来进行加减法计算。也可以使用getDisplayName(TextStyle style, Locale locale)
来格式化输出。代码演示如下:
public static void main(String[] args) {
System.out.println(DayOfWeek.FRIDAY); // FRIDAY
System.out.println(DayOfWeek.of(7)); // SUNDAY
DayOfWeek dayOfWeek = DayOfWeek.TUESDAY;
System.out.println(dayOfWeek.getValue()); // 2
System.out.println(dayOfWeek.plus(3)); // FRIDAY
System.out.println(dayOfWeek.minus(2)); // SUNDAY
Locale defaultLocal = Locale.getDefault();
System.out.println(dayOfWeek.getDisplayName(TextStyle.FULL, defaultLocal)); // 星期二
System.out.println(dayOfWeek.getDisplayName(TextStyle.SHORT, defaultLocal)); // 星期二
System.out.println(dayOfWeek.getDisplayName(TextStyle.NARROW, defaultLocal)); // 二
Locale locale = Locale.ENGLISH;
System.out.println(dayOfWeek.getDisplayName(TextStyle.FULL, locale)); // Tuesday
System.out.println(dayOfWeek.getDisplayName(TextStyle.SHORT, locale)); // Tue
System.out.println(dayOfWeek.getDisplayName(TextStyle.NARROW, locale)); // T
}
但是呢,在DayOfWeek
枚举类中,是没有maxLength(), minLength()和length(boolean leapYear)
这三个方法的,相信你们也知道是为什么。
最后说一句,由于Month
和DayOfWeek
只是枚举类,它们并不持有当前时间信息,所以就别妄想使用这两个枚举类来解决"今天是星期几","明天是几号"等问题了。
源码中的加减法计算
刚开始学Java的时候,计算月份/星期几乎是必备作业,不过当时用的是Date/Calendar类来计算,相当麻烦,Java 8使用枚举来表示月份和星期之后,进行相应的加减法计算就变的相对简单了,我们可以看一下是怎么实现的。
由于月份的计算和星期的计算原理是一样的,我们就只看Month
的加减法计算。
private static final Month[] ENUMS = Month.values();
public Month plus(long months) {
int amount = (int) (months % 12);
return ENUMS[(ordinal() + (amount + 12)) % 12];
}
public Month minus(long months) {
return plus(-(months % 12));
}
这里的处理方法很巧妙,减法直接调用加法的处理逻辑,当年我就没想到过,哈哈,值得学习。
LocalDate和LocalTime
重头戏来了,现在开始隆重介绍Java 8的常用的时间日期类:LocalDate
和LocalTime
。使用LocalDate
可以获取当前日期(注意只是日期,不包含时间),并可以进行相应处理。使用LocalTime
可以获取当前时间(注意只是时间,不包含日期)并进行相应处理。这样就更好的符合“单一职责原则”。
构造方法
根据不同的需求,提供了不同的创建方式,主要包括两个静态方法now()
和of()
方法。其实,在后面我们会看到,在Java 8中,创建时间和日期几乎都会用的这两个方法。
public static void main(String[] args) {
LocalDate date1 = LocalDate.now();
LocalDate date2 = LocalDate.of(1998, 2, 4);
LocalDate date3 = LocalDate.ofEpochDay(180);
System.out.println(date1); // 2016-07-11
System.out.println(date2); // 1998-02-04
System.out.println(date3); // 1970-06-30
LocalTime time1 = LocalTime.now();
LocalTime time2 = LocalTime.now().withNano(0);
LocalTime time3 = LocalTime.of(12, 30);
LocalTime time4 = LocalTime.ofSecondOfDay(60 * 60 * 2);
System.out.println(time1); // 10:56:04.772
System.out.println(time2); // 10:56:04
System.out.println(time3); // 12:30
System.out.println(time4); // 02:00
}
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)
。
下面用代码演示几个方法:
public static void main(String[] args) {
LocalDate now = LocalDate.now();
System.out.println(now.getYear()); // 2016
System.out.println(now.getDayOfWeek()); // MONDAY
System.out.println(now.getDayOfMonth()); // 11
System.out.println(now.withMonth(3)); // 2016-03-11
System.out.println(now.minusWeeks(2)); // 2016-06-27
System.out.println(now.plusDays(10)); // 2016-07-21
LocalDate firstDayOfYear = LocalDate.of(2016,1,1);
System.out.println(now.compareTo(firstDayOfYear)); // 6
System.out.println(now.isAfter(firstDayOfYear)); // true
System.out.println(now.isEqual(firstDayOfYear)); // false
}
LocalTime
LocalDate
类中的方法和LocalDate
中的类似,同样可以分为:获取时间信息,修改时间信息,加减法运算和时间对象间的比较。方法的具体描述我就不写了。根据LocalDate
类中列举的常用方法,你也能猜得出在LocalTime
类中有哪些对应的常用方法。下面还是用代码演示几个方法:
public static void main(String[] args) {
LocalTime now = LocalTime.now();
System.out.println(now.getHour()); // 14
System.out.println(now.getMinute()); // 15
System.out.println(now.getSecond()); // 22
System.out.println(now.getNano()); // 881000000
System.out.println(now.withNano(0)); // 14:15:22
System.out.println(now.minusHours(3)); // 11:15:22.881
System.out.println(now.minusMinutes(15)); // 14:00:22.881
System.out.println(now.minusSeconds(20)); // 14:15:02.881
LocalTime halfOfDay = LocalTime.of(12 ,0);
System.out.println(now.compareTo(halfOfDay)); // 1
System.out.println(now.isAfter(halfOfDay)); // true
}
不过有几点需要说明:
-
LocalTime
中没有isEqual()
方法。 - 在
getNano()
中,nano指的是纳秒(毫微秒),1秒等于1亿纳秒。
LocalDateTime
或许有人觉得,将日期和时间分开处理有些不方便。我想将时间和日期一起处理怎么办?当然可以,Java 8中还提供了LocalDateTime
来满足你的这个需求。
构造方法
和前面的类似,可以使用静态方法now()
和静态方法of()
来创建一个LocalDateTime
对象。比如:
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
System.out.println(now); // 2016-07-11T14:27:20.169
LocalDateTime dateTime1 = LocalDateTime.of(1990, 1, 1, 12, 3);
LocalDateTime dateTime2 = LocalDateTime.of(2000, 2, 4, 8, 4, 20);
System.out.println(dateTime1); // 1990-01-01T12:03
System.out.println(dateTime2); // 2000-02-04T08:04:20
}
通常,你需要在of()
方法中传入6个参数,表示年月日时分秒。关于月份,既可以传入Month对象,也可以传入int值(当然1表示一月份)。也可以将秒这个参数省略了,传入5个参数。也可以增加一个纳秒参数,变为7个参数。
常用方法
这个不想再说了,和LocalDate
及LocalTime
类似。
和LocalDate、LocalTime之间的转化
LocalDateTime既然是“集LocalDate和LocalTime的大成者”,自然能将LocalDateTime转化位LocalDate或者LocalTime,而且方法很简单,只需要调用toLocalDate()
或者toLocalTime()
方法,就像下面演示的那样:
public static void main(String[] args) {
LocalDateTime dateTime = LocalDateTime.now();
System.out.println(dateTime); // 2016-07-11T16:57:41.217
LocalDate date = dateTime.toLocalDate();
LocalTime time = dateTime.toLocalTime();
System.out.println(date); // 2016-07-11
System.out.println(time); // 16:57:41.217
}
解析和格式化
在Date和Calendar统治的时代,想要格式化一个日期,只能用Date来格式化,并且SimpleDateFormat还有线程安全隐患,无疑很麻烦。而现在,在Java 8中,这些问题都不复存在了。
有一点需要再次强调,再Java 8中,时间日期的格式是按照ISO-8061的时间和日期标准来显示的。年份为4位数,月日时分秒都是2位数,不足两位用0补齐,日期之间需要用短横线连接,时间之间要用:
连接。必须按照此规则来进行解析,比如:
public static void main(String[] args) {
LocalDate date = LocalDate.parse("2016-09-08");
LocalTime time = LocalTime.parse("12:24:43");
System.out.println(date); // 2016-09-08
System.out.println(time); // 12:24:43
}
当然,Java是宽容的,如果你不按照ISO-8061
的格式传入,也有解决办法,这个可以使用parse(CharSequence text, DateTimeFormatter formatter)
这个方法,第二个参数传入你所想要的格式类型。
或者,你也可以使用如下的方法,来解析一个日期字符串
public static void main(String[] args) {
String input = "20160708";
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDate date = LocalDate.parse(input, formatter);
System.out.printf("%s%n", date); // 2016-07-08
}
然后,格式化一个时间日期对象也很简单,就想下面一样:
public static void main(String[] args) {
DateTimeFormatter format = DateTimeFormatter.ofPattern("MMM d yyyy hh:mm a");
LocalDateTime date = LocalDateTime.now();
String dateStr = date.format(format);
System.out.println(dateStr); // 七月 11 2016 05:54 下午
}
调节器(Temporal Adjuster)
如果说,新版的时间日期处理方法,和我们以前使用的Date和Calendar类有什么使用上的区别的话,最明显的使用区别就是调节器的使用了。调节器有什么用呢?比如要获取下周星期一的日期,用之前的方法不容易获得,而这时使用调节器就能轻松解决。
先看一下使用方法:
public static void main(String[] args) {
LocalDate date = LocalDate.now();
DayOfWeek dotw = date.getDayOfWeek();
System.out.printf("%s is on a %s%n", date, dotw); // 2016-07-11 is on a MONDAY
System.out.printf("Next Monday: %s%n",
date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)));// Next Monday: 2016-07-18
System.out.printf("First day of Month: %s%n",
date.with(TemporalAdjusters.firstDayOfMonth())); // First day of Month: 2016-07-01
System.out.printf("First Monday of Month: %s%n",
date.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)));// First Monday of Month: 2016-07-04
}
很简单,得到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号,如果实在下半月,则是该月的最后一天进货。如果进货的那天恰逢周六周日,则提前到该周周五进货(货车司机也要双休嘛),那么如何自定义一个调节器,计算下一次的进货时间呢?
我们可以使用下面的代码实现一个自定义调节器:
public class PurchaseAdjuster implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal input) {
LocalDate date = LocalDate.from(input);
int day;
if (date.getDayOfMonth() < 15) {
day = 15;
} else {
day = date.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
}
date = date.withDayOfMonth(day);
if (date.getDayOfWeek() == DayOfWeek.SATURDAY ||
date.getDayOfWeek() == DayOfWeek.SUNDAY) {
date = date.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
}
return input.with(date);
}
}
这里面使用到了LocalDate.from(TemporalAccessor temporal)
方法,该方法获取一个Temporal
对象,返回一个LocalDate
对象(LocalTime
和LocalDateTime
都有此静态方法),然后使用内置的TemporalAdjuster
静态工厂方法完成逻辑处理,最后返回修改之后的Temporal对象。
看到这里,得谈一谈Temporal这个接口了,
让我们看看效果如何:
public static void main(String[] args) {
LocalDate date = LocalDate.now();
LocalDate nextPayday = date.with(new PurchaseAdjuster());
System.out.println(nextPayday); // 2016-07-15
LocalDate customDate = LocalDate.now().withDayOfMonth(18);
LocalDate otherPayday = customDate.with(new PurchaseAdjuster());
System.out.println(otherPayday); // 2016-07-29
}
7月29日是星期五,嗯,看来效果不错哈?
最后,其实还有很多没说完
聊了这么多,但是仍然有很多没聊完。Java 8中对时间和日期的处理比较复杂,涉及的东西比较广泛,本篇只说了一些常用的类和方法,希望通过这些方法,让你能熟悉使用Java 8中的时间和日期的处理。如果需要更多的信息,可以去查阅官方文档。