Java日期时间API完全解析

时区

GMT(Greenwich Mean Time):格林尼治时间,格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间。

UTC(Universal Time Coordinated):统一协调时间,其以原子时秒长为基础,在时刻上尽量接近于格林尼治标准时间,标准 UTC 时间格式 yyyy-MM-dd’T’HH:mm:ss.SSSXXX。

格林尼治时间已经不再被作为标准时间使用,UTC 是最主要的世界时间标准。

Java提供了获取当前时间的方法

  • System.currentTimeMillis() 返回当前时间,以毫秒为单位。
    表示是当前时刻至 1970-01-01 00:00:00.000 的毫秒差值。返回的long值可以用来初始化java.util.Date, java.sql.Date, java.sql.Timestamp和java.util.GregorianCalendar对象。

  • System.nanoTime() 返回一个时间值(系统计时器的当前值),精确到纳秒。
    它是由 JVM 提供的一个时间,主要用来精确衡量两个时间段之间的时间

时间粒度:System.currentTimeMillis()方法的时间粒度是大于1毫秒的。如果你反复执行这个方法,短时间内得到的结果是相同的,或又突然在某一次结果增加了几十毫秒,这是正常的。

旧的时间API

  • Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。
  • java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
  • 对于时间、时间戳、格式化以及解析,并没有一些明确定义的类。对于格式化和解析的需求,我们有java.text.DateFormat抽象类,但通常情况下,SimpleDateFormat类被用于此类需求。
  • 所有的日期类都是可变的,因此他们都不是线程安全的,这是Java日期类最大的问题之一。
  • 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
java.util.Date

java.util.Date类用于封装日期及时间信息,一般仅用它显示某个日期,不对他作任何操作处理,作处理推荐用Calendar类,计算方便。

构造方法

Date() :分配 Date对象并初始化此对象,以表示分配它的时间(精确到毫秒)。

Date(long date):分配 Date对象并初始化此对象,以表示自从标准基准时间(称为“历元(epoch)”,即 1970 年 1 月 1 日 00:00:00 GMT)以来的指定毫秒数。

Calendar与GregorianCalendar

java.util.Calendar类用于封装日历信息,其主作用在于其方法可以对时间分量进行运算。Calendar类是一个抽象类,它为特定瞬间与一组诸如 YEAR、MONTH、DAY_OF_MONTH、HOUR 等 日历字段之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法。

Calendar类是抽象类,构造方法是protected的,API中提供了getInstance方法用来创建对象。

Java只提供java.util.GregorianCalendar这一种java.util.Calendar的实现类。

SimpleDateFormat

SimpleDateFormat(String pattern),pattern -为描述日期和时间格式的模式,注:此构造方法可能不支持所有语言环境。要覆盖所有语言环境,请使用 DateFormat 类中的工厂方法。

1
2
public final String format(Date date)将一个 Date 格式化为日期/时间字符串
public Date parse(String source)throws ParseException从给定字符串的开始解析文本,以生成一个日期。
DateFormat

java.text.DateFormat类(抽象类)是SimpleDateFormat类的父类

java.sql.Date

java.sql.Date继承java.util.Date,为了把前者转为后者,不支持Date参数的构造器,传入long类型的时间。

java.sql.Date是SQL中的单纯的日期类型,没有时分秒。如果同时需要日期和时间,应该使用Timestamp。它也是 java.util.Date 的子类,Timestamp 则包含时间戳的完整信息。

java.sql.Timestamp是java.util.Date的派生类(继承),所以在java.util.Date上能做的事,也可以在java.sql.Timestamp上做。

现在的Date类中大部分方法已经弃用,现在一般使用旧的API,Date只负责存储一个时间,并对Calendar和DateFormat提供操作接口。Calendar获取Date中特定的信息,对日期时间进行操作,SimpleDateFormat对日期时间进行格式化输入输出。

总的来说,Date、Calendar 和 DateFormat 已经能够处理一般的时间日期问题了。但是它们依然很繁琐,不好用并且这些日期类都是可变且线程不安全的。

Java 8 时间日期API

特性

1
2
3
4
5
不变性:新的日期/时间API中,所有的类都是不可变的,这对多线程很有好处。
关注点分离:借鉴了Joda库的一些优点,新的API将人可读的日期时间和机器时间(unix timestamp)明确分离,它为日期(Date)、时间(Time)、日期时间(DateTime)、时间戳(unix timestamp)以及时区定义了不同的类。
清晰:在所有的类中,方法都被明确定义用以完成相同的行为。例如要拿到当前实例我们可以使用now()方法,在所有的类中都定义了format()和parse()方法,而不是像以前那样专门有一个独立的类。为了更好的处理问题,所有的类都使用了工厂模式和策略模式,一旦你使用了其中某个类的方法,与其他类协同工作并不困难。
实用操作:所有新的日期/时间API类都实现了一系列方法用以完成通用的任务,如:加、减、格式化、解析、从日期/时间中提取单独部分,等等。
可扩展性:新的日期/时间API是工作在ISO-8601日历系统上的,但我们也可以将其应用在非IOS的日历上。

