Java 的时间和日期 API 在 1.0 有一个 Date 类,事后证明它过于简单,1.1 引入 Calendar 类之后,Date 类中的大部分方法就被弃用了。但是,Calendar API 还不够给力。第3次升级很吸引人,那就是 Java SE 8 中引入的 java.time API,它修正了过去的缺陷。
日期时间相关类
JDK 1.0 - java.util.Date 类
java.util 包提供了 Date 类来封装当前的日期和时间。Date 类其内部存储一个 long 类型的整数,而这个整数的值就是相对于英国格林尼治标准时间(1970年1月1日0时0分0秒)的毫秒数。现在的 Date 类中接近百分之八十的方法都已废弃,被标记为 @Deprecated。目前给 Date 的定位是,唯一表示一个时刻,所以它的内部应该围绕着那个整型的毫秒,而不再着重于各种年历时区等信息。
Date 允许通过以下两种构造器实例化一个对象:
private transient long fastTime;
public Date() {
this(System.currentTimeMillis());
}
public Date(long date) {
fastTime = date;
}
这里的 fastTime 属性存储的就是时刻所对应的毫秒数,两个构造器还是很简单,如果调用的是无参构造器,那么虚拟机将以系统当前的时刻值对 fastTime 进行赋值。
Date 对象创建以后,可以调用一些方法,以下列出了为数不多没有被废弃的方法。
版本 | 方法和描述 |
---|---|
1.0 |
public long getTime() 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来,此 Date 对象表示的毫秒数。 |
1.0 |
public void setTime(long time) 用自 1970 年 1 月 1 日 00:00:00 GMT 以后,time 毫秒数设置时间和日期。 |
1.0 |
public boolean before(Date when) 当调用此方法的 Date 对象在指定日期之前,则返回 true,否则返回 false。 |
1.0 |
public boolean after(Date when) 当调用此方法的 Date 对象在指定日期之后,则返回 true,否则返回 false。 |
1.2 |
public int compareTo(Date anotherDate) 比较当调用此方法的 Date 对象和指定日期。两者相等时,返回 0。调用对象在指定日期之前,则返回负数。调用对象在指定日期之后,则返回正数。 |
1.8 |
public static Date from(Instant instant) 将 Java 8 中的新日期时间 Instant 对象转换为 Date 对象。 |
1.8 |
public Instant toInstant() 将当前的 Date 对象转换为 Java 8 中新日期时间 Instant 对象。 |
JDK 1.1 - java.util.Calendar 类
Calendar 用于表示年月日等日期信息,它是一个抽象类,所以一般通过以下四种工厂方法获取它的实例对象。
public static Calendar getInstance()
{
return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}
public static Calendar getInstance(TimeZone zone)
{
return createCalendar(zone, Locale.getDefault(Locale.Category.FORMAT));
}
public static Calendar getInstance(Locale aLocale)
{
return createCalendar(TimeZone.getDefault(), aLocale);
}
public static Calendar getInstance(TimeZone zone,
Locale aLocale)
{
return createCalendar(zone, aLocale);
}
其实内部最终会调用同一个内部方法:
private static Calendar createCalendar(TimeZone zone, Locale aLocale)
该方法需要两个参数,一个是时区,一个是国家和语言,也就是说,构建一个 Calendar 实例最少需要提供这两个参数信息,否则将会使用系统默认的时区或语言信息。
因为不同的时区与国家语言对于时刻和年月日信息的输出是不同的,所以这也是为什么一个 Calendar 实例必须传入时区和国家信息的一个原因。
Calendar 中也定义了很多静态常量和一些属性数组:
public final static int ERA = 0;
public final static int YEAR = 1;
public final static int MONTH = 2;
public final static int WEEK_OF_YEAR = 3;
public final static int WEEK_OF_MONTH = 4;
public final static int DATE = 5;
public final static int DAY_OF_MONTH = 5;
public final static int DAY_OF_YEAR = 6;
public final static int DAY_OF_WEEK = 7;
public final static int DAY_OF_WEEK_IN_MONTH = 8;
public final static int AM_PM = 9;
public final static int HOUR = 10;
public final static int HOUR_OF_DAY = 11;
public final static int MINUTE = 12;
public final static int SECOND = 13;
public final static int MILLISECOND = 14;
......
protected int fields[];
protected boolean isSet[];
有关日期的所有相关信息都存储在属性数组中,而这些静态常量的值往往表示的就是一个索引值。
java 8 java.time API
Java 8 中一个新增的重要特性就是引入了新的时间和日期 API,它们被包含在 java.time
包中。借助新的时间和日期 API 可以以更简洁的方法处理时间和日期。
在 Java 8 之前,所有关于时间和日期的 API 都存在各种使用方面的缺陷,主要有:
- Java 的
java.util.Date
和java.util.Calendar
类易用性差,不支持时区,而且它们都不是线程安全的。 - 用于格式化日期的类
DateFormat
被放在java.text
包中,它是一个抽象类,所以我们需要实例化一个SimpleDateFormat
对象来处理日期格式化,并且DateFormat
也是非线程安全,这意味着如果你在多线程程序中调用同一个DateFormat
对象,会得到意想不到的结果。 - 对日期的计算方式繁琐,而且容易出错,因为月份是从 0 开始的,从
Calendar
中获取的月份需要加一才能表示当前月份。
由于以上这些问题,出现了一些三方的日期处理框架,例如 Joda-Time,date4j 等开源项目。但是,Java 需要一套标准的用于处理时间和日期的框架,于是 Java 8 中引入了新的日期 API。新的日期 API 是 JSR-310 规范的实现,Joda-Time 框架的作者正是 JSR-310 的规范的倡导者,所以能从 Java 8 的日期 API 中看到很多 Joda-Time 的特性。
Java 8 的日期和时间类包含 LocalDate、LocalTime、Instant、Duration 以及 Period,这些类都包含在 java.time
包中。
LocalDate 和 LocalTime
LocalDate 类表示一个具体的日期,但不包含具体时间,也不包含时区信息。可以通过 LocalDate 的静态方法 of() 创建一个实例,LocalDate 也包含一些方法用来获取年份,月份,天,星期几等:
LocalDate localDate = LocalDate.of(2017, 1, 4); // 初始化一个日期:2017-01-04
int year = localDate.getYear(); // 年份:2017
Month month = localDate.getMonth(); // 月份:JANUARY
int dayOfMonth = localDate.getDayOfMonth(); // 月份中的第几天:4
DayOfWeek dayOfWeek = localDate.getDayOfWeek(); // 一周的第几天:WEDNESDAY
int length = localDate.lengthOfMonth(); // 月份的天数:31
boolean leapYear = localDate.isLeapYear(); // 是否为闰年:false
也可以调用静态方法 now()
来获取当前日期:
LocalDate now = LocalDate.now();
LocalTime
和 LocalDate
类似,他们之间的区别在于 LocalDate
不包含具体时间,而 LocalTime
包含具体时间,例如:
LocalTime localTime = LocalTime.of(17, 23, 52); // 初始化一个时间:17:23:52
int hour = localTime.getHour(); // 时:17
int minute = localTime.getMinute(); // 分:23
int second = localTime.getSecond(); // 秒:52
LocalDateTime
LocalDateTime
类是 LocalDate
和 LocalTime
的结合体,可以通过 of()
方法直接创建,也可以调用 LocalDate
的 atTime()
方法或 LocalTime
的 atDate()
方法将 LocalDate
或 LocalTime
合并成一个 LocalDateTime
:
LocalDateTime ldt1 = LocalDateTime.of(2017, Month.JANUARY, 4, 17, 23, 52);
LocalDate localDate = LocalDate.of(2017, Month.JANUARY, 4);
LocalTime localTime = LocalTime.of(17, 23, 52);
LocalDateTime ldt2 = localDate.atTime(localTime);
LocalDateTime
也提供用于向 LocalDate
和 LocalTime
的转化:
LocalDate date = ldt1.toLocalDate();
LocalTime time = ldt1.toLocalTime();
Instant
Instant
用于表示一个时间戳,它与我们常使用的 System.currentTimeMillis()
有些类似,不过 Instant
可以精确到纳秒(Nano-Second),System.currentTimeMillis()
方法只精确到毫秒(Milli-Second)。如果查看 Instant
源码,发现它的内部使用了两个常量,seconds
表示从 1970-01-01 00:00:00 开始到现在的秒数,nanos
表示纳秒部分(nanos
的值不会超过 999,999,999
)。Instant
除了使用 now()
方法创建外,还可以通过 ofEpochSecond
方法创建:
Instant instant = Instant.ofEpochSecond(120, 100000);
ofEpochSecond()
方法的第一个参数为秒,第二个参数为纳秒,上面的代码表示从 1970-01-01 00:00:00 开始后两分钟的10万纳秒的时刻,控制台上的输出为:
1970-01-01T00:02:00.000100Z
Duration
Duration
的内部实现与 Instant
类似,也是包含两部分:seconds
表示秒,nanos
表示纳秒。两者的区别是 Instant
用于表示一个时间戳(或者说是一个时间点),而 Duration
表示一个时间段,所以 Duration
类中不包含 now()
静态方法。可以通过 Duration.between()
方法创建 Duration
对象:
LocalDateTime from = LocalDateTime.of(2017, Month.JANUARY, 5, 10, 7, 0); // 2017-01-05 10:07:00
LocalDateTime to = LocalDateTime.of(2017, Month.FEBRUARY, 5, 10, 7, 0); // 2017-02-05 10:07:00
Duration duration = Duration.between(from, to); // 表示从 2017-01-05 10:07:00 到 2017-02-05 10:07:00 这段时间
long days = duration.toDays(); // 这段时间的总天数
long hours = duration.toHours(); // 这段时间的小时数
long minutes = duration.toMinutes(); // 这段时间的分钟数
long seconds = duration.getSeconds(); // 这段时间的秒数
long milliSeconds = duration.toMillis(); // 这段时间的毫秒数
long nanoSeconds = duration.toNanos(); // 这段时间的纳秒数
Duration
对象还可以通过 of()
方法创建,该方法接受一个时间段长度,和一个时间单位作为参数:
Duration duration1 = Duration.of(5, ChronoUnit.DAYS); // 5天
Duration duration2 = Duration.of(1000, ChronoUnit.MILLIS); // 1000毫秒
Period
Period
在概念上和 Duration
类似,区别在于 Period
是以年月日来衡量一个时间段,比如 2 年 3 个月 6天:
Period period = Period.of(2, 3, 6);
Period
对象也可以通过 between()
方法创建,值得注意的是,由于 Period
是以年月日衡量时间段,所以 between() 方法只能接收 LocalDate 类型的参数:
// 2017-01-05 到 2017-02-05 这段时间
Period period = Period.between(
LocalDate.of(2017, 1, 5),
LocalDate.of(2017, 2, 5));