随着Java8的发布,Oracle同时推出了一套全新的时间API,算是填坑吧。旧版本时间API的坑相信大家也早有耳闻:
当然,我个人觉得全新的时间API并不是特别重要,只要了解即可,所以本文不会介绍得很深。
首先要说明一点,新的时间API禁止了new的方式,全部通过工厂方式创建时间对象,其中最常用的就是now/of。
public class NewDateApiTest {public static void main(String[] args) {// 旧的方式,可以newDate date = new Date();System.out.println("old api" + date);// 新的方式,只能通过给定的方法获取LocalDate newDate = LocalDate.now(); // 日期 2020-12-12LocalTime newTime = LocalTime.now(); // 时间 16:30:00:000LocalDateTime newDateTime = LocalDateTime.now(); // 日期+时间 2020-12-12T16:30:00.000System.out.println("newDate" + newDate);System.out.println("newTime" + newTime);System.out.println("newDateTime" + newDateTime);// 还可以组合哟LocalDateTime combineDateTime = LocalDateTime.of(newDate, newTime);System.out.println("combineDateTime" + combineDateTime);// 创建指定时间LocalDate customDate = LocalDate.of(2020, 11, 5);LocalTime customTime = LocalTime.of(16, 30, 0);LocalDateTime customDateTime = LocalDateTime.of(2020, 11, 5, 16, 30, 0);System.out.println("customDate" + customDate);System.out.println("customTime" + customTime);System.out.println("customDateTime" + customDateTime);}}
结果
上面这段代码有两个重点和一个疑问:
修改时间有两个含义:
先介绍增减时间的方法。
要特别注意,新的时间类都是final修饰的,不可修改且线程安全(看注释),所以随便折腾。
如果你有过实际开发经验,就会明白老的时间API对于时间计算有多么不友好。举个实际开发经常会遇到的例子吧:
你知道吗,为了用旧版本时间API计算昨天的当前时间,我甚至百度了:
public class NewDateApiTest {public static void main(String[] args) {Date today = new Date();Calendar calendar = Calendar.getInstance();calendar.setTime(today);calendar.add(Calendar.DAY_OF_MONTH, -1);Date yesterday = calendar.getTime();System.out.println("today:" + today);System.out.println("yesterday:" + yesterday);}
}
至于后面的两个需求,我根本写不出来,还是只能靠百度。比如我百度的结果是:
public class NewDateApiTest {public static void main(String[] args) {long current = System.currentTimeMillis();long zeroT = current / (1000 * 3600 * 24) * (1000 * 3600 * 24) - TimeZone.getDefault().getRawOffset();long endT = zeroT + 24 * 60 * 60 * 1000 - 1;Date thisDayBegin = new Date(zeroT);Date thisDayEnd = new Date(endT);}
}
何必呢...
但这是我能力不行吗?NO,绝对是API设计得不好!API设计出来就是给普通开发人员用的,你搞得这么乱,这不是坑人嘛!很多人根本不知道去哪找对应的方法。
但新版时间API就不一样了,肯定是封装在LocalDate、LocalTime、LocalDateTime里的:
public class NewDateApiTest {public static void main(String[] args) {// 获取时间参数的年、月、日(有时需求要用到)System.out.println("获取时间参数的年、月、日:");LocalDateTime param = LocalDateTime.now();System.out.println("year:" + param.getYear());System.out.println("month:" + param.getMonth());System.out.println("day:" + param.getDayOfMonth());System.out.println("hour:" + param.getHour());System.out.println("minute:" + param.getMinute());System.out.println("second:" + param.getSecond() + "n");// 计算昨天的同一时刻(由于对象不可修改,所以返回的是新对象)System.out.println("计算前一天的当前时刻:");LocalDateTime today = LocalDateTime.now();LocalDateTime yesterday = today.plus(-1, ChronoUnit.DAYS);System.out.println("today:" + today);System.out.println("yesterday:" + yesterday);System.out.println("same object:" + today.equals(yesterday) + "n");// 计算当天的00点和24点(你看,这里就看到组合的威力了)System.out.println("计算当天的00点和24点:");LocalDateTime todayBegin = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);LocalDateTime todayEnd = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);System.out.println("todayBegin:" + todayBegin);System.out.println("todayEnd:" + todayEnd + "n");System.out.println("计算一周、一个月、一年前的当前时刻:");LocalDateTime oneWeekAgo = today.minus(1, ChronoUnit.WEEKS);LocalDateTime oneMonthAgo = today.minus(1, ChronoUnit.MONTHS);LocalDateTime oneYearAgo = today.minus(1, ChronoUnit.YEARS);System.out.println("oneWeekAgo" + oneWeekAgo);System.out.println("oneMonthAgo" + oneMonthAgo);System.out.println("oneYearAgo" + oneYearAgo + "n");}}
结果
获取时间参数的年、月、日:
year:2020
month:DECEMBER
day:5
hour:18
minute:7
second:31
计算前一天的当前时刻:
today:2020-12-05T22:07:31.831
yesterday:2020-12-04T22:07:31.831
same object:false
计算当天的00点和24点:
todayBegin:2020-12-05T00:00
todayEnd:2020-12-05T23:59:59.999999999
计算一周、一个月、一年前的当前时刻:
oneWeekAgo:2020-11-28T22:07:31.831
oneMonthAgo:2020-11-05T22:07:31.831
oneYearAgo:2019-12-05T22:07:31.831
ChronoUnit是JDK提供的枚举,放心用就是了。plus/minus呢大概就这么个用法,上面展示的是最最通用的plus/minus(long amoutToAdd, TemporalUnit unit),数字+单位,随机应变。虽然plus配合负数时效果等同于minus,但如果要减去时间,还是建议使用minus。
你也可以选择以下方法进行时间的增减:
但这些都可以用plus/minus(long amoutToAdd, TemporalUnit unit)代替:
从广义上来说,修改时间和增减时间是一样的,但稍微有点区别。我们通过几个案例体会一下即可:
public class NewDateApiTest {public static void main(String[] args) {LocalDateTime now = LocalDateTime.now();System.out.println("now:" + now);// 将day修改为6号LocalDateTime modifiedDateTime = now.with(ChronoField.DAY_OF_MONTH, 6);System.out.println("modifiedDateTime:" + modifiedDateTime);}}
注意一下,之前我们plus时用的是ChronoUnit,表示增幅单位,而这里ChronoField则是指定修改的字段。
同样的,上面演示的也是最最通用的API,大家也可以用以下方法:
也就是说,直接在方法层面已经指定了要修改的字段。
public class NewDateApiTest {public static void main(String[] args) {LocalDateTime today = LocalDateTime.now();LocalDateTime after = today.plusSeconds(1);boolean result = after.isAfter(today);System.out.println("result=" + result);}}
大家之前可能应该已经多多少少见过zone,那么什么是zone,为什么需要它呢?
由于地球是圆的,并且自西向东自转,美国的黑夜是我们的白昼,我们的早上8点是地球另一面的晚上8点。当一个国际友人问你现在几点钟时,你不能回答“现在是早上8点”,而应该说“现在是北京时间早点8点”或者“现在是美国时间晚上8点”。
上面介绍的LocalDateTime等都是不包含时区信息的,但我们可以将它们转为包含时区信息的对象:
public class NewDateApiTest {public static void main(String[] args) {// 当地时间LocalDateTime now = LocalDateTime.now();System.out.println("localDateTime:" + now);// 时区(id的形式),默认的是本国时区ZoneId zoneId = ZoneId.systemDefault();// 为localDateTime补充时区信息ZonedDateTime beijingTime = now.atZone(zoneId);System.out.println("beijingTime:" + beijingTime);}
}
结果
localDateTime:2020-12-05T18:50:56.623
beijingTime:2020-12-05T18:50:56.623+08:00[Asia/Shanghai]
别问我为什么是上海时间,问就是不知道(安装Linux虚拟机也是上海时间)。
这里有个容易犯错的点,需要和大家说明一下:LocalDateTime转为ZonedDateTime时间不会变,仅仅是丰富了时区信息而已。比如:
public class NewDateApiTest {public static void main(String[] args) {// 当地时间LocalDateTime now = LocalDateTime.now();System.out.println("localDateTime:" + now);// 时区(id的形式)ZoneId zoneId = ZoneId.of("Asia/Tokyo");// 补充时区信息ZonedDateTime tokyoTime = now.atZone(zoneId);System.out.println("tokyoTime:" + tokyoTime);}
}
结果
localDateTime:2020-12-05T19:01:32.546
tokyoTime:2020-12-05T19:01:32.546+09:00[Asia/Tokyo]
时间是一样的,只是多了时区。
那么,怎样转换时区时间呢?
用withZoneSameInstant():
public class NewDateApiTest {public static void main(String[] args) {// 当地时间LocalDateTime now = LocalDateTime.now();System.out.println("localDateTime:" + now);// 时区(id的形式)ZoneId zoneId = ZoneId.of("Asia/Shanghai");// 补充时区信息ZonedDateTime shanghai = now.atZone(zoneId);System.out.println("上海时间:" + shanghai);// 当前上海时间对应东京时间是几点呢?ZonedDateTime tokyoHot = shanghai.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));System.out.println("东京时间:" + tokyoHot);}
}
结果
localDateTime:2020-12-05T19:07:23.271
上海时间:2020-12-05T19:07:23.271+08:00[Asia/Shanghai]
东京时间:2020-12-05T20:07:23.271+09:00[Asia/Tokyo]
所以,现在明白为什么新的时间API叫LocalXxx了吗?Local表示它不带时区,只是一个普普通通的时间概念,比如你的生日是1998-12-11 12:00:00,没有时区。当你需要带上时区,可以转为ZonedDateTime。
那么我怎么知道北京时间是"Asia/Shanghai",东京时间是"Asia/Tokyo"呀?
别问,问就是百度,反正我找了半天也没找到枚举。但是JDK提供了全部的ZoneId(没啥卵用):
public class NewDateApiTest {public static void main(String[] args) {Set
}
最后,ZonedDateTime转回LocalDateTime:
public class NewDateApiTest {public static void main(String[] args) {// 当地时间LocalDateTime now = LocalDateTime.now();System.out.println("localDateTime:" + now);// 时区(id的形式)ZoneId zoneId = ZoneId.of("Asia/Tokyo");// 补充时区信息ZonedDateTime tokyoTime = now.atZone(zoneId);System.out.println("tokyoTime:" + tokyoTime);// ZonedDateTime转LocalDateTimeLocalDateTime localDateTime = tokyoTime.toLocalDateTime();}
}
toLocalDateTime()即可。
媒介是Instant(格林尼治时间)
LocalDateTime转Date
public class NewDateApiTest {public static void main(String[] args) {// 先把LocalDateTime变为ZonedDateTime,然后调用toInstant()LocalDateTime now = LocalDateTime.now();ZonedDateTime zonedDateTime = now.atZone(ZoneId.systemDefault());// Instant是代表本初子午线的时间,所以比我们的东八区要晚8小时Instant instant = zonedDateTime.toInstant();System.out.println("zonedDateTime:" + zonedDateTime);System.out.println("instant:" + instant);// 转为DateDate date = Date.from(instant);System.out.println("date:" + date);}
}
Date转LocalDateTime
public class NewDateApiTest {public static void main(String[] args) {Date date = new Date();// Date也有toInstant()Instant instant = date.toInstant();System.out.println("date:" + date);System.out.println("instant:" + instant);// 不带时区:LocalDateTime.of(),带时区:LocalDateTime.ofInstant()LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());System.out.println("localDateTime:" + localDateTime);}
}
差点忘了,转成秒也非常常用哟:
public class NewDateApiTest {public static void main(String[] args) {Date date = new Date();System.out.println(date.getTime() / 1000);LocalDateTime now = LocalDateTime.now();long result = now.toEpochSecond(ZoneOffset.ofHours(8));System.out.println(result);LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(result, 0, ZoneOffset.ofHours(8));System.out.println(localDateTime);}
}
好家伙,又学了两个方法:toEpochSecond()、ofEpochSecond()。
但还是那句话,你要的方法都在LocalXxx类中,只要你稍微找一下就能找到,还是很方便的!
public class NewDateApiTest {public static void main(String[] args) {LocalDateTime now = LocalDateTime.now();System.out.println("格式化前:" + now);String format = now.format(DateTimeFormatter.ISO_DATE_TIME);System.out.println("默认格式:" + format);String other = now.format(DateTimeFormatter.BASIC_ISO_DATE);System.out.println("其他格式:" + other);}
}
上面的代码只想透露3点信息:
之前说了,LocalXxx系列都是线程安全的,所以不会发生多线程下的格式转换错误问题。要想明白SimpleDateFormat为什么线程不安全,请戳:
https://www.bilibili.com/video/BV1tt4y1U7hL?p=3www.bilibili.com但一般我们都需要自定义格式,怎么做?
先观察默认的格式化是怎么做的:
哦?DateTimeFormatter.ISO_DATE_TIME其实返回的是DateTimeFormatter对象。
来看一下正确定的自定义格式化:
public class NewDateApiTest {public static void main(String[] args) {LocalDateTime now = LocalDateTime.now();System.out.println("格式化前:" + now);String format = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));System.out.println("格式化后:" + format);}
}
结果
格式化前:2020-12-05T19:30:11.788
格式化后:2020-30-05 19:30:11
为什么可以传入DateTimeFormatter.ofPattern()呢?不是需要对象吗?
和之前学习Stream API时遇到的一样,某些方法的返回值其实也是自身类型:
最后了解一下反格式化:
public class NewDateApiTest {public static void main(String[] args) {LocalDateTime now = LocalDateTime.now();System.out.println("格式化前:" + now);String format = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));System.out.println("格式化后:" + format);LocalDateTime parse = LocalDateTime.parse(format, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));System.out.println("反格式化:" + parse);}
}
结果
格式化前:2020-12-05T19:34:44.817
格式化后:2020-12-05 19:34:44.817
反格式化:2020-12-05T19:34:44.817
简而言之,一个format(),一个parse(),都要传入DateTimeFormatter对象,可以自定义也可以使用默认的。
如果你怕写错"yyyy-MM-dd HH:mm:ss.SSS",可以自定义一个常量类,或者使用第三方定义的。
public final class DatePattern {public static final String YYYY_MM_DD_HH_MM_SS_SSS = "yyyy-MM-dd HH:mm:ss.sss";public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";public static final String YYYY_MM_DD_WITH_SLASH = "yyyy/MM/dd";public static final String YYYY_MM_DD_WITH_STRIKE = "yyyy-MM-dd";// ...}
@RestController
@RequestMapping("/user")
public class UserController {@PostMappingpublic UserPojo addUser(@RequestBody UserPojo userPojo) {userPojo.setBirthday(userPojo.getBirthday().plusDays(1));return userPojo;}}@Data
public class UserPojo {private String name;private LocalDateTime birthday;}
返回值是:
加了一天。
时间格式有点怪异,但是没关系啊,点到为止,下一个阶段再介绍。
这是Java8新特性的最后一节啦,撒花。
谢谢朋友们!
总之,不要怕记不住,反正所有方法都在LocalXxx内部,靠IDEA的提示找一下就行啦~
本文来自Java小册,如果你希望在工作一年时就收获两年的经验值,欢迎加入我们一起学习~
https://zhuanlan.zhihu.com/p/212191791zhuanlan.zhihu.com