-
Notifications
You must be signed in to change notification settings - Fork 0
Java time operation
在处理时间相关的问题的时候,首先需要明确几个基本概念:时间戳,绝对时间和相对时间。
时间戳在广义上是指,能够唯一地标识某一刻的时间。在狭义上(下文中的时间戳都按狭义的概念理解)就是一个long类型的值,指代某一个时刻。比如在Java和其他一些语言里面long timestamp = 0L
,就表示0时区里1970年1月1日0时0分0秒。相应换算成我们常用的北京时间(+08:00)的话,就是1970年1月1日8时0分0秒。
这里要说的绝对时间主要是指,long类型和带时区的字符串类型。比如0L和"1970-1-1 00:00:00+00:00”,这样就能唯一确定一个时刻。相应的,"1970-1-1 00:00:00”就是一个相对时间。如果不提供时区信息,就无法准确定位某一个时刻。
在日常的开发中,往往需要做一些时间相关的处理,比如用户输入了一个时间戳,想把它转化为一个“可读”的日期字符等。
Joda-Time provides a quality replacement for the Java date and time classes.
在jdk1.7以前,java内置的时间处理相关的类功能比较弱,还有一些并发上的问题,所以多用joda这个库中所提供的API来操作时间。
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.1</version>
</dependency>
Joda中最常用的类有3个。
- Instant - 封装了时间戳的对象
- DateTimeZone - 时区类
- DateTime - full date and time with time-zone
import org.joda.time.DateTime;
import org.joda.time.Instant;
// 获取当前系统时间戳
Instant now = Instant.now();
// 从给定的long类型数字构造事假戳对象
Instant instant = Instant.ofEpochMilli(0);
// 获取这个对象保存的时间戳
instant.getMillis();
// 转化为DateTime对象
DateTime dateTime = instant.toDateTime();
// 从一个字符串中按ISO格式解析并构造时间戳对象
Instant newYear1 = Instant.parse("2019-1-1T00:00:00+08:00");
// 从一个字符串中按自定义格式解析并构造时间戳对象
Instant newYear2 = Instant.parse("2019.1.1T00:00:00+08:00", DateTimeFormat.forPattern("yyyy.MM.dd'T'HH:mm:ssZZ"));
import org.joda.time.DateTimeZone;
// 获取当前默认的时区
DateTimeZone now = DateTimeZone.getDefault();
// 都是构造+08:00时区
DateTimeZone bejing = DateTimeZone.forID("+08:00");
DateTimeZone shanghai =DateTimeZone.forID("Asia/Shanghai");
// 获取DateTimeZone对象保存的时区信息
System.out.println(shanghai.getID());
// 获取所有可用的时区
for(String zone : DateTimeZone.getAvailableIDs()) {
System.out.println(zone);
}
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
// 用系统当前时间戳和默认时区构造
DateTime dateTime1 = new DateTime();
DateTime dateTime5 = DateTime.now();
// 用给定时间戳和默认时区构造
DateTime dateTime2 = new DateTime(0);
// 用给定时间戳和给定的时区构造
DateTime dateTime3 = new DateTime(0, DateTimeZone.getDefault());
// 用系统当前时间戳和给定的时区构造
DateTime dateTime6 = DateTime.now(DateTimeZone.getDefault());
// 从一个字符串中按ISO格式解析并构造DateTime对象
DateTime dateTime7 = DateTime.parse("2019-1-1T00:00:00+08:00");
// 从一个字符串中按自定义格式解析并构造DateTime对象
DateTime dateTime8 = DateTime.parse("2019.1.1T00:00:00+08:00", DateTimeFormat.forPattern("yyyy.MM.dd'T'HH:mm:ssZZ"));
// 转化为时间戳
long timestamp = dateTime8.getMillis();
// 获取保存的时区
DateTimeZone zone = dateTime8.getZone();
// 按ISO格式转化成字符串
System.out.println(dateTime8.toString());
// 按自定义格式转化成字符串
System.out.println(dateTime8.toString("yyyy.MM.dd'T'HH:mm:ssZZ"));
System.out.println(dateTime8.toString(DateTimeFormat.forPattern("yyyy/MM/dd'T'HH:mm:ssZZ")));
在JDK1.8之后,Joda就已经作为官方标准加入到了Java内部类中,放置在java.time
包下,但名字和用法发生了相应的变化。
Joda-Time is the de facto standard date and time library for Java prior to Java SE 8. Users are now asked to migrate to java.time (JSR-310).
其中主要的变化有两个。
- ZoneId - 和DateTimeZone用法和作用范围基本一致,API的名字略微变化
- ZoneOffset - ZoneId的子集,只能接受
+08:00
的格式,不支持Asia/Shanghai
的格式
// 将ZoneId转化为ZoneOffset
public static ZoneOffset toZoneOffset(ZoneId zoneId) {
return zoneId.getRules().getOffset(Instant.now());
}
- LocalDateTime - 只提供,不带时区的相对时间(比如"2019-1-1T00:00:00")相关的处理。如果让想要从一个带时区的字符串中构造LocalDateTime,将报错
- ZonedDateTime - 只提供,带时区的时间(比如"2019-1-1T00:00:00+08:00")相关的处理。如果让想要从一个不带时区的字符串中构造ZonedDateTime,将报错
- OffsetDateTime - ZonedDateTime的子集,也只能处理带时区的字符串,而且时区的格式接受
+08:00
的格式,不支持Asia/Shanghai
的格式
ZonedDateTime
和OffsetDateTime
都可以接受一个时间戳作为构造函数的参数,但需要先手动封装成java.time.Instant对象。
可以看到,将DateTime分解之后,通用性降低了,用户需要知道自己的字符串是不是带时区的,才能选择对应的类进行处理,不像Joda中直接用DateTime
就可以了。虽然降低了解析出错的可能性,但是确实给编程的增加了很多麻烦。
假如能够保证输入的日期是带时区的,但是格式可能是"yyyy.MM.dd'T'HH:mm:ssZZ",也可能是"yyyy/MM/dd HH:mm:ssZZ",怎么优雅地解析这些字符串呢?
从下面的代码中可以看到,先用DateTimeFormatter构造若干种基本的模式('2011-12-03','2011/12/03','10:15:30'),然后再把它们组合起来。
public class DatetimeUtils {
// such as '2011-12-03'.
public static final DateTimeFormatter ISO_LOCAL_DATE_WIDTH_1_2;
static {
ISO_LOCAL_DATE_WIDTH_1_2 = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NEVER)
.appendLiteral('-')
.appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NEVER)
.toFormatter();
}
// such as '2011/12/03'.
public static final DateTimeFormatter ISO_LOCAL_DATE_WITH_SLASH;
static {
ISO_LOCAL_DATE_WITH_SLASH = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
.appendLiteral('/')
.appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NEVER)
.appendLiteral('/')
.appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NEVER)
.toFormatter();
}
// such as '10:15:30' or '10:15:30.123'.
public static final DateTimeFormatter ISO_LOCAL_TIME_WITH_MS;
static {
ISO_LOCAL_TIME_WITH_MS = new DateTimeFormatterBuilder()
.appendValue(ChronoField.HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
.appendLiteral(':')
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
.optionalStart()
.appendLiteral('.')
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.optionalEnd()
.toFormatter();
}
// such as '2011-12-03T10:15:30+01:00' or '2011-12-03T10:15:30.123+01:00'.
public static final DateTimeFormatter ISO_OFFSET_DATE_TIME_WITH_MS;
static {
ISO_OFFSET_DATE_TIME_WITH_MS = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(ISO_LOCAL_DATE_WIDTH_1_2)
.appendLiteral('T')
.append(ISO_LOCAL_TIME_WITH_MS)
.appendOffsetId()
.toFormatter();
}
// such as '2011/12/03T10:15:30+01:00' or '2011/12/03T10:15:30.123+01:00'.
public static final DateTimeFormatter ISO_OFFSET_DATE_TIME_WITH_SLASH;
static {
ISO_OFFSET_DATE_TIME_WITH_SLASH = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.append(ISO_LOCAL_DATE_WITH_SLASH)
.appendLiteral('T')
.append(ISO_LOCAL_TIME_WITH_MS)
.appendOffsetId()
.toFormatter();
}
public static final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
// such as '2011-12-03T10:15:30+01:00' or '2011-12-03T10:15:30.123+01:00'.
.appendOptional(ISO_OFFSET_DATE_TIME_WITH_MS)
// such as '2011/12/03T10:15:30+01:00' or '2011/12/03T10:15:30.123+01:00'.
.appendOptional(ISO_OFFSET_DATE_TIME_WITH_SLASH)
.toFormatter();
public static long convertDatetimeStrToLong(String str) {
ZonedDateTime zonedDateTime = ZonedDateTime.parse(str, formatter);
return zonedDateTime.toInstant().toEpochMilli();
}
}
做项目的时候发现了一个容易忽略的问题,如果自己试一下下面的代码会发现,所有表示为"+00:00"的ZoneOffset的toString()方法返回的都是"Z",而不是预想中的"+00:00"。
因此如果通过解析ZoneOffset的数字,计算和0时区差多少小时的时候,要特别注意它自己是不是零时区。
// convert ZoneId to ZoneOffset
public static ZoneOffset toZoneOffset(ZoneId zoneId) {
return zoneId.getRules().getOffset(Instant.now());
}
public static void main(String[] args){
for(String zoneId : ZoneId.getAvailableZoneIds()){
System.out.println(zoneId + ": " + DatetimeUtils.toZoneOffset(ZoneId.of(zoneId)));
}
}
--------------------
...
Africa/Casablanca: Z
Europe/Belfast: Z
America/Fortaleza: -03:00
Etc/GMT0: Z
Africa/Dakar: Z
Jamaica: -05:00
Africa/Bissau: Z
Asia/Tehran: +03:30
WET: Z
Europe/Astrakhan: +04:00
UCT: Z
EET: +02:00
Etc/UTC: Z
...