本文目的:掌握 Java 中日期和时间常用 API 的使用。
参考:Jakob Jenkov的英文教程Java Date Time TutorialJavaDoc

概览

Java 8 新增 API

众所周知,在 Java 8 中添加了一个全新的日期时间 API 位于 java.time 包中,主要变化是,自1970年1月1日以来,日期和时间现在不再由单个毫秒数表示,而是由1970年1月1日以来的秒数和纳秒数表示。
秒数既可以是正的,也可以是负的,用 long 表示。纳秒数始终为正,由 int 表示。

Java 7 具有以下日期和时间类和方法:

应该使用所有这些类中的哪一个取决于想要做什么,如果你需要做简单的计时, System.currentTimeMillis() 方法就可以了。

  • 如果只需要一个对象来保存日期,例如作为简单域模型对象中的属性,则可以使用 java.util.Date 类。
  • 如果需要读取和写入数据库的日期和时间,则使用 java.sql.Date 和 java.sql.Timestamp 类。
  • 如果您需要进行日期计算,例如将日期或月份添加到另一个日期,或者查看工作日(星期一,星期二等)这些给定日期,或者转换时区之间的日期和时间,请使用 java.util.Calendar 和 java .util.GregorianCalendar 类。

System.currentTimeMillis()

currenttimemillis() 静态方法以毫秒为单位返回自1970年1月1日以来的时间。返回的值是long。这里有一个例子:

long timeNow = System.currentTimeMillis();

这个返回值可以用来初始化 java.util.Date, java.sql.Date, java.sql.Timestamp 和 java.util.GregorianCalendar 对象,它还可以用于在程序中测量时间。

currenttimemillis() 方法的粒度大于 1 毫秒,这取决于操作系统,还可能更大,许多操作系统以几十毫秒为单位测量时间。如果需要更精确的计时,请使用 System.nanoTime() ,但是这个方法返回的时间是从任意一个时刻计算的,甚至有可能是负数,所以不能用于初始化日期时间对象,只适合用于计算两个时间点的时间差。

java.util.Date

用来表示日期,包含年月日时分秒 ,目前该类中的大多数方法都不赞成使用了,一般用 Calendar 类来代替它,但还是有必要简单了解一下。
下面是一些使用例子:

Date dateNow = new Date(); // 使用当前日期和时间创建

Date 类的默认构造器,源码是这样的:

public Date() {
        this(System.currentTimeMillis());
}

也可以使用一个 long 型的有参构造函数:

Date date = new Date(long);

Date 类还有一个 getTime() 实例方法,这个方法的返回值就是 new Date(long) 时指定的 long 参数。

从 Java 8 开始,新增了和 Instant 互相转换的方法,关于 Instant 请参考本文下部分,这里了解就行:

static Date from(Instant instant);
Instant toInstant();

java.sql.Date

此类是上述 java.util.Date 类的子类,所以它继承了 java.util.Date 的所有方法和字段。一般在 JDBC API 中使用它,比如可以在 PreparedStatement 上设置日期,或者从 ResultSet 获取日期,

和 java.util.Date 最大的区别就是它只记日期,不记时间,即只有年月日,如果构造的时候包含了时间信息,那么时间信息会被舍弃,如果要记时间,需要用到 java.sql.Timestamp 类。

java.sql.Timestamp

此类也继承了 java.util.Date,包含的信息有年月日时分秒纳秒,是的,它还扩展了纳秒,一个使用示例如下:

long time = System.currentTimeMillis();
java.sql.Timestamp timestamp = new java.sql.Timestamp(time);

timestamp.setNanos(123456);
int nanos = timestamp.getNanos(); // nanos = 123456

java.util.Calendar 和 GregorianCalendar

Calendar 抽象类用于执行日期和时间换算,无法使用构造器实例化它,原因是世界上有不止一个日历。
但是其提供了一个 getInstance() 方法,可以获取对应当前时间的 Calendar 对象:

Calendar rightNow = Calendar.getInstance();

getInstance() 方法底层是如下这样实现的:

public static Calendar getInstance() {
        return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}

没错,很容易想到,此方法还有重载的,可以提供部分指定初始化参数的版本,如下:

getInstance(TimeZone zone);
getInstance(Locale aLocale);
getInstance(TimeZone zone, Locale aLocale);

