Java中的时间和日期(下)

上篇文章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
2
3
4
5
6
7
8
9
10
11
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()就行。就像下面的代码演示的那样:

1
2
3
4
5
6
7
8
9
10
11
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)来格式化输出。代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)这三个方法的,相信你们也知道是为什么。

最后说一句,由于MonthDayOfWeek只是枚举类,它们并不持有当前时间信息,所以就别妄想使用这两个枚举类来解决”今天是星期几”,”明天是几号”等问题了。

源码中的加减法计算

刚开始学Java的时候,计算月份/星期几乎是必备作业,不过当时用的是Date/Calendar类来计算,相当麻烦,Java 8使用枚举来表示月份和星期之后,进行相应的加减法计算就变的相对简单了,我们可以看一下是怎么实现的。

由于月份的计算和星期的计算原理是一样的,我们就只看Month的加减法计算。

1
2
3
4
5
6
7
8
9
10
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的常用的时间日期类:LocalDateLocalTime。使用LocalDate可以获取当前日期(注意只是日期,不包含时间),并可以进行相应处理。使用LocalTime可以获取当前时间(注意只是时间,不包含日期)并进行相应处理。这样就更好的符合“单一职责原则”。

构造方法

根据不同的需求,提供了不同的创建方式,主要包括两个静态方法now()of()方法。其实,在后面我们会看到,在Java 8中,创建时间和日期几乎都会用的这两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)

下面用代码演示几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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类中有哪些对应的常用方法。下面还是用代码演示几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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对象。比如:

1
2
3
4
5
6
7
8
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个参数。

常用方法

这个不想再说了,和LocalDateLocalTime类似。

和LocalDate、LocalTime之间的转化

LocalDateTime既然是“集LocalDate和LocalTime的大成者”,自然能将LocalDateTime转化位LocalDate或者LocalTime,而且方法很简单,只需要调用toLocalDate()或者toLocalTime()方法,就像下面演示的那样:

1
2
3
4
5
6
7
8
9
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补齐,日期之间需要用短横线连接,时间之间要用:连接。必须按照此规则来进行解析,比如:

1
2
3
4
5
6
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)这个方法,第二个参数传入你所想要的格式类型。

或者,你也可以使用如下的方法,来解析一个日期字符串

1
2
3
4
5
6
7
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
}

然后,格式化一个时间日期对象也很简单,就想下面一样:

1
2
3
4
5
6
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类有什么使用上的区别的话,最明显的使用区别就是调节器的使用了。调节器有什么用呢?比如要获取下周星期一的日期,用之前的方法不容易获得,而这时使用调节器就能轻松解决。

先看一下使用方法:

1
2
3
4
5
6
7
8
9
10
11
 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号,如果实在下半月,则是该月的最后一天进货。如果进货的那天恰逢周六周日,则提前到该周周五进货(货车司机也要双休嘛),那么如何自定义一个调节器,计算下一次的进货时间呢?

我们可以使用下面的代码实现一个自定义调节器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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对象(LocalTimeLocalDateTime都有此静态方法),然后使用内置的TemporalAdjuster静态工厂方法完成逻辑处理,最后返回修改之后的Temporal对象。

让我们看看效果如何:

1
2
3
4
5
6
7
8
9
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中的时间和日期的处理。如果需要更多的信息,可以去查阅官方文档。

如果觉得文章对你有帮助,请我喝杯可乐吧