b1cat`s

Java日期时间

Word count: 1.9kReading time: 8 min
2020/02/17

Java日期时间

Date和Calendar

在计算机中,以整数存储的时间戳(从1970年1月1日零点到现在所经历的秒数)表示某一个时刻,它打印出的各种各样的时间表示,则是数据的展示。展示有多种形式,本质上就是一个转换方法。

1
String toDisplay(int n){...}

Epoch Time又称为时间戳,在不同的编程语言中,会有几种存储方式:

  • 以秒为单位的整数:1574208900,缺点是精度只能到秒;
  • 以毫秒为单位的整数:1574208900123,最后3位表示毫秒数;
  • 以秒为单位的浮点数:1574208900.123,小数点后面表示零点几秒

Java标准库API

提供了两套处理日期和时间的API:

  • java.util包,主要包括Data, Calendar, TimeZone
  • Java8新API定义在java.time包,主要包括LocalDateTime, ZonedDateTime, ZoneId

Date

java.util.Date用于表示一个日期和时间的对象,实际上存储了一个long类型以毫秒表示的时间戳。

1
2
3
4
public class Date implements Serializable, Cloneable, Comparable<Date>{
private transient long fastTime;
....
}

基本方法

1
2
3
4
5
6
7
8
Date date = new Date();
date.getYear() + 1900 ; // 获取年份
date.getMonth() + 1 ; //0-11
date.getDate(); //1-31

date.toString(); //转化为String
date.toGMTString(); //转化为GMT
date.toLocaleString(); //转化为本地时区

自定义格式输出

1
2
3
Date date = new Date();
var sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sd.format(date);
  • M:输出9
  • MM:输出09
  • MMM:输出Sep
  • MMMM:输出September

缺点:无法转化其他时区,很难对日期和时间进行计算

Calendar

与Date比多了一个可以做简单的日期和时间计算的功能。

1
2
3
4
5
6
7
8
9
Calendar c = Calendar.getInstance();
int y = c.get(Calendar.YEAR);
int m = 1 + c.get(Calendar.MONTH); //月份加1
int d = c.get(Calendar.DAY_OF_MOUTH);
int w = c.get(Calendar.DAY_OF_WEEK); //1-7表示周日-周六
int hh = c.get(Calendar.HOUR_OF_DAY);
int mm = c.get(Calendar.MINUTE);
int ss = c.get(Calendar.SECOND);
int ms = c.get(Calendar.MILLISECOND);

利用Calendar.getTime()可以将一个Calendar对象转化为Date对象,然后用SimpleDateFormat进行格式化了。

计算:

1
2
3
4
5
6
7
8
Calendar c = Calendar.getInstance();
c.clear();
c.set(2019, 10, 20, 8, 15, 0);
c.add(Calendar.DAY_OF_MOUTH, 5);
c.add(Calendar.HOUR_OF_DAY, -2);
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d = c.getTime();
System.out.println(sdf.format(d)); // 2019-10-25 6:15:00

TimeZone

DateCalendar相比,提供了时间转换的功能。时区用TimeZone对象表示:

1
2
3
4
TimeZone tzDefault = TimeZone.getDefault(); //当前时区
TimeZone tzGMT9 = TimeZone.getTimeZone("GMT+09:00"); //GMT+9时区
TimeZone tzNy = TimeZone.getTimeZone("America/New_York"); // 纽约时区
//要列出系统支持的所有ID,请使用TimeZone.getAvailableIDs()

转化时区

1
2
3
4
5
6
7
8
9
Calendar c = Calendar.getInstance();
c.clear(); //清除所有字段
c.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
c.set(2019, 10, 20, 8, 15, 0); //设置年月日时分秒

var sdf = new SimpleDateFormat("yyyy=MM-dd HH:mm:ss"); //设置输出格式
sdf.setTimeXone(TimeZone.getTimeZone("America/New_York"));
System.out.println(sdf.format(c.getTime())); //2019-11-19 19:15:00
//格式化获取的Date对象(注意Date对象无时区信息,时区信息存储在SimpleDateFormat中)

LocalDateTime

从java8开始,java.time提供了新的日期和时间API,主要涉及的类型有:

  • 本地日期和时间:LocalDateTime, LocalDate, LocalTime
  • 带时区的日期和时间:ZoneDateTime
  • 时刻:Instant
  • 时区:ZoneID, ZoneOffset
  • 时间间隔:Duration
  • 格式类型:DateTimeFormatter

另外修正了:

  • Month的范围有1-12表示
  • Week范围从1-7表示

LocalDateTime

1
2
3
LocalDate d = LocalDate.now();
LocalTime t = LocalTime.now();
LocalDateTime dt = LocalDateTime.now();

创建日期:

1
2
3
LocalDateTime dt = LocalDateTime.of(2019, 11, 30, 15, 16, 17);

LocalDateTime dt2 = LocalDateTime.parse("2019-11-19T15:16:17"); //字符串转为LocalDateTime

注意ISO 8601规定的日期和时间分隔符是T。标准格式如下:

  • 日期:yyyy-MM-dd
  • 时间:HH:mm:ss
  • 带毫秒的时间:HH:mm:ss.SSS
  • 日期和时间:yyyy-MM-dd’T’HH:mm:ss
  • 带毫秒的日期和时间:yyyy-MM-dd’T’HH:mm:ss.SS

自定义输出的格式DateTimeFormatter

1
2
3
4
5
6
7
8
9
//自定义格式化
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(dtf.format(LocalDateTime.now()));
//自定义格式解析
LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17");
System.out.println(dt2);

//另一种创建DateTimeFormatter的方法传入字符串时指定Locale
DateTimeFormatter formatter = DateTimeFormatter.ofParttren("E, yyyy-MM-dd HH:mm", Locale.US);

SimpleDateFormat不同的是,DateTimeFormatter不但是不变对象,它还是线程安全的。因为SimpleDateFormat不是线程安全的,使用的时候,只能在方法内部创建新的局部变量。而DateTimeFormatter可以只创建一个实例,到处引用。

LocalDateTime加减

1
LocalDataTime dt = dt.plusDays(5).minusHours(3); //加5天,减3小时

加减月份会自动调整

对日期和时间调整则使用withXXX()方法

1
2
3
4
5
6
//调整年:withYear()
//调整月:withMonth()
//调整日:withDayOfMonth()
//调整时:withHour()
//调整分:withMinute()
//调整秒:withSecond()

更复杂的运算:

1
2
3
LocalDateTime fDay = LocalDate.now().withDayOfMonth(1).atStartOfDay(); //从本月第一天00:00时刻

LocalDate lDay = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()); //本月最后一天

要判断两个LocalDateTime先后,可以使用isBefore(), isAfter()

注:LocalDateTime无法与时间戳进行转换,因为LocalDateTime没有时区

Duration和Period

Duration表示两个时刻之前的时间间隔,Period表示两个日期之间的天数

1
2
3
4
5
LocalDateTime start = LocalDateTime.of(2019, 11, 11, 1, 2, 0);
LocalDateTime end = LocalDateTime.of(2020, 1, 1, 2, 2, 0);
Duration d = Duration.between(start, end); //PT1225H

Period p = LocalDate.of(2019, 11, 11).until(LocalDate.of(2020, 1, 1)); //P1M21D

DurationPeriod的表示方法也符合ISO 8601的格式,它以P...T...的形式表示,P...T之间表示日期间隔,T后面表示时间间隔。如果是PT...的格式表示仅有时间间隔。利用ofXxx()或者parse()方法也可以直接创建Duration

1
2
Duration d1 = Duration.ofHours(10); //10hours
Duration d2 = Duration.parse("P1DT2H3M"); // 1 day, 2 hours, 3 minutes

ZonedDateTime

要表示一个带时区的日期和时间ZonedDateTime ,可以理解成LocalDateTimeZoneId

ZoneIdjava.time引入新的时区类,创建ZonedDateTime对象:

  • ZonedDateTime zbj = ZonedDateTime.now(); //默认时区
    • ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York"));
  • 通过LocalDateTime附加一个ZoneId
1
2
LocalDateTime ldt = LocalDateTime.of(2020, 1, 1, 1, 0, 0);
ZonedDateTime zbj = ldt.atZone(ZoneId.systemDefault());

时区转换

要转换时区,使用ZonedDateTime对象的withZoneSameInstant()

1
2
ZonedDataTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDataTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));

转化为本地时间:

LocalDateTime ldt = zdt.toLocalDateTime();

转化后丢失时区信息。

Instant

计算机存储的当前时间,本质上只是一个不断递增的整数。Java提供的System.currentTimeMillis()返回的就是以毫秒表示的当前时间戳。

这个时间戳在java.time以Instant类型表示,和Instant.now()获取时间戳效果类似。

1
2
3
System.out.println(Instant.now()); //2020-02-16T15:59:28.433728Z
System.out.println(Instant.now().getEpochSecond()); //1581868861 long类型
//toEpochMilli() 返回毫秒

实际上,Instant内部只有两个核心字段:

1
2
3
4
public final class Instant implemet ...{
private final long seconds;
private final int nanos; //纳秒级别精度
}

既然Instant是时间戳,加上一个时区,就可以创建出ZonedDateTime

1
2
Instant ins =Instant.ofEpochSecond(1581868861);
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());

所以,LocalDateTimeZoneIdInstantZonedDateTimelong都可以互相转换。

需注意:long类型以毫秒还是秒为单位

版本日期转化

旧API转新API

DateCalendar,通过toInstant转为Instantd对象,再继续转为ZonedDateTime

旧的TimeZone提供了一个toZoneId(),可以把自己变成新的ZoneId

新API转为旧API

只能借助long

1
2
3
4
ZonedDateTime zdt = ZonedDateTime.now();
long ts = zdt.toEpochSecond() * 1000;

Date date = new Date(ts);

新的ZoneId转换为旧的TimeZone,需要借助ZoneId.getId()返回的String完成

数据库中存储日期和时间

java.sql.Date,继承自java.util.Date,但会自动忽略所有时间相关信息(也就是不会有时区区分)

在数据库,存在几种时间和日期类型:

image-20200217001739288

实际上,在数据库中,我们需要存储的最常用的是时刻(Instant),因为有了时刻信息,就可以根据用户自己选择的时区,显示出正确的本地时间。

因此在数据库中存储时间戳时,尽量使用long型时间戳,它具有省空间,效率高,不依赖数据库的优点。

CATALOG
  1. 1. Java日期时间
    1. 1.1. Date和Calendar
      1. 1.1.1. Java标准库API
        1. 1.1.1.1. Date
        2. 1.1.1.2. Calendar
        3. 1.1.1.3. TimeZone
    2. 1.2. LocalDateTime
      1. 1.2.0.1. LocalDateTime
      2. 1.2.0.2. LocalDateTime加减
      3. 1.2.0.3. Duration和Period
  2. 1.3. ZonedDateTime
    1. 1.3.0.1. 时区转换
  • 1.4. Instant
  • 1.5. 版本日期转化
    1. 1.5.0.1. 旧API转新API
    2. 1.5.0.2. 新API转为旧API
    3. 1.5.0.3. 数据库中存储日期和时间