那些年踩过的Java日期时间的坑

2022年6月23日08:14:20

那些年踩过的Java日期时间的坑

在 Java 8 之前,我们处理日期时间需求时,使用 Date、Calender 和 SimpleDateFormat,来声明时间戳、使用日历处理日期和格式化解析日期时间。这 些类的 API 的缺点比较明显,比如可读性差、易用性差、使用起来冗余繁琐,还有线程安全问题。

下面来看看常见的java8之前日期时间的常见的问题:

Calendar设置问题

Calendar设置时间12小时和24小时问题

例如:

Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR, 10);
System.out.println(c.getTime());
Wed Feb 24 22:09:17 CST 2021

原因解析:

我们设置了10小时,但运行结果是22点,而不是10点。因为Calendar.HOUR默认是按12小时制处理的,需要使用Calendar.HOUR_OF_DAY,因为它才是按24小时处理的。

Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 10);
Wed Feb 24 10:09:17 CST 2021

Calendar获取的月份比实际数字少1

例如:

Calendar calendar = Calendar.getInstance();
System.out.println("获取当前月:"+calendar.get(Calendar.MONTH)+" month ");

获取当前月:1 month

原因:Calendar中的月份从0开始 到11所以要计算正确的月份结果要加1

Calendar calendar = Calendar.getInstance();
System.out.println("当前"+(calendar.get(Calendar.MONTH)+1)+"月份");

Java日期格式化YYYY 、DD 和hh的坑

例如

Calendar calendar = Calendar.getInstance();
calendar.set(2020, Calendar.DECEMBER, 31,15,35);
Date testDate = calendar.getTime();

SimpleDateFormat dtf = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("2020-12-31 转 YYYY-MM-dd 格式后 " + dtf.format(testDate));

SimpleDateFormat dtf1 = new SimpleDateFormat("yyyy-MM-DD");
System.out.println("2020-12-31 转 yyyy-MM-DD 格式后 " + dtf1.format(testDate));

SimpleDateFormat dtf2 = new SimpleDateFormat("yyyy-MM-dd hh:mm");
System.out.println("2020-12-31 转 yyyy-MM-dd hh:mm 格式后 " + dtf2.format(testDate));
2020-12-31 转 YYYY-MM-dd 格式后 2021-12-31
2020-12-31 转 yyyy-MM-DD 格式后 2020-12-366
2020-12-31 转 yyyy-MM-dd hh:mm 格式后 2020-12-31 03:35

解析

YYYY问题:2020年12月31号变成了2021年12月31号,是因为YYYY是基于周来计算年的,它指向当天所在周属于的年份,一周从周日开始算起,周六结束,这里2020年12月31号跨年了,那么这一周就算成下一周了,所以正确使用方式是用yyyy

DD问题:DD和dd表示的不一样,DD表示的是一年中的第几天,而dd表示的是一年中的第几天,所以正确的使用方式是用dd

hh问题:设置时间是15点,运行的结果是3点,因为hh是12小时制的日期格式,当时间为15点,会处理为3点,所以正确的使用方式是用HH 它才是12小时制。

Calendar calendar = Calendar.getInstance();
calendar.set(2020, Calendar.DECEMBER, 31,15,35);
Date testDate = calendar.getTime();
SimpleDateFormat dtf3 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
System.out.println("2020-12-31 转 yyyy-MM-dd HH:mm 格式后 " + dtf3.format(testDate));

2020-12-31 转 yyyy-MM-dd HH:mm 格式后 2020-12-31 15:35

SimpleDateFormat 解析时间问题

定义的 static 的 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(() -> {
                try {
                    Date parseDate = sdf.parse("2021-01-01 11:12:13");
                    System.out.println(parseDate);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }

}

运行程序后大量报错:

Fri Jan 25 13:07:00 CST 6104
Fri Jan 01 11:12:13 CST 2021
Fri Jan 01 11:12:13 CST 2021
Fri Jan 01 11:12:13 CST 2021
	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:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at org.geekbang.time.commonmistakes.datetime.newdate.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:20)
	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: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	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:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)

没有报错的输出结果也不正常.

解析:

