Java 的时间类


此篇是十月第一周的内容,拖延症太严重了,不得已鸽掉了九月。

分四个部分讲,分别是 Java 基础时间类、Java sql 时间类、Joda Time 时间类、 java.time 时间类。

既然本篇主讲时间,就顺便把我的博文更新的周数算法讲一下:每月1日所在的周,是本月的第一周,如果这周的前几天属于上一个月,则这几天归入到本月所在周中,例如 2019 年 10 月 1 日是周二,则该周是 10 月的第一周,同时 9 月 30 日也算作 10 月的第一周,而不属于 9 月的第六周。这种周数算法的原因在于,我喜欢一点点任务透支的感觉,以及重新开始的感觉。本周数算法从 10 月起正式生效,之前的博文既往不咎。


Java 基础时间类

Java 的基础时间类有三个,分别是 java.util 包下的 Date 类和 Calendar 类,以及 java.text 包下的 SimpleDateFormat 类。

简单提两嘴这两个包,java.util 包是 Java 的实用工具类库包,包含 Java 的集合类、时间类、事件模型类等;java.text 包是跟文本、格式化打交道的包,比如处理时间、数字等。这两个包都是 Java 的上古基础包。


Date

Date 类是 Java 里时间类中的老爹,它出场早,脾气倔,在保留着日期和时间最基本用法的同时,总能用其偏执的使用思路让人觉得拧巴。但是在程序员懒得找其他替代品时,它又几乎是第一选择,因为它的确是足够基础,当然了,使用时还要容忍它的反人类。

Date 类有两种构造函数,一种是获取当前时间,一种是根据你的输入时间来实例化时间。

1
2
3
4
5
6
7
8
9
// 当前时间
public Date() {
this(System.currentTimeMillis());
}

// 指定时间
public Date(long var1) {
this.fastTime = var1;
}

Date 类有很多方法,大部分均已标记 @Deprecated (已废弃),剩下的几个基本只有一个方法能用:getTime() ,而这个方法非常硬核,它 get 到的 Time 不是年月日,而是一个 long 类型的毫秒数,表示在格林尼治时间 1900 年开始之后,经过了多少毫秒。

1
2
3
4
Date date = new Date();
System.out.println(date.getTime());

// 控制台打印结果:1569456444308

Date 类的反人类处

  • Date 类计算年份时,是跟 1900 年进行比较的,比如今年(2019 年)对于 Date 类而言,它是 119 年,当调用 getYear() 方法时(该方法已被官方建议不要使用),它会说这是 119 年。
  • Date 类计算月份是,是从 0 开始计算的,因此这个月(9 月)对于 Date 类而言,它是 8 。
  • getDay() 方法是获取当周的第几天, getDate() 方法是获取当月的第几天(这两个方法也已被遗弃)。
  • Date 类自己的一切 getXXX() 方法,实际上只剩下了 getTime() 方法,表示获取当前时间(精确到毫秒)与 1900 年 1 月 1 日 0 时 0 分 0 秒这一瞬间(包含时差),中间间隔的毫秒数,其他获取年月日等时间的方法均被遗弃,官方建议使用 Calendar 类。
  • Date 类的 getTime() 方法是指获取毫秒数,比如现在时间的 getTime() 的值为 1569454704886 ,太硬核了。更硬核的是,Date 类的构造函数只剩下两种能用,一种是无参构造(得到当下的时间),另一种是以刚才的那个毫秒数为参数。
  • Date 类的值不是 final 的,是可以在实例化之后通过 setTime() 改变值的,非线程安全。
  • 当调用 Date 类的 toString() 方法时,它会打印出例如 Tue Sep 10 00:00:00 CST 2019 (中国标准时间 2019 年 9 月 10 日 0 时 0 点 0 分)的值,你发现这里包含时区信息 CST,但令人无语的是,这个时区信息是 Date 类在调用 toString() 方法时,根据系统时区动态打印的。换句话说,刚才那个时间的程序在中国执行,时区是 CST,在美国执行,那时区就是 PDT。

