Jdk8时间处理避坑指南:求求你别再用Date了
孙罗蒙 Lv4

Java时间处理文档

前言

我们在Java时间处理中,常用的是SimpleDateFormat这个方法格式化,使用的是Date这个util包的类,但是在Java8中提供了全新的时间类

看看SimpleDateFormat中的format方法

private StringBuffer format (Date date, StringBuffer toAppendTo, FieldDelegate delegate)
{        
    // Convert input date to time field list        
    calendar.setTime(date);
    boolean useDateFormatSymbols = useDateFormatSymbols();
    for (int i = 0; i < compiledPattern.length; ) {
        int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] << 16;
            count |= compiledPattern[i++];
        }
        switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char) count);
                break;
            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;
            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
        }
    }
    return toAppendTo;
}

在执行过程中,会使用一个成员变量calendar来保存时间。calendar.setTime(date);在看calendar。

public abstract class DateFormat extends Format { protected Calendar calendar; }
calendar.setTime(date)这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
1.线程1调用format方法,改变了calendar这个字段。
2.中断来了。
3.线程2开始执行,它也改变了calendar。
4.又中断了。
5.线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种安全问题;

在阿里Java开发规约中,也有强制性的提到SimpleDateFormat 是线程不安全的类 ,在使用的时候应当注意线程安全问题,如下:

img

注意最后的‘说明’:如果是JDK8的应用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat;

java.time

img

java.time包

​ 在Java8中提供了日期时间工具库 —-java.time,其中就包含了上文提到的几个类;

java.time包下有5个包组成

java.time – 包含值对象的基础包
java.time.chrono – 提供对不同的日历系统的访问
java.time.format – 格式化和解析时间和日期
java.time.temporal – 包括底层框架和扩展特性
java.time.zone – 包含时区支持的类

java.time 包是在JDK8新引入的,提供了用于日期、时间、实例和周期的主要API。

java.time包定义的类表示了日期-时间概念的规则,包括instants, durations, dates, times, time-zones and periods。这些都是基于ISO日历系统,它又是遵循 Gregorian规则的。

所有类都是不可变的、线程安全的

一些方法前缀的含义,统一了api:

of:静态工厂方法(用类名去调用)。

parse:静态工厂方法,关注于解析(用类名去调用)。

now: 静态工厂方法,用当前时间创建实例(用类名去调用)

get:获取某些东西的值。

is:检查某些东西的是否是true。

with:返回一个部分状态改变了的时间日期对象拷贝(单独一个with方法,参数为TemporalAdjusters类型)

plus:返回一个时间增加了的、时间日期对象拷贝(如果参数是负数也能够有minus方法的效果)

minus:返回一个时间减少了的、时间日期对象拷贝

to:把当前时间日期对象转换成另外一个,可能会损失部分状态.

at:把这个对象与另一个对象组合起来,例如: date.atTime(time)。

format :根据某一个DateTimeFormatter格式化为字符串

新时间API类都实现了一系列方法用以完成通用的任务,如:加、减、格式化、解析、从日期/时间中提取单独部分,等等

time包里面的类实例如果用了上面的方法而被修改了,那么会返回一个新的实例过来,而不像Calendar那样可以在同一个实例进行不同的修改,体现了不可变

  • 介绍
    Clock Clock,使用时区对应的前时刻、日期和时间。
    Duration 一个Duration对象表示两个Instant间的一段时间量。
    Instant Instant对象为时间线上的时间点。(时间戳)
    LocalDate ISO-8601日历系统中没有设置时区的日期,如“2007-12-03”。
    LocalDateTime ISO-8601日历系统中没有设置时区的日期时间,如“2007-12-03T10:15:30”。
    LocalTime ISO-8601日历系统中没有设置时区的时间,如“10:15:30”。
    MonthDay ISO-8601日历系统中的一个月日,如“–12-03”。
    OffsetDateTime ISO-8601日历系统中设置了UTC偏移的日期时间,如“2007-12-03T10:15:30+01:00(UTC/Greenwich)”。
    OffsetTime ISO-8601日历系统中设置了与UTC偏移的时间,例如“10:15:30+01:00(UTC/Greenwich)”。
    Period ISO-8601日历系统中根据年、月和日来模拟一个数量或时间量,如“2年、3个月和4天”,通常用作时间比较。
    Year ISO-8601日历系统中的一年,如“2007”。
    YearMonth ISO-8601日历系统中的一个年月,如“2007-12”。
    ZonedDateTime ISO-8601日历系统中设置了时区的日期-时间,如“2007-12-03T10:15:30+01:00 Europe/Paris”。
    ZoneId 时区标识,如“Europe/Paris”。
    ZoneOffset 与格林尼治/世界时的时区偏移,如“+02:00”。

