自从 Java 8 发布以后,古老的 java.util.Date 不再是我们 Java 里操作日期时间的唯一选择。Java 8 借鉴第三方开源库 Joda-Time 的优秀设计,重新设计了一套新的日期时间 API,这套新的日期 API 是 JSR-310 规范的实现,相比之前,可以说好用不少。
每位 Java 开发人员都至少应该了解这套新的 API 中的几个主要核心类:
- LocalDate:表示不带时间的日期
- LocalTime:表示不带日期的时间
- LocalDateTime:日期和时间类
- ZoneId:时区
- ZonedDateTime:一个带时区的完整时间
- Instant:Unix 时间,它代表的是时间戳,比如 2018-01-14T02:20:13.592Z
- Clock:获取某个时区下当前的瞬时时间,日期或者时间
- Duration:表示一个绝对的精确跨度,使用毫秒为单位
- Period:这个类表示与 Duration 相同的概念,但是以人们比较熟悉的单位表示,比如年、月、周
- DateTimeFormatter:格式化输出
- TemporalAdjusters:获得指定日期时间等,如当月的第一天、今年的最后一天等
在几乎所有的类中,方法都被明确定义用以完成相同的行为。例如,获取当前实例我们可以使用 now() 方法,在所有的类中都定义了 format() 和 parse() 方法。一旦你使用了其中某个类的方法,对于使用其他类也是十分容易上手。
下面看看这些类具体如何使用:
LocalDate、LocalTime、LocalDateTime
Java 8 中将日期和时间进行分离,LocalDate 是用来表示无时间的日期的,也不附带任何与时区相关的信息。它提供 plus()/minus() 方法可以用来增加减少日、星期或者月,ChronoUnit 则用来表示这个时间单位。这些方法返回的是一个新的 LocalDate 实例的引用,因为 LocalTime 是不可变的,任何修改操作都会返回一个新的实例。
LocalTime 类关注时分秒,而 LocalDateTime 是两者的结合体。它们的实例都是一个不可变的对象,因此任何修改操作都会返回一个新的实例。
LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plus(1, ChronoUnit.WEEKS); //等价于 today.plusWeeks(1)
LocalDate date = LocalDate.of(2018,11,16);
LocalTime time = LocalTime.of(12,23,20);
LocalDateTime dateTime = LocalDateTime.of(date,time);
System.out.println(dateTime);
//LocalDate结合LocalTime成一个LocalDateTime
LocalDateTime dateTime2 = date.atTime(time);
System.out.println(dateTime2); //2018-11-16T12:23:20
格式化与解析时间对象 DateTimeFormatter
格式器用于解析日期字符串和格式化日期输出,创建格式器最简单的方法是通过 DateTimeFormatter 的静态工厂方法以及常量。创建格式器一般有如下三种方式:
- 常用 ISO 格式常量,如 ISO_LOCAL_DATE
- 字母模式,如 ofPattern(“yyyy/MM/dd”)
- 本地化样式,如 ofLocalizedDate(FormatStyle.MEDIUM)
和旧的 java.util.DateFormat 相比较,所有的 DateTimeFormatter 实例都是线程安全的。
LocalDateTime localDateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); S
tring formatDateTime = localDateTime.format(formatter);
System.out.println(formatDateTime);
//DateTimeFormatter提供了一些默认的格式化器,DateTimeFormatter.ISO_LOCAL_DATE_TIME 格式 yyyy-MM-ddTHH:mm:ss.SSS String dateTime2 = localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println(dateTime2);
LocalDate localDate = LocalDate.parse("2018/11/11",DateTimeFormatter.ofPattern("yyyy/MM/dd")); System.out.println(localDate); //2018-11-11
Duration 与 Period
Duration 表示一个时间段,Duration 包含两部分:seconds 表示秒,nanos 表示纳秒,它们的组合表达了时间长度。
因为 Duration 表示时间段,所以 Duration 类中不包含 now() 静态方法。注意,Duration 不包含毫秒这个属性
LocalDateTime from = LocalDateTime.of(2018, Month.JANUARY, 5, 10, 7, 0); // 2018-01-05 10:07:00
LocalDateTime to = LocalDateTime.of(2018, Month.FEBRUARY, 5, 10, 7, 0); // 2018-02-05 10:07:00
Duration duration = Duration.between(from, to); // 表示从 2018-01-05 10:07:00 到 2018-02-05 10:07:00 这段时间
Duration d = Duration.ofSeconds(6000);
System.out.println("6000秒相当于" + d.toMinutes() + "分");
System.out.println("6000秒相当于" + d.toHours() + "小时");
System.out.println("6000秒相当于" + d.toDays() + "天");
Period 在概念上和 Duration 类似,区别在于 Period 是以年月日来衡量一个时间段。Duration 用于计算两个时间间隔,Period 用于计算两个日期间隔,所以 between() 方法只能接收 LocalDate 类型的参数。
LocalDate start = LocalDate.of(2018, Month.JANUARY, 1);
LocalDate end = LocalDate.of(2020, Month.NOVEMBER, 11);
System.out.println("相隔月数:"+Period.between(start, end).getMonths());
System.out.println("相隔天数:"+Period.between(start, end).getDays());
这里我们观察下输出,会发现结果根本就不是我们想要的。
其实这里需要注意一点:Period 得到的是差值的绝对值(对应年月日直接计算数学上的差值),而并不表示真正的区间距离。
那么我如何计算两个时间的区间距离呢?API 提供了简便的方法:
long distanceMonth = start.until(end, ChronoUnit.MONTHS);
long distanceDay= start.until(end, ChronoUnit.DAYS);
System.out.println(distanceMonth);
System.out.println(distanceDay);
Instant 与 Clock
Instant 表示时间线上的一点(与 Date 类似),而不需要任何上下文信息,例如时区。概念上讲,它只是简单地表示自 1970 年 1 月 1 日 0 时 0 分 0 秒(UTC)开始的秒数。
Instant 由两部分组成,一是从原点开始到指定时间点的秒数 s, 二是距离该秒数 s 的纳秒数。它以 Unix 时间戳的形式存储日期时间,不提供处理人类意义上的时间单位(年月日等)。
//第一个参数是秒,第二个是纳秒参数,纳秒的存储范围是0至999,999,999
//2s之后的在加上100万纳秒(1s)
Instant instant = Instant.ofEpochSecond(2,1000000000);
System.out.println(instant3); //1970-01-01T00:00:03Z
Instant instant1 = Instant.now();
System.out.println(instant); //1970-01-01T00:00:00Z
Instant instant2 = Instant.parse("2018-11-11T10:12:35.342Z");
System.out.println(instant2); //2018-11-11T10:12:35.342Z
//在instant3的基础上添加5小时4分钟
Instant instant3 = instant2.plus(Duration.ofHours(5).plusMinutes(4));
System.out.println(instant3); //2018-11-11T15:16:35.342Z
//java.util.Date与Instant可相互转换
Instant timestamp = new Date().toInstant();
Date.from(Instant.now());
Clock 是时钟系统,用于查找当前时刻。你可以用它来获取某个时区下当前的日期或者时间。可以用 Clock 来替代旧的 System.currentTimeInMillis() 与 TimeZone.getDefault() 方法。
//系统默认时间
Clock clock = Clock.systemDefaultZone();
System.out.println(clock.instant().toString());
//世界协调时UTC
Clock clock = Clock.systemUTC();
//通过Clock获取当前时刻
System.out.println("当前时刻为:" + clock.instant());
//获取clock对应的毫秒数,与System.currentTimeMillis()输出相同
System.out.println(clock.millis());
System.out.println(System.currentTimeMillis());
//在clock基础上增加6000秒,返回新的
Clock Clock clock2 = Clock.offset(clock, Duration.ofSeconds(6000));
//纽约时间
Clock clock = Clock.system(ZoneId.of("America/New_York"));
System.out.println("Current DateTime with NewYork clock: " + LocalDateTime.now(clock));
System.out.println(clock.millis());
ZoneId 和 ZonedDateTime
Java 8 不仅将日期和时间进行了分离,同时还有时区。Java 使用 ZoneId 来标识不同的时区。
时区的常见情况,是从基准 UTC 开始的一个固定偏移。ZoneId 的子类 ZoneOffset,代表了这种从伦敦格林威治零度子午线开始的时间偏移,也就是时差;而 ZonedDateTime 代表的是带时区的时间。ZonedDateTime 类似于 Java 8 以前的 GregorianCalendar 类,你可以通过本地时间或时间点来创建 ZoneDateTime。
//所有可用的zoneid
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
//所有合法的“区域/城市”字符串
zoneIds.forEach(System.out::println);
ZoneId china = ZoneId.of("Asia/Shanghai");
ZonedDateTime dateAndTimeInChina = ZonedDateTime.of(LocalDateTime.now(), china);
System.out.println("特定时区下的日期和时间 : " + dateAndTimeInChina);
ZoneId america = ZoneId.of("America/New_York");
System.out.println(ZonedDateTime.now(america));
//GregorianCalendar与ZonedDateTime相互转换
ZonedDateTime zonedDateTime = new GregorianCalendar().toZonedDateTime();
GregorianCalendar.from(zonedDateTime);
使用 TemporalAdjuster 类灵活操纵日期
前面看到的所有日期操作都是相对比较直接的。有的时候,你需要进行一些更加灵活复杂的操作,比如,将日期调整到下个周日、下个工作日,或者是本月的最后一天。这时,就需要时间修改器 TemporalAdjuster,可以更加灵活地处理日期。TemporalAdjusters 工具提供了一些通用的功能,并且你还可以新增你自己的功能。
// TemporalAdjuster是一个函数式接口
TemporalAdjuster firstDayOfMonth = (temporal) -> temporal.with(DAY_OF_MONTH, 1); System.out.println(localDate.plusMonths(1).with(firstDayOfMonth));
// 等价于下面语句
System.out.println(localDate.plusMonths(1).with(TemporalAdjusters.firstDayOfMonth()));
// 计算下一个工作日的日期
TemporalAdjuster nextWorkDay = TemporalAdjusters.ofDateAdjuster(
tdate->{
DayOfWeek work = tdate.getDayOfWeek();
nt addDays=0;
if (work.equals(DayOfWeek.FRIDAY)) {
addDays=3;
}else if(work.equals(DayOfWeek.SATURDAY)){
addDays=2;
}else {
addDays=1;
} return tdate.plusDays(addDays);
}
);
LocalDate localDate1 = LocalDate.now().with(nextWorkDay);
System.out.println(localDate1);
java.util.Date 与 LocalDate、LocalTime、LocalDateTime 转换
有时候对于老的遗留项目我们需要将 java.util.Date 转换为新的日期 API。将 Date 转换为 LocalDate、LocalTime、LocalDateTime 可以借助于 ZonedDateTime 和 Instant。
实现如下:
Date date = new Date();
System.out.println("current date: " + date);
//Date -> LocalDateTime
LocalDateTime localDateTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); System.out.println("localDateTime by Instant: " + localDateTime);
//Date -> LocalDate
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
System.out.println("localDate by Instant: " + localDate);
//Date -> LocalTime
LocalTime localTime = date.toInstant().atZone(ZoneId.systemDefault()).toLocalTime();
System.out.println("localTime by Instant: " + localTime);
//Date -> LocalDateTime 另一种方式
localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
System.out.println("localDateTime by ofInstant: " + localDateTime);
//Calendar --> Instant
Calendar.getInstance().toInstant();
Java 8 在 Date 类中引入了 2 个方法,from 和 toInstant,我们可以借助 from 方法来实现 LocalDateTime 到 Date 的转换。
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("localDateTime: " + localDateTime);
//LocalDateTime -> Date
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
System.out.println("LocalDateTime -> current date: " + date);
//LocalDate -> Date
LocalDate localDate = LocalDate.now();
date = Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
System.out.println("LocalDate -> current date: " + date);
新的时间与日期 API 中很重要的一点是,它定义清楚了基本的时间与日期的概念,比方说日期、时间、瞬时时间、持续时间、时区及时间段。它们都是基于 ISO8601 日历系统,它是世界民用历法,也就是我们所说的公历。
新的 API 区分各种日期时间概念并且各个概念使用相似的方法定义模式,这种相似性非常有利于 API 的学习。总结一下一般的方法或者方法前缀:
- of:静态工厂方法,用于创建实例
- now:静态工厂方法,用当前时间创建实例
- parse:静态工厂方法,从字符串解析得到对象实例
- get:获取时间日期对象的部分状态。
- is:检查某些东西的是否是 true,例如比较时间前后
- with:返回一个部分状态改变了的时间日期对象拷贝
- plus:返回一个时间增加了的、时间日期对象拷贝
- minus:返回一个时间减少了的、时间日期对象拷贝
- to:转换到另一个类型
- at:把这个对象与另一个对象组合起来,例如 date.atTime(time)
- format:提供格式化时间日期对象的能力
最后再次声明,Java 8 中新的时间与日期 API 中的所有类都是不可变且线程安全的,任何修改操作都会返回一个新的实例,而之前 java.util.Date、Calendar 以及 SimpleDateFormat 这些关键的类都不是线程安全的。
时间日期 API 中的设计模式
- 工厂模式:now()、of() 等工厂方法直接生成日期或者日期时间。
- 策略模式:LocalDate/LocalTime/LocalDateTime/ZonedDateTime,针对日期、时间、日期和时间、带时区的日期时间,使用具体的时间日期类处理。策略模式在设计一整套东西时,对开发者特别友好。
前面也提到,所有新的日期时间 API 类都实现了一系列方法用以完成通用的任务,如:加、减、格式化、解析、从日期/时间中提取单独部分。一旦你使用了其中某个类的方法,那么非常容易上手其他类的使用。
- 构建者模式:Java 8 开始在 Calendar 中加入了构建者类,可以按如下方式生成新的 Calendar 对象。
Calendar cal = new Calendar.Builder().setCalendarType("iso8601")
.setWeekDate(2018, 1,MONDAY).build();
这里设计模式与标准的教科书式的设计模式可能有所区别,所以我们在使用设计模式时也应灵活处理,不是一成不变的。
使用示例
- 例一
SimpleDateFormat 在多线程下的异常:
public class TimeFormatTest {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
// 请求总数
public static int clientTotal = 100;
// 同时并发执行的线程数
public static int threadTotal = 10;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
for (int i = 0; i< clientTotal; i ++) {
executorService.execute(()->{
try {
semaphore.acquire();
try {
simpleDateFormat.parse("20180208");
} catch (ParseException e) {
e.printStackTrace();
}
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
执行后会产生如下异常:
java.lang.NumberFormatException: multiple points
在 Java 8 之前可以使用线程本地变量解决这个问题,然后在 Java 8 中不存在这个问题:
public class TimeFormatTest {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
// 请求总数
public static int clientTotal = 100;
// 同时并发执行的线程数
public static int threadTotal = 10;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
LocalDate date = LocalDate.parse("20180208", DateTimeFormatter.ofPattern("yyyyMMdd"));
System.out.println(date);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
- 例二
有时候业务中我们需要处理时间日期的重复性,例如某人每年的某一天都是他的生日。MonthDay 这个类由月日组合,不包含年信息,也就是说你可以用它来代表每年重复出现的一些日子。当然也有一些别的组合,比如说 YearMonth 类。它和新的时间日期库中的其它类一样也都是不可变且线程安全的。
//是否是生日
MonthDay monthDay = MonthDay.parse("--05-18");
MonthDay monthDay1 = MonthDay.from(LocalDate.now());
System.out.println(monthDay.getMonthValue());
if (monthDay.equals(monthDay1))
{
System.out.println("今天是生日");
}else {
System.out.println("今天不是生日");
}
//判断是否闰年
YearMonth yearMonth = YearMonth.from(LocalDate.now());
yearMonth.isLeapYear();
//信用卡过期,只有年月信息
YearMonth creditCardExpiry = YearMonth.of(2018, Month.FEBRUARY);
System.out.printf("信用卡过期时间:", creditCardExpiry);
- 例三
特定日期计算:
LocalDate localDate = LocalDate.now();
//计算下一个月的第一天的日期
localDate.plusMonths(1).with(TemporalAdjusters.firstDayOfMonth());
LocalDateTime dateTime = LocalDateTime.now();
//计算一年前的第二个月的最后一天的日期
dateTime = dateTime.minusYears(1) // 一年前
.withMonth(2) // 设置为2月
.with(TemporalAdjusters.lastDayOfMonth()); // 一个月中的最后一天
//当月最后一个满足是星期四的日期
LocalDate date = localDate.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
//当前日期的前一个星期天,如果当天就是星期天 LocalDate date11 = localDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
end。
未经允许不得转载:极客萌动 » Java8 后的时间日期 API