Calendar

calendar 是日历的意思,因此见名知意,Calendar 类是用来跟年月日等时间打交道的类。

Calendar 类本身是一个抽象类,它代表着日历类的标准与规范,有 GregorianCalendar 类(格林尼治日历时间)等实现类。实例化一个 Calendar 类,如果不使用子类,那就要通过工厂方法获得了。

1
2
3
4
5
// Calendar类的实例化方法
Calendar calendar = Calendar.getInstance();

// 下面这种是错误的
Calendar calendar = new Calendar();

这里通过 Calendar.getInstance() 获得的,是一个 GregorianCalendar 对象。

Calendar 类最实用的方法是它的 get() 方法,用这个方法获取年、月、日、周、小时等等 17 类不同的信息。

1
2
3
4
5
6
7
8
// 获取calendar的年份信息 如2019
int year = calendar.get(Calendar.YEAR);

// 获取calendar的月份信息 如9
int month = calendar.get(Calendar.MONTH);

// 获取calendar的当月天数信息,如10
int date = calendar.get(Calendar.DATE);

上面诸如 Calendar.YEAR 之类的值,其实是 Calendar 类定义的常量值,Calendar.YEAR 其实就是 1,换句话说,下面两行代码是一样的:

1
2
int year1 = calendar.get(Calendar.YEAR);
int year2 = calendar.get(1);

通过 Calendar 类的 get() 方法能得到 17 类不同的时间信息,这 17 个常量值列在下面:

常量名 常量值 意义
ERA 0 纪元(0:BC 即公元前,1:AD 即公元后)
YEAR 1
MONTH 2
WEEK_OF_YEAR 3 本年第几周
WEEK_OF_MONTH 4 本月第几周
DATE 5 本月第几日
DAY_OF_MONTH 5 本月第几日,与 DATE 完全相同
DAY_OF_YEAR 6 本年第几日
DAY_OF_WEEK 7 本周第几日
DAY_OF_WEEK_IN_MONTH 8 当前月第几周
AM_PM 9 上午/下午(0:AM 即上午,1:PM 即下午)
HOUR 10 当天第几个小时(12 小时制)
HOUR_OF_DAY 11 当天第几个小时(24 小时制)
MINUTE 12 当小时第多少分钟
SECOND 13 当分钟第多少秒
MILLISECOND 14 当秒第多少毫秒
ZONE_OFFSET 15 距 GMT 时区偏移时间(以毫秒为单位,mdzz)
DST_OFFSET 16 夏令时偏移时间(以毫秒为单位)

具体的使用可以参考这一篇文章:《Calendar的基本使用和属性说明》,整理得很细致。

Calendar 类相比于 Date 类更为先进的地方是,它可以通过人类能理解的方式设置和变更时间,例如:

1
2
3
4
5
6
7
8
// 设置了一个时间:2019年9月1日0时0分0秒
calendar.set(2019, Calendar.JANUARY, 1, 0, 0, 0);

// 把时间往前12个月
calendar.add(Calendar.MONTH, -12);

// 把时间往后8个月(但是只改变月份,不改变其他年份等其他时间)
calendar.roll(Calendar.MONTH, 8);

但让人依旧无语的是,Calendar 类的月份也是从 0 开始的。此外 Calendar 类不支持格式化。


SimpleDateFormat

SimpleDateFormat 类是一个【格式化】和【解析日期】的工具类,即 Date -> Text 或者 Text -> Date,而且能够按照要求格式转换,如输出 2019-09-10 12:00:00 这种时间文本。

下面就是最常见的用法,声明好格式之后,使用 format() 方法把时间转换成字符串。

1
2
3
4
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
String dateStr = simpleDateFormat.format(date);

// dateStr : "2019-09-26 23:49:26 CST"

另一种常见的用法是把字符串转换成时间,但是要异常处理,毕竟是处理字符串。