Instant

Instant (java.time.Instant)

Instant标识某个时间(类似Date),精确到纳秒(不像Date到毫秒),因为精确到纳秒,所以用一位Long类型是不够的的,所以实际是有两个Long字段组成,第一部分保存的是子1970年1月1日开始到现在的秒数,滴而部分保存的是纳秒数;

//获取当前时间
Instant now = Instant.now();
System.out.println(now);

·控制台输出
2020-09-30T05:38:09.319Z


//获取当前时区时间
//atZone()返回类型为ZonedDateTime,该类带转换方法
Instant zonedDateTime = Instant.now().atZone(ZoneId.systemDefault()).toInstant();
System.out.println(zonedDateTime);

·控制台输出
2020-09-30T06:02:27.241Z

如果当我们需要更精确的数值的时候,我们也可以如下写:

//将当前毫秒转换为Instant 
Instant instant = Instant.ofEpochMilli(System.currentTimeMillis());

//从字符串类型中创建Instant类型的时间 
//当使用parse(String s);这个方法时,注意Intant是无时区的概念的,所以必须传入符合UTC格式的字符串;
instant = Instant.parse("1995-10-23T10:12:35Z");

Instant也提供了计算的方法

//比如:在现在的时间上加上5个小时4分钟。
Instant instant = Instant.now();
instant.plus(Duration.ofHours(5).plusMinutes(4));

那么这个例子中,使用了多少个 java.time.Instant 实例呢?答案是两个。Java.time 这个包是线程安全的,并且和其他大部分类一样,是不可变类。Instant 也遵守这个规则,因此 plus() 方法会产生一个新的实例,如:

Instant instant = Instant.now();
Instant instant1 = instant.plus(Duration.ofHours(5).plusMinutes(4)); 
System.out.println("instant==instant1 return: " + (instant == instant1));

·控制台输出
instant==instant1 return: false

LocalDate和LocalTime

LocalDate 表示不带时区的日期,比如 2000-1-1。LocalTime 表示不带时区的时间,比如 04:44:50.12,和之前提到的 Instant 类是从1970年计算偏移量不同,这两个类的输出是人们习惯阅读的日期和时间;

LocalDate 和 LocalTime 和 Instant 一样遵守同样的线程规定―― 如它们的实例子都是不可变的。LocalDate 和 LocalTime 和 Instant 有同样的计算和比较方法(其中有些方法是在 java.time.temporal.Temporal接口中定义的,并且上面这些类都实现了这些方法):

