Java 中的时间操作

发布于 2024-05-24 12:41:12 字数 12400 浏览 23 评论 0

日常开发中必不可少要与时间打交道,而关于时间的处理网上有很多文章,下面基于大神的文章和我自己的理解对时间做一个整理,开始之前我们得先明确下一些概念,有助于后面程序代码得理解:

  • 时间:类似于08:002020 年 10 月 1 日 12:00,这个叫做时间,时间是一个相对得概念。在国内我们说得时间和国外得时间表示得就不一样,比如现在是 2020 年 10 月 1 日 10:00,在美国芝加哥得话现在就是 2020 年 09 月 30 日 21:00,所以一般需要说当地时间(本地时间)
  • 时刻:时刻就是一个准确得时间,如现在我们和国外同时做了一个事情,如果以上帝得视角来看,那这个时间点就是可以确定得哪个时刻(程序里就是时间戳)
  • 时区: 但我们不是上帝,我们对应得如果要准确得表示一个事情得时间,就得加上时区了。全球一共分为 24 个时区,伦敦所在的时区称为标准时区,其他时区按东/西偏移的小时区分,北京所在的时区是东八区

总结一下,如果要表示准确得时间,得这样说: 北京时间(东八区) 2020 年 10 月 1 日 12:00

下面我们就看下在 Java 中对于时间得处理

我们都知道 Java8 加入了 LocalDateTime 等基础类,改进了时间得运算处理,所以如果是新项目想都不要想,直接上 java8+,如果是旧项目或者有引用库还是 java7 及以下得也有方法可以解决,但不管怎样,放弃 Date 或者 Calendar 从现在开始

新 API 的类型几乎全部是不变类型,可以在多线程情况下放心使用, 并且修正了旧 API 一些不合理的常量设计:

  • Month 的范围用 1~12 表示 1 月到 12 月(再也不用记要不要加 1 减 1 了)
  • Week 的范围用 1~7 表示周一到周日
  • 处理加减会自动调整日期,例如从 2019-10-31 减去 1 个月得到的结果是 2019-09-30,因为 9 月没有 31 日

System.currentTimeMillis(),这个数值代表什么呢?从 Javadoc 可以看出,它是返回当前时间和 1970 年 1 月 1 号 UTC 时间相差的毫秒数,这个数值与夏 / 冬令时并没有关系,所以并不受其影响

重要类

  • LocalDateTime: 表示一个带日期得时间(LocalDateTime.now())
  • LocalDate: 表示一个日期(不带时分秒得时间)
  • LocalTime: 表示一个不带日期得时间

当需要在程序上显示时就用到了:

  • DateTimeFormatter: 格式化显示时间

当需要对时间进行计算时就用到了

  • Duration: 两个 LocalDateTime 之间的差值(相差多少时间),比如 PT1235H10M30S,表示 1235 小时 10 分钟 30 秒
  • Period:两个 LocalDate 之间的差值(相差多少天), 比如 P1M21D,表示 1 个月 21 天

当需要准确得表示时间时就得用到时区

  • ZoneId: 仅表示时区
  • ZonedDateTime: 带时区的时间(ZoneId+LocalDateTime)
  • 当需要表示时刻时就得用时间戳*:
  • Instant: 用 Instant.now() 获取当前时间戳,比 System.currentTimeMillis() 多了一个更高精度的纳秒
//获取当前时间戳
Instant instant = Instant.now();
System.out.println("当前时间戳: " + instant.toEpochMilli());//与 System.currentTimeMillis 类似

//加一个时区组合成 ZonedDateTime 默认东八区
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());

//输出显示
System.out.println("北京时间: " + zonedDateTime.format(DateTimeFormatter.ISO_DATE));

