玩转时间操作 + 面试题
在 JDK 8 之前,Java 语言为我们提供了两个类用于操作时间,它们分别是:java.util.Date 和 java.util.Calendar,但在 JDK 8 的时候为了解决旧时间操作类的一些缺陷,提供了几个新的类,用于操作时间和日期,它们分别是:LocalTime、LocalDateTime、Instant,都位于 java.time 包下。
时间的操作在我们日常的开发中经常见到,比如,业务数据都要记录创建时间和修改时间,并要把这些时间格式化之后显示到前端页面,再比如我们需要计算业务数据的时间间隔等,都离不开对时间的操作,那如何正确而优雅地使用时间?这就是我们接下来要讨论的话题。
时间基础知识科普
格林威治时间
格林威治(又译格林尼治)是英国伦敦南郊原格林威治天文台的所在地,它是世界计算时间和地球经度的起点,国际经度会议 1884 年在美国华盛顿召开,会上通过协议,以经过格林威治天文台的经线为零度经线(即本初子午线),作为地球经度的起点,并以格林威治为“世界时区”的起点。
格林威治时间和北京时间的关系
格林威治时间被定义为世界时间,就是 0 时区,北京是东八区。也就是说格林威治时间的 1 日 0 点,对应到北京的时间就是 1 日 8 点。
时间戳
时间戳是指格林威治时间 1970-01-01 00:00:00(北京时间 1970-01-01 08:00:00)起至现在的总秒数。
JDK 8 之前的时间操作
1 获取时间
1 | Date date = new Date(); |
2 获取时间戳
1 | long ts = new Date().getTime(); |
3 格式化时间
1 | SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
SimpleDateFormat 构造参数的含义,请参考以下表格信息:
字符 | 含义 | 示例 |
---|---|---|
y | 年 | yyyy-1996 |
M | 月 | MM-07 |
d | 月中的天数 | dd-02 |
D | 年中的天数 | 121 |
E | 星期几 | 星期四 |
H | 小时数(0-23) | HH-23 |
h | 小时数(1-12) | hh-11 |
m | 分钟数 | mm-02 |
s | 秒数 | ss-03 |
Z | 时区 | +0800 |
使用示例:
- 获取星期几:new SimpleDateFormat(“E”).format(new Date())
- 获取当前时区:new SimpleDateFormat(“Z”).format(new Date*())
注意事项:在多线程下 SimpleDateFormat 是非线程安全的,因此在使用 SimpleDateFormat 时要注意这个问题。在多线程下,如果使用不当,可能会造成结果不对或内存泄漏等问题。
4 时间转换
1 | SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
注意事项:当使用 SimpleDateFormat.parse() 方法进行时间转换的时候,SimpleDateFormat 的构造函数必须和待转换字符串格式一致。
5 获得昨天此刻时间
1 | Calendar calendar = Calendar.getInstance(); |
JDK 8 时间操作
JDK 8 对时间操作新增了三个类:LocalDateTime、LocalDate、LocalTime。
- LocalDate 只包含日期,不包含时间,不可变类,且线程安全。
- LocalTime 只包含时间,不包含日期,不可变类,且线程安全。
- LocalDateTime 既包含了时间又包含了日期,不可变类,且线程安全。
线程安全性
值得一提的是 JDK 8 中新增的这三个时间相关的类,都是线程安全的,这极大地降低了多线程下代码开发的风险。
1 获取时间
1 | // 获取日期 |
2 获取时间戳
1 | long milli = Instant.now().toEpochMilli(); // 获取当前时间戳(精确到毫秒) |
3 时间格式化
1 | // 时间格式化① |
4 时间转换
1 | String timeStr = "2019-10-10 06:06:06"; |
5 获得昨天此刻时间
1 | LocalDateTime today = LocalDateTime.now(); |
相关面试题
1. 获取当前时间有几种方式?
答:获取当前时间常见的方式有以下三种:
- new Date()
- Calendar.getInstance().getTime()
- LocalDateTime.now()
2. 如何获取昨天此刻的时间?
答:以下为获取昨天此刻时间的两种方式:
1 | // 获取昨天此刻的时间(JDK 8 以前) |
3. 如何获取本月的最后一天?
答:以下为获取本月最后一天的两种方式:
1 | // 获取本月的最后一天(JDK 8 以前) |
4. 获取当前时间的时间戳有几种方式?
答:以下为获取当前时间戳的几种方式:
- System.currentTimeMillis()
- new Date().getTime()
- Calendar.getInstance().getTime().getTime()
- Instant.now().toEpochMilli()
- LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli()
其中,第四种和第五种方式是 JDK 8 才新加的。
5. 如何优雅地计算两个时间的相隔时间?
答:JDK 8 中可以使用 Duration 类来优雅地计算两个时间的相隔时间,代码如下:
1 | LocalDateTime dt1 = LocalDateTime.now(); |
6. 如何优雅地计算两个日期的相隔日期?
答:JDK 8 中可以使用 Period 类来优雅地计算两个日期的相隔日期,代码如下:
1 | LocalDate d1 = LocalDate.now(); |
7. SimpleDateFormat 是线程安全的吗?为什么?
答:SimpleDateFormat 是非线程安全的。因为查看 SimpleDateFormat 的源码可以得知,所有的格式化和解析,都需要通过一个中间对象进行转换,这个中间对象就是 Calendar,这样的话就造成非线程安全。试想一下当我们有多个线程操作同一个 Calendar 的时候后来的线程会覆盖先来线程的数据,那最后其实返回的是后来线程的数据,因此 SimpleDateFormat 就成为了非线程的了。
8. 怎么保证 SimpleDateFormat 的线程安全?
答:保证 SimpleDateFormat 线程安全的方式如下:
- 使用 Synchronized,在需要时间格式化的操作使用 Synchronized 关键字进行包装,保证线程堵塞格式化;
- 手动加锁,把需要格式化时间的代码,写到加锁部分,相对 Synchronized 来说,编码效率更低,性能略好,代码风险较大(风险在于不要忘记在操作的最后,手动释放锁);
- 使用 JDK 8 的 DateTimeFormatter 替代 SimpleDateFormat。
9. JDK 8 中新增的时间类都有哪些优点?
答:JDK 8 中的优点具体有以下几个优点,如下:
- 线程安全性
- 使用的便利性(如获取当前时间戳的便利性、增减日期的便利性等)
- 编写代码更简单优雅,如当前时间的格式化:LocalDateTime.now().format(DateTimeFormatter.ofPattern(“yyyy-MM-dd HH:mm:ss”));
10. 如何比较两个时间(Date)的大小?
答:时间比较有以下三种方式:
- 获取两个时间的时间戳,得到两个 long 类型的变量,两个变量相减,通过结果的正负值来判断大小;
- 通过 Date 自带的 before()、after()、equals() 等方法比较,代码示例 date1.before(date2);
- 通过 compareTo() 方法比较,代码示例:date1.compareTo(date2),返回值 -1 表示前一个时间比后一个时间小,0 表示两个时间相等,1 表示前一个时间大于后一个时间。
总结
JDK 8 之前使用 java.util.Date 和 java.util.Calendar 来操作时间,它们有两个很明显的缺点,第一,非线程安全;第二,API 调用不方便。JDK 8 新增了几个时间操作类 java.time 包下的 LocalDateTime、LocalDate、LocalTime、Duration(计算相隔时间)、Period(计算相隔日期)和 DateTimeFormatter,提供了多线程下的线程安全和易用性,让我们可以更好的操作时间。