1. 旧版本日期和时间API存在的问题
1)设计差:在java.util和java.sql的包中都包含了日期类,java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期。此外,用于格式化和解析的类在java.text包中定义
2)非线程安全:java.utl.Date是非线程安全的,所有的日期类都是可变的,这是java日期类最大的问题之一
3)时区处理麻烦:日期类并不提供国际化,没有时区支持,因此,java引入了java.util.Calendar和java.util.TimeZone类,但是,他们同样存在上述所有的问题。
先来看一下在之前我们处理DateFormate做线程安全一般是怎么做的
package com.bjc.time.traditional;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TraditionalTimeUtils {
private static final ThreadLocal<DateFormat> local = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static Date convert(String date) throws ParseException {
return local.get().parse(date);
}
}
调用:
package com.bjc.time.traditional;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class TraditionalTime {
public static void main(String[] args) throws Exception {
List<Future<Date>> dates = new ArrayList<>();
ExecutorService pool = Executors.newFixedThreadPool(10);
for(int i = 0 ; i < 10 ; i++) {
Future<Date> submit = pool.submit(() -> {
try {
return TraditionalTimeUtils.convert("2020-05-14");
} catch (ParseException e) {
e.printStackTrace();
}
return null;
});
dates.add(submit);
}
dates.forEach(fu -> {
try {
System.out.println(fu.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
pool.shutdownNow();
}
}
2. 新日期时间API概览
JDK8中新增了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于java.time包中,下面是一些关键类:
1)localDate:表示日期,包含年月日,格式为 2019-10-16
2)LocalTime:表示时间,包含时分秒,格式为 16:58:54.158549300
3)LocalDateTime:表示日期时间,包含年月日,时分秒,格式为:2019-10-16T16:58:54.158549300
4)DateTimeFormatter:日期格式化类
5)Instant:时间戳,表示一个特定的时间瞬间
6)Duration:用于计算2个时间(LocalTime,时分秒)的距离
7)Period:用于计算2个日期(LocalDate,年月日)的距离。
8)ZoneDateTime:包含时区的时间。
在java中,使用的历法是世界民用历法ISO 8601日历系统,也就是我们所说的公立。平均一年365天,闰年366天。此外java8还提供了4套其他历法,分别是:
1)ThaiBuddhistDate:泰国佛教历
2)MinguoDate:中华民国历
3)JapaneseDate:日本历
4)HijrahDate:伊斯兰历
3. JDK8日期与时间
3.1 日期与时间类
LocalDate、LocalTime、LocalDateTime类的实例是不可变的对象,分别表示使用ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
3.1.1 LocalDate类
表示日期,有年月日
1)LocalDate.now():获取当前日期
2)LocalDate.of(int year,int month,int day):获取指定日期时间
3)获取单独的年月日:使用LocalDate对象分别调用getYear()、getMonth()、getDayOfMonth()
注意:这里获取到的月份是英文表示,这是因为getMonth()得到的是一个Month,它是一个枚举,要想得到数字月份,可以使用getMonthValue
例如:
public void testLocalDate(){
LocalDate now = LocalDate.now();
System.out.println(now); // 2020-02-24
LocalDate ofDate = LocalDate.of(2020, 2, 2);
System.out.println(ofDate); // 2020-02-02
int year = now.getYear();
Month month = now.getMonth();
int dayOfMonth = now.getDayOfMonth();
int dayOfYear = now.getDayOfYear();
DayOfWeek dayOfWeek = now.getDayOfWeek();
// 年:2020 月:FEBRUARY 日:24 dayOfYear:55 dayOfWeek:MONDAY
System.out.println("年:" + year + " 月:" + month + " 日:" + dayOfMonth + " dayOfYear:" + dayOfYear + " dayOfWeek:" + dayOfWeek);
// 获取数字月
int monthValue = now.getMonthValue();
System.out.println(monthValue); // 2
}
3.1.2 LocalTime类
表示时间,有时分秒
1)LocalTime.now():获取当前时间的时分秒
2)LocalTime.of(int hour, int minute, int sec):获取指定的时分秒
3)获取单独的时分秒:使用LocalTime对象分别调用getHour、getMinute、getSecond
public void testLocalTime(){
LocalTime now = LocalTime.now();
System.out.println(now); // 09:14:30.840
LocalTime time = LocalTime.of(13, 55, 24);
System.out.println(time); // 13:55:24
// 获取具体的时分秒
int hour = now.getHour();
int minute = now.getMinute();
int second = now.getSecond();
int nano = now.getNano();
}
3.1.3 LocalDateTime类
使用方式都差不多,例如;
public void testLocalDateTime(){
// 获取当前时间的年月日时分秒
LocalDateTime now = LocalDateTime.now(); // 2020-02-24T09:21:28.300
LocalDateTime of = LocalDateTime.of(2020, 5, 25, 10, 55, 23); // 2020-05-25T10:55:23
int year = now.getYear();
int month = now.getMonthValue();
int dayOfMonth = now.getDayOfMonth();
}
3.1.4 LocalDate系列类的设置时间
三个类设置时间的方式都是一样的,但是与我们之前的习惯所不同的是,其修改事件不是用set方法,而是用的with方法。
对日期时间的修改,对已存在的LocalDate对象,创建它的修改版,最简单的方式是使用withAttribute方法。该方法会创建对象的一个副本,并按照其属性去修改。
1)withAttribute设置时间
值得注意的是,使用withAttribute比如:withYear,是重新创建一个新的LocalDate对象,不会修改原来的LocalDate对象。
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 1. 修改时间
LocalDateTime localDateTime = now.withDayOfMonth(3); // 2020-02-03T09:28:06.631
LocalDateTime localDateTime1 = now.withYear(2025); // 2025-02-24T09:29:02.857
2)plusAttribute与minusAttribute加减时间
与withAttribute一样,返回的也是一个全新的对象.
使用plusAttribute加减时间,新增就是正数,减少就是负数
使用minusAttribute加减时间,正好相反,减少就是正数,新增就是负数
例如:
// 2. 增加或减去时间
LocalDateTime localDateTime2 = now.plusYears(3); // 2023-02-24T09:36:13.917
LocalDateTime localDateTime3 = now.plusYears(-3); // 2017-02-24T10:52:49.922
LocalDateTime localDateTime4 = now.minusYears(3); // 2017-02-24T10:53:53.852
LocalDateTime localDateTime5 = now.minusYears(-3); // 2023-02-24T10:54:24.212
3)比较时间
在LocalDate中有三个方法用于比较两个时间,一个是isAfter一个是isBefore另一个是isEquals
例如;
public void testEqual(){
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
LocalDateTime of = LocalDateTime.of(2020, 5, 25, 10, 55, 23);
System.out.println(now.isAfter(of)); // false
System.out.println(now.isBefore(of)); // true
System.out.println(now.isEqual(of)); // false
}
3.2 时间格式化与解析
通过java.time.format.DateTimeFormatter类可以进行日期时间解析与格式化
3.2.1 使用format格式化时间
DateTimeFormatter中提供了很多默认的日期时间格式供我们使用,例如:
DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME;
String format = dtf.format(now);
System.out.println(format); // 2020-02-24T11:12:01.398
如果我们想使用我们自定义的时间格式了,我们也可以使用ofPattern函数自己指定,例如;
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String format1 = timeFormatter.format(now);
System.out.println(format1); // 2020-02-24
3.2.2 使用parse解析
LocalDateTime parse = LocalDateTime.parse("2020-02-24T11:26:00.515", DateTimeFormatter.ISO_DATE_TIME);
System.out.println(parse); // 2020-02-24
注意:这里需要注意一点LocalDateTime.parse解析的时间类型字符串需要是这样子的2020-02-24T11:26:00.515
3.3 Instant类
Instant时间戳/时间线,内部保存了从1970年1月1日 00:00:00以来的秒和纳秒,一般不是给用户使用的,而是方便我们程序做一些统计使用的,比方说,计算方法的耗时等。
public void testInstant(){
Instant now = Instant.now();
System.out.println(now); // 2020-02-24T04:48:24.130Z
}
其他常用API
now.plusSeconds(30);// 增加30秒
now.minusSeconds(30);//减少30秒
now.getEpochSecond(); // 得到秒
now.getNano(); // 得到纳秒
3.4 计算日期时间差的类
3.4.1 Duration类
用于计算两个时间(LocalTime,时分秒)的距离
public void testDuration(){
// 时间
Duration duration = Duration.between(LocalTime.now(),LocalTime.of(10,25,30));
System.out.println("相差的天数:" + duration.toDays()); // 0
System.out.println("相差的小时数:" + duration.toHours()); // -2
System.out.println("相差的毫秒数:" + duration.toMillis()); // -9281042
System.out.println("相差的分钟:" + duration.toMinutes()); // -154
}
3.4.2 Period类
用于计算2个日期(LocalDate,年月日)的距离
public void testPeriod(){
// 日期
Period period = Period.between(LocalDate.now(), LocalDate.of(2025, 12, 20));
System.out.println("相差的年份数:" + period.getYears()); // 5
System.out.println("相差的月份数:" + period.getMonths()); // 9
System.out.println("相差的天数:" + period.getDays()); // 26
}
注意:使用between的时候,注意参数的位置,其实现是用后面的时间-前面的时间。
3.5 时间校正器
有时候我们需要获取例如:将日期调整到“下一个月的第一天”等,这时候就可以使用事件校正器来进行
3.5.1 TemporalAdjuster
时间校正器,是一个函数式接口,只有一个抽象方法
Temporal adjustInto(Temporal temporal);
注意:参数temporal就是要调整的时间
例如:将日期调整到“下一个月的第一天”
public void test(){
LocalDateTime dateTime = LocalDateTime.now();
System.out.println(dateTime); // 2020-02-24T13:24:59.909
// 定义时间校正器:调整到下个月的1号
TemporalAdjuster ts = (temporal) -> {
// 将需要调整的时间temporal转成localDateTime对象
LocalDateTime localDateTime = (LocalDateTime) temporal;
// 调整时间
return localDateTime.plusMonths(1).withDayOfMonth(1);
};
LocalDateTime newTime = dateTime.with(ts);
System.out.println(newTime); // 2020-03-01T13:24:59.909
}
注意:通过上例可以知道,时间校正器通常是作为LocalDate的with方法的参数使用的。
3.5.2 TemporalAdjusters
该类通过静态方法,提供了大量的常用TemporalAdjuster的实现
例如:调整到下个月第一天
LocalDateTime dateTime = LocalDateTime.now();
TemporalAdjuster temporalAdjuster = TemporalAdjusters.firstDayOfNextMonth();
LocalDateTime localDateTime = dateTime.with(temporalAdjuster);
System.out.println(localDateTime); // 2020-03-01T13:30:47.863
3.6 设置日期时间的时区
java8中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。
其中每个时区都对应着ID,ID的格式为“区域/城市”。例如:Asia/Shanghai等
3.6.1 ZoneId类
ZoneId类中包含了所有的时区信息。
获取所有的时区ID
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
3.6.2 ZonedDateTime
ZonedDateTime类也有now方法,其有三个重载形式
1)创建世界标准时间
ZonedDateTime zonedDateTime = ZonedDateTime.now(Clock.systemUTC());
2)创建本地时区的时间
ZonedDateTime now = ZonedDateTime.now(); // 2020-02-24T13:48:01.799+08:00[Asia/Shanghai]
本地时区的时间也就是我们之前使用的LocalDateTime.now()得到的时间。但是,通过时区类得到的本地时区的时间,很明显的与通过LocalDateTime.now()得到的时间有区别,我们通过时区类得到的时间带有时区信息
3)创建指定的时区创建日期时间
ZonedDateTime zonedDateTime1 = ZonedDateTime.now(ZoneId.of("America/Cuiaba"));
// 输出结果:2020-02-24T01:51:02.236-04:00[America/Cuiaba]
3.6.3 时区的其他操作API
1)withZoneSameInstant
修改时区
ZonedDateTime zonedDateTime1 = ZonedDateTime.now(ZoneId.of("America/Cuiaba"));
System.out.println(zonedDateTime1); // 2020-02-24T01:54:25.444-04:00[America/Cuiaba]
ZonedDateTime zonedDateTime2 = zonedDateTime1.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));
System.out.println(zonedDateTime2); // 2020-02-24T13:54:25.444+08:00[Asia/Shanghai]
2)withZoneSameLocal
该方法也是用于修改时间的,但是与withZoneSameInstant不同的是,withZoneSameLocal只修改时区,不更改时间。
总结:Java8对于时间API的优势
1)新版时间和日期API中,日期和时间的变化是不可变的,操纵的日期不会影响老值,而是生成一个新的实例
2)新的API提供了两种不同的时间表示方式,有效的区分了人和机器的不同需求
3)TemporalAdjuster可以更精确的操纵日期,还可以自定义日期调整期
4)是线程安全的。