Java 日期时间进化论

前言

  计算机中日期时间是一个很大的概念,现有的系统基本都是利用从1970.1.1 00:00:00 到当前时间的毫秒数进行计时,这个1970.1.1 00:00:00 UTC称为epoch Time,也就是所谓的“纪元时”。JDK中关于日期时间的api的修复可谓是一段漫长的填坑之旅,从最早惨不忍睹的Date,到勉强能用的Calendar,再到1.8发布的java.time包中的全新日期时间API,可以明显感到api对开发人员的友好度大幅增强,本文意在记录上述三个日期时间标志性API,仅用于个人学习,如有认识不足之处,请各位见谅,欢迎给作者留言,共同进步。

java.util.Date(非线程安全)

  Date作为我们最早接触到的关于日期时间的api,是JDK1.1之前的主流日期时间表达方式。


jdk中关于Date的描述

正如JDK API文档中的描述,Date允许将日期解释为年、月、日、小时、分钟和秒值,还可以格式化和解析日期字符串。不幸的是这些功能的API不适合国际化。 从JDK 1.1开始, Calendar类应该用于在日期和时间字段之间进行转换,并且DateFormat类应用于格式化和解析日期字符串。 在相应的方法Date被弃用。
  Date内部有一个不可序列化的long值来存储距离纪元时的毫秒数。Date类为可变的,在多线程并发环境中会有线程安全问题。

public class Date
    implements java.io.Serializable, Cloneable, Comparable{
        private transient long fastTime;
        Date(){
            this(System.currentTimeMillis());
        }
        Date(long date){
            fastTime = date;
        }
        //……
    }

java.util.Calendar(非线程安全)

  Calendar是一个可以操作日期和时间的抽象类,和Date一样,有一个表示从Epoch Time到当前瞬时偏移量的毫秒值。此外,Calendar还定义了一个count=17的数组,用于表示日历中的各个字段(如年份、月份、本年度的周数 etc.)。

proteted long time;
proteted int fields[];

  简单来说Calendar是一个工具类,将瞬时时间的毫秒值自动转化为各个日历字段fileds。 并提供了日期、月份、天数相加减等多种方法。Calendar的子类为可变的,在多线程并发环境中会有线程安全问题。
  Calendar还有一个很有意思的坑,简单记录一下,当系统时间为10月31日时,由于代码里面没有设定日期,所以此时calendar对象的日期是31,但是11月的最大天数为30天,所以导致当前calendar的时间向后顺延了一天,月份变为了12月份,代码最后获取到的当月最大天数就是31日。所以构建Calendar的开始时间时应该 设置具体开始日期。

    public static void main(String[] args) {
        int year = 2019;
        int month = 10;
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR,year);
        calendar.set(Calendar.MONTH,month);
        calendar.getTime();
        int date =calendar.getActualMaximum(Calendar.DATE);
        System.out.println("date = " + date);
    }
    // 系统时间为2019年11月1号
    date = 30
    // 系统时间为2019年10月31号
    date = 31
    // 系统时间为2019年10月01号
    date = 30

jdk1.8全新日期时间API(线程安全)

jdk1.8之前API存在的问题

  • 有关时间日期的操作,会用到Date;
  • 有关日期、时间的计算操作,会用到Calendar;
  • 关于时间日期的格式化,会用到SimpleDateFormat或DateFormat下的其他子类;
      但是上述对象都是可变的、线程不安全的,而且存在设计差,时区处理复杂等问题!

1.非线程安全 − Date 和 Calendar 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
2.设计很差 − Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
3.时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。

包介绍

  • java.time包:这是新的Java日期/时间API的基础包,所有的主要基础类都是这个包的一部分,如:LocalDate, LocalTime, LocalDateTime, Instant, Period, Duration等等。所有这些类都是不可变的和线程安全的,在绝大多数情况下,这些类能够有效地处理一些公共的需求。
  • java.time.chrono包:这个包为非ISO的日历系统定义了一些泛化的API,我们可以扩展AbstractChronology类来创建自己的日历系统。
  • java.time.format包:这个包包含能够格式化和解析日期时间对象的类,在绝大多数情况下,我们不应该直接使用它们,因为java.time包中相应的类已经提供了格式化和解析的方法。
  • java.time.temporal包:这个包包含一些时态对象,我们可以用其找出关于日期/时间对象的某个特定日期或时间,比如说,可以找到某月的第一天或最后一天。你可以非常容易地认出这些方法,因为它们都具有“withXXX”的格式。
  • java.time.zone包:这个包包含支持不同时区以及相关规则的类。

