根据廖雪峰老师的教程,梳理了Java中日期时间类的常见用法
1. 基本概念
日期是一个离散的概念,即非连续的。
时间有两个概念,一种不带日期如12:34:44
,另一种是带日期的:2020-1-1 20:21:59
。不带日期的时间不能确定唯一时刻。
本地时间
当我们说现在是22:00的时候,我们说的是我所在地区(时区)的时间,而地球上其他地方的时间在同一时刻可能是不同的。
时区
为了唯一表示一个时间,我们需要给时间加上时区。
一种是GMT一种是UTC。例如GMT+08:00
或者UTC+08:00
表示东八区。他们可以认为基本是等价的。UTC使用更精确的原子钟计时,每隔几年会有一个闰秒。在写代码的时候可以忽略。
另一种是缩写,例如CST表示China Standard Time
, 但是也可以表示Central Standard Time USA
, 因此我们应该尽量避免使用缩写带来的混淆。
最后一种以洲/城市表示。例如,Asia/Shanghai
表示上海所在地的时区。城市不是任意的,是由国际标准组织规定的。
夏令时
夏天开始的时候,将时间往后拨一小时,夏天结束的时候再往前拨一小时。美国现在就在使用这种方法。具体的原因主要是和生产生活有关,详细缘由可以见WIKI。
计算夏令时请使用标准库提供的相关类,不要试图自己计算夏令时。
本地化
在计算机中,通常使用Locale
表示一个国家或地区的日期、时间、数字、货币等格式。Locale
由语言_国家
的字母缩写构成,例如,zh_CN
表示中文+中国,en_US
表示英文+美国。语言使用小写,国家使用大写。
对于日期来说,不同的Locale,例如,中国和美国的表示方式如下:
- zh_CN:2016-11-30
- en_US:11/30/2016
2. 旧API
java.util.Date
区别于java.sql.Date(它是用来存数据库的数据类型)
- 基本用法
public class Main { public static void main(String[] args) { // 获取当前时间: Date date = new Date(); System.out.println(date.getYear() + 1900); // 必须加上1900 System.out.println(date.getMonth() + 1); // 0~11,必须加上1 System.out.println(date.getDate()); // 1~31,不能加1 // 转换为String: System.out.println(date.toString()); // 转换为GMT时区: System.out.println(date.toGMTString()); // 转换为本地时区: 不同的地区可能会有不同的结果 System.out.println(date.toLocaleString()); } }
如果想要对输出日期的结果进行格式化,可以使用SimpleDateFormat
。
public class Main { public static void main(String[] args) { // 获取当前时间: Date date = new Date(); var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.format(date)); } }
更多的格式可以从JDK文档查看。
Date的问题在于不能转换时区,他总是以当前计算机的默认时区输出,也比较难对日期进行加减操作。
Calendar
它和Date的主要的区别在于它可以做一些简单的日期和时间的运算。
基本用法:
public class Main { public static void main(String[] args) { // 获取当前时间: Calendar c = Calendar.getInstance(); int y = c.get(Calendar.YEAR); int m = 1 + c.get(Calendar.MONTH); int d = c.get(Calendar.DAY_OF_MONTH); int w = c.get(Calendar.DAY_OF_WEEK); int hh = c.get(Calendar.HOUR_OF_DAY); int mm = c.get(Calendar.MINUTE); int ss = c.get(Calendar.SECOND); int ms = c.get(Calendar.MILLISECOND); System.out.println(y + "-" + m + "-" + d + " " + w + " " + hh + ":" + mm + ":" + ss + "." + ms); } }
Calendar只有一种获取方法, Calendar.getInstance(), 而且包含有当前时刻,如果想要设置特定时间,需要先清除。
public class Main { public static void main(String[] args) { // 当前时间: Calendar c = Calendar.getInstance(); // 清除所有: c.clear(); // 设置2019年: c.set(Calendar.YEAR, 2019); // 设置9月:注意8表示9月: c.set(Calendar.MONTH, 8); // 设置2日: c.set(Calendar.DATE, 2); // 设置时间: c.set(Calendar.HOUR_OF_DAY, 21); c.set(Calendar.MINUTE, 22); c.set(Calendar.SECOND, 23); System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(c.getTime())); // 2019-09-02 21:22:23 } }
利用Calendar.getTime()可以将得到一个Date对象,进而用SimpleDateFormat格式化。
Timezone
和前两者相比,Timezone提供了时区转换。
public class Main { public static void main(String[] args) { TimeZone tzDefault = TimeZone.getDefault(); // 当前时区 TimeZone tzGMT9 = TimeZone.getTimeZone("GMT+09:00"); // GMT+9:00时区 TimeZone tzNY = TimeZone.getTimeZone("America/New_York"); // 纽约时区 System.out.println(tzDefault.getID()); // Asia/Shanghai System.out.println(tzGMT9.getID()); // GMT+09:00 System.out.println(tzNY.getID()); // America/New_York } }
时区的唯一标识符是字符串的ID,可以用TimeZone.getAvailableIDs()
获取所有可用的时区ID。
利用Calendar
进行时区转换的步骤是:
- 清除所有字段;
- 设定指定时区;
- 设定日期和时间;
- 创建
SimpleDateFormat
并设定目标时区; - 格式化获取的
Date
对象(注意Date
对象无时区信息,时区信息存储在SimpleDateFormat
中)。
因此,本质上时区转换只能通过SimpleDateFormat
在显示的时候完成。
public class Main { public static void main(String[] args) { // 当前时间: Calendar c = Calendar.getInstance(); // 清除所有: c.clear(); // 设置为北京时区: c.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); // 设置年月日时分秒: c.set(2019, 10 /* 11月 */, 20, 8, 15, 0); // 显示时间: var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); sdf.setTimeZone(TimeZone.getTimeZone("America/New_York")); System.out.println(sdf.format(c.getTime())); // 2019-11-19 19:15:00 } }
Calender也可以对日期简单加减
public class Main { public static void main(String[] args) { // 当前时间: Calendar c = Calendar.getInstance(); // 清除所有: c.clear(); // 设置年月日时分秒: c.set(2019, 10 /* 11月 */, 20, 8, 15, 0); // 加5天并减去2小时: c.add(Calendar.DAY_OF_MONTH, 5); c.add(Calendar.HOUR_OF_DAY, -2); // 显示时间: var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date d = c.getTime(); System.out.println(sdf.format(d)); // 2019-11-25 6:15:00 } }
3. 新API java.time包
从Java8开始, java.time提供了新的日期和时间的API
- 本地日期和时间:LocalDateTime,LocalDate,LocalTime
- 带时区的日期和时间:ZonedDateTime
- 时刻:Instant
- 时区:ZoneId,ZoneOffSet
- 时间间隔:Duration
以及一套用于取代SimpleDateFormat的格式化类型DateTimeFormatter。
新API严格区分了时刻、本地日期、本地时间和带时区的日期时间,并且,对日期和时间进行运算更加方便。
此外还修正了常量设计:
- Month的范围用1~12表示1月到12月;
- Week的范围用1~7表示周一到周日。
最后,新的API类型几乎都是不变类型,可以放心使用。
LocalDateTime
public class Main { public static void main(String[] args) { LocalDate d = LocalDate.now(); // 当前日期 LocalTime t = LocalTime.now(); // 当前时间 LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间 System.out.println(d); // 严格按照ISO 8601格式打印 System.out.println(t); // 严格按照ISO 8601格式打印 System.out.println(dt); // 严格按照ISO 8601格式打印 } }
本地日期和时间通过now()获取到的总是以当前默认时区返回的,和旧API不同,LocalDateTime
、LocalDate
和LocalTime
默认严格按照ISO 8601规定的日期和时间格式进行打印。
上述代码其实有一个小问题,在获取3个类型的时候,由于执行一行代码总会消耗一点时间,因此,3个类型的日期和时间很可能对不上(时间的毫秒数基本上不同)。为了保证获取到同一时刻的日期和时间,可以改写如下:
LocalDateTime dt = LocalDateTime.now(); // 当前日期和时间
LocalDate d = dt.toLocalDate(); // 转换到当前日期
LocalTime t = dt.toLocalTime(); // 转换到当前时间
反过来,通过指定的日期和时间创建LocalDateTime
可以通过of()
方法:
// 指定日期和时间:
LocalDate d2 = LocalDate.of(2019, 11, 30); // 2019-11-30, 注意11=11月
LocalTime t2 = LocalTime.of(15, 16, 17); // 15:16:17
LocalDateTime dt2 = LocalDateTime.of(2019, 11, 30, 15, 16, 17);
LocalDateTime dt3 = LocalDateTime.of(d2, t2);
因为严格按照ISO 8601的格式,因此,将字符串转换为LocalDateTime
就可以传入标准格式:
LocalDateTime dt = LocalDateTime.parse("2019-11-19T15:16:17");
LocalDate d = LocalDate.parse("2019-11-19");
LocalTime t = LocalTime.parse("15:16:17");
注意ISO 8601规定的日期和时间分隔符是T
。标准格式如下:
- 日期:yyyy-MM-dd
- 时间:HH:mm:ss
- 带毫秒的时间:HH:mm:ss.SSS
- 日期和时间:yyyy-MM-dd'T'HH:mm:ss
- 带毫秒的日期和时间:yyyy-MM-dd'T'HH:mm:ss.SSS
DateTimeFormatter
如果要自定义输出的格式,或者要把一个非ISO 8601格式的字符串解析成LocalDateTime
,可以使用新的DateTimeFormatter
public class Main { public static void main(String[] args) { // 自定义格式化: DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); System.out.println(dtf.format(LocalDateTime.now())); // 用自定义格式解析: LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17", dtf); System.out.println(dt2); } }
LocalDateTime提供了对日期和时间进行加减的链式调用
public class Main { public static void main(String[] args) { LocalDateTime dt = LocalDateTime.of(2022, 07, 21, 21, 30, 00); // dt ==> 2022-07-21T21:30 LocalDateTime dt2 = dt.plusDays(5).minusHours(3); // dt2 ==> 2022-07-26T18:30 dt2.minusMonths(1) // $38 ==> 2022-06-26T18:30 } }
注意到月份加减会自动调整日期,例如从2019-10-31
减去1个月得到的结果是2019-09-30
,因为9月没有31日。
对日期和时间进行调整则使用withXxx()
方法,例如:withHour(15)
会把10:11:12
变为15:11:12
:
- 调整年:withYear()
- 调整月:withMonth()
- 调整日:withDayOfMonth()
- 调整时:withHour()
- 调整分:withMinute()
- 调整秒:withSecond()
public class Main { public static void main(String[] args) { LocalDateTime dt = LocalDateTime.of(2019, 10, 26, 20, 30, 59); System.out.println(dt); // 日期变为31日: LocalDateTime dt2 = dt.withDayOfMonth(31); System.out.println(dt2); // 2019-10-31T20:30:59 // 月份变为9: LocalDateTime dt3 = dt2.withMonth(9); System.out.println(dt3); // 2019-09-30T20:30:59 } }
LocalDateTime
还有一个通用的with()
方法允许我们做更复杂的运算。
public class Main { public static void main(String[] args) { // 本月第一天0:00时刻: LocalDateTime firstDay = LocalDate.now().withDayOfMonth(1).atStartOfDay(); System.out.println(firstDay); // 本月最后1天: LocalDate lastDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()); System.out.println(lastDay); // 下月第1天: LocalDate nextMonthFirstDay = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()); System.out.println(nextMonthFirstDay); // 本月第1个周一: LocalDate firstWeekday = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)); System.out.println(firstWeekday); } }
要判断两个LocalDateTime
的先后,可以使用isBefore()
、isAfter()
方法,对于LocalDate
和LocalTime
类似
public class Main { public static void main(String[] args) { LocalDateTime now = LocalDateTime.now(); LocalDateTime target = LocalDateTime.of(2019, 11, 19, 8, 15, 0); System.out.println(now.isBefore(target)); System.out.println(LocalDate.now().isBefore(LocalDate.of(2019, 11, 19))); System.out.println(LocalTime.now().isAfter(LocalTime.parse("08:15:00"))); } }
LocalDateTime
无法与时间戳进行转换,因为LocalDateTime
没有时区,无法确定某一时刻。
ZonedDateTime
相当于LocalDateTime
加时区的组合,它具有时区,可以与long
表示的时间戳进行转换。
Duration 和 Period
Duration
表示两个时刻之间的时间间隔。另一个类似的Period
表示两个日期之间的天数
public class Main { public static void main(String[] args) { LocalDateTime start = LocalDateTime.of(2019, 11, 19, 8, 15, 0); LocalDateTime end = LocalDateTime.of(2020, 1, 9, 19, 25, 30); Duration d = Duration.between(start, end); System.out.println(d); // PT1235H10M30S Period p = LocalDate.of(2019, 11, 19).until(LocalDate.of(2020, 1, 9)); System.out.println(p); // P1M21D } }
Duration
和Period
的表示方法也符合ISO 8601的格式,它以P...T...
的形式表示,P...T
之间表示日期间隔,T
后面表示时间间隔。如果是PT...
的格式表示仅有时间间隔。利用ofXxx()
或者parse()
方法也可以直接创建Duration
:
Duration d1 = Duration.ofHours(10); // 10 hours Duration d2 = Duration.parse("P1DT2H3M"); // 1 day, 2 hours, 3 minutes
3. ZoneDateTime
要创建一个ZonedDateTime
对象,有以下几种方法,一种是通过now()
方法返回当前时间
public class Main { public static void main(String[] args) { ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区 ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定时区获取当前时间 System.out.println(zbj); System.out.println(zny); } } // 这两种方法获得的是同一时刻(有误差,语句执行时间)
另一种方式是通过给一个LocalDateTime
附加一个ZoneId
,就可以变成ZonedDateTime
public class Main { public static void main(String[] args) { LocalDateTime ldt = LocalDateTime.of(2019, 9, 15, 15, 16, 17); ZonedDateTime zbj = ldt.atZone(ZoneId.systemDefault()); ZonedDateTime zny = ldt.atZone(ZoneId.of("America/New_York")); System.out.println(zbj); System.out.println(zny); } } //这种方法获取到的是看似一样的时间,但是时区不同所以是不同的时刻
时区转换
public class Main { public static void main(String[] args) { // 以中国时区获取当前时间: ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")); // 转换为纽约时间: ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York")); System.out.println(zbj); System.out.println(zny); } }
涉及到时区时,千万不要自己计算时差,否则难以正确处理夏令时。
4. DateTimeFormatter
不变,线程安全
public class Main { public static void main(String[] args) { ZonedDateTime zdt = ZonedDateTime.now(); var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ"); System.out.println(formatter.format(zdt)); var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA); System.out.println(zhFormatter.format(zdt)); var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US); System.out.println(usFormatter.format(zdt)); } }
5. Instant
Instant.now()
获取当前时间戳,效果和System.currentTimeMillis()
类似,它可以和ZonedDateTime
以及long
互相转换
4. 最佳实践
旧转新API,可以将Date或者Calendar转为Instant,再转为新的ZonedDatetime
// Date -> Instant: Instant ins1 = new Date().toInstant(); // Calendar -> Instant -> ZonedDateTime: Calendar calendar = Calendar.getInstance(); Instant ins2 = calendar.toInstant(); ZonedDateTime zdt = ins2.atZone(calendar.getTimeZone().toZoneId());
新转旧,只能借助long
// ZonedDateTime -> long: ZonedDateTime zdt = ZonedDateTime.now(); long ts = zdt.toEpochSecond() * 1000; // long -> Date: Date date = new Date(ts); // long -> Calendar: Calendar calendar = Calendar.getInstance(); calendar.clear(); calendar.setTimeZone(TimeZone.getTimeZone(zdt.getZone().getId())); calendar.setTimeInMillis(zdt.toEpochSecond() * 1000);
在数据存日期和时间
除了旧式的java.util.Date
,我们还可以找到另一个java.sql.Date
,它继承自java.util.Date
,但会自动忽略所有时间相关信息。这个奇葩的设计原因要追溯到数据库的日期与时间类型。
在数据库中,也存在几种日期和时间类型:
DATETIME
:表示日期和时间;DATE
:仅表示日期;TIME
:仅表示时间;TIMESTAMP
:和DATETIME
类似,但是数据库会在创建或者更新记录的时候同时修改TIMESTAMP
在使用Java程序操作数据库时,我们需要把数据库类型与Java类型映射起来。下表是数据库类型与Java新旧API的映射关系:
数据库 | 对应Java类(旧) | 对应Java类(新) |
---|---|---|
DATETIME | java.util.Date | LocalDateTime |
DATE | java.sql.Date | LocalDate |
TIME | java.sql.Time | LocalTime |
TIMESTAMP | java.sql.Timestamp | LocalDateTime |
在数据库中,我们需要存储的最常用的是时刻(Instant
),因为有了时刻信息,就可以根据用户自己选择的时区,显示出正确的本地时间。所以,最好的方法是直接用长整数long
表示,在数据库中存储为BIGINT
类型。
一个例子
import java.time.*; import java.time.format.*; import java.util.Locale; public class Main { public static void main(String[] args) { long ts = 1574208900000L; System.out.println(timestampToString(ts, Locale.CHINA, "Asia/Shanghai")); System.out.println(timestampToString(ts, Locale.US, "America/New_York")); } static String timestampToString(long epochMilli, Locale lo, String zoneId) { Instant ins = Instant.ofEpochMilli(epochMilli); DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT); return f.withLocale(lo).format(ZonedDateTime.ofInstant(ins, ZoneId.of(zoneId))); } }
感谢廖雪峰老师,原文:[Java教程:日期与时间-廖雪峰的官方网站](