此外,一般可以通过其子类 GregorianCalendar 来访问日期时间信息,一个例子如下:

Calendar calendar = new GregorianCalendar();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); // 一月 Jan = 0, 不是 1
int dayOfWeek  = calendar.get(Calendar.DAY_OF_WEEK);
int weekOfYear = calendar.get(Calendar.WEEK_OF_YEAR);
int weekOfMonth= calendar.get(Calendar.WEEK_OF_MONTH);

int hour = calendar.get(Calendar.HOUR);        // 12 小时制
int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY); // 24 小时制
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int millisecond= calendar.get(Calendar.MILLISECOND);

calendar.set(Calendar.YEAR, 2018);
calendar.set(Calendar.MONTH, 11); // 11 = december,十二月
calendar.set(Calendar.DAY_OF_MONTH, 24); // 圣诞节

年月日等的加减

Calendar calendar = new GregorianCalendar();
// 加 1 天
calendar.add(Calendar.DAY_OF_MONTH, 1);
// 当第二个参数为负数时,表示减,下面就是减 1 天
calendar.add(Calendar.DAY_OF_MONTH, -1);

Calendar/Date/String 的互相转换

// Calendar to Date
Calendar calendar = Calendar.getInstance();
java.util.Date date = calendar.getTime();

// Date to Calendar
calendar.setTime(new java.util.Date());

// Calendar to String
Calendar calendat = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateStr = sdf.format(calendar.getTime());

// String to Calendar
String str = "2018-12-3";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse(str);
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);

// Date to String
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateStr = sdf.format(new Date());

// String to Date
String str = "2018-12-3";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date dateParse = sdf.parse(str);

Calendar 容易犯错的地方

  1. Calendar 类的 MONTH 字段不是往常的从 1 到 12 。而是从 0 到 11 ,其中 0 是一月,11 是十二月。
  2. 一周的某一天是从 1 到 7 表示,这点不出意料,但是一周的第一天是星期日而不是星期一,这意味着 1 =星期日,2 =星期一,7 =星期六。
  3. 如果需要进行复杂的日期和时间计算,最好在官方JavaDoc中阅读java.util.Calendar的类文档。 类文档包含有关类的特定行为的更多详细信息。 例如,如果将日期设置为 2018 年 1 月 34 日,那么实际日期是什么?

java.util.TimeZone

TimeZone 是一个表示时区的类,在跨时区执行日历计算时非常有用,一般和 Calendar 一起使用。

注意:在 Java 8 日期时间 API 中,时区由 java.time.ZoneId 类表示。 如果使用的是 Java 8 日期时间类(如 ZonedDateTime 类)的话,则只需要使用 ZoneId 类就行了。 如果使用的是 Calendar (来自Java 7和更早的日期时间API),那么仍然可以使用 java.util.TimeZone 类。

从 Calendar 中获取TimeZone

Calendar calendar = new GregorianCalendar();
// 从 Calendar 获取时区
TimeZone timeZone = calendar.getTimeZone();

// 为 Calendar 设置时区
calendar.setTimeZone(timeZone);

创建 TimeZone 对象

// 获取默认时区对象
TimeZone timeZone = TimeZone.getDefault();
// 获取指定时区对象
TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone timeZone = TimeZone.getTimeZone("Europe/Copenhagen");

TimeZone.getTimeZone() 方法的参数可以是一个 zone ID ,可以查看 JavaDoc 获取全部 ID 。

注意:如果 getTimeZone(String zoneID);方法的 zoneID 设置错误(不匹配系统支持的任意值),比如 "Asiannn/Shanghai",那也不会抛出任何异常,而是默默地设置 zoneID 为 GMT0 ,即格林威治时间。

时区的名称、ID和偏移量

我们可以查看给定时区的显示名称、ID和时间偏移量,如下所示

TimeZone timeZone = TimeZone.getDefault();
System.out.println(timeZone.getDisplayName());
System.out.println(timeZone.getID());
System.out.println(timeZone.getOffset(System.currentTimeMillis()));

以上代码将输出:

中国标准时间
Asia/Shanghai
28800000

getOffset() 方法以 int 类型返回该时区在指定日期的 UTC 偏移量(毫秒)。上例中的 28800000 毫秒,也就是 8 h ,我们在东八区(+8)。

