在 JDK 8 之前,Java 语言为我们提供了两种操作时间,即:java.util.Date 和 java.util.Calendar,但在 JDK 8 为了解决旧时间操作的一些缺陷,为操作时间和日期提供了几个新的类别,它们分别是:LocalTime、LocalDateTime、Instant,都位于 java.time 包下。
时间操作经常出现在我们的日常开发中,例如,业务数据记录创建时间和修改时间,并显示这些时间格式化到前端页面,如我们需要计算业务数据时间间隔,不能与时间操作分开,那么如何正确优雅地使用时间呢?这是我们接下来要讨论的话题。
基础知识科普格林威治时间格林威治(又译格林尼治)是英国伦敦南郊原格林威治天文台的所在地,是世界计算时间和地球经度的起点,也是国际经度会议 1884 2000年,它在美国华盛顿举行。会议通过协议,以格林威治天文台的经线为零度经线(即初子午线),作为地球经度的起点,以格林威治为“世界时区”的起点。
格林威治时间与北京时间的关系格林威治时间被定义为世界时间,即 0 北京是东八区。也就是说,格林威治时间 1 日 0 点,对应北京的时间是 1 日 8 点。
时间戳时间戳是指格林威治时间戳 1970-01-01 00:00:00(北京时间 1970-01-01 08:00:00)到现在的总秒数。
JDK 8 之前的时间操作1 获取时间Date date = new Date();System.out.println(date);Calendar calendar = Calendar.getInstance();Date time = calendar.getTime();System.out.println(time);
2 获取时间戳long ts = new Date().getTime();System.out.println(ts);long ts2 = System.currentTimeMillis();System.out.println(ts2);long ts3 = Calendar.getInstance().getTimeInMillis();System.out.println(ts3);
3 格式化时间SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");System.out.println(sf.format(new Date())); // output:2019-08-16 21:46:22
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 时间转换SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// String 转 DateString str = "2019-10-10 10:10:10";System.out.println(sf.parse(str));//时间戳字符串 转 DateString tsString = "1556788591462";// import java.sqlTimestamp ts = new Timestamp(Long.parseLong(tsString)); // 字符串转时间戳 DateSystem.out.println(sf.format(ts));
注意事项:使用时 SimpleDateFormat.parse() 当时间转换方法时,SimpleDateFormat 构造函数必须与待转换字符串格式一致。
5 获得昨天此刻的时间Calendar calendar = Calendar.getInstance();calendar.add(Calendar.DATE, -1);System.out.println(calendar.getTime());
JDK 8 时间操作JDK 8 时间操作新增了三类:LocalDateTime、LocalDate、LocalTime。
- LocalDate 只包括日期,不包括时间,不可变,线程安全。
- LocalTime 只包括时间,不包括日期,不可变,线程安全。
- LocalDateTime 既包含时间,又包含日期,不可变,线程安全。
线程安全性
值得一提的是 JDK 8 这三类与线程安全有关,大大降低了多线程下代码开发的风险。
1 获取时间// Localdate获取日期 localDate = LocalDate.now();System.out.println(localDate); // output:2019-08-16// Localtime获取时间 localTime = LocalTime.now();System.out.println(localTime); // output:21:09:13.708// LocalDatetime的获取日期和时间 localDateTime = LocalDateTime.now();System.out.println(localDateTime); // output:2019-08-16T21:09:13.708
2 获取时间戳long milli = Instant.now().toEpochMilli(); // 获取当前时间戳(精确到毫秒)long second = Instant.now().getEpochSecond(); // 获取当前时间戳(精确到秒)System.out.println(milli); // output:1569324357.out.println(second); // output:1565932435
3 时间格式化// 时间格式化①DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");String timeFormat = dateTimeFormatter.format(LocalDateTime.now());System.out.println(timeFormat); // output:2019-08-16 21:15:43// 时间格式化②String timeFormat2 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));System.out.println(timeFormat2); // output:2019-08-16 21:17:48
4 时间转换String timeStr = "2019-10-10 06:06:06";LocalDateTime dateTime = LocalDateTime.parse(timeStr,DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));System.out.println(dateTime);
5 获得昨天此刻的时间LocalDateTime today = LocalDateTime.now();LocalDateTime yesterday = today.plusDays(-1);System.out.println(yesterday);
相关面试题1. 获取当前时间的方法有多少?答:获取当前时间的常见方法有三种:
- new Date()
- Calendar.getInstance().getTime()
- LocalDateTime.now()
答:以下是获得昨天此刻时间的两种方式:
// 获得昨天此刻的时间(JDK 8 以前)Calendar c = Calendar.getInstance();c.add(Calendar.DATE,-1);System.out.println(c.getTime());// 获取昨天此刻的时间(JDK 8)LocalDateTime todayTime = LocalDateTime.now();System.out.println(todayTime.plusDays(-1));
3. 这个月的最后一天是怎样获得的?答:本月最后一天有两种获取方式:
// 这个月的最后一天(JDK 8 以前)Calendar ca = Calendar.getInstance();ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH));System.out.println(ca.getTime());// 这个月的最后一天(JDK 8)LocalDate today = LocalDate.now();System.out.println(today.with(TemporalAdjusters.lastDayOfMonth()));
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 优雅地计算两个时间的相隔时间,代码如下:
LocalDateTime dt1 = LocalDateTime.now();LocalDateTime dt2 = dt1.plusSeconds(60);Duration duration = Duration.between(dt1, dt2);System.out.println(duration.getSeconds()); // output:60
6. 如何优雅地计算两个日期的相隔日期?答:JDK 8 中可以使用 Period 优雅地计算两个日期之间的相隔日期,代码如下:
LocalDate d1 = LocalDate.now();LocalDate d2 = d1.plusDays(2);Period period = Period.between(d1, d2);System.out.println(period.getDays()); //output:2
7. SimpleDateFormat 是线程安全吗?为什么?答:SimpleDateFormat 非线程安全。因为查看 SimpleDateFormat 可以知道,所有的格式化和分析都需要通过中间对象进行转换,这就是中间对象 Calendar,这将导致非线程安全。想象一下,我们有多个线程来操作同一个线程 Calendar 后来的线程会覆盖先来线程的数据,最后实际上会返回后来线程的数据,所以 SimpleDateFormat 它变成了非线程。
8. 怎么保证 SimpleDateFormat 线程安全?答:保证 SimpleDateFormat 线程安全的方式如下:
- 使用 Synchronized,使用需要时间格式化的操作 Synchronized 包装关键字,确保线程堵塞格式化;
- 手动加锁,将需要格式化时间的代码写入加锁部分,相对 Synchronized 编码效率低,性能稍好,代码风险大(风险在于操作结束时不要忘记手动释放锁);
- 使用 JDK 8 的 DateTimeFormatter 替代 SimpleDateFormat。
答:JDK 8 具体优点如下:
- 线程安全性
- 使用方便(如获取当前时间戳的方便性、增减日期的方便性等。)
- 比如当前时间的格式化,编写代码更简单优雅:LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
答:时间比较有三种方式:
- 获得两个时间戳,获得两个时间戳 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,在多线程下提供线程安全性和易用性,使我们能够更好地运行时间。