目录

Java Date and Time API

弃用 Date,支持在项目中只使用 JSR 310 日期时间类型。

JSR 310: Date and Time API

JSR 310 的类都在 java.time 包中。JSR 310 的类都是线程安全的。

  • Instant——代表的是时间戳(另外可参考Clock类)
  • LocalDate——不包含具体时间的日期,如2020-12-12。它可以用来存储生日,周年纪念日,入职日期等
  • LocalTime——代表的是不含日期的时间,如18:00:00
  • LocalDateTime——包含了日期及时间,不过没有偏移信息或者说时区
  • ZonedDateTime——包含时区的完整的日期时间还有时区,偏移量是以UTC/格林威治时间为基准的
  • Timezone——时区。在新API中时区使用ZoneId来表示。时区可以很方便的使用静态方法of来获取到

同时还有一些辅助类,如:Year、Month、YearMonth、MonthDay、Duration、Period等等。

时区

在JDK8之前,Java对时区和偏移量都是使用java.util.TimeZone来表示的。一般情况下,使用静态方法TimeZone#getDefault()即可获得当前JVM所运行的时区。

Asia/Shanghai 表示中国区 ZoneId

zoneId的列表是jre维护的一个文本文件 \jre\lib\tzmappings

1
2
3
4
// 所有可用的 zoneId
TimeZone.getAvailableIDs()
TimeZone.getTimeZone("Asia/Shanghai")
TimeZone.getTimeZone("GMT+08:00")

Java 设置默认时区

  • JDK 强制将时区设为北京时区 TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
  • JVM 参数 -Duser.timezone=GMT+8
  • OS 时区 将操作系统主机时区设置为北京时区,这是推荐方式,可以完全对开发者无感,也方便了运维统一管理

日期时间

/images/java/jdk/java-time.png
日期时间

  • LocalDateTime:本地日期时间
  • OffsetDateTime:带偏移量的日期时间
  • ZonedDateTime:带时区的日期时间

LocalDateTime 不存储时区,所以适合日期的描述,比如用于生日、deadline等等。但是请记住,如果没有偏移量/时区等附加信息,一个时间是不能表示时间线上的某一时刻的。

OffsetDateTime是一个带有偏移量的日期时间类型。存储有精确到纳秒的日期时间,以及偏移量。可以简单理解为 OffsetDateTime = LocalDateTime + ZoneOffset。

OffsetDateTime、ZonedDateTime 和 Instant 它们三都能在时间线上以纳秒精度存储一个瞬间(请注意:LocalDateTime是不行的),也可理解我某个时刻。OffsetDateTime和Instant可用于模型的字段类型,因为它们都表示瞬间值并且还不可变,所以适合网络传输或者数据库持久化。ZonedDateTime不适合网络传输/持久化,因为即使同一个ZoneId时区,不同地方获取到瞬时值也有可能不一样。

LocalDateTime ISO-8601日历系统中不带时区的日期时间。

ISO-8601日历系统中与UTC偏移量有关的日期时间。OffsetDateTime是一个带有偏移量的日期时间类型。存储有精确到纳秒的日期时间,以及偏移量。可以简单理解为 OffsetDateTime = LocalDateTime + ZoneOffset。

OffsetDateTime、ZonedDateTime和Instant它们三都能在时间线上以纳秒精度存储一个瞬间(请注意:LocalDateTime是不行的),也可理解我某个时刻。OffsetDateTime和Instant可用于模型的字段类型,因为它们都表示瞬间值并且还不可变,所以适合网络传输或者数据库持久化。

ZonedDateTime不适合网络传输/持久化,因为即使同一个ZoneId时区,不同地方获取到瞬时值也有可能不一样

ZonedDateTime ISO-8601国际标准日历系统中带有时区的日期时间。它存储所有的日期和时间字段,精度为纳秒,以及一个时区,带有用于处理不明确的本地日期时间的时区偏移量。

ZonedDateTime可简单认为是LocalDateTime和ZoneId的组合。而ZoneOffset是其内置的动态计算出来的一个次要信息,以确保输出一个瞬时值而存在,毕竟在某个瞬间偏移量ZoneOffset肯定是确定的。ZonedDateTime也可以理解为保存的状态相当于三个独立的对象:LocalDateTime、ZoneId和ZoneOffset。某个瞬间 = LocalDateTime + ZoneOffset。ZoneId确定了偏移量如何改变的规则。所以偏移量我们并不能自由设置(不提供set方法,构造时也不行),因为它由ZoneId来控制的。

OffsetDateTime 和 ZonedDateTime 的区别