//去掉时区显示时间
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
System.out.println("当前时间: " + localDateTime.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")));

//去掉日期
LocalTime localTime = localDateTime.toLocalTime();
System.out.println("当前时间: " + localTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")));

//计算两个时间
LocalDateTime localDateTime1 = LocalDateTime.of(2000, 9, 1, 9, 20);
Duration duration = Duration.between(localDateTime1, localDateTime);
System.out.println("duration 相差: " + duration);

//计算两个日期
Period period = Period.between(localDateTime1.toLocalDate(), localDateTime.toLocalDate());
System.out.println("period 相差: " + period);

新老版本转换

LocalDateTime,ZoneId,Instant,ZonedDateTime 和 long 都可以互相转换:

┌─────────────┐
│LocalDateTime│────┐
└─────────────┘ │ ┌─────────────┐
├───>│ZonedDateTime│
┌─────────────┐ │ └─────────────┘
│ ZoneId │────┘ ▲
└─────────────┘ ┌─────────┴─────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Instant │<───>│ long │
└─────────────┘ └─────────────┘

转换的时候,需要留意 long 类型以毫秒还是秒为单位即可

旧 API 转新 API

//long -> Instance
Instant instant = Instant.ofEpochMilli(System.currentTimeMillis());

// Date -> Instant
Instant ins1 = new Date().toInstant();

// Calendar -> Instant
Calendar calendar = Calendar.getInstance();
Instant ins2 = Calendar.getInstance().toInstant();

//Instance + ZoneId -> ZonedDateTime
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
//注意,如果源数据是 calendar,采用 calendar 中保存的时区
ZonedDateTime zdt = ins2.atZone(calendar.getTimeZone().toZoneId());


//ZonedDateTime -> LocalDateTime
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();

新 API 转旧 API

// LocalDateTime -> ZonedDateTime -> long -> Date:
ZonedDateTime zdt = LocalDateTime.now().atZone(ZoneId.systemDefault());
long ts = zdt.toInstant().toEpochMilli();
Date date = new Date(ts);
System.out.println("date: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));

// LocalDateTime -> ZonedDateTime -> long -> Calendar:
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTimeZone(TimeZone.getTimeZone(zdt.getZone().getId()));
calendar.setTimeInMillis(ts);

SpringBoot 中处理

以上配置针对于 SpringBoot 2.x 及以上版本

推荐 jackson, SpringBoot 内置,既能处理 json 也能处理 xml故这里只讲 jackson 的处理,其他 json 库网上搜索即可

一般我们都会用 json 来作为前后台传递数据用的格式,LocalDateTime 类型如果不处理话,直接传递客户端使用是不太方便的:

public class MyUser {

private String name;

private LocalDateTime createTime;

private LocalDate localDate;

private LocalTime localTime;

private Date date;

//getter setter
}

//json 返回
{
"createTime": "2020-10-23T05:47:07.670Z",
"date": "2020-10-23T05:47:07.670Z",
"localDate": "string",
"localTime": {
"hour": 0,
"minute": 0,
"nano": 0,
"second": 0
},
"name": "string"
}

ISO 8601 规定的日期和时间分隔符是 T

故需要作一些配置修改

  • 如果是部分实体需要,直接在属性上加上 @JsonFormat 注解即可:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate localDate;

@JsonFormat(pattern = "HH:mm:ss")
private LocalTime localTime;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date date;

//返回 json
{
"name": "jone",
"createTime": "2020-10-23 13:50:48",
"localDate": "2020-10-23",
"localTime": "13:50:48",
"date": "2020-10-23 05:50:48"
}

如果项目涉及到国内外,还需加上时区如:@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”,timezone=”GMT+8”)

  • 全局设置
@Configuration
public class JsonConfig {

/**
* DateTime 格式化字符串
*/
private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

/**
* Date 格式化字符串
*/
private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";

/**
* Time 格式化字符串
*/
private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss";

@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> builder
.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
.serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
.serializerByType(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN)))
.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
.deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
.deserializerByType(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN), DEFAULT_DATETIME_PATTERN))
;
}

}

将前端获取的时间转换为一个符合自定义格式的时间格式存储到数据库 @DateTimeFormat

关于 @JsonFormat 与 @DateTimeFormat 的用法

@JsonFormat 是 jackson 提供的,@DateTimeFormat 由 spring 提供的

  • 前端 传给 后端。
    • 当前端传来的是键值对,用 @DateTimeFormat 规定接收的时间格式。
    • 当前端传来 json 串,后台用 @ReuqestBody 接收,用 @JsonFormat 规定接收的时间格式。
  • 后端 传给 前端。
    • 后端返回给前端的时间值,只能用 @JsonFormat 规定返回格式,@DateTimeFormat 无法决定返回值的格式。

数据库相关处理

数据库对应 Java 类(旧)对应 Java 类(新)
DATETIMEjava.util.DateLocalDateTime
DATEjava.sql.DateLocalDate
TIMEjava.sql.TimeLocalTime
TIMESTAMPjava.sql.TimestampLocalDateTime

DATETIME 与 TIMESTAMP 区别:datetime 的存储范围是 1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999,而 timestamp 的范围是 1970-01-01 00:00:01.000000 到 2038-01-19 03:14:07.999999