在时区之间转换

TimeZone timeZoneCN = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone timeZone0 = TimeZone.getTimeZone("Etc/GMT0");

Calendar calendar = new GregorianCalendar();

calendar.setTimeZone(timeZoneCN);
long timeCN = calendar.getTimeInMillis();
System.out.println(calendar.getTimeZone().getDisplayName());
System.out.println("timeCN = " + timeCN);
System.out.println("hour = " + calendar.get(Calendar.HOUR_OF_DAY));

calendar.setTimeZone(timeZone0);
System.out.println(calendar.getTimeZone().getDisplayName());
long time0 = calendar.getTimeInMillis();
System.out.println("time0 = " + time0);
System.out.println("hour = " + calendar.get(Calendar.HOUR_OF_DAY));

以上程序将会输出如下:

中国标准时间
timeCN = 1543850448183
hour = 23
格林威治时间
time0 = 1543850448183
hour = 15

可以看到,以毫秒为单位的时间在两个时区是相同的,但是已从23点变成15点钟了,因为中国标准时间比格林威治时间快 8 小时,如此,我们设置不同时区获取对应时区的正确时间,这样就实现的换算的目的。

使用 SimpleDateFormat 解析和格式化日期

java.text.SimpleDateFormat 类可以解析字符串中的日期,也可以格式化字符串中的日期,本文将展示几个例子:

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

String dateString = format.format(new Date());
Date date = format.parse ("2018-12-03");  

作为参数传递给 SimpleDateFormat 类的字符串是一种模式(模板),用于说明如何解析和格式化日期。 在上面的示例中使用了模式“yyyy-MM-dd”,表示年份 4 位数(yyyy),月份 2 位数(MM)和日期 2 位数(dd)的表示形式,"2018-12-03"中使用‘-’分割是因为在模式中也是用‘-’分割字母的。

以下是常见模式字母列表,具体请看 JavaDoc :

y   = year   (yy or yyyy)
M   = month  (MM)
d   = day in month (dd)
h   = hour (0-12)  (hh)
H   = hour (0-23)  (HH)
m   = minute in hour (mm)
s   = seconds (ss)
S   = milliseconds (SSS)
z   = time zone  text        (e.g. Pacific Standard Time...)
Z   = time zone, time offset (e.g. -0800)

//一下是一些示例:
yyyy-MM-dd HH:mm:ss  (2018-12-3 23:59:59)
HH:mm:ss.SSS (23:59.59.999)
yyyy-MM-dd HH:mm:ss.SSS   (2009-12-31 23:59:59.999)
yyyy-MM-dd HH:mm:ss.SSS Z   (2009-12-31 23:59:59.999 +0100)       

如果指定 “dd” 来解析new SimpleDateFormat("yyyy-MM-dd");那么天数肯定被表示为 2 位,比如 3 号就是 03。
如果是指定 “d” 来解析new SimpleDateFormat("yyyy-MM-d"); 那么天数优先是 1 位,比如 3 号就是 3, 如果超出 1 位,那会自动扩展为 2 位,比如 13 号,那么就是 13 。

Instant 表示某一瞬间

Java .time.Instant 类表示时间线上的一个特定时刻,被定义为自原点起的偏移量,原点是1970年1月1日00点格林,也就是格林尼治时间。 时间以每天 86400 秒为单位,从原点向前移动。

Java.time 这个包是线程安全的,并且和其他大部分类一样,是不可变类。Instant 也不例外。

使用 Instant 类的工厂方法之一创建实例。例如,要创建一个表示当前时刻的时间点,可以调用 instance .now() ,如下所示:

Instant now = Instant.now();

Instant 对象包含秒和纳秒,来表示其包含的时间, 自纪元以来的秒数是上完提到的自原点以来的秒数。 纳秒是 Instant 的一部分,不到一秒钟。分别可以通过如下 2 个方法获取:

long getEpochSecond();
int getNano();

Instant 运算

Instant类还有几种方法可用于相对于Instant进行计算。 这些方法中的一些(不是全部)是:

  • plusSeconds()
  • plusMillis()
  • plusNanos()
  • minusSeconds()
  • minusMillis()
  • minusNanos()

