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 | // 当前时间 |
Date 类有很多方法,大部分均已标记 @Deprecated
(已废弃),剩下的几个基本只有一个方法能用:getTime()
,而这个方法非常硬核,它 get 到的 Time 不是年月日,而是一个 long
类型的毫秒数,表示在格林尼治时间 1900 年开始之后,经过了多少毫秒。
1 | Date date = new Date(); |
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 | // Calendar类的实例化方法 |
这里通过 Calendar.getInstance()
获得的,是一个 GregorianCalendar 对象。
Calendar 类最实用的方法是它的 get()
方法,用这个方法获取年、月、日、周、小时等等 17 类不同的信息。
1 | // 获取calendar的年份信息 如2019 |
上面诸如 Calendar.YEAR
之类的值,其实是 Calendar 类定义的常量值,Calendar.YEAR
其实就是 1,换句话说,下面两行代码是一样的:
1 | int year1 = calendar.get(Calendar.YEAR); |
通过 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 | // 设置了一个时间:2019年9月1日0时0分0秒 |
但让人依旧无语的是,Calendar 类的月份也是从 0 开始的。此外 Calendar 类不支持格式化。
SimpleDateFormat
SimpleDateFormat 类是一个【格式化】和【解析日期】的工具类,即 Date -> Text
或者 Text -> Date
,而且能够按照要求格式转换,如输出 2019-09-10 12:00:00
这种时间文本。
下面就是最常见的用法,声明好格式之后,使用 format()
方法把时间转换成字符串。
1 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz"); |
另一种常见的用法是把字符串转换成时间,但是要异常处理,毕竟是处理字符串。
1 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz"); |
字母与时间的对应关系如下(数据来源):
字母 | 日期或时间元素 | 表示 | 示例 |
---|---|---|---|
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 |
说这三个类废物,不是没有根据的:
- 这三个时间类的构造方法都只有一种(另一种官方废弃不建议使用),那就是【当下时间距离 1970 年 1 月 1 日 0 时 0 分 0 秒 0 毫秒】的毫秒数,反人类。
- 这三个时间类没有诸如
getDate()
之类获取时间的方法,它们有什么方法呢,只有转换成别的时间类的方法 :) - 除了这三个类之外,别的时间类也可以对接数据库。
一无是处。
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
包下的类,大致可以分为三种:
- 时间类(类似于上文的 Date 类)
- 格式化与解析类(类似于上文的 SimpleDateFormat 类)
- 时间跨度类
时间类就是真正用来记录如 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
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");获取时间信息
同样有超级多的方法,以下只列举几种:
1
2
3
4
5
6
7
8// 获取当前月份
int monthOfYear = dateTime.getMonthOfYear();
// 获取当天过了多少秒
int secondOfDay = dateTime.getSecondOfDay();
// 获取自1970年1月1日0时之后过了多少微秒
long millis = dateTime.getMillis();修改时间信息(会返回一个全新的 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);其他操作
再再次有超级多的方法,以下只列举几种:
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 类。其实不使用这三个类,也可以实现时间解析与格式化处理,直接用字符串指定好样式就可以了,使用这三个类是为了简便和统一操作。
DateTimeFormatter 类
时间解析与格式化处理类,用于日期和时间与字符串之间的转换。
当
时间类 -> 字符串
时,使用时间类的toString(DateTimeFormatter formatter)
方法。1
2
3
4DateTimeFormatter dateTimeFormatter = DateTimeFormat.fullDateTime();
String dateTimeStr = dateTime.toString(dateTimeFormatter);
// dateTimeStr: "2019年10月1日 星期二 下午01时00分00秒 CST"当
字符串 -> 时间类
时,使用时间类的静态方法parse(String str, DateTimeFormatter formatter)
。1
2
3DateTime dateTime = DateTime.parse("2019年10月1日 星期二 下午01时00分00秒 CST", dateTimeFormatter);
// dateTime.toString: "2019-10-01T13:00:00.000-05:00"
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
DateTimeFormatterBuilder 类
这个类是用作生成复杂时间样式的类,可以自由拼接时间,自由指定间隔样式等等,例如“十月 01 日 星期二 下午”。这个类本身是可以改变的(非线程安全),但是它可以转换成 DateTimeFormatter 类,此时就是不能改变的(线程安全)。
本类的操作跟 StringBuilder 类几乎是一致的,使用场景不多,用起来也比较顺手,只贴出一段代码示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16DateTimeFormatterBuilder 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
包下。
官方提供的全新时间类,仍然可以分成三种类型:
- 时间类(类似于上文的 Date 类)
- 格式化与解析类(类似于上文的 SimpleDateFormat 类)
- 时间跨度类
当一个个类接触下去之后,你会发现 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 | // 错误! |
因为这个类构造方法是私有(private)的,不对外公开。
实例化对象,需要使用类静态方法(有很多个静态方法,这里只展示两个有代表性的):
1 | // 静态方法 now() 当前时间 |
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 如出一辙,那就写到这里吧。