前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java中的时间和日期(一):有关java时间的哪些坑

Java中的时间和日期(一):有关java时间的哪些坑

作者头像
冬天里的懒猫
发布2020-08-11 14:50:50
2.1K0
发布2020-08-11 14:50:50
举报

从一开始学习java到现在,我们都一直在使用java.util.Date这个对象来表示时间和日期。使用也很方便:

Date date = new Date();
System.out.println(date.toString());

这样很容易的就得到了一个基于当前时间的字符串输出:

Wed Aug 05 10:47:21 CST 2020

另外结合系统中的一些列日期的工具类,我们可以完成很多基于时间的操作。利用Calendar实现指定时间设置,通过SimpleDateFormat来实现日期的格式化等等。但是使用的过程中,经常会出现各种各样的错误。

1.容易混淆的日期格式字符串

如下例子所示,我们希望将2020-12-29日通过格式化输出:

Calendar calendar = Calendar.getInstance();
calendar.set(2020,Calendar.DECEMBER,29);
SimpleDateFormat format = new SimpleDateFormat("YYYY-MM-dd");

结果却不是我们希望的那样:

2021-12-29

结果变成了2021年。这是因为,大写的Y表示 Week year。即本周所在的年份。2020年12月29日位于2021年的第一周,那么自然时间就变成了2021年。

在这里插入图片描述
在这里插入图片描述

实际上应该用小写的y来表示。也就是说,这个时间格式字符串,大小写有不同的意义。月份是大写的MM,而不是小写的m。自然,这个情况在新版本的阿里规范中也有说明:

在这里插入图片描述
在这里插入图片描述

专门有两条进行说明,可见,这也是在日常编码过程中容易出BUG的地方。

2.static的SimpleDateFormat

这是一个非常著名的坑,问题在于,SimpleDateFormat并不是线程安全的。如果static在多线程场景下容易导致并发问题。我们可以用如下代码测试:

private final static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

public static void main(String[] args) {

	ExecutorService pool = new ThreadPoolExecutor(10, 10,
			0L, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(20));

	IntStream.range(0,10).forEach((i) -> {
		pool.execute(() -> {
			IntStream.range(0,20).forEach((j) -> {
				try {
					System.out.println(simpleDateFormat.parse("2020-08-05"));
				} catch (ParseException e) {
					e.printStackTrace();
				}
			});
		});
	});
}

创建一个10个线程的threadPool,之后,提交10个任务循环对simpleDateFormat对象 parse相同的时间。 可以看到结果中:

Wed Aug 05 00:00:00 CST 2020
Thu Sep 24 00:00:00 CST 2020
Wed Aug 05 00:00:00 CST 2020
Thu Sep 24 00:00:00 CST 2020
Wed Aug 05 00:00:00 CST 2020
Wed Aug 05 00:00:00 CST 2020
java.lang.NumberFormatException: For input string: "E.800088E22.800088E2"
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
	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.dhb.date.test.SimpleDateFormatTest.lambda$null$0(SimpleDateFormatTest.java:21)
	at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
	at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:559)
	at com.dhb.date.test.SimpleDateFormatTest.lambda$null$1(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)
java.lang.NumberFormatException: For input string: "E.800088E22"
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
	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.dhb.date.test.SimpleDateFormatTest.lambda$null$0(SimpleDateFormatTest.java:21)
	at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
	at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:559)
	at com.dhb.date.test.SimpleDateFormatTest.lambda$null$1(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)
java.lang.NumberFormatException: For input string: "E.800088E22"
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
	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.dhb.date.test.SimpleDateFormatTest.lambda$null$0(SimpleDateFormatTest.java:21)
	at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
	at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:559)
	at com.dhb.date.test.SimpleDateFormatTest.lambda$null$1(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)
Sun Dec 05 00:00:00 CST 7999

有的地方转换时间日期变成了错误的值,有的地方还造成了异常。导致数据无法转换。造成这个问题的根源就是SimpleDateFormat不是线程安全的。 我们可以看看其源码: SimpleDateFormat 继承了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;
}
    
public abstract Date parse(String source, ParsePosition pos);

这个抽象方法再有具体的实现类来实现。 而在这个具体的方法中,有一段关键的代码:

  Date parsedDate;
    try {
        parsedDate = calb.establish(calendar).getTime();
        // If the year value is ambiguous,
        // then the two-digit year == the default start year
        if (ambiguousYear[0]) {
            if (parsedDate.before(defaultCenturyStart)) {
                parsedDate = calb.addYear(100).establish(calendar).getTime();
            }
        }
    }

estahlish方法依赖于calendar这个成员变量:

    protected Calendar calendar;

