坑爹的年月日
我们从 Date 对象中获取年月日,代码如下。
val date = Date()
println(date) // Wed Mar 30 17:12:34 CST 2022
println(date.year) // 122
println(date.month) // 2
println(date.date) // 30
what?年份是 122 年,这什么鬼?月份返回 2,明明是三月呀,这又是什么鬼?还好 30 号这个日期是对的。
让我们来看一下 getMonth()、getYear() 这两个方法的 Javadoc。
/**
* Returns a number representing the month that contains or begins with the instant in
* time represented by this Date object. The value returned is between 0 and 11, with
* the value 0 representing January.
*/
public int getMonth() {
return normalize().getMonth() - 1; // adjust 1-based to 0-based
}
/**
* Returns a value that is the result of subtracting 1900 from the year that contains
* or begins with the instant in time represented by this Date object, as interpreted
* in the local time zone.
*/
public int getYear() {
return normalize().getYear() - 1900;
}
尼玛,原来 2022 - 1900 = 122是这么来的。月份,竟然从0开始,这是学的谁呢?
初始化日期时间问题
如果要初始化一个 2021 年 12 月 31 日 12 点 23 分 59 秒这样的时间,可以使用下面的代码吗?
val date = Date(2021, 12, 31, 12, 23, 59)
println(date) // Tue Jan 31 12:23:59 CST 3922
可以看到,输出的时间是 3922 年 1 月 31 日 12 点 23 分 59 秒。
于是,我们再来看看 Date(year, month, date, hrs, min, sec) 这个方法的 Javadoc。
/**
* Allocates a Date object and initializes it so that it represents the instant at the
* start of the second specified by the year, month, date, hrs, min, and sec arguments
* in the local time zone.
*/
public Date(int year, int month, int date, int hrs, int min, int sec) {
int y = year + 1900;
// month is 0-based. So we have to normalize month to support Long.MAX_VALUE.
if (month >= 12) {
y += month / 12;
month %= 12;
} else if (month < 0) {
y += CalendarUtils.floorDivide(month, 12);
month = CalendarUtils.mod(month, 12);
}
BaseCalendar cal = getCalendarSystem(y);
cdate = (BaseCalendar.Date) cal.newCalendarDate(TimeZone.getDefaultRef());
cdate.setNormalizedDate(y, month + 1, date).setTimeOfDay(hrs, min, sec, 0);
getTimeImpl();
cdate = null;
}
方法的注释并没有告诉我们:为什么年份返回 3922 年? 为什么月份返回 1 月?
于是看方法实现,得到了如下的一个代码逻辑:
原来,3922 年 1 月 31 日 12 点 23 分 59 秒 是这么来的。
所以,正确的初始化应该是:
val date = Date(2021 - 1900, 12 - 1, 31, 23, 59)
哎,代码也太不易读了。
时区问题
因为日期时间的特殊性,不同的国家地区在同一时刻(时间戳)显示的日期时间应该是不一样的,但 Date 做不到,因为它底层代码是这样的:
也就是说它表示的是一个具体时刻(时间戳),这个数值放在全球任何地方都是一模一样的,也就是说 Date() 和 System.currentTimeMillis() 没啥两样。
JDK 提供了 TimeZone 表示时区的概念,但它在 Date 里并无任何体现,只能使用在格式化器上,这种设计着实让人再一次看不懂了。
日期时间计算问题
关于日期时间的计算,有一个常踩的坑。就是直接使用时间戳进行时间计算,比如希望得到当前时间之后 30 天的时间,会这么写代码:直接把 new Date().getTime() 方法得到的时间戳加 30 天对应的毫秒数,也就是 (30 天 * 24 小时 * 60 分钟 * 60 秒 * 1000 毫秒):
val now = Date()
val nextMonth = Date(now.time + 30 * 24 * 60 * 60 * 1000)
println(now) // Wed Mar 30 20:08:31 CST 2022
println(nextMonth) // Fri Mar 11 03:05:43 CST 2022
得到的日期居然比当前日期还要早,根本不是晚 30 天的时间。
出现这个问题,其实是因为 int 发生了溢出。修复方式就是把 1000 改为 1000L,让其成为一个 long:
val now = Date()
val nextMonth = Date(now.time + 30 * 24 * 60 * 60 * 1000L)
println(now) // Wed Mar 30 20:09:48 CST 2022
println(nextMonth) // Fri Apr 29 20:09:48 CST 2022
这样就可以得到正确结果了。不难发现,手动在时间戳上进行计算操作的方式非常容易出错。
使用 Java 8 的日期时间类型,可以直接进行各种计算,更加简洁和方便:
val localDateTime = LocalDateTime.now()
println(localDateTime.plusDays(30)) // 2022-04-29T20:11:39.432906
跨年问题
每到年底,就有很多开发同学踩时间格式化的坑,比如“这明明是一个 2021 年的日期,怎么使用 SimpleDateFormat 格式化后就提前跨年了”。我们来重现一下这个问题。
初始化一个 Calendar,设置日期时间为 2020 年 12 月 29 日,使用大写的 YYYY 来初始化 SimpleDateFormat:
Locale.setDefault(Locale.SIMPLIFIED_CHINESE)
println(Locale.getDefault()) // zh_CN
val calendar = Calendar.getInstance()
calendar.set(2020, Calendar.DECEMBER, 29, 0, 0 ,0)
val simpleDateFormat = SimpleDateFormat("YYYY-MM-dd")
println(simpleDateFormat.format(calendar.time)) // 2021-12-29
println(calendar.weekYear) // 2021
出现这个问题的原因在于,这位同学混淆了 SimpleDateFormat 的各种格式化模式。JDK 的文档中有说明:小写 y 是年,而大写 Y 是 week year,也就是所在的周属于哪一年。
没有特殊需求,针对年份的日期格式化,应该一律使用 “y” 而非 “Y”。
线程安全问题
定义的 static 的 SimpleDateFormat 可能会出现线程安全问题。比如像这样,使用一个 100 线程的线程池,循环 20 次把时间格式化任务提交到线程池处理,每个任务中又循环 10 次解析 2020-01-01 11:12:13 这样一个时间表示:
val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val threadPool = Executors.newFixedThreadPool(100)
for (i in 0..19) {
//提交20个并发解析时间的任务到线程池,模拟并发环境
threadPool.execute {
for (j in 0..9) {
try {
println(simpleDateFormat.parse("2020-01-01 11:12:13"))
} catch (e: ParseException) {
e.printStackTrace()
}
}
}
}
threadPool.shutdown()
threadPool.awaitTermination(1, TimeUnit.HOURS)
运行程序后大量报错,且没有报错的输出结果也不正常,比如 2020 年解析成了 1111 年:
SimpleDateFormat 的作用是定义解析和格式化日期时间的模式。这,看起来这是一次性的工作,应该复用,但它的解析和格式化操作是非线程安全的。我们来分析一下相关源码:
public abstract class DateFormat extends Format {
protected Calendar calendar;
}
public class SimpleDateFormat extends DateFormat {
@Override
public Date parse(String text, ParsePosition pos)
{
CalendarBuilder calb = new CalendarBuilder();
parsedDate = calb.establish(calendar).getTime();
return parsedDate;
}
}
class CalendarBuilder {
Calendar establish(Calendar cal) {
...
cal.clear();//清空
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;
}
}
通过源码我们可以发现:
- SimpleDateFormat 继承了 DateFormat,DateFormat 有一个字段 Calendar;
- SimpleDateFormat 的 parse 方法调用 CalendarBuilder 的 establish 方法,来构建 Calendar;
- establish 方法内部先清空 Calendar 再构建 Calendar,整个操作没有加锁。
显然,如果多线程池调用 parse 方法,也就意味着多线程在并发操作一个 Calendar,可能会产生一个线程还没来得及处理 Calendar 就被另一个线程清空了的情况。
format 方法也类似,可以自己分析。因此只能在同一个线程复用 SimpleDateFormat,比较好的解决方式是,通过 ThreadLocal 来存放 SimpleDateFormat:
private static ThreadLocal<SimpleDateFormat> threadSafeSimpleDateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
时间格式不匹配问题
当需要解析的字符串和格式不匹配的时候,SimpleDateFormat 表现得很宽容,还是能得到结果。比如,我们期望使用 yyyyMM 来解析 20210901 字符串:
val dateStr = "20210901"
val simpleDateFormat = SimpleDateFormat("yyyyMM")
println(simpleDateFormat.parse(dateStr)) // Sun Jan 01 00:00:00 CST 2096
居然输出了 2096 年 1 月 1 日,原因是把 0901 当成了月份,相当于 75 年。
对于上面说的 SimpleDateFormat 的这三个坑,我们使用 Java 8 中的 DateTimeFormatter 就可以都避过去。