新系统最好使用 DATETIME,因为 TIMESTAMP 存储了不在范围内的时间值时,会直接抛出异常

还有一种建议即直接用 long 表示,在数据库中存储为 BIGINT 类型, 显示的时候再转换

2038 年问题

Mybatis

由于日常开发 Mybatis 用的比较多,故只讲下 Mybatis 下的用法,JPA 的有兴趣可以自行搜索下

首先检查下项目中引用的 Mybatis 版本,当然所有的第三方库配合 SpringBoot 的话,优先看是否存在 xxx-spring-boot-starter 或者 xxx-starter(珍惜时间,远离加班)

如果是新项目建议使用最新版本,如目前我使用的是:

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>

//此版本引用的 mybatis 版本是 3.5.3

MyBatis 从 3.4.5 版本开始就完全支持了 LocalDateTime, 如果使用的是旧版本请升级下,如果是由于三方库使用的 mybatis 版本较低,可以排除内嵌的引用,单独引用新版本

旧系统处理

当项目无法升级到 Java8+环境的,也无需担心,可以使用Joda-Time这个库,值得一提的是,Joda-Time 的作者 Stephen Colebourne 和 Oracle 一起共同参与了 java8 新的 API 的设计和实现,用法上基本同 Java8 一致

当然 Joda-Time 也有一些功能(如用来表示一对时间的 Interval) 是 java8 还没支持的,所以 Stephen Colebourne 又提供了一个新的第三方库Threeten来弥补 Java 8 的不足

Threeten 主要提供两种发行包:

  • ThreeTen-Backport:对 Java 6 和 Java 7 的项目提供 Java 8 的 date-time 类的支持
  • ThreeTen-Extra:为 Java 8 的 date-time 类提供额外的增强功能(比如:Interval 等)

Android 项目

Android 到棉花糖上运行在 Java 7 (Android N 是第一个引入 Java 8 语言特性的版本)。因此,除非你只针对 Android NoGuAT 和以上,否则你不能依赖 Java 8 语言特性

因为 Android 项目对于内存要求较高,Joda-Time 不是最好的选择,ThreeTen-Backport 也存在一些性能问题(使用 JAR 资源加载时区信息),这个时候 Android 界的大神jakewharton提供了 threetenabp , 他的 github 上也说明了为什么不推荐使用 Joda-Time 和 ThreeTen-Backport

所以如果你的 Android 项目需兼容老版本(现在还有需要兼容 4.4-的):

  • 在 app/build.gradle 中引用 threetenabp
implementation 'com.jakewharton.threetenabp:threetenabp:1.2.4'
  • 在 Application.onCreate() 方法中初始化时区信息:
@Override public void onCreate() {
super.onCreate();
AndroidThreeTen.init(this);
}

就可以愉快的使用 LocalDateTime 等新类了

Date 及 Calendar 不便之处

前面说了 Java8+的新 api,下面我们说一说旧的 Date 及 Calendar 的不便之处,以便放弃

  • 首先就是获取年月日的时候的加减 1 了
System.out.println(date.getYear() + 1900); // 必须加上 1900
System.out.println(date.getMonth() + 1); // 0~11,必须加上 1
System.out.println(date.getDate()); // 1~31,不能加 1

  • Date 不能直接转换时区,并且总是以当前计算机系统的默认时区为基础进行输出(Date 对象无时区信息,时区信息存储在 SimpleDateFormat 中)
  • 相比较 locaDateTime 丰富的时间处理(获取前一天,两个时间差等),Date 需要额外编写 DateUtils(网上各种搜索,还不一定正确)

Calendar 可以用于获取并设置年、月、日、时、分、秒,多了可以做简单的日期和时间运算的功能,但是:

  • Calendar 只有一种方式获取,即 Calendar.getInstance(),而且一获取到就是当前时间。如果我们想给它设置成特定的一个日期和时间,就必须先清除所有字段
  • Calendar 获取年月日这些信息变成了 get(int field),返回的年份不必转换,返回的月份仍然要加 1,返回的星期要特别注意,1~7 分别表示周日,周一,……,周六。
  • 最后就是常见的多线程安全的问题了, SimpleDateFormat 线程不安全(原因网上自行搜索下)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

那些过往

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

新人笑

文章 0 评论 0

mb_vYjKhcd3

文章 0 评论 0

小高

文章 0 评论 0

来日方长

文章 0 评论 0

哄哄

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文