全局变量的SimpleDateFormat,在并发情况下,存在安全性问题。

  • SimpleDateFormat继承了 DateFormat,DateFormat 维护一个全局的 Calendar变量;
  • sdf.parse(dateStr)和sdf.format(date),都是由Calendar引用来储存的。SimpleDateFormat 的 parse 方法调用 CalendarBuilder 的 establish 方法,来构建 Calendar; establish 方法内部先清空 Calendar 再构建 Calendar,整个操作没有加锁。
  • 如果SimpleDateFormat是static全局共享的,Calendar引用也会被共享。
  • 因为Calendar内部并没有线程安全机制,所以全局共享的SimpleDateFormat不是线性安全的。

解决SimpleDateFormat线性不安全问题,有三种方式:

  • 将SimpleDateFormat定义为局部变量
  • 在同一个线程复用 SimpleDateFormat, 通过 ThreadLocal 来存放 SimpleDateFormat:
  • 方法加同步锁synchronized。

正确方式:

 private static ThreadLocal<SimpleDateFormat> threadSafeSimpleDateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

private static void wrongfix() throws InterruptedException {
    ExecutorService threadPool = Executors.newFixedThreadPool(100);

    for (int i = 0; i < 20; i++) {
        threadPool.execute(() -> {
            for (int j = 0; j < 10; j++) {
                try {
                    System.out.println(threadSafeSimpleDateFormat.get().parse("2020-01-01 11:12:13"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        });
    }
    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);
}

SimpleDateFormat 解析的时间问题

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String time = "2021-01";
System.out.println(sdf.parse(time));
java.text.ParseException: Unparseable date: "2021-01"
	at java.text.DateFormat.parse(DateFormat.java:366)
	at org.geekbang.time.commonmistakes.datetime.newdate.SimpleDateFormatTest.formatUnMatch(SimpleDateFormatTest.java:23)
	at org.geekbang.time.commonmistakes.datetime.newdate.SimpleDateFormatTest.main(SimpleDateFormatTest.java:16)

**解析:**SimpleDateFormat 可以解析长于/等于它定义的时间精度,但是不能解析小于它定义的时间精度。

当需要解析的字符串和格式不匹配的时候,SimpleDateFormat 表现得很宽容

例如:

String dateString = "20210201";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMM");
System.out.println("result:" + dateFormat.parse(dateString));

result:Tue Sep 01 00:00:00 CST 2037

居然输出了 2037 年原因是把 0201 当成了月份进行运算了。

解决方法需要匹配时间格式进行解析。

日期计算

例如:直接使用时间戳进行时间计算:如:Date().getTime 方法得到的时间戳加 30 天对应的毫秒数

Date today = new Date();
Date nextMonth = new Date(today.getTime() + 30 * 1000 * 60 * 60 * 24);
System.out.println(nextMonth);
结果输出Fri Feb 05 00:27:10 CST 2021

分析:这里得到的日期比当前日期还要早,并不是1个月以后的日期。出现这个原因是因为 int 发生了溢出 ,修改方式需要在乘法运算时候加个L使得运算结果成为Long就避免整数溢出了。

更加推荐的使用方式是使用 Calendar方法进行操作,如下

Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.DAY_OF_MONTH, 30);
System.out.println(c.getTime());

使用 Java 8 的日期时间类型更加简洁方便:

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime.plusDays(30));

总结:

  • Java的java.util.Datejava.util.Calendar类易用性差,不支持时区,而且不是线程安全的;

  • 用于格式化日期的类SimpleDateFormat对象来处理日期格式化,是非线程安全,在多线程程序中调用复用同一个DateFormat对象,会有线程安全问题。

  • SimpleDateFormat 很容易因为yyyy dd hh 大小写问题出错。

  • Calendar`中获取的月份需要加一才能表示当前月份,还有就是12小时制和24小时制的转换容易出错。

Java 8 推出了新的日期时间类。 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter 每一个类功能明确清晰、类之间协作简单、 义清晰不踩坑,API 功能强大无需借助外部工具类即可完成操作,并且线程安全下一遍来看看java8 新的日期类。

  • 作者:house.zhang
  • 原文链接:https://blog.csdn.net/pop_xiaohao/article/details/114031824
    更新时间:2022年6月23日08:14:20 ,共 6531 字。