主要类介绍

  • LocalDate:表示没有时区的日期(只含年月日的日期对象),不可变并且线程安全的
  • LocalTime:表示没有时区的时间(只含时分秒的时间对象),不可变并且线程安全的
  • LocalDateTime:表示没有时区的日期时间(同时包含年月日时分秒的日期对象),不可变并且线程安全的
  • ZoneId:时区ID,用来确定Instant和LocalDateTime互相转换的规则
  • ZonedDateTime:一个带时区的完整时间
  • Instant:用来表示时间线上的一个点(瞬时)
  • Clock:获取某个时区下当前的瞬时时间,日期或者时间
  • Duration:表示一个绝对的精确跨度,用于计算时间间隔,使用秒或纳秒为单位
  • Period:这个类表示与 Duration 相同的概念,但是以人们比较熟悉的单位表示,比如年、月、日、周
  • DateTimeFormatter:提供时间格式化的类型
  • TemporalAdjusters:获得指定日期时间等,如当月的第一天、今年的最后一天等
  • ChronoUnit:时间单位枚举,用于加减操作
  • ChronoField:字段枚举,用于设置字段值。


    类图

全新API的优点

  • 不可变,线程安全:新的日期/时间API中,所有的类都是final修饰的,都是不可变的,线程安全的。