1
2
3
4
5
6
7
8
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
try {
Date date = simpleDateFormat.parse("2019-09-26 23:49:26 CST");
} catch (ParseException e) {
e.printStackTrace();
}

// date.toString() : Thu Sep 26 23:49:26 CST 2019

字母与时间的对应关系如下(数据来源):

字母 日期或时间元素 表示 示例
G Era 标志符 Text AD
y Year 1996; 96
M 年中的月份 Month July; Jul; 07
w 年中的周数 Number 27
W 月份中的周数 Number 2
D 年中的天数 Number 189
d 月份中的天数 Number 10
F 月份中的星期 Number 2
E 星期中的天数 Text Tuesday; Tue
a Am/pm 标记 Text PM
H 一天中的小时数(0-23) Number 0
k 一天中的小时数(1-24) Number 24
K am/pm 中的小时数(0-11) Number 0
h am/pm 中的小时数(1-12) Number 12
m 小时中的分钟数 Number 30
s 分钟中的秒数 Number 55
S 毫秒数 Number 978
z 时区 General time zone Pacific Standard Time; PST; GMT-08:00
Z 时区 RFC 822 time zone -0800

虽然使用 SimpleDateFormat 类能进行文本处理了,但是使用起来还是挺不方便的,而且还要考虑异常处理,此外它还线程不安全。


java.sql 包中的时间类

在 java 中有一个与数据库相对应的类包,是 java.sql 包,该包下有三个对应数据库时间类型的类,分别是 Date 类、 Time 类和 TimeStamp 类,这是三个废物类,一无是处。

这三个类是与数据库中的时间数据类型完全对应的:

数据库的时间类型 java.sql 时间类型 示例
DATE Date 2019-09-01
TIME Time 12:00:00
TIMESTAMP Timestamp 2019-09-01 12:00:00.000

说这三个类废物,不是没有根据的:

  1. 这三个时间类的构造方法都只有一种(另一种官方废弃不建议使用),那就是【当下时间距离 1970 年 1 月 1 日 0 时 0 分 0 秒 0 毫秒】的毫秒数,反人类。
  2. 这三个时间类没有诸如 getDate() 之类获取时间的方法,它们有什么方法呢,只有转换成别的时间类的方法 :)
  3. 除了这三个类之外,别的时间类也可以对接数据库。

一无是处。



Joda Time 时间类

从上面两部分的时间类看得出,用 JDK 自带的时间类编程,还是比较痛苦的:其一,想完成某个需求,可能需要好几个时间类同时使用;其二,上述时间类还存在着许多例如月份从 0 开始计数、时区信息伪造等暗坑。

这种痛苦的局面在 JDK 8 得到了解决,因为 JDK 8 设计了全新的时间类,但是低版本的 JDK 依旧痛苦。想解决这种痛苦,就要使用第三方类库,Joda Time 就是一个优秀的第三方时间类库。

此外想吐槽的一点是,Joda Time 有一种死心塌地的备胎感,因为它的官网在所有地方都在反复提及:如果使用 JDK 8 及其之后的版本,应该使用 JDK 8 提供的 java.time 包中的时间类,不要使用 Joda Time。真是让人感动,当然我猜这是因为 Joda Time 的作者参与了 JDK 8 的 java.time 包的设计,人家暗中备胎转正了。


org.joda.time 包下的类,大致可以分为三种:

  1. 时间类(类似于上文的 Date 类)
  2. 格式化与解析类(类似于上文的 SimpleDateFormat 类)
  3. 时间跨度类

时间类就是真正用来记录如 2019-10-07 22:48:03 这类时间的类,格式化与解析类是把时间类型和字符串类型进行相互转换的类,时间跨度类是记录如 2年零3个月 这类间隔时间的类。

接下来逐个类型讲解。


Joda Time 的时间类

首先要注意:Joda Time 所设计的时间类,统统都不可改变(immutable),跟 String 是一样的,一经实例化,不得改变其值,从源头上实现了线程安全。当需要改变时间时,Joda Time 会返回一个全新的 Joda 实例,也跟 String 的设计是一样的。

