• JAVA处理闰年日期应该注意的问题
  • 发布于 2个月前
  • 156 热度
    0 评论
前言
今年是2024年,刚好是闰年。大家都知道,闰年是有366天的,其中二月份有29天。最近有个项目组出了一个生产问题,跟闰年相关的。所以写篇文章跟大家讲讲这个bug,顺便讲讲Java日期处理的一些坑,让大家避坑~

一. 闰年处理的坑

这个bug是如何产生的呢?我给大家举个例子:比如产品要求支持查询一年之间内的交易流水,并且对这个时间区间的检验。用户选了开始时间和结束时间后,前端先检验,检验通过后,传到后端。后端也检验,然后开始查询。

1.假设当天时间2024.02.29日,客户选的开始时间是2023.02.28,结束时间是2024.02.29.

2.前端检验的时候,是用终止时间2024.02.29减12个月,得到开始时间2023.02.28,因此认为区间是合法的
3.后端检验的时候,用开始时间2023.02.28 去加上12个月,得到2024.02.28,即得到2023.02.28 ~ 2024.02.28,因此认为这个时间范围是不合法的。这就前后端不一致了,产生bug了。
后端伪代码是这样实现的:
public class Test {

    public static void main(String[] args) {
        // 从前端获取的日期字符串
        String startDateString = "20230228"; // 替换为从前端获取的startDate
        String endDateString = "20240229";   // 替换为从前端获取的endDate

        // 将字符串转换为LocalDate对象
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        LocalDate startDate = LocalDate.parse(startDateString, formatter);
        LocalDate endDate = LocalDate.parse(endDateString, formatter);

        // 将startDate增加12个月
        LocalDate startDatePlus12Months = startDate.plusMonths(12);

        // 检查是否在时间区间范围内
        if (startDatePlus12Months.isBefore(endDate)) {
            System.out.println("时间区间超过一年");
            throw new RuntimeException();
        } else {
            System.out.println("时间区间小于一年");
        }

    }
}
运行结果抛出了一场,输出时间区间超过一年。其实这个例子,就是我们是否把闰年的02.29日这一天算进去,怎么理解都算合理,但是就是要避免前后端不一致的坑。关于闰年处理的坑,我总结为主要是这几个方面:
1.在一个日期值上加或减时间的代码,比如像加减1年或1个月的代码
2.数据库查询,报表这些,月度和年度统计可能会少算 1 天
3.证书/密码/密钥/缓存 等的过期时间,可能会比预期的早了一天 固定长度数组,长度365时不够的
4.判断是否是闰年的方法, 4年一润,百年不润,四百年又润

二. 日期格式yyyy-MM-dd HH:mm:ss 大小写含义区别
YYYY 和 yyyy 区别
年份大写YYYY: 当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,那么这周就算进入下一年。
yyyy: 表示四位年份。
比如看这个例子:

从输出结果,大家可以发现,同一个日期,只是转化了一下格式,日期就变了。就是因为年份YYYY大写的问题。

HH 和 hh 区别
小时大写HH:代表24小时制的小时。
hh:是12制的日期格式,当时间为12点,会处理为0点。

三.SimleDateFormat的format初始化问题

四. DateTimeFormatter 日期本地化问题
反例:
String dateStr = "Wed Mar 18 10:00:00 2020";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy");
LocalDateTime dateTime = LocalDateTime.parse(dateStr, formatter);
System.out.println(dateTime);
运行结果:
Exception in thread "main" java.time.format.DateTimeParseException: Text 'Wed Mar 18 10:00:00 2020' could not be parsed at index 0
 at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
 at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
 at java.time.LocalDateTime.parse(LocalDateTime.java:492)
 at com.example.demo.SynchronizedTest.main(SynchronizedTest.java:19)
解析:DateTimeFormatter 这个类默认进行本地化设置,如果默认是中文,解析英文字符串就会报异常。可以传入一个本地化参数(Locale.US)解决这个问题

正例:
String dateStr = "Wed Mar 18 10:00:00 2020";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy",Locale.US);
LocalDateTime dateTime = LocalDateTime.parse(dateStr, formatter);
System.out.println(dateTime);
五. Calendar设置时间的坑
反例:
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR, 10);
System.out.println(c.getTime());
运行结果:Thu Mar 26 22:28:05 GMT+08:00 2020
解析:
我们设置了10小时,但运行结果是22点,而不是10点。因为Calendar.HOUR默认是按12小时制处理的,需要使用Calendar.HOUR_OF_DAY,因为它才是按24小时处理的。
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 10);
六.日期计算的坑
日期计算,比如new Date()加一个30天,有些小伙伴可能会这么写:

得到的日期居然比当前日期还要早,根本不是晚 30 天的时间。这是因为发生了int发生了溢出。可以把30修改为 30L,或者直接用Java 8的加减API,如下:

七. SimpleDateFormat 线性安全问题
比如这个例子:
public class SimpleDateFormatTest {

    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000));

        while (true) {
            threadPoolExecutor.execute(() -> {
                String dateString = sdf.format(new Date());
                try {
                    Date parseDate = sdf.parse(dateString);
                    String dateString2 = sdf.format(parseDate);
                    System.out.println(dateString.equals(dateString2));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
SimpleDateFormat 是个共享变量,多线程跑的时候,就会有问题:
Exception in thread "pool-1-thread-49" java.lang.NumberFormatException: For input string: "5151."
 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
 at java.lang.Long.parseLong(Long.java:589)
 at java.lang.Long.parseLong(Long.java:631)
 at java.text.DigitList.getLong(DigitList.java:195)
 at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
 at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
 at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
 at java.text.DateFormat.parse(DateFormat.java:364)
 at com.example.demo.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:19)
 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)
Exception in thread "pool-1-thread-47" java.lang.NumberFormatException: For input string: "5151."
 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
 at java.lang.Long.parseLong(Long.java:589)
 at java.lang.Long.parseLong(Long.java:631)
 at java.text.DigitList.getLong(DigitList.java:195)
 at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
 at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
 at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
 at java.text.DateFormat.parse(DateFormat.java:364)
 at com.example.demo.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:19)
 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)

或者直接使用DateTimeFromatter

八. Java日期的夏令时问题
反例:
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.parse("1986-05-04 00:30:00"));
运行结果:
Sun May 04 01:30:00 CDT 1986
解析:先了解一下夏令时
夏令时,表示为了节约能源,人为规定时间的意思。一般在天亮早的夏季人为将时间调快一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。各个采纳夏时制的国家具体规定不同。目前全世界有近110个国家每年要实行夏令时。1986年4月,中国中央有关部门发出“在全国范围内实行夏时制的通知”,具体作法是:每年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时。(1992年起,夏令时暂停实行。)

夏时令这几个时间可以注意一下哈,1986-05-04, 1987-04-12, 1988-04-10, 1989-04-16, 1990-04-15, 1991-04-14。


结合demo代码,中国在1986-05-04当天还在使用夏令时,时间被拨快了1个小时。所以0点30分打印成了1点30分。如果要打印正确的时间,可以考虑修改时区为东8区。

正例:
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.parse("1986-05-04 00:30:00"));

用户评论