JAVA8时间API 最佳实践
实践一:初始化时间日期
方式一:JDK1.0 java.util.Date#Date(int, int, int, int, int, int)
// jdk 1.0 写法,yinw date的日期是从1900开始,此方法已过时
Date date1 = new Date(2020-1900, 10, 10, 10, 10, 10);
// Tue Nov 10 10:10:10 CST 2020
System.out.println(date1);
需要注意的是这里的月份是从0开始的。即月份10 代表11月
方式二:JDK1.1 开始使用Calendar类
使用Calendar之后,初始化年份是直接使用当前年份即可,不需要转换。
// jdk1.1 开始使用Calendar类
Calendar calendar = Calendar.getInstance();
calendar.set(2020,Calendar.NOVEMBER,10,10,10,10);
Date date2 = calendar.getTime();
System.out.println(date2);// Tue Nov 10 10:10:10 CST 2020
在使用了Calendar类之后,就引入了时区的概念了。 查看Calendar类的方法。
可以看到多个一个参数 TimeZone。
测试下不同时区下设置相同时间输出结果会是怎么样?
// jdk1.1 开始使用Calendar类
System.out.println("起始时间:" + new Date(0));
Calendar calendar = Calendar.getInstance();
calendar.set(2020,Calendar.NOVEMBER,10,10,10,10);
Date date2 = calendar.getTime();
System.out.println("北京时间:"+date2);// Tue Nov 10 10:10:10 CST 2020
// 设置成美国的时区
Calendar calendar2 = Calendar.getInstance(TimeZone.getTimeZone("America/Honolulu"));
calendar2.set(2020,Calendar.NOVEMBER,10,10,10,10);
Date date3 = calendar2.getTime();
System.out.println("美国时间:"+date3);// Tue Nov 10 18:10:10 CST 2020
可以看到不同时区相同时间,时间戳是不一样的,UTC的时间戳相差了8小时,因此使用Calendar实例化日期是,需要意识到有时区的概念。
而Date类是没有时区的概念的。
对于Date类,保存的是UTC时间,UTC是是以原子时秒长为基础,不以太阳作为参照,没有时区的概念。 因此,date保存的是一个时间戳,代表是从1970年1月1日0点到现在的毫秒数(纪元时间)。
代码System._out_.println("起始时间:" + new Date(0));
的输出为 输出:起始时间:Thu Jan 01 08:00:00 CST 1970 因为是纪元时间,在北京时间(CST)就是早上8点。
对于国际化的团队来说,日期的时期就很重要了。一般有两种处理方式:
- 使用UTC保存,就不涉及时区了。即使用Date的时间戳保存。
- 使用文本保存,但是需要记录时区的信息。而那到时间时,需要设置好时区。
例如,在北京时间发了一条消息,在美国的服务器获取发送时间的时候就需要进行转换了。 如果是使用UTC保存,直接将时间戳读取转换即可,如果使用文本记录,那么就需要根据文本记录的日期,转换成当地的时间了。
这里涉及到国际的业务推荐的最佳实践是使用UTC保存。
方式三:JDK1.8 使用java.time.* 下的时间工具类 (推荐)
java.time.LocalDateTime
Java 8 推出了新的时间日期类 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime、 DateTimeFormatter,处理时区问题更简单清晰。
使用 java.time.LocalDateTime 创建时间
LocalDateTime localDateTime = LocalDateTime.of(2020,10,10,10,10,10);
System.out.println(localDateTime); // 2020-10-10T10:10:10
UTC转本地时间时间带T Z
时间中包含T Z (如:2018-01-31T14:32:19Z) UTC统一时间 T代表后面跟着是时间,Z代表0时区(相差北京时间8小时)
需要注意的是java.time.LocalDateTime
是不带时区属性的,设计本类的目的是便于人阅读使用,可以理解为时间的字符串,和现实中挂着的时钟是一样的含义。所有名称中有 loacl 。他仅仅是一个本地时间的表示。
A date-time without a time-zone in the ISO-8601 calendar system, such as { @code 2007-12-03T10:15:30}.
其次,这个是个不可变的时间对象(immutable)
那么这个类可以做什么呢?
- 可以获取某一个日期,例如10月的第一个周
- 可以计算两个时间点之间的差值
在java8中如果需要代表适合全球的时间,那么就需要使用带时区的对象ZonedDateTime。
java.time.ZonedDateTime
使用ZonedDateTime 设置时间,需要使用 java.time.ZoneId 类设置时区
// 这里的月份10 代表的就是十月
LocalDateTime localDateTime = LocalDateTime.of(2020,10,10,10,10,10);
System.out.println(localDateTime.toString()); // 2020-10-10T10:10:10
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Shanghai"));
System.out.println(zonedDateTime); // 2020-10-10T10:10:10+08:00[Asia/Shanghai]
可以看到输出的时间带有时区信息。
这里要正确处理国际化的实际问题,这里推荐使用ZonedDateTime。因为java8的日期工具提供了根据丰富的方法,本文就不详细展开了。 建议查阅 《JAVA8实战》 第12章
实践2:解析和格式化日期
SimpleDateFormat 会遇到的“坑”
情况1:日期格式化跨年bug
BUG还原:
@Test
public void test03(){
Calendar calendar = Calendar.getInstance();
calendar.set(2019,Calendar.DECEMBER,29,0,0,0);
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("格式化:"+sdf.format(calendar.getTime()));
}
输出: 格式化:2020-12-29 原因分析:出现这个问题的原因在于,小写 y 是年,而大写 Y 是 week year,也就是所在的周属于哪一年。这个案例告诉我们,针对年份的日期格式化,应该一律使用 “y” 。阿里巴巴的JAVA开发手册中也有规定:
** 《Java 开发手册》
**
情况2:static的SimpleDataFormat 有线程安全问题
bug演示:
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 演示SimpleDataFormat 线程安全问题
*/
@Test
public void test04(){
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++){
executorService.execute(() -> {
for (int j = 0; j < 100; j++){
try {
System.out.println(sdf.parse("2020-10-10 10:10:10"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
}
while (true){
}
}
打印结果
打印中可以看到 出现不同日期和异常信息。
原因分析: 查看_sdf_.parse(), 方法调用的是父类方法。 java.text.DateFormat#parse(java.lang.String)。 之后调用 java.text.DateFormat#parse(java.lang.String, java.text.ParsePosition)方法,simpleDateFormat 对其进行了重写。
在parse重写方法中 使用CalendarBuilder创建Calendar,
parsedDate = calb.establish(calendar).getTime();
而传入 java.text.CalendarBuilder#establish的参数是DateFormat的一个字段,
public abstract class DateFormat extends Format {
/**
* The {@link Calendar} instance used for calculating the date-time fields
* and the instant of time. This field is used for both formatting and
* parsing.
*
* <p>Subclasses should initialize this field to a {@link Calendar}
* appropriate for the {@link Locale} associated with this
* <code>DateFormat</code>.
* @serial
*/
protected Calendar calendar;
// ...
}
establish内部先对Calendar进行clear 之后,再构建。但是对这个操作没有加锁。因此在对于calendar存在线程安全问题
解决办法,使用ThreadLocal进行处理。 改造后的代码如下:
private static ThreadLocal<SimpleDateFormat> threadLocalSdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
/**
* 演示SimpleDataFormat 线程安全问题 解决方案
*/
@Test
public void test05(){
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++){
executorService.execute(() -> {
for (int j = 0; j < 100; j++){
try {
System.out.println(threadLocalSdf.get().parse("2020-10-10 10:10:10"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
}
while (true){
}
}
再次运行,输出的日期就正常了。
同样的原因,format方法也存在线程安全问题。
情况3:错误的格式进行错误的匹配
重现bug
/**
* SimpleDateFormat 错误的格式进行错误的匹配
*/
@Test
public void test06() throws ParseException {
String str = "20201010";
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
System.out.println(sdf.parse(str)); //Fri Feb 01 00:00:00 CST 2104
}
输出
Fri Feb 01 00:00:00 CST 2104
预期的结果应该是 2020年,错输出了2104年2月。
原因在于 程序将1010 当成了月份。1010/12 = 84年余2个月。输出年份就变成了 2020+84=2104年
使用 java8提供的 java.time.format.DateTimeFormatter
处理日期和时间对象时,格式化以及解析日期-时间对象是另一个非常重要的功能。新的java.time.format包就是特别为这个目的而设计的.
这个包中,最重要的类是DateTimeFormatter。创建格式器最简单的方法是通过它的静态工厂方法以及常量。 像BASIC_ISO_DATE和 ISO_LOCAL_DATE 这 样 的 常 量 是 DateTimeFormatter 类 的 预 定 义 实 例 。 所 有 的DateTimeFormatter实例都能用于以一定的格式创建代表特定日期或时间的字符串。
/**
* 最佳实践 DateTimeFormatter
*/
@Test
public void test07() throws ParseException {
LocalDate date = LocalDate.of(2014, 3, 18);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
System.out.println(date.format(DateTimeFormatter.ISO_LOCAL_DATE)); // 2014-03-18
System.out.println(date.format(formatter)); // 18/03/2014
System.out.println(date.format(italianFormatter)); //18. marzo 2014
DateTimeFormatter complexFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.getDefault());
System.out.println(date.format(complexFormatter)); // 18. 三月 2014
}
使用 java.time.format.DateTimeFormatter 有以下的优点:
- 所有的DateTimeFormatter实例都是线程安全的。所以,你能够以单例模式创建格式器实例,就像DateTimeFormatter所定义的那些常量,并能在多个线程间共享这些实例。
- 解析格式严格,不会出现类似情况3中的解析格式错误
实践3:操作日期时间
方式一:使用Date类
想要获取30天后的时间。
/**
* 操作日期的方式
*/
@Test
public void test09(){
// 获取30天后的时间
Date date = new Date(System.currentTimeMillis() + 30 * 24 * 60 * 60 * 1000L);
System.out.println(date);
}
这种方式需要注意的是用使用long类型,否则数值太大会造成int溢出。可以看出这种方式不够灵活,语义不明显
方式二: 使用Calendar
/**
* 操作日期的方式
*/
@Test
public void test10(){
// 获取30天后的时间
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.add(Calendar.DAY_OF_MONTH,30);
System.out.println(calendar.getTime());
}
这种方式相比与第一种方式语义明确多了。但是当我们需要更为复杂的时间计算时,例如 获取下个月的最后一天、同一个月中,最后一个符合星期几的要求的值,这个时候使用Calendar就太简便了。
方式三:Java 8 日期时间 API (最佳实践)
/**
* JAVA8 操作日期的方式
*/
@Test
public void test11(){
LocalDateTime localDateTime = LocalDateTime.now();
// 获取年份
int year = localDateTime.getYear();
/**
* 以相对方式修改时间
*
*/
localDateTime.plusWeeks(3); //3周后
localDateTime.minusHours(9); // 9小时前
localDateTime.plus(1, ChronoUnit.MONTHS); //1个月之后
// with 方法 以该 Temporal 对象为模板,对某些状态进行修改创建该对象的副本
LocalDateTime newDateTime = localDateTime.with(ChronoField.DAY_OF_MONTH, 4);
// 生成本月最后一天
LocalDateTime dateTime2 = newDateTime.with(TemporalAdjusters.lastDayOfMonth());
}
- plus和minus方法都声明于Temporal接口中。通过这些方法,对TemporalUnit对象加上或者减去一个数字,我们能非常方便地将Temporal对象前溯或者回滚至某个时间段,通过ChronoUnit枚举我们可以非常方便地实现TemporalUnit接口
- TemporalAdjusters类提供了默认中的工厂方法 ,也可以自定义自己的TemporalAdjuster方法。下面是java8中默认提供的方法。
参考引用
- 《JAVA8实战》第12章