org.joda.time 包下有五个常用的时间类,以表格形式列在下面:

类名 作用 示例(2019 年 10 月 1 日 12 时整)
DateTime 日期+时间(含时区信息) 2019-10-01T00:00:00.000+08:00
Instant 日期+时间(格林威治时间,存疑) 2019-10-01T12:00:00.000Z
LocalDateTime 日期+时间(不含时区信息) 2019-10-01T12:00:00.000
LocalDate 日期(不含时区信息) 2019-10-01
LocalTime 时间(不含时区信息) 12:00:00.000

Instant 类是指时间轴上一个确定的时间点(精确到微妙),但是我自认为用处实在是不多,还是其他四个类:DateTime 类、LocalDateTime 类、LocalDate 类、LocalTime 类使用比较频繁,如果需要时区信息则使用第一个,如果不需要时区信息,那就使用后面三个以“Local”开头的类。

以上五个时间类,使用方法可以用随心所欲来形容,你想怎么用就怎么用。以 DateTime 类为例,介绍 Joda Time 时间类的主要用法:

  1. 得到一个时间对象

    有超级多的方法,以下只列举几种:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 当前时间
    DateTime dateTime1 = new DateTime();

    // 任意一种时间类的实例,自动转换
    DateTime dateTime2 = new DateTime(dateTime1);

    // 手动填写年月日时间等信息,有9种填写规则
    DateTime dateTime3 = new DateTime(2019, 10, 1, 12, 0);

    // 自1970年1月1日0日整之后的毫秒数
    DateTime dateTime4 = new DateTime(1569902400000L);

    // 使用静态方法,读取一个字符串转换成时间
    DateTime dateTime5 = DateTime.parse("2019-10-01T12:00:00.000+08:00");
  2. 获取时间信息

    同样有超级多的方法,以下只列举几种:

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取当前月份
    int monthOfYear = dateTime.getMonthOfYear();

    // 获取当天过了多少秒
    int secondOfDay = dateTime.getSecondOfDay();

    // 获取自1970年1月1日0时之后过了多少微秒
    long millis = dateTime.getMillis();
  3. 修改时间信息(会返回一个全新的 DateTime 实例)

    再次有超级多的方法,以下只列举几种:

    1
    2
    3
    4
    5
    6
    7
    8
    // 天数 + 1(plus)
    DateTime plusDateTime = dateTime.plusDays(1);

    // 小时数 - 10(minus)
    DateTime minusDateTime = dateTime.minusHours(2);

    // 设置月份为 8 月(with)
    DateTime withDateTime = dateTime.withMonthOfYear(8);
  4. 其他操作

    再再次有超级多的方法,以下只列举几种:

    1
    2
    3
    4
    5
    6
    7
    8
    // 该时间是否比当下时间早
    boolean beforeNow = dateTime.isBeforeNow();

    // 对比另一个时间(随意一个时间类的实例),判断是否在其之后
    boolean afterTime = dateTime.isAfter(dateTime2);

    // 转换成废物的java.util.date类
    Date date = dateTime.toDate();

我知道你懒得看,你只要知道,Joda Time 几乎无所不能,使用时随心所欲,就可以了。


Joda Time 的格式化与解析类