Java8日期时间的默认格式如下:yyyy-MM-dd-HH-mm-ss.zzz

主要的 核心类:

1
2
3
4
5
6
7
8
9
10
11
12
LocalDate:日期类,不带时间
LocalTime:时间类,不带日期
LocalDateTime:日期和时间类
ZonedDateTime:时区日期时间类
OffsetDateTime:按UTC时间偏移来得到日期时间
Clock:获取某个时区下当前的瞬时时间,日期或者时间
Instant:Unix时间,代表时间戳,比如 2018-01-14T02:20:13.592Z
Duration:两个时间之间,表示一个绝对的精确跨度,使用毫秒为单位
Period:两个日期之间
ZoneId:时区
DateTimeFormatter:格式化输出
TemporalAdjusters:获得指定日期时间等,如当月的第一天、今年的最后一天等等
LocalDate、LocalTime、LocalDateTime

LocalDate是不变的日期时间对象代表一个日期,往往被视为年月日。其他日期字段,如一年中的一天,一周和一周的一天,也可以访问。

LocalTime是不变的日期时间对象代表一个时间,往往被视为小时分钟秒。时间为代表的纳秒级精度。

LocalDateTime是不变的日期时间对象代表一个日期时间,往往被视为年、月、日、时、分、秒。其他日期和时间字段,如一年中的一天,一周和一周的一天,也可以访问。时间为代表的纳秒级精度。

DateTimeFormatter

格式器用于解析日期字符串和格式化日期输出,创建格式器最简单的方法是通过 DateTimeFormatter 的静态工厂方法以及常量。创建格式器一般有如下三种方式:

1
2
3
常用 ISO 格式常量,如 ISO_LOCAL_DATE
字母模式,如 ofPattern("yyyy/MM/dd")
本地化样式,如 ofLocalizedDate(FormatStyle.MEDIUM)

使用DateTimeFormatter完成格式化

1
2
3
4
5
6
7
8
9
LocalDateTime localDateTime = LocalDateTime.now();
//创建一个格式化程序使用指定的模式。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatDateTime = localDateTime.format(formatter);
System.out.println(formatDateTime);

//DateTimeFormatter提供了一些默认的格式化器,DateTimeFormatter.ISO_LOCAL_DATE_TIME 格式 yyyy-MM-ddTHH:mm:ss.SSS
String dateTime2 = localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println(dateTime2);

使用DateTimeFormatter完成格式化

1
2
LocalDate localDate = LocalDate.parse("2018/11/11",DateTimeFormatter.ofPattern("yyyy/MM/dd"));
System.out.println(localDate); //2018-11-11

和旧的 java.util.DateFormat 相比较,所有的 DateTimeFormatter 实例都是线程安全的。

Instant

Instant 表示时间线上的一点(与 Date 类似),它只是简单地表示自 1970 年 1 月 1 日 0 时 0 分 0 秒(UTC)开始的秒数。

Instant 由两部分组成,一是从原点开始到指定时间点的秒数 s, 二是距离该秒数 s 的纳秒数。它以 Unix 时间戳的形式存储日期时间。

Instant类的工厂方法创建一个Instant实例

1
2
3
4
5
6
7
Instant now = Instant.now();
//第一个参数是秒,第二个是纳秒参数,纳秒的存储范围是0至999,999,999 2s之后的在加上100万纳秒(1s)
Instant instant = Instant.ofEpochSecond(2,1000000000);
System.out.println(instant); //1970-01-01T00:00:03Z
//java.util.Date与Instant可相互转换
Instant timestamp = new Date().toInstant();
Date.from(Instant.now());
Clock

Clock用于查找当前时刻,可以用来获取某个时区下当前的日期和时间,也可以用来代替旧的System.currentTimeMillis()方法和TimeZone.getDefault()方法。

Duration

一个Duration实例是不可变的,当创建出对象后就不能改变它的值了。你只能通过Duration的计算方法,来创建出一个新的Durtaion对象.

Period

Period 是以年月日来衡量一个时间段,用于计算两个日期间隔,所以 between() 方法只能接收 LocalDate 类型的参数。

ZonedDateTime和ZonedId

ZonedDateTime类是Java 8中日期时间功能里,用于表示带时区的日期与时间信息的类。ZonedDateTime 类的值是不可变的,所以其计算方法会返回一个新的ZonedDateTime 实例。

Java 使用 ZoneId 来标识不同的时区,从基准 UTC 开始的一个固定偏移。

TemporalAdjusters

有的时候,你需要进行一些更加复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。

1
2
3
4
5
6
7
8
9
10
11
LocalDate localDate = LocalDate.now();  
// 1. 本月第一天
LocalDate firstDayOfMonth = localDate.with(TemporalAdjusters.firstDayOfMonth());
// 2. 本月最后一天
LocalDate lastDayOfMonth = localDate.with(TemporalAdjusters.lastDayOfMonth());
// 3. 本年第一天
LocalDate firstDayOfYear = localDate.with(TemporalAdjusters.firstDayOfYear());
// 4. 下个月第一天
LocalDate firstDayOfNextMonth = localDate.with(TemporalAdjusters.firstDayOfNextMonth());
// 5. 本年度最后一天
LocalDate lastDayOfYear = localDate.with(TemporalAdjusters.lastDayOfYear());

