探索Java8:(四)DateTime API的使用
感谢
千呼万唤始出来,探索Java8系列第四篇闪亮登场。探索Java8第一篇是2018年4月9号发布的,也就是说本篇距离探索Java8第一篇发布已经整整过去了两年半的时间。前三篇我发布到了简书、博客园、CSDN、掘金、SegmentFault,累计阅读量估计在10W左右。正是因为这个,所以我才有动力写下本系列的第四篇。非常感谢有这么多人阅读我的文章,如果对你有任何的帮助那真是我的荣幸。
Java8中的一个新特性是就是对老旧的时间API的改进,Java8中引入了全新的DateTime API。分别为LocalDate、LocalTime、LocalDateTime。当然,作为工具类,这三个类都是final类。你可能会问Java中已经有了java.util.Date
,为什么现在又要引入另一套时间Api?
为什么我们需要新的时间Api?
直接告诉你新版的API怎么用似乎不能体现出新版时间API的威力,所以我引入两个例子来先让你看看新版时间API的强大之处。
例1:判断当前时间在时间范围内
Java8之前版本的java.util.Date
实在是难用的可以。现在我还依稀记得刚毕业时开发的一个JDK1.6版本的项目。项目中需要判断一个时间是不是在某个时间范围内,需要三个参数,分别是当前时间cur,开始时间start,结束时间end,返回结果是布尔类型。
boolean isValidDate(String cur,String start,String end);
看起来是个很简单的需求,不过当你用旧版本的Date去实现的话,要洋洋洒洒写个十几行的代码。文章的链接你可以点击此处JAVA判断当前时间在时间范围内。
我把上面的文章的代码整理成下面这样(实际上我简化了代码),方便我们来给这个代码挑挑毛病。代码如下图所示。
public boolean isEffectiveDate(String now, String start, String end) throws ParseException{
SimpleDateFormat ft = new SimpleDateFormat ("yyyy-MM-dd hh:mm:ss");
Date startTime = ft.parse(start);
Date endTime = ft.parse(end);
Date nowTime = ft.parse(now);
//逻辑代码
if (nowTime.getTime() == startTime.getTime()
|| nowTime.getTime() == endTime.getTime()) {
return true;
}
Calendar date = Calendar.getInstance();
date.setTime(nowTime);
Calendar begin = Calendar.getInstance();
begin.setTime(startTime);
Calendar end = Calendar.getInstance();
end.setTime(endTime);
//逻辑代码
return date.after(begin) && date.before(end);
}
可以看出来,上面的代码看起来调理也很清晰,但是就是感觉怪怪的。你肯定会想为什么我实现这么一个简单的需求就要写这么多没用代码。逻辑代码其实只有两行。但是对字符串和时间的转换却需要写十几行的代码。而且
上面的代码是非线程安全的。在生产环境,这样的代码是可能造成灾难的。因此如果执意使用Date来实现的话,建议对SimpleDateFormat
做一个这样的操作。详情见阿里巴巴Java开发手册泰山版-并发处理-第五条
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
那么Java8中的DateTime API实现上面的需求该怎么写呢?
public boolean isValidDate(String cur, String start, String end) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime current = LocalDateTime.parse(cur,dateTimeFormatter);
LocalDateTime startTime = LocalDateTime.parse(start,dateTimeFormatter);
LocalDateTime endTime = LocalDateTime.parse(end,dateTimeFormatter);
//逻辑代码
return current.isBefore(endTime) && current.isAfter(startTime) ||
(current.isEqual(startTime) && current.isEqual(endTime));
}
跟旧版的类似,我们需要formatter来进行格式化,转换的代码依然存在,但只有三行,也不需要使用Calendar
来进行处理。逻辑代码依然是两行。除了实现简洁之外,上面的代码是线程安全的。
例2:获取任意时间所在周的周一
上面这个例子你觉得可能代码简洁了一点,但是并没有看出来DateTimeApi的威力。下面我再给出一个简单的例子。一个简单的需求是,获取当年任意一个时间所在周的周一。传入一个String
类型的日期,然后输出此日期所在周的周一。
String getMondayByDate(String date);
跟上面的例子一样,我们先看看旧时代的底裤长什么样。获取任意时间所在周的周一。我引用了这个文章给出的例子,不过为了简洁期间我同样把这个文章里面的代码简化一下,方便我们进行新旧代码的对比。
Date实现
private static String getWeekByDate(String date) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date time = sdf.parse(date);
Calendar cal = Calendar.getInstance();
cal.setTime(time);
// 判断要计算的日期是否是周日,如果是则减一天计算周六的,否则会出问题,计算到下一周去了
// 获得当前日期是一个星期的第几天
int dayWeek = cal.get(Calendar.DAY_OF_WEEK);
if (1 == dayWeek) {
cal.add(Calendar.DAY_OF_MONTH, -1);
}
// 设置一个星期的第一天,按中国的习惯一个星期的第一天是星期一
cal.setFirstDayOfWeek(Calendar.MONDAY);
// 获得当前日期是一个星期的第几天
int day = cal.get(Calendar.DAY_OF_WEEK);
// 根据日历的规则,给当前日期减去星期几与一个星期第一天的差值
cal.add(Calendar.DATE, cal.getFirstDayOfWeek() - day);
return sdf.format(cal.getTime());
}
整个代码的逻辑也很简洁,就是这么一个简单的需求似乎计算起来稍显复杂。那么我们废话不多说,我甩出来代码,看看新版本时间Api是如何解决这种看似复杂的需求的。
LocalDate实现
private static String getMondayByDate(String date) {
return LocalDate.parse(date).with(TemporalAdjusters.next(DayOfWeek.MONDAY))
.minusWeeks(1)
.toString();
}
你没有看错,使用LocalDate实现这个需求就是这么简单。
二、DateTime Api
在Java 8中,日期和时间被明确划分为LocalDate和LocalTime,LocalDate无法包含时间,LocalTime无法包含日期。当然,LocalDateTime才能同时包含日期和时间。
LocalDate
LocalDate 的时间管理严格按照ISO标准。
// 直接获取当前时间
LocalDate now = LocalDate.now(); // -> 2020-11-06
// 根据字符串取,时间的格式必须遵循yyyy-MM-dd验证
LocalDate endOfFeb = LocalDate.parse("2020-11-06");
// 错误示范
LocalDate.parse("2020-11-31"); // DateTimeParseException: Invalid date
配合全新的格式化工具DateTimeFormatter
,你也可以自定义时间格式:
DateTimeFormatter dateTimeFormatter= DateTimeFormatter.ofPattern("yyyy年-MM月-dd日");
String date=LocalDate.now().format(dateTimeFormatter); // -> 2020年-11月-06日
常用一些操作也很简单
LocalDate now=LocalDate.now();
// 取本月第1天:
LocalDate firstDayOfThisMonth = now.with(TemporalAdjusters.firstDayOfMonth()); // ->2020-11-01
// 取本月第2天:
LocalDate secondDayOfThisMonth = now.withDayOfMonth(2); // -> 2020-11-02
// 取本月最后一天
LocalDate lastDayOfThisMonth = now.with(TemporalAdjusters.lastDayOfMonth()); // ->2020-11-30
//获取本年首月的第一个周一,这个计算用calendar能累死
LocalDate firstDayOfYear= date.with(TemporalAdjusters.firstDayOfYear())
.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
// -> 2020-01-06
LocalTime
LocalTime
默认包含毫秒,格式跟ISO标准是一致的。使用java.util.Date
的时候,我们处理时间只能跟日期混在一起。使用LocalTime就可以专心处理时间。
//获取当前的时间
LocalTime now = LocalTime.now(); // -> 17:54:21.323
//当前时间加两小时
LocalTime time=now.plusHours(2); // -> 19:54:21.323
//构造时间
LocalTime zero = LocalTime.of(0, 0, 0); // -> 00:00:00
//字符串转换时间
LocalTime customTime = LocalTime.parse("10:10:00"); // 10:10:00
LocalDateTime
LocalDateTime
包含日期和时间,个人认为最常用时间类型非他莫属。兼具LocalDate和LocalTime的特性,在项目里面用的最多的就是它。
//直接获取当前日期时间,默认是带毫秒的ISO时间格式yyyy-MM-dd'T'HH:mm:ss.SSS
LocalDateTime current = LocalDateTime.now(); // -> 2020-11-09T16:59:49.612
//直接构造日期时间
DateTimeFormatter formatter=DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
LocalDateTime dateTime=LocalDateTime.parse("2020-11-09T16:59:49.612",formatter);
//获取当前日期时间所在月的第一天并减去两个小时
LocalDateTime.now().with(TemporalAdjusters.firstDayOfMonth()).minusHours(2); // -> 2020-11-01T15:03:53.672