Joda Time 的格式化与解析类,常用的有三个类,分别是 DateTimeFormatter 类、DateTimeFormat 类和 DateTimeFormatterBuilder 类。其实不使用这三个类,也可以实现时间解析与格式化处理,直接用字符串指定好样式就可以了,使用这三个类是为了简便和统一操作。

  1. DateTimeFormatter

    时间解析与格式化处理类,用于日期和时间与字符串之间的转换。

    时间类 -> 字符串时,使用时间类的toString(DateTimeFormatter formatter)方法。

    1
    2
    3
    4
    DateTimeFormatter dateTimeFormatter = DateTimeFormat.fullDateTime();
    String dateTimeStr = dateTime.toString(dateTimeFormatter);

    // dateTimeStr: "2019年10月1日 星期二 下午01时00分00秒 CST"

    字符串 -> 时间类时,使用时间类的静态方法parse(String str, DateTimeFormatter formatter)

    1
    2
    3
    DateTime dateTime = DateTime.parse("2019年10月1日 星期二 下午01时00分00秒 CST", dateTimeFormatter);

    // dateTime.toString: "2019-10-01T13:00:00.000-05:00"
  2. DateTimeFormat

    这是 DateTimeFormatter 类的工厂类,提供各种创建 DateTimeFormatter 类的实例方法。

    工厂方法名 示例( 2019 年 10 月 1 日 13 时整)
    fullDateTime() 2019年10月1日 星期二 下午01时00分00秒 CST
    fullDate() 2019年10月1日 星期二
    fullTime() 下午01时00分00秒 CST
    longDateTime() 2019年10月1日 下午01时00分00秒
    longDate() 2019年10月1日
    longTime() 下午01时00分00秒
    mediumDateTime() 2019-10-1 13:00:00
    mediumDate() 2019-10-1
    mediumTime() 13:00:00
    shortDateTime() 19-10-1 下午01:00
    shortDate() 19-10-1
    shortTime() 下午01:00
    forPattern(String pattern) 自定义格式
    forStyle(String style) 按样式,建议阅读官方API
    patternForStyle(String style, Locale locale) 根据地区告知样式内容,返回一个样式字符串
    如样式是”MM”且地区为中国时,返回:yyyy’年’M’月’d’日’ EEEE ahh’时’mm’分’ss’秒’ z
  1. DateTimeFormatterBuilder

    这个类是用作生成复杂时间样式的类,可以自由拼接时间,自由指定间隔样式等等,例如“十月 01 日 星期二 下午”。这个类本身是可以改变的(非线程安全),但是它可以转换成 DateTimeFormatter 类,此时就是不能改变的(线程安全)。

    本类的操作跟 StringBuilder 类几乎是一致的,使用场景不多,用起来也比较顺手,只贴出一段代码示例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    DateTimeFormatterBuilder dateTimeFormatterBuilder = new DateTimeFormatterBuilder()
    .appendEraText() // 纪元(由于是Text,不需要自己指定精度)
    .appendLiteral(' ') // 分隔(此处用空格分隔)
    .appendYear(4, 4) // 年(参数表示数字精度,最少4位,最多四位)
    .appendLiteral(' ')
    .appendMonthOfYear(2) // 月份
    .appendLiteral(' ')
    .appendDayOfMonth(2) // 日
    .appendLiteral(' ')
    .appendDayOfWeekText() // 周几
    .appendLiteral(" 该天已度过") // 分隔(此处用语句分隔)
    .appendFractionOfDay(2, 2) // 当天已过去多少百分比
    .appendLiteral("%");
    DateTimeFormatter dateTimeFormatter = dateTimeFormatterBuilder.toFormatter();

    // 打印示例:公元 2019 10 01 星期二 该天已度过54%

Joda Time 的时间跨度类

Joda Time 设计了三个类,用来表示时间跨度,分别是 Duration 类、Period 类和 Interval 类。

  • Duration 类保存了一个精确的毫秒数,比如你设置它为一天,它会记录下这是 86400 秒(如有毫秒会精确到小数点后三位)。
  • Period 类保存了一段时间,例如 1 年 10 个月 1 小时 1 毫秒(它记录成 P1Y10MT-1H-0.001S)
  • Interval 类保存了一个开始时刻和一个结束时刻,因而也能够表示一段时间。

我觉得官方 API 的说明对理解很有帮助:

Duration: An immutable duration specifying a length of time in milliseconds.

Period: An immutable time period specifying a set of duration field values.

Interval: Interval is the standard implementation of an immutable time interval.

虽然在我测试和学习之后,感觉这三个类有蛮多道道,也有设计精巧的地方,但我认为,这三个类没有太多的应用场景,时间跨度不是一个常见的需求,用到的时候看一看方法名就能大致猜到了,不写了。