用重载版本的with方法,向其传递一个提供了更多定制化选择的TemporalAdjuster对象,更加灵活地处理日期。TemporalAdjusters类通过静态方法提供了大量的常用的TemporalAdjuster的实现供我们使用。

这些日期类之间转换

java.util.Date 与 LocalDate、LocalTime、LocalDateTime 转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//将Date转换为LocalDate,LocalTime,LocalDateTime可以借助于ZonedDateTime和Instant,实现如下:

Date date = new Date();

// Date -> LocalDateTime
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
System.out.println("localDateTime by Instant: " + localDateTime);

// Date -> LocalDate
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

// Date -> LocalTime
LocalTime localTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalTime();

//2. Date -> LocalDateTime
localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
System.out.println("localDateTime by ofInstant: " + localDateTime);

由于JDK8实现了向下兼容,所以Date里在JDK8版本引入了2个方法,from和toInstant,所以我们可以借助这两个方法来实现LocalDateTime到Date的转换。
将LocalDateTime转为Date如下:

1
2
3
4
5
6
7
8
LocalDateTime localDateTime = LocalDateTime.now();

// LocalDateTime -> Date
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());

// LocalDate -> Date,时间默认都是00
LocalDate localDate = LocalDate.now();
date = Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());

日期与字符串的转换

1
2
3
4
5
6
7
8
9
10
11
12
//通过LocalDate,LocalTime,LocalDateTime的parse方法和DateTimeFormatter来实现:

//字符串->日期
LocalDate localDate = LocalDate.parse("2018-09-09", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
LocalDateTime localDateTime = LocalDateTime.parse("2018-09-10 12:12:12", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

//日期->字符串
String localDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
String localDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 也可以通过DateTimeFormatter的format方法
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
localDateTime = dateTimeFormatter.format(LocalDateTime.now());

时间戳与LocalDateTime转换

1
2
3
4
5
6
7
8
9
10
11
12
13
//时间戳->LocalDateTime
public static LocalDateTime convertToDate(long timestamp) {
// ofEpochSecond 以秒为单位, ofEpochMilli 以毫秒为单位
// Instant.ofEpochSecond(timestamp);
Instant instant = Instant.ofEpochMilli(timestamp);
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
}

//LocalDateTime->时间戳
public static long convertToTimestamp() {
LocalDateTime localDateTime = LocalDateTime.now();
return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}

新的 API 区分各种日期时间概念并且各个概念使用相似的方法定义模式,这种相似性非常有利于 API 的学习。总结一下一般的方法或者方法前缀:

1
2
3
4
5
6
7
8
9
10
11
of:静态工厂方法,用于创建实例
now:静态工厂方法,用当前时间创建实例
parse:静态工厂方法,从字符串解析得到对象实例
get:获取时间日期对象的部分状态。
is:检查某些东西的是否是 true,例如比较时间前后
with:返回一个部分状态改变了的时间日期对象拷贝
plus:返回一个时间增加了的、时间日期对象拷贝
minus:返回一个时间减少了的、时间日期对象拷贝
to:转换到另一个类型
at:把这个对象与另一个对象组合起来,例如 date.atTime(time)
format:提供格式化时间日期对象的能力

参考文章:https://www.cnblogs.com/kiwenzhang/p/10960741.html

Joda-Time

Joda-Time提供了一组Java类包用于处理包括ISO8601标准在内的date和time。可以利用它把JDK Date和Calendar类完全替换掉,而且仍然能够提供很好的集成。

Joda 与 JDK 是百分之百可互操作的,因此您无需替换所有 Java 代码,只需要替换执行日期/时间计算的那部分代码。

如果时间需要按照今天,昨天这样返回,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class DateFormatUtil {
public static String format(Date date) {
DateTime now = new DateTime();
DateTime today_start = new DateTime(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), 0, 0, 0);
DateTime today_end = today_start.plusDays(1);
DateTime yesterday_start = today_start.minusDays(1);

if(date.after(today_start.toDate()) && date.before(today_end.toDate())) {
return String.format("今天 %s", new DateTime(date).toString("HH:mm:ss"));
} else if(date.after(yesterday_start.toDate()) && date.before(today_start.toDate())) {
return String.format("昨天 %s", new DateTime(date).toString("HH:mm:ss"));
}

return new DateTime(date).toString("yyyy-MM-dd HH:mm:ss");
}
public static String formats(String dateString) {
if(StringUtils.isEmpty(dateString)){
return "";
}
try {
SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = dateFormat.parse(dateString);
return format(date);
}catch (ParseException e){
e.printStackTrace();
}
return dateString;
}
/*public static void main(String[] args) throws ParseException {
System.out.println(formats("2019-06-01 10:12:05"));
System.out.println(formats("2019-07-12 10:12:05"));
System.out.println(formats("2019-07-11 10:12:05"));
System.out.println(formats("2019-07-10 10:12:05"));
}*/
}

需要引入包:

1
2
3
4
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>