public final class LocalDateTime
        implements Temporal, TemporalAdjuster, ChronoLocalDateTime, Serializable {
  • 类细化:新的API将格式化的日期时间和机器时间(unix timestamp)明确分离,它为日期(Date)、时间(Time)、日期时间(DateTime)、时间戳(unix timestamp)以及时区都定义了不同的类。不同时间分解成了各个类,比如:LocalDate, LocalTime, LocalDateTime,Instant,Year,Month,YearMonth,MonthDay,DayOfWeek等,满足各种不同场景使用需求。
  • 方法统一:在所有的类中,方法都被明确定义用以完成相同的行为。比如所有的类中都定义了format()和parse()方法,而不是像以前那样专门有一个独立的类(SimpleDateFormat)。方法作用明确,清晰,统一,方便好记。
  • 封装常用方法:所有新的日期/时间API类都实现了一系列方法用以完成通用的任务,如:加、减、格式化、解析、从日期/时间中提取单独部分,等等。


    LocalDateTime常用方法
  • 自定义日期变化:可以使用TemporalAdjuster自定义复杂日期操作,更灵活地处理日期。
  • 对比老的Date和Calendar的优化细节:
      1.new Date(2020,01,01)实际是3920年2月。因为Date的构造函数 的年份表示的始于1900年的差值。
LocalDate localDate = LocalDate.of(2020, 1, 1);

  2.老的month是从0开始的。LocalDate month是从1开始的。

    LocalDate localDate = LocalDate.of(2020, 1, 1);
    // 输出结果 1
    System.out.println(localDate.getMonthValue());

  3.老的 DAY_OF_WEEK 的取值,是从周日(1)开始的。LocalDate week是从周一(1)开始的。

    LocalDate localDate = LocalDate.of(2020, 1, 1);
    // 输出结果  WEDNESDAY
    System.out.print(localDate.getDayOfWeek());
    // 输出结果  3
    System.out.print(localDate.getDayOfWeek().getValue());

  4.Date如果不格式化,打印出的日期可读性差。LocalDate的输出默认格式化。

        LocalDate localDate = LocalDate.of(2020, 1, 1);
        // 输出结果  2020-01-01
        System.out.println(localDate.toString());

  5.老的日期类并不提供国际化,没有时区支持。新的时间类都支持了时区操作。

         //服务器所在时间,输出时不包含时区
         LocalDateTime ldt = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
         // 输出结果  2020-09-22T22:33:11.011
         System.out.println(ldt);
        //意大利罗马时间,输出时包含时区
        ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Rome"));
        // 输出结果  2020-09-22T15:33:11.015+01:00[Europe/Rome]
        System.out.println(zdt);

其他时间相关的类(了解即可)

  • java.util.TimeZone
      TimeZone 表示时区偏移量,也可以计算夏令时。通常可以使用getDefault获取程序运行的默认时区,了解即可,目前大部分公司的国际化业务均会部署当地的服务器,所以不需要特别的时区处理 。
  • java.util.Locale
      Locale对象代表具体的地理,政治或文化地区,因为不同的区域,时间表示方式都不同。同样Locale.getDefault()可以获取默认的地区。
  • java.util.DateFormat(非线程安全)
      DateFormat是日期/时间格式化子类的抽象类,它以语言无关的方式格式化和分析日期或时间。 日期的字符串展现形式与TimeZone、Locale以及格式化风格有关。格式化风格主要分为日期格式化风格以及时间格式化风格。
    // 使用静态工厂方法创建DateFormat对象来格式化时间,getTimeInstance只处理时间,getDateInstance只处理日期,getDateTimeInstance处理日期和时间。
    DateFormat.getTimeInstance();
    DateFormat.getDateInstance();
    DateFormat.getDateTimeInstance();
  
    Calendar calendar = Calendar.getInstance();
    // 格式化结果为 23:11:23
    DateFormat.getTimeInstance().format(calendar.getTime());
    // 格式化结果为 2010-09-20
    DateFormat.getDateInstance().format(calendar.getTime());
    // 格式化结果为 2010-09-20 23:11:39  
    DateFormat.getDateTimeInstance().format(calendar.getTime());

  DateFormat及其子类SimpleDateFormat都不是线程安全的,DateFormat中设置的calendar是共享变量。

public abstract class DateFormat extends Format {
    /**
      * The {@link Calendar} instance used for calculating the date-time fields
      * and the instant of time. This field is used for both formatting and
      * parsing.
      *
      * 

Subclasses should initialize this field to a {@link Calendar} * appropriate for the {@link Locale} associated with this * DateFormat. * @serial */ protected Calendar calendar;

  • java.util.SimpleDateFormat(非线程安全)
      SimpleDateFormat是DateFormat的子类,可以自定义日期格式,较DateFormat提供了更精确的日期格式化控制。
    // 日期和时间模式字符串
    String pattern = "yyyy-MM-dd";
    // 格式化结果为 2010-09-20
    String sysDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date());

  SimpleDateFormat的格式化可以由日期和时间模式字符串指定。 在日期和时间模式字符串中,从'A'到'Z'和从'a'到'z'的非引号的字母被解释为表示日期或时间字符串的组件的模式字母。

模式字母

模式字符串

  SimpleDateFormat的另一个特点是线程不安全,从format方法的实现中可以发现,date是共享变量,并且没有做线程安全控制。当多个线程同时使用相同的SimpleDateFormat对象【如用static修饰的SimpleDateFormat】调用format方法时,多个线程会同时调用calendar.setTime方法,可能一个线程刚设置好time值另外的一个线程马上把设置的time值给修改了导致返回的格式化时间可能是错误的。同样parse方法也不是线程安全的。

    // Called from Format after creating a FieldDelegate
    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;
    }