java.time 包中的时间类

当我们迎来 JDK 8 时,我们再也不需要 Joda Time 之类的第三方类库了,因为官方给我们提供了全新的时间类,这些类都属于 java.time 包下。

官方提供的全新时间类,仍然可以分成三种类型:

  1. 时间类(类似于上文的 Date 类)
  2. 格式化与解析类(类似于上文的 SimpleDateFormat 类)
  3. 时间跨度类

当一个个类接触下去之后,你会发现 JDK 8 所提供的 java.time 包中的时间类,和 Joda Time 是何其相似,相似到你觉得简直像是 Joda Time 备胎转正,我猜想这跟 Joda Time 的作者参与到 java.time 包的开发中有关。

我感觉这两个类库的设计,最主要的区别在于,Joda Time 习惯于 new 一个对象出来,而 java.time 习惯于用静态工厂方法实例化一个对象出来。


java.time 包的时间类

java.time 包中的时间类设计,几乎与 Joda Time 一模一样,也是有五个类。

类名 作用 示例(2019 年 10 月 1 日 12 时整)
ZonedDateTime 日期+时间(含时区信息) 2019-10-01T00:00:00.000+08:00[Asia/Shanghai]
Instant 日期+时间(格林威治时间,存疑) 2019-10-01T12:00:00.000Z
LocalDateTime 日期+时间(不含时区信息) 2019-10-01T12:00:00.000
LocalDate 日期(不含时区信息) 2019-10-01
LocalTime 时间(不含时区信息) 12:00:00.000

你若将 java.time 包与 Joda Time 所表示时间的五个类进行比较,你会发现,后四个类的类名、作用、具体示例是一模一样的,完全相同,只有第一个【包含时区信息的日期+时间】的类,两个是稍有不同的。

在 Joda Time 中类名是 DateTime,而在 java.time 包中是 ZonedDateTime,更明显地表现出这是一个跟时区有关系的类,这两个类通过 toString() 方法打印出来,也仅仅是最末尾相差了一处地区信息:[Asia/Shanghai],除此之外,分毫未差。

这五个类在使用上也几乎没什么区别,最主要的的区别只有一处,那就是:JDK 8 的 java.time 包中的时间类,不再具有 public 的构造方法,而只有类静态方法。

也就是说,通过类构造方法实例化对象,是错误的:

1
2
// 错误!
LocalDate localDate = new LocalDate();

因为这个类构造方法是私有(private)的,不对外公开。

实例化对象,需要使用类静态方法(有很多个静态方法,这里只展示两个有代表性的):

1
2
3
4
5
// 静态方法 now() 当前时间
LocalDate localDate = LocalDate.now();

// 静态方法 of() 构造时间
LocalDate localDate = LocalDate.of(2019, 10, 1);

java.time 包的格式化与解析类

java.time 包省去了 DateTimeFormat 类,将其方法合并到 DateTimeFormatter 类中,实例化一个 DateTimeFormatter 对象也就只能用这个类自己的静态工厂方法,但是使用起来和 Joda Time 并没有什么太大的区别。

java.time 包保留了 DateTimeFormatterBuilder 类,使用起来跟 Joda Time 也是几乎完全一样。

这部分不写了,具体的内容可以参考这篇文章:《Java 8 新日期时间 API ( 下 ) - 格式化》


java.time 包的时间跨度类

java.time 包去除掉了 Joda Time 中鸡肋的 Interval 类(保存一个开始时间和一个结束时间),保留下了另外两个类:Duration 类和 Period 类。

跟 Joda Time 的设计稍微不同,Duration 类保存了一个精确到纳秒的时间(例如 5 小时零 1 纳秒是 PT5H0.000000001S),但是 Period 类是一致的,仍然是保存一段时间。在使用上,Duration 类更偏向于时间(time),而 Period 类更偏向于日期(calendar)。

具体的方法、使用的逻辑,与 Joda Time 如出一辙,那就写到这里吧。