一个例子如下:

Instant now = Instant.now(); // 现在这一瞬间

Instant later = now.plusSeconds(3); // 3 秒后的瞬间
Instant earlier = now.minusSeconds(3); // 3 秒前的瞬间

因为 Instant 是不可变的,所以上面的计算方法,是返回一个代表计算结果的新的 Instant 对象。

Duration 表示时间间隔

java.time.Duration 表示两个 Instant 之间的一段时间,Duration 实例是不可变的,因此一旦创建它,就不能更改它的值。但可以基于一个 Duration 对象创建新的 Duration 对象。

创建 Duration 对象

可以使用 Duration 类的工厂方法之一创建 Duration 对象,有 between()/ofDays()/ofSeconds()/from() 等方法,但其底层都是调用了同一个构造方法,其源码如下:

 private Duration(long seconds, int nanos) {
        super();
        this.seconds = seconds;
        this.nanos = nanos;
    }

下面是一个使用 between() 方法创建的示例:

Instant first = Instant.now();
// 其他耗时操作
Instant second = Instant.now();
Duration duration = Duration.between(first, second);

访问 Duration 对象的时间信息

从上述构造器源码可知,Duration 在内部维护两个值:

  • final int nanos;
  • final long seconds;

请注意没有单独的毫秒部分,只有纳秒和秒。但可以可以将整个时间间隔 Duration 转换为其他时间单位,如纳秒、分钟、小时或天:

  • long toNanos()
  • long toMillis()
  • long toMinutes()
  • long toHours()
  • long toDays()

toNanos() 与 getNano() 的不同之处在于 getNano() 仅返回持续时间小于一秒的部分(即整个时间段中不到 1 秒的那部分)。 toNanos() 方法返回的是转换为纳秒的整个时间段(即秒部分转成纳秒+纳秒部分)。

没有 toSeconds() 方法,因为 getSeconds() 方法已经可以获取 Duration 的秒部分。

Duration 的计算

Duration 类包含一组可用于基于 Duration 对象执行计算的方法。其中一些方法是:

  • Duration plus(Duration duration)
  • Duration plusNanos(long)
  • Duration plusMillis(long)
  • Duration plusSeconds(long)
  • Duration plusMinutes(long)
  • Duration plusHours(long)
  • Duration plusDays(long)
  • Duration minusXxx(long) 上面所有对应 minus 方法

这些方法的使用大同小异,一下是一个例子:

Duration start = ...
Duration added = start.plusDays(3); // 加 3 天
Duration subtracted = start.minusDays(3); // 减 3 天

同样,为了使Duration对象保持不可变,所有计算方法都返回表示计算结果的新的 Duration 对象。

LocalDate 表示本地日期

java.time.LocalDate 表示本地日期,没有时区信息。当地的日期可以是生日或法定假日等,与一年中的某一天有关,和一天中的某一时间无关。这个类对象也是不可变的,计算操作会返回一个新的 LocalDate 对象。
下面是一个创建 LocalDate 对象的例子:

LocalDate localDate1 = LocalDate.now();
LocalDate localDate2 = LocalDate.of(2018, 11, 11);

还有很多方法可以创建 LocalDate 对象,我列出一部分下面,具体的请查看 JavaDoc 。

访问 LocalDate 中的日期信息

LocalDate 中一共有 3 个日期信息字段,分别是:

  • final int year;
  • final short month;
  • final short day;

对应一些获取信息的方法:

  • int getYear()
  • Month getMonth()
  • int getDayOfMonth()
  • int getDayOfYear()
  • DayOfWeek getDayOfWeek()

LocalDate 计算

  • LocalDate plusYears(long yearsToAdd)
  • LocalDate plusMonths(long monthsToAdd)
  • LocalDate plusWeeks(long weeksToAdd)
  • LocalDate plusDays(long daysToAdd)
  • LocalDate minusXxx(long xxxToSubtract) 对应上面 plus 方法的 minus 版本

下面是一个例子:

LocalDate localDate = LocalDate.of(2018, 12, 12);

LocalDate localDate1 = localDate.plusYears(3); // 加 3 年
LocalDate localDate2 = localDate.minusYears(3);

LocalTime 表示本地时间