多线程并发保证SimpleDateFormat线程安全的方法

  1.避免线程之间共享一个SimpleDateFormat对象,每个线程使用时都创建一次SimpleDateFormat对象 -> 创建和销毁对象的开销大
  2.对使用format和parse方法的地方进行加锁 -> 线程阻塞性能差
  3.使用ThreadLocal保证每个线程最多只创建一次,SimpleDateFormat对象 -> 较好的方法(参考下图实现)
  4.使用JDK8 DateTimeFormatter 替代 -> 最佳方法

    public static final ThreadLocal df = new ThreadLocal() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
    System.out.println(df.get().format(new Date()));
  • java.time.format.DateTimeFormatter
      使用旧的Date对象时,可以使用SimpleDateFormat进行格式化显示。而格式化新的LocalDateTime等日期时间对象时,需要使用DateTimeFormatter。和SimpleDateFormat不同的是,DateTimeFormatter不但是不变对象,它还是线程安全的。由于SimpleDateFormat不是线程安全的,使用时只能在方法内部创建新的局部变量。而DateTimeFormatter可以只创建一个实例,到处引用。
    public static void main(String[] args) {
        ZonedDateTime zdt = ZonedDateTime.now();
        var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
        System.out.println(formatter.format(zdt));
        var zhFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
        System.out.println(zhFormatter.format(zdt));
        var usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
        System.out.println(usFormatter.format(zdt));
    }
    // 输出结果
    2020-09-21T17:14 GMT
    2020 9月 21 周一 17:14
    Mon, September/21/2020 17:14
  • java.sql.Date
      java.sql.Date继承于java.util.Date,为了符合SQL DATE ,由java.sql.Date实例包装的毫秒值必须通过在实例关联的特定时区中将小时,分钟,秒和毫秒设置为零来“归一化”,简单来说就是java.sql.Date只保留了日期,而没有后面的时分秒,其他与java.util.Date并无差异。
    // 结果为 2010-09-20
    long time = System.currentTimeMillis();
    java.sql.Date date = new java.sql.Date(time);
  • java.sql.Time
      java.sql.Time继承于java.util.Date,仅保留了时间
    // 结果为 23:15:44
    long time = System.currentTimeMillis();
    java.sql.Time sqlTime = new java.sql.Time(time);
  • java.sql.Timestamp
      java.sql.Timestamp同样继承于java.util.Date,增加了保持SQL TIMESTAMP小数秒的能力,允许将秒数的规格精确到纳秒。
    long time = System.currentTimeMillis();
    // 结果为 2020-09-20 00:44:48.413
    java.sql.Timestamp sqlTimestamp = new java.sql.Timestamp(time);
    // 结果为 413000000
    sqlTimestamp.getNanos();
  • TimeUnit
      TimeUnit是一个时间单位枚举类,主要用于并发编程,表示给定的粒度单位的持续时间,并且提供了跨单元转换的实用方法,以及在这些单元中执行定时和延迟操作。TimeUnit不保留时间信息,只能帮助组织和使用可能在不同上下文中单独维护的时间表示。 一纳秒定义为千分之一秒,微秒为千分之一毫秒,毫秒为千分之一秒,一分钟为六十秒,一小时为六十分钟,一天为二十四小时。
// 定义锁的获取时间单元为50毫秒
Lock lock = ...;
   if (lock.tryLock(50L, TimeUnit.MILLISECONDS)) ...

xk-time

  最后给大家安利一个超强的工具,xk-time 是时间转换,计算,格式化,解析,日历和cron表达式等的工具,使用Java8,线程安全,简单易用,多达70几种常用日期格式化模板,支持Java8时间类和Date,轻量级,无第三方依赖。

为什么要开发这个工具?

  • java8以前的Date API设计不太好,使用不方便,往往会有线程安全问题。
  • 常见的DateUtil,往往将时间转换,计算,格式化,解析等功能都放在同一个类中,导致类功能复杂,方法太多,查找不方便。
  • 为了将与时间紧密相关的节假日、农历、二十四节气、十二星座、十二生肖、十二时辰和日历等功能集中起来开发成工具,方便使用。

主要功能类

1.日期转换工具类 DateTimeConverterUtil
2.日期计算工具类 DateTimeCalculatorUtil
3.日期格式化和解析工具类 DateTimeFormatterUtil
4.日历工具类 CalendarUtil
5.农历日期类 LunarDate
6.节假日计算类 Holiday
7.Cron表达式工具类 CronExpressionUtil
8.计算耗时工具 CostUtil

参考文章(尊重他人劳动成果)

1.LocalDateTime和Date的比较与区别
2.Java日期时间API系列

你可能感兴趣的:(Java 日期时间进化论)