Java8 后的时间日期 API

自从 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

赞 (1)

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址