LocalDateTime好理解,一般都没有异议。

  • OffsetDateTime = LocalDateTime + 偏移量ZoneOffset;ZonedDateTime = LocalDateTime + 时区ZoneId
  • OffsetDateTime可以随意设置偏移值,但ZonedDateTime无法自由设置偏移值,因为此值是由时区ZoneId控制的
  • OffsetDateTime无法支持夏令时等规则,但ZonedDateTime可以很好的处理夏令时调整
  • OffsetDateTime得益于不变性一般用于数据库存储、网络通信;而ZonedDateTime得益于其时区特性,一般在指定时区里显示时间非常方便,无需认为干预规则
  • OffsetDateTime代表一个瞬时值,而ZonedDateTime的值是不稳定的,需要在某个瞬时根据当时的规则计算出来偏移量从而确定实际值

总的来说,OffsetDateTime和ZonedDateTime的区别主要在于ZoneOffset和ZoneId的区别。如果你只是用来传递数据,请使用OffsetDateTime,若你想在特定时区里做时间显示那么请务必使用ZonedDateTime。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
public void test101() {
    OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(-4));
    System.out.println("-4偏移量时间为:" + offsetDateTime);

    // 转换为ZonedDateTime的表示形式
    System.out.println("ZonedDateTime的表示形式:" + offsetDateTime.toZonedDateTime());
    System.out.println("ZonedDateTime的表示形式:" + offsetDateTime.atZoneSameInstant(ZoneId.of("America/New_York")));
    System.out.println("ZonedDateTime的表示形式:" + offsetDateTime.atZoneSimilarLocal(ZoneId.of("America/New_York")));
}
1
2
3
4
-4偏移量时间为:2021-01-17T19:43:28.320-04:00
ZonedDateTime的表示形式:2021-01-17T19:43:28.320-04:00
ZonedDateTime的表示形式:2021-01-17T18:43:28.320-05:00[America/New_York]
ZonedDateTime的表示形式:2021-01-17T19:43:28.320-05:00[America/New_York]

本例有值得关注的点:

  • atZoneSameInstant():将此日期时间与时区结合起来创建ZonedDateTime,以确保结果具有相同的Instant
    • 所有偏移量-4 -> -5,时间点也从19 -> 18,确保了Instant保持一致嘛
  • atZoneSimilarLocal:将此日期时间与时区结合起来创建ZonedDateTime,以确保结果具有相同的本地时间
    • 所以直接效果和toLocalDateTime()是一样的,但是它会尽可能的保留偏移量(所以你看-4变为了-5,保持了真实的偏移量)

时间格式化

yyyy-MM-dd HH:mm:ss 年-月-日 时:分:秒 M 大写是为了区分“ 月”与“ 分”

顺便说下HH为什么大写,是为了区分 12小时制与 24小时制。 小写的h是12小时制,大写的H是24小时制。 yyyy-M-d H:m:s mm与m等,它们的区别为 是否有前导零:H,m,s表示 非零开始,HH,mm,ss表示 从零开始。 比如凌晨1点2分,HH:mm显示为 01:02,H:m显示为 1:2

yyyy/yyy/yy/y 显示为 2014/2014/14/4

(3个y与4个y是一样的,为了便于理解多写成4个y)

MMMM/MMM/MM/M 显示为 一月/一月/01/1

(4个M显示全称,3个M显示缩写,不过中文显示是一样的,英文就是January和Jan)

dddd/ddd/dd/d 显示为 星期三/周三(有的语言显示为“三”)/01/1

(在英文中同M一样,4个d是全称,3个是简称;

dddd/ddd表示星期几,dd/d表示几号)

HH/H/hh/h 显示为 01/1/01 AM/1 AM