java.time.LocalTime 表示没有任何时区信息的特定时间,例如,上午 10 点。同样,这是一个不可变类。
下面是一个创建 LocalTime 对象的例子:

LocalTime localTime1 = LocalTime.now();
LocalTime localTime2 = LocalTime.of(21, 30, 59, 11001);

LocalTime 内部维护了 4 个变量维护时间信息:

  • final byte hour;
  • final byte minute;
  • final byte second;
  • final int nano;

也包含了必要的计算时间的方法,例如 LocalTime plusHours(long hoursToAdd); 其他的和 LocalDate 大同小异,就不展开讲了。

LocalDateTime 表示本地日期和时间

java.time.LocalDateTime 类表示没有任何时区信息的本地日期和时间,同样是不可变类。

查看其源码发现其内部就是维护了一个 LocalDate 对象和一个 LocalTime 对象来表示日期时间信息。

final LocalDate date;
final LocalTime time;

所以完全可以把它看成是 LocalDate 和 LocalTime 的结合。
下面是一个创建 LocalDateTime 对象的例子:

LocalDateTime localDateTime1 = LocalDateTime.now();
LocalDateTime localDateTime2 =LocalDateTime.of(2018, 11, 11, 10, 55, 36, 123);

上面第二行代码使用 of() 工厂方法创建对象,其参数分别对应年月日时分秒纳秒。

其他获取日期时间信息和计算请参考 LocalDate 和 LocalTime 的。

ZonedDateTime 表示带有时区信息的日期和时间

java.time.ZonedDateTime 可以用来代表世界上某个特定事件的开始,比如会议、火箭发射等等。
它同样是不可变类,下面是一个创建此类对象的例子:

ZonedDateTime zonedDateTime = ZonedDateTime.now();
ZoneId zoneId = ZoneId.of("UTC+1");
ZonedDateTime zonedDateTime2 = ZonedDateTime.of(2015, 11, 30, 23, 45, 59, 1234, zoneId);

时区

时区由 ZoneId 类表示,如前面的示例所示。可以使用 ZoneId.now() 方法创建 ZoneId 对象。也可以使用 of() 方法指定时区信息,下面是一个例子:

ZoneId zoneId1 = ZoneId.of("UTC+1");
ZoneId zoneId2 = ZoneId.of("Europe/Paris");

传递给 of() 方法的参数是要为其创建 ZoneId 的时区的ID。在上面的例子中,ID 是“UTC+1”,它是 UTC (格林威治)时间的偏移量。另外也可以直接指定具体的时区 ID 字符串,这在本文开头有介绍。

ZonedDateTime 相比 LocalDateTime 只是多了地区信息,其内部维护了下面这 3 个变量来表示日期信息和地区:

  • final LocalDateTime dateTime;
  • final ZoneOffset offset;
  • final ZoneId zone;

所以其他的方法如获取日期时间信息和计算时间,请参考上述。

DateTimeFormatter

java.time.DateTimeFormatter 类用于解析和格式化用 Java 8 日期时间 API 中的类表示的日期。

预定义 DateTimeFormatter 对象

DateTimeFormatter 类包含一组预定义的(常量)实例,这些实例可以解析和格式化来自标准日期格式的日期。这省去了为 DateTimeFormatter 定义日期格式的麻烦。包含的部分预定义实例如下:

BASIC_ISO_DATE

ISO_LOCAL_DATE
ISO_LOCAL_TIME
ISO_LOCAL_DATE_TIME

ISO_OFFSET_DATE

ISO_ZONED_DATE_TIME

这些预定义的 DateTimeFormatter 实例中的每一个都预先配置为格式化和解析不同格式的日期。 这里不解释所有这些预定义的 DateTimeFormatter 实例。 可以在 JavaDoc 中查看。

格式化 Date 的例子

DateTimeFormatter formatter = DateTimeFormatter.BASIC_ISO_DATE;

String formattedDate = formatter.format(LocalDate.now());
System.out.println(formattedDate); // 20181204

String formattedZonedDate = formatter.format(ZonedDateTime.now());
System.out.println("formattedZonedDate = " + formattedZonedDate);// 20181204+0800

最后一行输出 20181204+0800 代表 UTC+8 时区的 2019 年、第 12 个月(12 月)和第 4 天(第 4 天)。


12-04 17:21