但是不幸这个成员变量本身没有做任何保护措施。这就导致在多线程的情况下,第一个线程可能还没来得及处理完,第二个线程就将这个值就行了修改。这也是并发问题产生的根源。

    Calendar establish(Calendar cal) {
        boolean weekDate = isSet(WEEK_YEAR)
                            && field[WEEK_YEAR] > field[YEAR];
        if (weekDate && !cal.isWeekDateSupported()) {
            // Use YEAR instead
            if (!isSet(YEAR)) {
                set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
            }
            weekDate = false;
        }

        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;
                }
            }
        }

可以看到,每次都会clear。这样会把之前的结果删除。这就导致了出现的各种错误。 对于这个情况,在阿里规范中也有约定:

在这里插入图片描述
在这里插入图片描述

建议配合ThreadLocal来实现我们期望的这个功能。另外最好是用jdk1.8中新提供的时间对象。 阿里建议的修改方式:

private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd");
    }
};

在前面对ThreadLocal进行过讨论,我们知道实际上threadLocal的内容在不需要的时候最好是remove。因此如果是jdk1.8的环境,那么我们最好是用jdk新提供的日期工具。后面将专门对这些类进行介绍。

3.格式字符串不匹配的坑

对于SimpleDateFormat,最隐蔽的问题还不是因为格式字符串出错或者线程安全问题。这两类问题都较容易发现。但是还有一类问题,如果出现在生产环境中,将会导致严重问题:

public static void main(String[] args) throws Exception{
	SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
	String dataStr = "20200850";
	System.out.println(format.parse(dataStr));
	dataStr = "202008050";
	System.out.println(format.parse(dataStr));
}

可以看到,我们定义了yyyyMMdd的日期格式字符串,来进行转换,但是我们这个日期格式是有问题的。8月份没有50天。则SimpleDateFormat识别到了dd对应的是50,则在之前2020年8月的基础上加上了50天,这就变成了:

Sat Sep 19 00:00:00 CST 2020
Sat Sep 19 00:00:00 CST 2020

而且更严重的是后面这个错误,SimpleDateFormat不会报错。如果我们在生产环境中处理对生产的数据进行处理的话,这种情况将会非常隐蔽的导致我们处理的结果出错。

4.讨厌的日期计算

个人觉得,涉及到java.util.Date的日期计算绝对不是一个令人可以愉快写代码的事情。如下,如果把一个日期增加30天:

Date date = new Date(System.currentTimeMillis() + 30*24*60*60*1000);
System.out.println(date);

想当然的用这种方式,肯定会得不到想要的结果:

Thu Jul 16 23:35:10 CST 2020

这样会因为后面的int类型时间计算溢出而得不到想要的结果。如果变成long可以解决这个问题,但是并不是一个好办法,还是得用Colander来解决。

Date date1 = new Date(System.currentTimeMillis() + 30L*24*60*60*1000);
System.out.println(date1);
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.DAY_OF_MONTH,30);
System.out.println(calendar.getTime());

这两种方式都能得到相同的结果:

Fri Sep 04 16:47:28 CST 2020
Fri Sep 04 16:47:28 CST 2020

当然,最好的解决办法还是用java8中新引入的时间工具类。这个在后面详细介绍。

5.阿里规范的其他约定

在阿里规范中,除了本文上述问题,还有如下问题:

  • 获取当前毫秒数,用System.currentTimeMillis();
在这里插入图片描述
在这里插入图片描述

这是因为,new Date()的源码中:

  /**
     * Allocates a <code>Date</code> object and initializes it so that
     * it represents the time at which it was allocated, measured to the
     * nearest millisecond.
     *
     * @see     java.lang.System#currentTimeMillis()
     */
    public Date() {
        this(System.currentTimeMillis());
    }

实际上也是System.currentTimeMillis(),因此new Date会带来额外的内存开销。

  • 不允许在程序任何地方中使用:1)java.sql.Date。 2)java.sql.Time。3)java.sql.Timestamp 对于这几个类,我们一般也接触得比较少,阿里规范是不建议使用的。
在这里插入图片描述
在这里插入图片描述
  • 关于年、月的天数,不应该在程序中固定,应该通过Calendar计算
在这里插入图片描述
在这里插入图片描述
  • 使用枚举值来指代月份。如果使用数字,注意Date,Calendar等日期相关类的月份month取值在0-11之间。 在Calendar中,月份是从0开始计数的。
在这里插入图片描述
在这里插入图片描述
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-08-05 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.容易混淆的日期格式字符串
  • 2.static的SimpleDateFormat
  • 3.格式字符串不匹配的坑
  • 4.讨厌的日期计算
  • 5.阿里规范的其他约定
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档