文章主要介绍了如何在Java中优雅地操纵时间,并提供了相关的概念和工具类。作者认为建议使用Java 8的时间API,因为它在安全性和易用性上都比java.util.Date高。文章还介绍了时间在计算机中的存储和展示方式,以及不同时间API的比较,如java.util、Calendar、TimeZone和java.time.*。同时,文章还提到了日期和时间的格式化,以及时间间隔的计算。最后,给出了一个基于Java.time.*实现的日期工具类的例子,并讨论了时间工具类在数据库映射中的变化。
时间在计算机中存储的本质是一个整数,称为Epoch Time(时间戳)。时间戳表示从1970年1月1日零点到现在所经历的秒数。时间戳不易理解,需要转换为易读的时间,并加上时区信息才能精准定位时刻。
Java中有两套处理日期/时间的API,分别是java.util和java.time.*。java.util在JDK8之前使用,而java.time.*在JDK8中引入,提供了更清晰、更易用的API。
Java.time.*提供了多种日期和时间的格式化方法,如DateTimeFormatter,用于将日期和时间格式化为字符串或解析字符串为日期和时间。
Java.time.*提供了用于计算时间间隔的类,如Duration和Period,用于表示两个时刻之间的时间间隔。
文章给出了一个基于Java.time.*实现的日期工具类的例子,提供了多种日期和时间的处理方法,如获取当前日期、时间、日期时间,格式化日期为字符串,增加指定天数等。
在Mybatis中,Java.time.*与数据库映射的支持需要添加相关依赖,以实现LocalDateTimeTypeHandler等类型处理器。
怎么才能在Java中优雅的操纵时间呢,作者整理了相关的概念和工具类,希望帮助大家在代码开发的过程中对时间的使用更加优雅。
在开发时候,发现有很多需要用到时间的地方,例如记录操作的时间、比较时间判断产品是否有效等。总而言之,时间是我们业务开发必须关注、时刻注意的点。但目前工程的代码中使用了非常多时间的工具类,一会儿用Java.util.Date记录时间,一会用Java.time.LocalDateTime记录时间,怎么才能在Java中优雅的操纵时间呢,我整理了相关的概念和工具类,希望帮助大家在代码开发的过程中对对时间的使用更加优雅。
这里先写一个结论:
时间以整数的方式进行存储:时间在计算机中存储的本质是一个整数,称为Epoch Time(时间戳),计算从1970年1月1日零点(格林威治时间/GMT+00:00)到现在所经历的秒数。
在java程序中,时间戳通常使用
long
表示毫秒数,通过
System.currentTimeMillis()
可以获取时间戳。时间戳对我们人来说是不易理解的,因此需要将其转换为易读的时间,例如,2024-10-7 20:21:59(实际上说的是本地时间),而同一时刻不同时区的人看到的本地时间是不一样,所以在时间展示的时候需要加上时区的信息,才能精准的找到对应的时刻。
时区与世界时间标准相关:
世界时间的标准在1972年发生了变化,但我们在开发程序的时候可以忽略GMT和UTC的差异, 因为计算机的时钟在联网的时候会自动与时间服务器同步时间。
本地时间等于我们所在(或者所使用)时区内的当地时间,它由与世界标准时间(UTC)之间的偏移量来定义。这个偏移量可以表示为 UTC- 或 UTC+,后面接上偏移的小时和分钟数。 例如:
GMT+08:00
或者
UTC+08:00
表示东八区,
2024-10-7 20:21:59 UTC+08:00
便可以精准的定位一个时刻。
JDK以版本8为界,有两套处理日期/时间的API。
在jdk8之前,Java使用java.util中的API对处理时间。
在获取年月日的时候,Date和Calendar需要进行不同的转换=>规则不统一。
Date
java.util.Date用于表示一个日期和时间的对象,实现很简单(实际上存储了一个long类型的以毫秒表示的时间戳,在通过
new Date()
获取当前时间的时候,实际上是通过System.currentTimeMillis()获取时间戳进行赋值)。
public class Date {
long fastTime;
public Date(long date) {
fastTime = date;
}
public long getTime() {
return fastTime;
}
}
java.util.Date承载的功能有限,且在利用Date类获取具体年/月/日的时候需要注意:
getYear()
返回的年份必须加上
1900
,
getMonth()
返回的月份是0-11分别表示1-12月,所以要加1,而
getDate()
返回的日期范围是
1
~
31
,又不能加1。
Calendar
Calendar
可以用于获取并设置年、月、日、时、分、秒,它和
Date
比,主要多了一个可以做简单的日期和时间运算的功能,但代码粗糙,API不好用,性能也不好。
Calendar对象getTime()可以获得Date对象:
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
int y = c.get(Calendar.YEAR);
int m = 1 + c.get(Calendar.MONTH);
int d = c.get(Calendar.DAY_OF_MONTH);
int w = c.get(Calendar.DAY_OF_WEEK);
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);
System.out.println(y + "-" + m + "-" + d + " " + w + " " + hh + ":" + mm + ":" + ss + "." + ms);
}
}
import java.text.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
c.clear();
c.set(2019, 10 , 20, 8, 15, 0);
c.add(Calendar.DAY_OF_MONTH, 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));
}
}
TimeZone
Calendar
和
Date
相比,它提供了时区转换的功能。时区用
TimeZone
对象表示。
时区的唯一标识是以字符串表示的ID,获取指定
TimeZone
对象也是以这个ID为参数获取,
GMT+09:00
、
Asia/Shanghai
都是有效的时区ID。可以通过
TimeZone.getAvailableIDs()
获取系统支持的所有ID。
import java.text.*;
import java.util.*;
public class learnTime {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
c.clear();
c.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
c.set(2024, 9 , 10, 8, 15, 0);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
System.out.println(sdf.format(c.getTime()));
}
}
java.text.SimpleDateFormat
Date和SimpleDateFormat使用解析时间:
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = format.parse("2024-10-07 16:10:22");
String str = format.format(date);
由于SimpleDateFormat线程不安全,为了提升性能,会使用ThreadLocalCache,如下:
static final ThreadLocal SIMPLE_DATE_FORMAT_LOCAL
= ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
开源社区开发了一个日期类Joda,API清晰,性能较好,提交了JSR-310,在java8中称为JDK基础类库。
-
本地日期和时间:
LocalDateTime(日期和时间)
,
LocalDate(日期)
,
LocalTime(时间)
;
-
带时区的日期和时间:
ZonedDateTime
;
-
时刻:
Instant
;
-
时区:
ZoneId
,
ZoneOffset
;
-
以及一套新的用于取代
SimpleDateFormat
的格式化类型
DateTimeFormatter
。
LocalDate/LocalTime/LocalDateTime
-
默认严格按照ISO 8601规定日期和时间格式进行打印(日期和时间的分隔符是T)。
-
LocalDateTime localDayTime=LocalDateTime.of(2024, 10, 07, 8, 15, 0);
LocalDate localDay=LocalDate.of(2024, 10, 07);
LocalTime localTime=LocalTime.parse("08:15:07");
-
有对日期和时间进行加减的非常简单的链式调用,通过plusXxx()/minusXxx()对时间进行变换:
public class learnTime {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.of(2024, 10, 10, 20, 30, 59);
System.out.println(dt);
LocalDateTime dt2 = dt.plusDays(5).minusHours(3);
System.out.println(dt2);
LocalDateTime dt3 = dt2.minusMonths(1);
System.out.println(dt3);
}
}
public class Main {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
System.out.println("当月第一天0:00时刻"+now.withDayOfMonth(1).atStartOfDay());
System.out.println("当月第一天:"+now.with(TemporalAdjusters.firstDayOfMonth()));
System.out.println("下月第一天:"+now.with(TemporalAdjusters.firstDayOfNextMonth()));
System.out.println("明年第一天:"+now.with(TemporalAdjusters.firstDayOfNextYear()));
System.out.println("本年第一天:"+now.with(TemporalAdjusters.firstDayOfYear()));
System.out.println("当月最后一天:"+now.with(TemporalAdjusters.lastDayOfMonth()));
System.out.println("本年最后一天:"+now.with(TemporalAdjusters.lastDayOfYear()));
System.out.println("当月第三周星期五:"+now.with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.FRIDAY)));
System.out.println("上周一:"+now.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)));
System.out.println("下周日:"+now.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)));
}
}
Duration和Period
-
Duration
:
基于时间值
(Instant/LocalDateTime)
,表示两个时刻时间的时间间隔,适合处理较短的时间,需要更高的精确性。
-
使用
between()
方法比较两个瞬间的差;
-
使用
getSeconds()
或
getNanosecends()
方法获取时间单元的值;
-
获得具体的粒度的间隔:
ofDays()
,
ofHours()
,
ofMillis()
,
ofMinutes()
,
ofNanos()
,
ofSeconds()
;
-
通过文本创建Duration对象,格式为 “PnDTnHnMn.nS”,
Duration.parse("P1DT1H10M10.5S")
;
-
使用
toDays()
,
toHours()
,
toMillis()
,
toMinutes()
方法把Duration对象可以转成其他时间单元;
-
通过
plusX()
、
minusX()
方法增加或减少Duration对象,其中X表示days, hours, millis, minutes, nanos 或 seconds。
-
Period
基于日期值,表示一段时间的年、月、日:
-
使用
between()
方法比较两个日期的差;
-
使用
getYears(),getMonhs(),getDays()
方法获取具体粒度差距(返回的类型是int);
-
通过文本创建
Period
对象,格式为 “PnYnMnD”:
Period.parse("P2Y3M5D")
;
-
可以通过
plusX()
、
minusX()
方法进行增加或减少,其中X表示日期单元;
ZonedDateTime
ZonedDateTime
是
LocalDateTime
加
ZoneId
。
-
ZonedDateTime 带时区时间的常见方法:
-
import java.time.*;
public class Main {
public static void main(String[] args) {
ZonedDateTime zbj = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime zny = zbj.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println(zbj);
System.out.println(zny);
}
}
ZoneId时区类
时区类,功能和java.util.TimeZone类似。
ZoneId支持两种类型格式初始化,一种是时区偏移的格式(基于UTC/Greenwich时),一种是地域时区的格式(eg:Europe/Paris)。ZoneId是抽象类,具体的逻辑实现由来子类完成,ZoneOffset处理时区偏移类型的格式,ZoneRegion处理基于地域时区的格式:
Instant
通过获取Instant的对象可以拿到此刻的时间,该时间由两部分组成:从1970-01-01 00:00:00 开始走到此刻的总秒数+不够1秒的纳秒数。
//1、创建Instant的对象,获取此刻时间信息
Instant now = Instant.now(); //不可变对象
//2、获取总秒数
long second = now.getEpochSecond();
system.out.println(second) ;
//3、不够1秒的纳秒数
int nano = now.getNano();
system.out.println(nano) ;
system.out.println(now);
//可以进行加减法
Instant instant = now.plusNanos(111);//将纳秒加111
// Instant对象的作用:做代码的性能分析,或者记录用户的操作时间点
Instant now1 = Instant.now();
//代码执行...
Instant now2 = Instant.now();
//用这两个时间点相减就可以知道这段代码运行了多少时间
DataTimeFormatter
import java.time.*;
import java.time.format.*;
import java.util.Locale;
public class Main {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
System.out.println(formatter.format(zdt));
DateTimeFormatter zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
System.out.println(zhFormatter.format(zdt));
DateTimeFormatter usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
System.out.println(usFormatter.format(zdt));
}
}
新老API转换参考:https://blog.csdn.net/qq_31635851/article/details/120150588
LocalDateTime
不包括时区,而——
为了从
LocalDateTime
转换到——
LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
Date date = Date.from(zonedDateTime.toInstant());
Date date = new Date();
Instant instant = date.toInstant();
LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
数据库映射变化
Mybatis中和时间相关的 jdbcType和javaType、typeHandler的对照关系
操作时间相关的工具
有一些对基础的API进行了封装便于我们在开发中有效的处理时间。
目前暂时没有发现基于Java.time*封装的公共的时间工具类。
在很多情况下,因为已有的工具类不能满足当下的业务需求,工程内部需要自己实现类似DateUtil的工具类,建议基于java.time*实现相关的工具类。
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
public class DateUtils {
public static LocalDate getCurrentDate() {
return LocalDate.now();
}
public static LocalTime getCurrentTime() {
return LocalTime.now();
}
public static LocalDateTime getCurrentDateTime() {
return LocalDateTime.now();
}
public static String formatLocalDate(LocalDate date, String pattern) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return date.format(formatter);
}
public static LocalDate parseLocalDate(String dateStr, String pattern) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return LocalDate.parse(dateStr, formatter);
}
public static LocalDate addDays(LocalDate date, long days) {
return date.plusDays(days);
}
public static LocalDate minusDays(LocalDate date, long days) {
return date.minusDays(days);
}
public static long getDaysBetween(LocalDate startDate, LocalDate endDate) {
return ChronoUnit.DAYS.between(startDate, endDate);
}