0001 - 掌握Java时间日期的API

1、概述:

  Java日期的API一直为世人所诟病,简陋的API以及非线程安全等问题,使得开发非常不便,知道后来Java 8的推出才改善这一问题。Java8除了加入了Lambda表达式和Stream操作等重大特性,另外还针对日期时间的操作,在Joda-Time等优秀工具包的基础上引入了一套全新的API,使得操作时间日期非常方便。

  在许多编程语言中,一般都采用Unix时间体系来表示时间,这个时间是绝对公立的,它和时区没有任何关系。Unix时间使用自1970-01-01T00:00:00Z(Z即为0时间的标志)至今的毫秒差表示时间的数值,并且移除期间的“闰秒”(例如 1998-12-31T23:59:59:60Z)。Unix时间体系中,每天固定86400秒。

  在Java中获取当前时间的方法是System.currentTimeMillis(),表示的是当前时刻至1970-01-01 00:00:00.000的毫秒差值

  Java还提供了一个更加精确的时间:System.nanoTime(),获取一个时间精确到纳秒,但是它是由JVM提供的一个时间,主要用来精确衡量两个时间段之间的时间,如计算一段代码的执行时间:

    long startTime = System.nanoTime();

    long endTime = System.nanoTime();

    endTime - startTime ;

2、时区

  • GMT:格林尼治时间,格林尼治标准时间的正午是指太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间。
  • UTC:统一协调时间,其以源自时秒长为基础,在时刻上尽量接近格林尼治标准时间,标准UTC时间格式yyyy-MM-yy'T'HH:mm:ss.SSSXXX

  由于地球每天的自传是有些不规则的,而且正在缓慢减速,因此格林尼治时间已经不再被作为标准时间使用,UTC是最主要的世界时间标准。

  时区作为地理概念,表示“遵守统一时间标准的一个地区”。整个地球分为24个时区,东部和西部各12个,每个时区都有自己的本地时间,时区的基准点是伦敦(基准UTC),往东,会领先UTC,往西会落后UTC。如北京属东八区(UTC+08:00),那么我们的是加你会领先基准--也就是我们的早上九点时,敦伦是早上1点。

3、旧的时间API

  旧的Java日期API也就是Java8之前的时间API之烂是公认的,一方面在于它设计分工不明确,往往一个类既能和处理日期又能处理时间,很混乱;另一方面,在某些年月日期的数值映射存储违反人们的正常认知,如0对应月份一月,11对应月份十二月等。

  比如现在我要输出 2018 年的 1 月 1 日:

  Date date = new Date(2018,1,1);
  System.out.println(date);

  看看输入的结果是否和你预期的一样:

  Fri Feb 01 00:00:00 CST 3918 

  年份是 3918 年,这是啥?原来这里存在一个起始年份 1900,实际年份是要在你的年份参数上加上个起始年份。

  然后旧的日期 API 中 Date 有 java.util.Date 和 java.sql.Date,两者的关系是 java.sql.Date 继承 java.util.Date。为了把前者转成后者,需要使用如下代码:

  Date date = new Date();
  java.sql.Date d = new java.sql.Date(date.getTime());

  java.sql.Date 不支持 Date 参数的构造器,只好传入 long 类型的时间,接着尝试把当前小时数取出来:

  System.out.println(d.getHours()); 

  结果悲剧了:

  Exception in thread "main" java.lang.IllegalArgumentException    at java.sql.Date.getHours(Date.java:177) 

  java.sql.Date 是 SQL 中的单纯的日期类型,不会有时分秒,这就是为什么把 java.sql.Date 通过 JDBC 插入数据库,会发现时分秒都没有了。因此如果你同时需要日期和时间,你应该使用 Timestamp,它也是 java.util.Date 的子类,Timestamp 则包含时间戳的完整信息。

  在旧的日期 API 中,Data 这个类扮演着诸多角色,它提供了操作时刻的方法,提供了操作年月日日期的方法,甚至还有关时区的方法。可以说日期时间相关操作它都可以管。但是管的东西多,自然就不能面面俱到。现在的 Date 类中大部分方法都已废弃,被标记为 @Deprecated

  现在一般使用旧的 API 时,Date 只负责存储一个绝对时间,并对 Calender 和 DateFormat 提供操作接口。Calendar 可以获取 Date 中的特定信息,比如这个时间是该年的第几个星期,此外,还可以进行日期时间的增减以及时间先后的比较。SimpleDateFormat 主要做一些格式化的输入输出。

  这样承载日期时间信息、日期之间的转换,不同日期格式的显示,以及分析日期字符串这三项职责明确分开了。

