本文目的:掌握 Java 中日期和时间常用 API 的使用。
参考:Jakob Jenkov的英文教程Java Date Time Tutorial 和 JavaDoc
概览
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 容易犯错的地方
- Calendar 类的 MONTH 字段不是往常的从 1 到 12 。而是从 0 到 11 ,其中 0 是一月,11 是十二月。
- 一周的某一天是从 1 到 7 表示,这点不出意料,但是一周的第一天是星期日而不是星期一,这意味着 1 =星期日,2 =星期一,7 =星期六。
- 如果需要进行复杂的日期和时间计算,最好在官方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 天)。