1.概述
在日常开发系统过程中,日期和时间的操作处理是一个常见的应用功能场景,Java提供了多种工具和库来处理日期和时间操作,其中主要分为:Java 8之前的提供java.util.Date
、java.util.Calendar
。Java 8引入了全新的日期时间API,提供了更好用且更强大的日期时间处理功能,主要的类包括LocalDate
、LocalTime
、LocalDateTime
和ZonedDateTime
。
在这篇博文中,我们将总结讲解一些常用的日期处理操作,证明Java 8使用全新的LocalDateTime
等类来替代以前老的Date
必要性。平时我们在使用Date
处理日期时间或多或少都有感觉到繁琐不方便,甚至出现逻辑不对、线程安全等问题。因为Java 8之前提供的API存在设计缺陷和使用上的复杂性,不过幸好的是很多开源框架提供了对日期时间处理操作封装的工具类,平时操作计算日期时间时不需要自己实现,直接调用工具包封装的方法即可,这里给大家推荐下常用的:hutool日期工具包
2.Date处理日期存在哪些问题
首先我想说Java 8引入操作日期时间的全新API,肯定不是空虚来风,多此一举,画蛇添足的。肯定是为了解决以前Date
处理日期的一些痛点问题而产生的。下面我们就来看看日常使用Date
处理日期时间存在的问题
2.1 日期时间计算
在平时业务开发过程中,根据当前日期进行加减天数计算时一个很常见的逻辑处理,因为大家都知道Date
中保存的是一个UTC时间戳,代表的是从 1970 年 1 月 1 日 0 点(Epoch 时间)到现在的毫秒数,所以很多同学计算日期时间时都喜欢转换为时间戳进行加减计算来获得结果时间戳之后再转换为日期(笔者刚刚工作时也喜欢这么干),这样计算就会踩坑了,例如计算30天以后得日期时间示例如下:
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
Date now = new Date();
System.out.println("now: " + simpleDateFormat.format(now));
// 加30天
Date afterDate = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
System.out.println("afterDate: " + simpleDateFormat.format(afterDate));
}
执行结果如下:
now: 2024-07-02 16:48:35
afterDate: 2024-06-12 23:45:48
加了30天日期反倒变早了???出现这个问题,这是因为时间戳now.getTime() + 30 * 24 * 60 * 60 * 1000 计算时 int 发生了溢出。计算表达式可以看出是在加一个int数,从而计算结果也变成一个int数从而导致数值溢出,解决该问题只需要计算时加上一个long数即可,1000变成1000L如下所示:
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
Date now = new Date();
System.out.println("now: " + simpleDateFormat.format(now));
// 加10天
Date afterDate = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000L);
System.out.println("afterDate: " + simpleDateFormat.format(afterDate));
}
执行结果就对了。由此看出计算日期时间使用时间戳相加减是非常容易出现意想不到的问题的,不建议使用这种方式。Java 8之前建议使用 Calendar
计算:
/**
* 加n天后的日期
*/
public static Date addDays(Date date, int n) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.DAY_OF_YEAR, n);
return cal.getTime();
}
使用 Java 8 的日期时间类型,可以直接进行各种计算,更加简洁和方便:
public static void main(String[] args) throws InterruptedException {
LocalDateTime now = LocalDateTime.now();
LocalDateTime localDateTime = now.plusDays(30);
System.out.println(localDateTime);
}
控制台直接打印如下:
2024-08-01T17:05:11.609
输出的就是我们常见的日期格式,比起Date
类型的日期输出格式更加一目了然。
2.2 格式"YYYY-MM-dd"跨年问题
有段时间我收到了关于这个格式问题的大量博文推送,标题确实蛮引人好奇了,譬如“使用YYYY-MM-dd格式化,元旦假期期间老板让回公司改线上bug”等。首先我们我都知道关于日期时间的格式化一般通用格式是:yyyy-MM-dd HH:mm:ss
。
平时我们一般都不咋在意年份是大写的Y还是小写的y,我们比较关注的是大写M代表月份,小写m代表分钟。下面就来看看YYYY-MM-dd
咋就跨年有问题了,话不多说上示例:
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd");
public static void main(String[] args) throws InterruptedException {
Calendar c = Calendar.getInstance();
// 2023-12-30 周六
c.set(2023, 11, 30);
System.out.println(simpleDateFormat.format(c.getTime()));
// 2023-12-31 周日
c.set(2023, Calendar.DECEMBER, 31);
System.out.println(simpleDateFormat.format(c.getTime()));
}
执行结果如下:
2023-12-30
2024-12-31
是的你没看错,2023-12-31
该日期格式化之后输出是2024-12-31
,这就是所谓的跨年bug。产生问题的原因:小写 y 是年,而大写 Y 是 week year,也就是所在的周属于哪一年。当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,那么这周就算入下一年,这下清楚为啥这样了吧。确实Java 8之前日期格式平时一不留神就容易写错,产生这样的未知问题。
你可能还注意到月份枚举是从0到11,和我们平时使用的112月不对应,一般开发都建议枚举、常量等数值和数组下标一样从0开始,这本身没啥问题,JDK源码编写大佬也是这么干的,但是他有点忽略实际使用场景了,有点死脑筋的感觉了😄,这么说那`Calendar`源码大佬肯定要回怼了,我没让你直接写数字,你最好用我定义好的常量`Calendar.DECEMBER`不就不出问题了嘛~~嗯,有点道理,道理不大,Java 8提供的全新的日期时间API就是你的无声反驳啦。
2.3 SimpleDateFormat线程安全问题
Date
类本身没有提供格式化和解析日期的直接支持。需要使用java.text.SimpleDateFormat
类,这个类在多线程环境中也不是线程安全的,容易引发问题。示例如下:
private static ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(20);
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for ( int i = 0; i < 50; i++) {
threadPoolExecutor.execute(() -> {
try {
// 字符串解析转日期
System.out.println(simpleDateFormat.parse("2024-07-02 18:30:00"));
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
执行结果:
Tue Jul 02 18:30:00 CST 2024
Tue Jul 02 18:30:00 CST 2024
Fri Jul 02 18:30:00 CST 7024
Tue Jul 02 18:30:00 CST 2024
Tue Jul 02 18:30:00 CST 2024
Tue Jul 02 18:30:00 CST 2024
Mon Dec 02 18:18:00 CST 3
Mon Dec 02 18:18:00 CST 3
Thu Jul 02 18:30:00 CST 33
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.shepherd.basedemo.date.DateTest.lambda$main$0(DateTest.java:34)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ".7024"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:578)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.shepherd.basedemo.date.DateTest.lambda$main$0(DateTest.java:34)
可以看出有异常报错,正常解析转换的日期也有错误数据,这就是并发导致的。
调试代码直接来看看SimpleDateFormat
的#parse()
方法会发现来到其父类DateFormat
的#parse()
:
public Date parse(String source) throws ParseException
{
ParsePosition pos = new ParsePosition(0);
Date result = parse(source, pos);
if (pos.index == 0)
throw new ParseException("Unparseable date: \"" + source + "\"" ,
pos.errorIndex);
return result;
}
最终来到SimpleDateFormat
的#parse()
,核心代码逻辑如下:
@Override
public Date parse(String text, ParsePosition pos)
{
CalendarBuilder calb = new CalendarBuilder()
try {
parsedDate = calb.establish(calendar).getTime();
}
return parsedDate;
}
接下来看看CalendarBuilder
是怎么构建Calender
,核心代码逻辑如下:
Calendar establish(Calendar cal) {
cal.clear();
// Set the fields from the min stamp to the max stamp so that
// the field resolution works in the Calendar.
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
for (int index = 0; index <= maxFieldIndex; index++) {
if (field[index] == stamp) {
cal.set(index, field[MAX_FIELD + index]); // 构建
break;
}
}
}
return cal;
}
变量 calendar
是SimpleDateFormat
对象一个字段属性,SimpleDateFormat
的 parse
方法调用 CalendarBuilder
的 #establish()
方法来构建calendar
;establish
方法内部先清空 calendar
再构建 calendar
,整个操作没有加锁。显然,如果多线程池调用parse
方法,也就意味着多线程在并发操作一个 calendar
,可能会产生一个线程还没来得及处理 calendar
就被另一个线程清空了了。解决办法要么把simpleDateFormat
声明为方法内的局部变量,要么使用threadLocal
来存放simpleDateFormat
,每个线程有个数据副本进行c操作隔离。
private static ThreadLocal<SimpleDateFormat> holder = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
使用 Java 8 中的 DateTimeFormatter
就能解决上面日期时间格式化碰到的问题
public static void main(String[] args) throws InterruptedException {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse("2024-07-02 18:30:22", formatter);
System.out.println(localDateTime);
}
总的来说,这些问题的存在就是Java 8引入全新api的大势所趋。
项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用
Github地址:https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent
微信公众号:Shepherd进阶笔记
交流探讨qun:Shepherd_126
3.Java 8全新API是如何处理日期时间的
前面列举了使用Date
操作日期时间存在总得的问题,所以如此说来这个Java 8提供全新日期时间处理类LocalDate、LocalDateTime
等是非换不可了,LocalDateTime
提供的处理日期时间的全新方法如下所示,可谓是应有尽有
接下来就来看看Java 8全新的API使用示例,带你丝滑上车。
3.1 获取当前日期时间
java.util.Date
类是可变的,这意味着它的实例在创建之后可以被修改。这在多线程环境中会导致线程安全问题,而Java 8提供的所有日期时间类都是不可变的(LocalDate
、LocalTime
、LocalDateTime
等),从而保证了线程安全
LocalDate today = LocalDate.now();
System.out.println("Current date: " + today);
LocalDateTime now = LocalDateTime.now();
System.out.println("now: " + now);
// 创建一个特定日期时间
LocalDateTime specificDateTime = LocalDateTime.of(2024, 6, 13, 10, 15);
System.out.println("Specific date and time: " + specificDateTime);
// Date的可变性问题
Date date = new Date();
System.out.println("Original date: " + date);
date.setTime(1000000000000L);
System.out.println("Modified date: " + date);
执行结果如下所示:
Current date: 2024-07-04
now: 2024-07-04T10:00:52.201
Specific date and time: 2024-06-13T10:15
Original date: Thu Jul 04 10:00:52 CST 2024
Modified date: Sun Sep 09 09:46:40 CST 2001
可以看出Java 8提供的日期时间类输出格式更符合我们的日期使用习惯。
3.2 加减日期时间
上面我们分析了日期时间加减常见出现的问题,以及给出了合理的处理方式,这里我将再次全面带你掌握Java 8全新API加减日期时间的丝滑顺畅操作:
// 当前日期
LocalDate today = LocalDate.now();
System.out.println("today: " + today);
LocalDate localDate = today.plusDays(30);
System.out.println("after 30 days: " + localDate);
LocalDate weeks = today.plusWeeks(2);
System.out.println("after 2 weeks: " + weeks);
LocalDate months = today.minusMonths(3);
System.out.println("before 3 months: " + months);
LocalDate years = today.minusYears(5);
System.out.println("before 5 years: " + years);
today = today.plus(Period.ofDays(1)).minus(1, ChronoUnit.DAYS);
System.out.println("plus 1 and minus 1, today: " + today);
// 当前日期时间
LocalDateTime now = LocalDateTime.now();
System.out.println("now: " + now);
LocalDateTime hours = now.plusHours(12);
System.out.println("after 12 hours: " + 12);
LocalDateTime minutes = now.minusMinutes(30);
System.out.println("before 30 minutes: " + minutes);
执行结果如下:
today: 2024-07-04
after 30 days: 2024-08-03
after 2 weeks: 2024-07-18
before 3 months: 2024-04-04
before 5 years: 2019-07-04
plus 1 and minus 1, today: 2024-07-04
now: 2024-07-04T19:01:52.154
after 12 hours: 12
before 30 minutes: 2024-07-04T18:31:52.154
3.3 判断两个日期前后、相差天数、月份等
日常开发过程比较两个日期大小,计算两个日期相差多少天是一个常见的场景逻辑,所以下面来看看示例:
LocalDate today = LocalDate.now();
System.out.println("today: " + today);
LocalDate date = LocalDate.of(2024, 7, 4);
System.out.println("date: " + date);
boolean b = today.equals(date);
System.out.println("today 与 date是否相等:" + b);
LocalDate afterDate = LocalDate.of(2024, 8, 10);
boolean after = afterDate.isAfter(today);
System.out.println("after today: " + after);
LocalDate beforeDate = LocalDate.of(2024, 7, 3);
boolean before = beforeDate.isBefore(today);
System.out.println("before today: " + before);
Period period = Period.between(today, afterDate);
System.out.println("相差天数:" + period.getDays());
System.out.println("相差月份:" + period.getMonths());
执行结果如下:
today: 2024-07-04
date: 2024-07-04
today 与 date是否相等:true
after today: true
before today: true
相差天数:6
相差月份:1
3.4 格式化日期时间
上面说了使用SimpleDateFormat
存在线程安全问题,所以Java 8提供了DateTimeFormatter
来格式化
// 当前日期时间
LocalDateTime now = LocalDateTime.now();
// 定义格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化日期时间
String formattedDateTime = now.format(formatter);
System.out.println("Formatted date and time: " + formattedDateTime);
// 解析日期时间
LocalDateTime parsedDateTime = LocalDateTime.parse(formattedDateTime, formatter);
System.out.println("Parsed date and time: " + parsedDateTime);
执行结果如下:
Formatted date and time: 2024-07-04 20:43:01
Parsed date and time: 2024-07-04T20:43:01
3.5 综合小案例
需求是这样,输入一个号数day,判断day是否晚于当前日期,晚于则计算为当前月day日期,否则计算为下个月day日期,但要考虑计算结果月份是否有day日期?听不懂的话来看看例子,加入当前日期2024-08-04 ,输入day=3,则计算的结果日期为2024-09-03;输入day=5,则结果日期为2024-08-05,输入day=31,结果为日期为2024-09-30,因为9月份没有31号。
int day = 3;
// 获取当前日期的年和月、日
LocalDate currentDate = LocalDate.now();
int currentYear = currentDate.getYear();
int currentMonth = currentDate.getMonthValue();
int currentDay = currentDate.getDayOfMonth();
// 计算下个月的年和月
int nextMonth = currentMonth + 1;
if (currentDay < day) {
nextMonth = currentMonth;
}
int nextMonthYear = currentYear;
if (nextMonth > 12) {
nextMonth = 1;
nextMonthYear = currentYear + 1;
}
// 获取下个月的YearMonth对象
YearMonth nextYearMonth = YearMonth.of(nextMonthYear, nextMonth);
// 获取下个月的天数
int daysInNextMonth = nextYearMonth.lengthOfMonth();
// 如果指定的日期超过了下个月的天数,则使用下个月的最后一天
int dayToUse = Math.min(day, daysInNextMonth);
LocalDate localDate = LocalDate.of(nextMonthYear, nextMonth, dayToUse);
System.out.println(localDate);
3.6 Date与LocalDate、LocalDateTime互转
由于历史原因,在一个项目开发中既有用Date的,也有使用LocalDateTime的,所以必然存在互相转换的情况
// 创建一个LocalDateTime对象
LocalDateTime localDateTime = LocalDateTime.now();
// 将LocalDateTime转换为Date
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
// 打印结果
System.out.println("LocalDateTime: " + localDateTime);
System.out.println("转换后的Date: " + date);
// 创建一个LocalDateTime对象
LocalDate localDate= LocalDate.now();
// 将LocalDateTime转换为Date
Date date1 = Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
// 打印结果
System.out.println("LocalDateTime: " + localDateTime);
System.out.println("转换后的Date1: " + date1);
// 创建一个java.util.Date对象
Date date2 = new Date();
// 将Date转换为Instant
Instant instant = date2.toInstant();
// 获取系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
// 将Instant转换为LocalDateTime
LocalDateTime localDateTime2 = LocalDateTime.ofInstant(instant, zoneId);
// 输出结果
System.out.println("Date2: " + date);
System.out.println("LocalDateTime2: " + localDateTime2);
4.总结
Java提供了多种方式来处理日期和时间,从早期的java.util.Date
和java.util.Calendar
到Java 8引入的现代化java.time
下的全新API(LocalDate, LocalDateTime...)
。新的API提供了更简单、直观且功能强大的日期时间处理能力,建议在新项目中尽量使用java.time
包来进行日期时间操作。
无论是获取当前日期时间,创建特定的日期时间,还是进行日期时间的加减运算,Java的日期时间API都能提供全面的支持。同时,通过DateTimeFormatter
类,还可以方便地对日期时间进行格式化和解析。