enter image description here

  总的来说,Date、Calendar 和 DateFormat 已经能够处理一般的时间日期问题了。但是不可避免的是,它们依然很繁琐,不好用并且这些日期类都是可变且线程不安全的。

4、使用Joda-Time

  从上述可以看出 Java 8 之前的时间 API 确实用着不方便,我们再来看下面的代码:

  Calendar calendar = Calendar.getInstance();
  calendar.set(2018, Calendar.JANUARY, 1, 11, 11, 11);   System.out.println(calendar.getTime()); 

  使用 Calendar 的静态方法 getInstance 可以获取 Calendar 的一个当前时刻的实例,而 getInstance 方法并未提供一个指定年月日和时分秒的重载方法。每次要指定特定的日期时间,必须先获取一个表示当前时间的 Calendar 实例,再去设值。

  输出结果:

  Sun Jan 01 11:11:11 CST 2018 

  注意前面说过给 Date 设置年份需要减去 1900。与 Date 不一样,Calendar 年份的设置并不需要减去 1900(月份的定义和 Date 还是一样),这种不一致绝对让有着强迫症的程序员们不适!

  出现问题,就有人站出来解决问题。由于上述种种问题,社区就出现了一些第三方的日期处理框架,例如 Joda-Time、DATA4J 等开源项目,而 Joda-Time 是最为广泛使用的。

使用 Joda-Time 操作日期时间十分方便,获取 Calendar 对象和设置时间完全可以合成一步完成:

  DateTime dateTime = new DateTime(2018, 1, 1, 11, 11, 11, 0); 

  DateTime 是 Joda-Time 中主要的日期时间操作类,注意这里一月份可以传 1 来表示了

  再如,如果要给上述时间增加 3 天再按格式输出的话,使用 Joda 也是十分便捷:

  dateTime.plusDays(3).toString("yyyy/MM/dd HH:mm:ss");

  //减去3天
  dateTime.minusDays(3).toString("yyyy/MM/dd HH:mm:ss"); 

  Joda-Time 分别提供了有关天、周、月和年的增减(plus/minus 打头的)方法。

  再举个例子。现在假设我希望输出这样一个日期:

距离 2018 年 1 月 1 日 45 天之后的某天在下一个月的当前周的最后一天的日期。

  坦白说,使用旧的 API 实现很繁琐且容易出错:

  Calendar calendar = Calendar.getInstance();
  calendar.set(2018, Calendar.JANUARY, 1);

  calendar.add(Calendar.DAY_OF_MONTH, 45);
  calendar.add(Calendar.MONTH, 1);

  int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);   // 获得入参日期相对于本周最后一天的偏移量(在国外,星期日是一周的第一天)   // 若入参日期是周日,直接就是本周最后一天   int nextMondayOffset = dayOfWeek == 0 ? 0: 8 -dayOfWeek;   // 增加到本周最后一天   calendar.add(Calendar.DAY_OF_MONTH, nextMondayOffset);   SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss E");   System.out.println(simpleDateFormat.format(calendar.getTime())); 

  看看如何使用 Joda-Time 处理:

  DateTime dateTime = new DateTime(2018, 1, 1, 0, 0, 0, 0);   System.out.println(dateTime.plusDays(45).plusMonths(1).dayOfWeek()    .withMaximumValue().toString("yyyy/MM/dd HH:mm:ss.SSS")); 

  使用 Joda-Time 计算两日期相差的天数:

  //joda-time  
  LocalDate start=new LocalDate(2018, 11,14);   LocalDate end=new LocalDate(2018, 11, 15);   int days = Days.daysBetween(start, end).getDays();   //旧API处理方式   Calendar start = Calendar.getInstance();   start.set(2018, Calendar.NOVEMBER, 14);   Calendar end = Calendar.getInstance();   end.set(2018, Calendar.NOVEMBER, 15);   long startTim = start.getTimeInMillis();   long endTim = end.getTimeInMillis();   long diff = endTim-startTim;   int days=(int) (diff/1000 / 3600 / 24); 

  使用旧的 API 处理看起来很 low,处理也很麻烦,而使用 Joda-Time 就显得非常优雅。

  通过上面的例子,相信你应该感受到了 Joda-Time 的强大,Joda-Time 提供了一组 Java 类包用于处理包括 ISO8601 标准在内的 date 和 time,令时间和日期值变得易于管理、操作和理解。Joda-Time 的主要设计目标就易于使用,而且其支持多种日历系统。

  同时 Joda 与 JDK 是百分之百可互操作的,因此对于旧的日期 API 操作,只需要替换执行日期时间计算的那部分即可。

  Date date = dateTime.toDate();
  Calendar calendar = dateTime.toCalendar(Locale.CHINESE);
  
  //通过jdk时间对象构造  
  Date date = new Date();  
  DateTime dateTime = new DateTime(date);  
  
  Calendar calendar = Calendar.getInstance();  
  dateTime = new DateTime(calendar); 

  另外,开发中经常用到时间与字符串直接地转换,Joda-Time 也可以非常方便地实现。

  public static String convertStringUTC(Date javaDate)   {    DateTime dateTime = new DateTime(javaDate,DateTimeZone.UTC);    return dateTime.toString();   }   public static Date convertDateFromUTC(String date)   {    DateTime dateTime = new DateTime().parse(date, DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"));    return dateTime.toDate();   } 

5、Java 8 时间日期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() 方法。一旦你使用了其中某个类的方法,对于使用其他类也是十分容易上手。

5.1、下面看看这些类具体如何使用。

  LocalDate、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 的静态工厂方法以及常量。创建格式器一般有如下三种方式:

  1. 常用 ISO 格式常量,如 ISO_LOCAL_DATE
  2. 字母模式,如 ofPattern("yyyy/MM/dd")
  3. 本地化样式,如 ofLocalizedDate(FormatStyle.MEDIUM)

  和旧的 java.util.DateFormat 相比较,所有的 DateTimeFormatter 实例都是线程安全的。

  LocalDateTime localDateTime = LocalDateTime.now();
  DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  String 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();
                int 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 这些关键的类都不是线程安全的。

6、时间日期 API 中的设计模式

  • 工厂模式:now()、of() 等工厂方法直接生成日期或者日期时间。
  • 策略模式:LocalDate/LocalTime/LocalDateTime/ZonedDateTime,针对日期、时间、日期和时间、带时区的日期时间,使用具体的时间日期类处理。策略模式在设计一整套东西时,对开发者特别友好。

enter image description here

  前面也提到,所有新的日期时间 API 类都实现了一系列方法用以完成通用的任务,如:加、减、格式化、解析、从日期/时间中提取单独部分。一旦你使用了其中某个类的方法,那么非常容易上手其他类的使用。

  • 构建者模式:Java 8 开始在 Calendar 中加入了构建者类,可以按如下方式生成新的 Calendar 对象。  

  Calendar cal = new Calendar.Builder().setCalendarType("iso8601")
                           .setWeekDate(2018, 1,MONDAY).build();
  这里设计模式与标准的教科书式的设计模式可能有所区别,所以我们在使用设计模式时也应灵活处理,不是一成不变的。

7、使用示例

    例一
    
    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));       

猜你喜欢

转载自www.cnblogs.com/Pibaosi/p/10421870.html