剩下的mm/m/ss/s只是前导零的问题了。

  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
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public class TimeIntroduction {
    public static void testClock() throws InterruptedException {
        //时钟提供给我们用于访问某个特定 时区的 瞬时时间、日期 和 时间的。  
        Clock c1 = Clock.systemUTC(); //系统默认UTC时钟(当前瞬时时间 System.currentTimeMillis())  
        System.out.println(c1.millis()); //每次调用将返回当前瞬时时间(UTC)  
        Clock c2 = Clock.systemDefaultZone(); //系统默认时区时钟(当前瞬时时间)  
        Clock c31 = Clock.system(ZoneId.of("Europe/Paris")); //巴黎时区  
        System.out.println(c31.millis()); //每次调用将返回当前瞬时时间(UTC)  
        Clock c32 = Clock.system(ZoneId.of("Asia/Shanghai"));//上海时区  
        System.out.println(c32.millis());//每次调用将返回当前瞬时时间(UTC)  
        Clock c4 = Clock.fixed(Instant.now(), ZoneId.of("Asia/Shanghai"));//固定上海时区时钟  
        System.out.println(c4.millis());
        Thread.sleep(1000);
        System.out.println(c4.millis()); //不变 即时钟时钟在那一个点不动  
        Clock c5 = Clock.offset(c1, Duration.ofSeconds(2)); //相对于系统默认时钟两秒的时钟  
        System.out.println(c1.millis());
        System.out.println(c5.millis());
    }
    public static void testInstant() {
        //瞬时时间 相当于以前的System.currentTimeMillis()  
        Instant instant1 = Instant.now();
        System.out.println(instant1.getEpochSecond());//精确到秒 得到相对于1970-01-01 00:00:00 UTC的一个时间  
        System.out.println(instant1.toEpochMilli()); //精确到毫秒  
        Clock clock1 = Clock.systemUTC(); //获取系统UTC默认时钟  
        Instant instant2 = Instant.now(clock1);//得到时钟的瞬时时间  
        System.out.println(instant2.toEpochMilli());
        Clock clock2 = Clock.fixed(instant1, ZoneId.systemDefault()); //固定瞬时时间时钟  
        Instant instant3 = Instant.now(clock2);//得到时钟的瞬时时间  
        System.out.println(instant3.toEpochMilli());//equals instant1  
    }
    public static void testLocalDateTime() {
        //使用默认时区时钟瞬时时间创建 Clock.systemDefaultZone() -->即相对于 ZoneId.systemDefault()默认时区  
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);
//自定义时区  
        LocalDateTime now2 = LocalDateTime.now(ZoneId.of("Europe/Paris"));
        System.out.println(now2);//会以相应的时区显示日期  
//自定义时钟  
        Clock clock = Clock.system(ZoneId.of("Asia/Dhaka"));
        LocalDateTime now3 = LocalDateTime.now(clock);
        System.out.println(now3);//会以相应的时区显示日期  
//不需要写什么相对时间 如java.util.Date 年是相对于1900 月是从0开始  
//2013-12-31 23:59  
        LocalDateTime d1 = LocalDateTime.of(2013, 12, 31, 23, 59);
//年月日 时分秒 纳秒  
        LocalDateTime d2 = LocalDateTime.of(2013, 12, 31, 23, 59, 59, 11);
//使用瞬时时间 + 时区  
        Instant instant = Instant.now();
        LocalDateTime d3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
        System.out.println(d3);
//解析String--->LocalDateTime  
        LocalDateTime d4 = LocalDateTime.parse("2013-12-31T23:59");
        System.out.println(d4);
        LocalDateTime d5 = LocalDateTime.parse("2013-12-31T23:59:59.999");//999毫秒 等价于999000000纳秒  
        System.out.println(d5);
//使用DateTimeFormatter API 解析 和 格式化  
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        LocalDateTime d6 = LocalDateTime.parse("2013/12/31 23:59:59", formatter);
        System.out.println(formatter.format(d6));
//时间获取  
        System.out.println(d6.getYear());
        System.out.println(d6.getMonth());
        System.out.println(d6.getDayOfYear());
        System.out.println(d6.getDayOfMonth());
        System.out.println(d6.getDayOfWeek());
        System.out.println(d6.getHour());
        System.out.println(d6.getMinute());
        System.out.println(d6.getSecond());
        System.out.println(d6.getNano());
//时间增减  
        LocalDateTime d7 = d6.minusDays(1);
        LocalDateTime d8 = d7.plus(1, IsoFields.QUARTER_YEARS);
//LocalDate 即年月日 无时分秒  
//LocalTime即时分秒 无年月日  
//API和LocalDateTime类似就不演示了  
    }
    public static void testZonedDateTime() {
        //即带有时区的date-time 存储纳秒、时区和时差(避免与本地date-time歧义)。  
//API和LocalDateTime类似,只是多了时差(如2013-12-20T10:35:50.711+08:00[Asia/Shanghai])  
        ZonedDateTime now = ZonedDateTime.now();
        System.out.println(now);
        ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("Europe/Paris"));
        System.out.println(now2);
//其他的用法也是类似的 就不介绍了  
        ZonedDateTime z1 = ZonedDateTime.parse("2013-12-31T23:59:59Z[Europe/Paris]");
        System.out.println(z1);
    }
    public static void testDuration() {
        //表示两个瞬时时间的时间段  
        Duration d1 = Duration.between(Instant.ofEpochMilli(System.currentTimeMillis() - 12323123), Instant.now());
//得到相应的时差  
        System.out.println(d1.toDays());
        System.out.println(d1.toHours());
        System.out.println(d1.toMinutes());
        System.out.println(d1.toMillis());
        System.out.println(d1.toNanos());
//1天时差 类似的还有如ofHours()  
        Duration d2 = Duration.ofDays(1);
        System.out.println(d2.toDays());
    }
    public static void testChronology() {
        //提供对java.util.Calendar的替换,提供对年历系统的支持  
        Chronology c = HijrahChronology.INSTANCE;
        ChronoLocalDateTime d = c.localDateTime(LocalDateTime.now());
        System.out.println(d);
    }
    /**
     * 新旧日期转换
     */
    public static void testNewOldDateConversion(){
        Instant instant=new Date().toInstant();
        Date date=Date.from(instant);
        System.out.println(instant);
        System.out.println(date);
    }
    public static void main(String[] args) throws InterruptedException {
        testClock();
        testInstant();
        testLocalDateTime();
        testZonedDateTime();
        testDuration();
        testChronology();
        testNewOldDateConversion();
    }
}

附录