LocalDate


        LocalDate localDate = LocalDate.now();                  //获取当前时间:2019-12-07
        LocalDate localDate2 = LocalDate.of(2019, 12, 8);      //根据参数设置日期,参数分别为年,月,日

        System.out.println("localDate -----"+localDate);
        System.out.println("localDate2 -----"+localDate2);

        //============ LoacalDate 获取当前时间属性  ============

        System.out.println(localDate.getYear());              //获取当前年份:2019
        System.out.println(localDate.getMonth());             //获取当前月份,英文:DECEMBER        
        System.out.println(localDate.getMonthValue());         //获取当前月份,数字:12

        System.out.println(localDate.getDayOfMonth());         //获取当前日期是所在月的第几天7
        System.out.println(localDate.getDayOfWeek());          //获取当前日期是星期几(星期的英文全称):SATURDAY
        System.out.println(localDate.getDayOfYear());          //获取当前日期是所在年的第几天:341

        System.out.println(localDate.lengthOfYear());          //获取当前日期所在年有多少天
        System.out.println(localDate.lengthOfMonth());         //获取当前日期所在月份有多少天
        System.out.println(localDate.isLeapYear());            //获取当前年份是否是闰年

        //============ LoacalDate 当前时间的加减  ============

        System.out.println(localDate.minusYears(1));           //将当前日期减1年
        System.out.println(localDate.minusMonths(1));          //将当前日期减1月
        System.out.println(localDate.minusDays(1));            //将当前日期减1天

        System.out.println(localDate.plusYears(1));            //将当前日期加1年
        System.out.println(localDate.plusMonths(1));           //将当前日期加1月
        System.out.println(localDate.plusDays(1));             //将当前日期加1天

        //============ LoacalDate 当前时间的判断 ============
        System.out.println("LoacalDate的判断");
        System.out.println(localDate.isAfter(localDate2));                 //localDate在localDate2日期之后
        System.out.println(localDate.isBefore(localDate2));                //localDate在localDate2日期之前
        System.out.println(localDate.isEqual(localDate2));                 //localDate和localDate2日期是否相等

        //============ LoacalDate 当前时间支持的类型  ============
        System.out.println(localDate.isSupported(ChronoField.DAY_OF_YEAR));   
        //当前时间支持的时间类型是:一年中的某一天,这个不知道应用场景
        System.out.println(localDate.isSupported(ChronoUnit.DAYS));            
        //当前日期支持的单元:天(说明当前时间是按天来算的)

        System.out.println(localDate.with(TemporalAdjusters.firstDayOfMonth()));            //本月的第1天
        System.out.println(localDate.with(TemporalAdjusters.firstDayOfNextMonth()));        //下月的第1天
        System.out.println(localDate.with(TemporalAdjusters.firstDayOfNextYear()));         //下年的第1天

     //============ LocalDate 指定时间的操作  ===============    
        System.out.println(localDate.withDayOfMonth(2));                                    //本月的第几天
        System.out.println(localDate.with(TemporalAdjusters.lastDayOfMonth()));             //本月的最后一天
        System.out.println(localDate.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY))); //上一周星期天是几号
        System.out.println(localDate.with(TemporalAdjusters.next(DayOfWeek.MONDAY)));     //下一周星期一是几号

LocalTime

 //============ LocalTime  ============
        LocalTime localTime = LocalTime.now();                //获取当前时间

                LocalTime.of(int hour, int minute) //根据参数设置时间,参数分别为时,分
                LocalTime.of(int hour, int minute, int second) //根据参数设置时间,参数分别为时,分,秒
        LocalTime localTime2 = LocalTime.of(18, 18);
        LocalTime localTime3 = LocalTime.of(18, 18,18);
        System.out.println(localTime2);
        System.out.println(localTime3);

        //============ LoacalDate 获取当前时间属性  ============
        System.out.println(localTime);
        System.out.println(localTime.getHour());
        System.out.println(localTime.getMinute());
        System.out.println(localTime.getSecond());

        System.out.println(localTime.plusHours(1));        //将当前时间加1时
        System.out.println(localTime.plusMinutes(1));    //将当前时间加1分钟
        System.out.println(localTime.plusSeconds(1));    //将当前时间加1秒

        System.out.println(localTime.minusHours(1));    //将当前时间减1小时
        System.out.println(localTime.minusMinutes(1));    //将当前时间减1分钟
        System.out.println(localTime.minusSeconds(1));    //将当前时间减1秒

LocalDateTime

最后来看下在简单日期和时间类中最重要的一个:LocalDataTeime。

它是LocalDate和LocalTime的组合体,表示的是不带时区的 日期及时间。

看上去,LocalDateTime和Instant很象,但记得的是“Instant中是不带时区的即时时间点。

可能有人说,即时的时间点 不就是日期+时间么?看上去是这样的,但还是有所区别,比如LocalDateTime对于用户来说,可能就只是一个简单的日期和时间的概念,考虑如下的 例子:

​ 两个人都在2013年7月2日11点出生,第一个人是在英国出生,而第二个是在加尼福利亚,如果我们问他们是在什么时候出生的话,则他们看上去都是 在同样的时间出生(就是LocalDateTime所表达的),但如果我们根据时间线(如格林威治时间线)去仔细考察,则会发现在出生的人会比在英国出生的人稍微晚几个小时(这就是Instant所表达的概念,并且要将其转换为UTC格式的时间)。

除此之外,LocalDateTime 的用法和上述介绍的其他类都很相似,如下例子所示:

LocalDateTime localDateTime = LocalDateTime.now(); 
//当前时间加上25小时3分钟 
LocalDateTime inTheFuture = localDateTime.plusHours(25).plusMinutes(3); 
// 同样也可以用在localTime和localDate中 
System.out.println(localDateTime.toLocalTime().plusHours(25).plusMinutes(3)); 
System.out.println(localDateTime.toLocalDate().plusMonths(2)); 
// 也可以使用实现TemportalAmount接口的Duration类和Period类 
System.out.println(localDateTime.toLocalTime().plus(Duration.ofHours(25).plusMinutes(3))); 
System.out.println(localDateTime.toLocalDate().plus(Period.ofMonths(2)));

·控制台输出
15:29:26.544
2020-11-30
15:29:26.544
2020-11-30

时区

TimeZone

TimeZone时区,默认时区为当前JVM配置的时区;演示:

TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai);

TimeZone.setDefault(tz);//将JVM的默认时区设为上海时间

时间比较

Duration

​ 此类用来计算两同类型日期的时间差,演示:

LocalDateTime start = LocalDateTime.of(2017, 1, 1, 1, 1);
LocalDateTime end = LocalDateTime.of(2017, 2, 1, 1, 1);

Duration result = Duration.between(start, end);
System.out.println(result.toDays()); //31
System.out.println(result.toHours()); //744
System.out.println(result.toMinutes()); //44640
System.out.println(result.toMillis()); //2678400000
System.out.println(result.toNanos()); //2678400000000000

​ 其中between方法计算两日期时间差,两参数都是Temporal接口类型的日期,来看看Temporal的实现类:

HijrahDate, Instant, JapaneseDate, LocalDate, LocalDateTime, LocalTime, MinguoDate, OffsetDateTime, OffsetTime, ThaiBuddhistDate, Year, YearMonth, ZonedDateTime

​ Duration类包含一系列的计算方法:

plusNanos()
plusMillis()
plusSeconds()
plusMinutes()
plusHours()
plusDays()
minusNanos()
minusMillis()
minusSeconds()
minusMinutes()
minusHours()
minusDays()

时间格式化

DateTimeFormatter

​ DateTimeFormatter是java8的新特性,是线程安全的。
​ 对时区的支持也比较好。

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("EE yyyy-MM-dd hh:mm:ss");
String format = dateTimeFormatter.format(datetime);
System.out.println(format);

// Locale.US 的作用是格式化时,会按照当地的习惯来格式化,如中国是 星期日,美国是Sun
DateTimeFormatter us = DateTimeFormatter.ofPattern("EE yyyy-MM-dd hh:mm:ss",Locale.US);
String us_format = us.format(datetime);
System.out.println(us_format);


//DateTimeFormatter自带很多常用的格式化,如:

//datetime,date,time
LocalDateTime now = LocalDateTime.now();
System.out.println(DateTimeFormatter.ISO_DATE_TIME.format(now));
System.out.println(DateTimeFormatter.ISO_DATE.format(now));
System.out.println(DateTimeFormatter.ISO_TIME.format(now));

//local  datetime,date,time
System.out.println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now));
System.out.println(DateTimeFormatter.ISO_LOCAL_DATE.format(now));
System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(now));

//毫秒
Instant now_instance = Instant.now();
System.out.println(DateTimeFormatter.ISO_INSTANT.format(now_instance));

JDBC

针对JDBC映射将把数据库的日期类型和Java 8的新类型关联起来:

date -> LocalDate
time -> LocalTime
timestamp -> LocalDateTime

与遗留代码转换

在之前的代码中你可能出现了大量的Date类,我们可以这样转换为time包中的方法

DateInstant互相转换

Date date = Date.from(Instant.now());
Instant instant = date.toInstant();

Date转换为LocalDateTime

LocalDateTime localDateTime = LocalDateTime.from(new Date());

LocalDateTimeDate

Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());

LocalDateDate

Date date = Date.from(LocalDate.now().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
  • 本文标题:Jdk8时间处理避坑指南:求求你别再用Date了
  • 本文作者:孙罗蒙
  • 创建时间:2020-10-09 15:25:58
  • 本文链接:https://lqcoder.com/p/755d3722.html
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!