在开发过程中,经常会遇到记录时间的场景,例如:创建时间createTime,抄表时间戳timestamp。
Java中有java.util.Date
、java.time.LocalDateTime
等类,还有Long
型时间戳,什么场景用什么类型就存在一定的差异性了。
java.util.Date
类Date类是我们最常用到的时间类了,常见用法如下:
Date date = new Date();
// 本质上也是调用了new Date(long timestamp)
Calendar.getInstance().getTime()
构造函数如下:
public class Date implements java.io.Serializable, Cloneable, Comparable<Date> {
private transient long fastTime;
public Date() {
this(System.currentTimeMillis());
}
public Date(long date) {
fastTime = date;
}
}
可以看出,Date对象本质上也是持有了一个时间戳long fastTime
,表示自格林威治时间( GMT)1970年1月1日0点至Date所表示时刻所经过的毫秒数。
Date类本身时没有时区概念的,所以在格式化为字符串或者存储到数据库中时,需要指定一个时区。一般接口中格式化时,会默认采用服务器时区,而存储到数据库时,会使用服务器的时区设置,例如:MongoDB默认时区为UTC,也就是+00:00,所以我们在数据库中看到的createTime
都比实际时间要向前推8小时。
说到Date类,不得不提的就是java.text.SimpleDateFormat
了,这个类非常坑——它是线程不安全的。
网上非常常见的做法是写一个DateUtils类,把SimpleDateFormat按照不同的格式化需求,实例化为多个常量,然后在其他地方调用SDF.parse()
等方法。
public class SimpleDateFormatTest extends Thread {
private static SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private String name;
private String date;
public SimpleDateFormatTest(String name, String date) {
this.name = name;
this.date = date;
}
@Override
public void run() {
try {
Date d = SDF.parse(date);
System.out.println(name + ": date:" + d);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(3);
es.execute(new SimpleDateFormatTest("A", "2023-01-01 08:00:00"));
es.execute(new SimpleDateFormatTest("B", "2022-01-01 10:00:00"));
}
}
在多线程环境下执行,经常会抛出异常,有时候还会看到如下错误结果。
第一次
A: date:Sun Jan 01 08:00:00 CST 2023
B: date:Sun Jan 01 08:00:00 CST 2023
第二次
B: date:Fri Nov 14 08:00:00 CST 2200
A: date:Fri Nov 14 08:00:00 CST 2200
而且这种多线程并发的问题还极难复现和排查,由此可以看出,整个Date类的使用过程中存在许多坑。
java.time
包曾经有个最烂Java类投票,Date/Calendar类排第二(第一是XML/DOM类),所以,在Java8中引入了JSR 310日期时间,都包括在java.time
包中。这里面有3个常用时间类,分别代表日期、时间、日期+时间。
java.time.LocalDate