在 Java 8 之前,我们处理日期时间需求时,使用 Date、Calender 和 SimpleDateFormat,来声明时间戳、使用日历处理日期和格式化解析日期时间。这 些类的 API 的缺点比较明显,比如可读性差、易用性差、使用起来冗余繁琐,还有线程安全问题。
下面来看看常见的java8之前日期时间的常见的问题:
例如:
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 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)+"月份");
例如
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
例如
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线性不安全问题,有三种方式:
正确方式:
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 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 可以解析长于/等于它定义的时间精度,但是不能解析小于它定义的时间精度。
例如:
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.Date
和java.util.Calendar
类易用性差,不支持时区,而且不是线程安全的;
用于格式化日期的类SimpleDateFormat
对象来处理日期格式化,是非线程安全,在多线程程序中调用复用同一个DateFormat
对象,会有线程安全问题。
SimpleDateFormat 很容易因为yyyy dd hh 大小写问题出错。
Calendar`中获取的月份需要加一才能表示当前月份,还有就是12小时制和24小时制的转换容易出错。
Java 8 推出了新的日期时间类。 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter 每一个类功能明确清晰、类之间协作简单、 义清晰不踩坑,API 功能强大无需借助外部工具类即可完成操作,并且线程安全下一遍来看看java8 新的日期类。