本文会详细介绍,Java8中关于时间、日期API的具体使用,以及注意问题,具体内容包括传统时间格式化的线程安全问题及解决方案、LocalDateTime、Duration和Period、时间校正器、时间格式化、时区处理等操作。
1、传统时间格式化的线程安全问题及解决方案
传统时间API的安全问题引入
- 使用java8之前的传统的关于时间日期的API 会有线程安全问题:
@Test
public void test1() throws ExecutionException, InterruptedException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Callable<Date> task = new Callable<Date>() {
@Override
public Date call() throws Exception {
return dateFormat.parse("2020-08-10");
}
};
ExecutorService pool = Executors.newFixedThreadPool(10);
List<Future<Date>> result = new ArrayList<>();
for (int i = 0; i < 10; i++) {
result.add(pool.submit(task));
}
for (Future<Date> future : result) {
System.out.println(future.get());
}
// 运行结果: java.util.concurrent.ExecutionException: java.lang.NumberFormatException: For input string: "E.2101E2"
}
运行结果如下图所示,显示无法正确解析日期字符串:
解决方案
- 日期线程安全的解决方案1:使用ThreadLocal来存储、解析转换Date对象
@Test
public void test2ThreadSafe() throws ExecutionException, InterruptedException {
Callable<Date> task = new Callable<Date>() {
@Override
public Date call() throws Exception {
return DateFormatThreadLocal.covert("2022-08-10");
}
};
ExecutorService threadPool = Executors.newFixedThreadPool(10);
try {
List<Future<Date>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(threadPool.submit(task));
}
for (Future<Date> future : list) {
System.out.println(future.get());
}
} finally {
threadPool.shutdown();
}
}
- 解决方案2:使用Java8的新时期API,用DateTimeFormatter 代替SimpleDateFormat,用LocalDate代替Date
@Test
public void test3() throws ExecutionException, InterruptedException {
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
Callable<LocalDate> callable = new Callable<LocalDate>() {
@Override
public LocalDate call() throws Exception {
return LocalDate.parse("2022-08-10",timeFormatter);
}
};
ExecutorService threadPool = Executors.newFixedThreadPool(10);
List<Future<LocalDate>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(threadPool.submit(callable));
}
for (Future<LocalDate> future : list) {
System.out.println(future.get());
}
threadPool.shutdown();
}
2、LocalDateTime
- LocalDateTime在前面解决线程安全问题时,已经使用过,下面进行详细讲解。LocalDate、LocalTime、LocalDateTime 类的实例是不可变的对象,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信 息,也不包含与时区相关的信息。 注:ISO-8601日历系统是国际标准化组织制定的现代公民的日期和时间的表示法
- 具体API使用如下:
- 代码示例:
@Test
public void test1(){
// 1.输出当前时间
LocalDateTime now = LocalDateTime.now();
System.out.println(now); // 输出格式形如: 2022-08-10T08:29:08.987
// 2.将指定时间格式化
LocalDateTime dateTime1 = LocalDateTime.of(2018,9,1,8,20,30);
System.out.println(dateTime1); // 2018-09-01T08:20:30
// 3.将指定时间上进行运算或判断
LocalDateTime dateTime2 = now.plusYears(2);
System.out.println(dateTime2);
LocalDateTime dateTime3 = now.minusMonths(2);
System.out.println(dateTime3);
System.out.println(now.getYear());
System.out.println(now.getMonthValue() + "<--->" + now.getMonth());
System.out.println(now.getDayOfMonth());
// 判断当前时间now 是否在 dateTime1之后
boolean after = now.isAfter(dateTime1);
System.out.println(after);
// 判断当前时间now是否在 dateTime2之前 (dateTime2 = now.plusYears(2);)
boolean before = now.isBefore(dateTime2);
System.out.println(before);
}
3、Duration和Period
- Instant时间戳
用于“时间戳”的运算。它是以Unix元年(传统的设定为UTC时区1970年1月1日午夜时分)开始,所经历的描述进行运算 - Duration:用于计算两个“时间”间隔(具体到时分秒),即Time
- Period:用于计算两个“日期”间隔(具体到年月日),即Date
使用示例:
@Test
public void test2() throws InterruptedException {
Instant start = Instant.now();
Thread.sleep(1000);
Instant end = Instant.now();
Duration duration = Duration.between(start, end);
System.out.println("耗费时间: " + duration);
System.out.println("human,耗费时间: " + duration.toMillis() + "毫秒");
// 在原本的持续时间上其他计算操作(增加10毫秒,1个小时,2天)
System.out.println(duration.plusMillis(10));
System.out.println(duration.plusHours(1));
System.out.println(duration.plusDays(2));
}
- 运行结果:
@Test
public void test3(){
LocalDate start = LocalDate.of(2018,10,10);
LocalDate now = LocalDate.now();
Period period = Period.between(start, now);
System.out.println("两时间相隔的年数: " +period.getYears());
System.out.println("两时间相隔的月数: " +period.getMonths());
System.out.println("两时间相隔的天数: " +period.getDays());
// isNegative(): 结果为false,now在start之前,为true,now在start之后
System.out.println(period.isNegative());
}
4、时间校正器
- TemporalAdjuster:时间校正器,有时我们可能需要调整某个日期的时间,例如:将日期调整到“下个周日”等操作。
- TemporalAdjusters: 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。
例如获取下个周日:
- 使用示例:
public static void main(String[] args) {
LocalDateTime dateTime1 = LocalDateTime.now();
System.out.println(dateTime1);
// 修改dateTime1变为当前月的16号
LocalDateTime dateTime2 = dateTime1.withDayOfMonth(16);
System.out.println(dateTime2);
// 获取下一个休息日(下一个星期六)
LocalDateTime vocation = dateTime1.with(TemporalAdjusters.next(DayOfWeek.SATURDAY));
System.out.println(vocation);
// 自定义下一个工作日
LocalDateTime nextWorkDay = dateTime1
// .plusDays(2) // 测试其他日期
.with((dt) -> {
LocalDateTime work = (LocalDateTime) dt;
if (work.getDayOfWeek().equals(DayOfWeek.FRIDAY)) {
return work.plusDays(3); // 当前是星期五,下一个工作日是在3天后
} else if (work.getDayOfWeek().equals(DayOfWeek.SATURDAY)) {
return work.plusDays(2); // 当前是星期六,下一个工作日是在2天后
} else {
return work.plusDays(1); // 星期天 或 星期一到星期五
}
});
System.out.println("下一个工作日:" +nextWorkDay);
}
补充:
LocalDateTime中的with方法中参数类型是TemporalAdjuster类型,TemporalAdjuster是函数式接口,唯一需要实现的方法就是adjustInto,需要传入一个参数并返回结果(可以使用Lambda表达式来实现)。
5、时间格式化
- java.time.format.DateTimeFormatter类:该类提供了两种格式化方法:
- 预定义的标准格式,比如:ISO_LOCAL_DATE、ISO_LOCAL_DATE_TIME、DateTimeFormatter.ISO_DATE
- 自定义的格式,通过
ofPattern()
指定输出日期和时间的格式,比如:
- 预定义的标准格式,比如:ISO_LOCAL_DATE、ISO_LOCAL_DATE_TIME、DateTimeFormatter.ISO_DATE
`DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss")`
- 代码示例
// DateTimeFormatter: 格式化时间/日期
@Test
public void test1(){
DateTimeFormatter timeFormatter = DateTimeFormatter.ISO_DATE_TIME;
LocalDateTime now = LocalDateTime.now();
String strDate = now.format(timeFormatter);
System.out.println(strDate);
System.out.println("------------");
// 指定输出时间的格式
DateTimeFormatter ofPattern = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
String str = ofPattern.format(now);
System.out.println(str);
}
6、时区处理
- Java8 中加入了对时区的支持,带时区的时间为分别为:ZonedDate、ZonedTime、ZonedDateTime
其中每个时区都对应着 ID,地区ID都为 “{区域}/{城市}”的格式 - 例如 :Asia/Shanghai 等
- ZoneId:该类中包含了所有的时区信息
- getAvailableZoneIds() : 可以获取所有时区时区信息
- of(id) : 用指定的时区信息获取ZoneId 对象
@Test
public void test2(){
// 获取所有可用时区
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
zoneIds.forEach(System.out::println);
// 输出日期和时间的基础上,还增加对应的时区信息
System.out.println(ZonedDateTime.now()); // 2022-08-10T11:05:45.394+08:00[Asia/Shanghai]
LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println("上海时区的当前时间: "+now);
// 获取 Europe/Athens时区的当前时间
LocalDateTime now2 = LocalDateTime.now(ZoneId.of("Europe/Athens"));
System.out.println("Europe/Athens时区的当前时间: "+now2); // 2022-08-10T06:01:22.482
LocalDateTime now3 = LocalDateTime.now();
System.out.println("当前所在地的时间,对应在Europe/Athens时区的时间: "+now3.atZone(ZoneId.of("Europe/Athens")));
// 2022-08-10T11:01:22.498+03:00[Europe/Athens] --> 与UTC时区相比相差三个小时
System.out.println(now3.atZone(ZoneId.of("UTC"))); // 2022-08-10T11:04:23.840Z[UTC]
}