写点什么

☕【Java 深层系列】「技术盲区」让我们一起完全吃透针对于时间和日期相关的 API 指南

作者:浩宇天尚
  • 2021 年 12 月 20 日
  • 本文字数:6291 字

    阅读完需:约 21 分钟

☕【Java深层系列】「技术盲区」让我们一起完全吃透针对于时间和日期相关的API指南

技术简介

java 中的日期处理一直是个问题,没有很好的方式去处理,所以才有第三方框架的位置比如 joda。文章主要对 java 日期处理的详解,用 1.8 可以不用 joda。

时间概念

首先我们对一些基本的概念做一些介绍,其中可以将 GMT 和 UTC 表示时刻大小等同。

UT 时间

UT 反应了地球自转的平均速度。是通过观测星星来测量的。

UTC

UTC 是用原子钟时间做参考,但保持和 UT1 在 0.9 秒内的时间,也就是说定时调整。


目前采用的时间标准是世界协调时 UTC(Universal Time Coordinated)。如果计算机不联网即使再精确也是不准的,因为 UTC 会进行调整,而且一般走的时间也是不精确的。

NTP

现在计算机一般用的网络时间协议 NTP(Network Time Protocol)是用于互联网中时间同步的标准互联网协议。用途是把计算机的时间同步到某些时间标准。

GMT(UT1)

GMT 是完全符合地球自转的时间,也被称为 UT1,格林尼治标准时间被用作英国的民用时间,或 UTC。GMT 被称为“UT1”,它直接对应于地球的自转,并受到该自转轻微不规则的影响。正是 UT1 和 UTC 之间的差异通过应用闰秒保持>低于 0.9 秒。

ISO 8601

一种时间交换的国际格式。有些接口调用表示 UTC/GMT 时间的时候用"yyyy-MM-dd'T'HH:mm:ss'Z'"格式显示。带毫秒格式"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"。

joda 中实现如下
// Alternate ISO 8601 format without fractional secondsprivate static final String ALTERNATIVE_ISO8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";  private static DateFormat getAlternativeIso8601DateFormat() {        SimpleDateFormat df = new SimpleDateFormat(ALTERNATIVE_ISO8601_DATE_FORMAT, Locale.US);        df.setTimeZone(new SimpleTimeZone(0, "GMT"));        return df;  }
复制代码

RFC 822

STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES


其中 ARPA 网络其实就是互联网的前身。


有些地方会用 RFC 822 里的时间格式,格式如下


 date-time = [ day "," ] date time ; dd mm yy                                                     ; hh:mm:ss zzz
复制代码
第二个相当于现在格式
"EEE, dd MMM yyyy HH:mm:ss z"
复制代码


有些头设置采用该格式。


joda 中实现如下


// RFC 822 Date Formatprivate static final String RFC822_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss z";private static DateFormat getRfc822DateFormat() {        SimpleDateFormat rfc822DateFormat =                new SimpleDateFormat(RFC822_DATE_FORMAT, Locale.US);        rfc822DateFormat.setTimeZone(new SimpleTimeZone(0, "GMT"));        return rfc822DateFormat;}
复制代码


创建 SimpleDateFormat 的 Locale.US 可以决定格式字符串某些字符的代替用哪个语言,比如 EEE 等。


SimpleDateFormat df1=new SimpleDateFormat("GGGG yyyy/MMMM/dd HH:mm:ss EEE aaa zzzz",Locale.CHINA);SimpleDateFormat df2=new SimpleDateFormat("GGGG yyyy/MMMM/dd HH:mm:ss EEE aaa zzzz",Locale.US);//公元 2016/三月/27 23:32:10 星期日 下午 中国标准时间//AD 2016/March/27 23:32:10 Sun PM China Standard Time
复制代码


gregorian Calendar, julian Calendar:这是两种历法,我们一般用的通用的 gregorian Calendar。

jdk1.8 之前

主要的类有记录时间戳的 Date,时间和日期进行转换的 Calendar,用来格式化和解析时间字符串的 DateFormat

java.util.Date

使用前要注意时间表示的规则。


还有这个类有很多过期方法不推荐使用,很多已经被 Calendar 代替。

构造方法

这个类代表某个时刻的毫秒值,既然是毫秒值也就说需要有一个参考值。


在接受或返回年、月、日期、小时、分钟和秒值的所有类日期方法中,使用以下表示形式:


年份 y 由整数 y-1900 表示。一个月由 0 到 11 的整数表示;0 是一月,1 是二月,依此类推;因此,11 月是 12 月。日期(月的某一天)通常由 1 到 31 之间的整数表示。小时由 0 到 23 之间的整数表示。因此,从午夜到凌晨 1 点的时间是 0 小时,从中午到下午 1 点的时间是 12 小时。一分钟通常由 0 到 59 之间的整数表示。第二个由 0 到 61 之间的整数表示;值 60 和 61 仅在闰秒内出现,甚至仅在实际正确跟踪闰秒的 Java 实现中出现。由于目前引入闰秒的方式,在同一分钟内出现两个闰秒的可能性极低,但本规范遵循 ISO C 的日期和时间约定。


当我们创建一个 Date 的时候获取的是哪一个毫秒值?


public Date() {        this(System.currentTimeMillis()); } public Date(long date) {      fastTime = date;}
复制代码


System.currentTimeMillis()是本地方法,the difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC。


这个可能会因为操作系统的时间而不准。有些操作系统不一定是用毫秒表示的。这个时间都是用的 UTC 时间,不和时区有关的,这个无关的意思是同一时刻每个时区下获得的值应该是一致的,可以简单用程序验证一下获取的时间表达内容。


long time = System.currentTimeMillis();System.out.println(time=(time/1000));System.out.println("秒:"+ time%60);System.out.println(time=(time/60));System.out.println("分钟:"+time%60);System.out.println(time=(time/60));System.out.println("小时:"+time%24);
复制代码


可以理解成和 UTC 的 1970 年 1 月 1 日零点的差值。而 fastTime 就是 Date 类保存这个时刻的变量。

成员变量

Date 对象打印出来是本地时间,而构造方法是没有时区体现的。那么哪里体现了时区呢?


下面是 Date 的成员变量

gcal

获取的是以下的对象。其中并没有自定义字段。可以说只是一个 gregorian(公历)时间工厂获取 CalendarDate 的子类。

jcal

在以下方法中用到


private static final BaseCalendar getCalendarSystem(BaseCalendar.Date cdate) {        if (jcal == null) {            return gcal;        }        if (cdate.getEra() != null) {            return jcal;        }        return gcal;    }    synchronized private static final BaseCalendar getJulianCalendar() {        if (jcal == null) {            jcal = (BaseCalendar) CalendarSystem.forName("julian");        }        return jcal;    }
复制代码


当时间戳在以下情况下用儒略历,并且,在用到的时候会自动设置儒略历,所以在 clone 的时候也没有这个参数。所以这个可以忽略。


 private static final BaseCalendar getCalendarSystem(int year) {        if (year >= 1582) {            return gcal;        }        return getJulianCalendar();    }    private static final BaseCalendar getCalendarSystem(long utc) {        // Quickly check if the time stamp given by `utc' is the Epoch        // or later. If it's before 1970, we convert the cutover to        // local time to compare.        if (utc >= 0            || utc >= GregorianCalendar.DEFAULT_GREGORIAN_CUTOVER                        - TimeZone.getDefaultRef().getOffset(utc)) {            return gcal;        }        return getJulianCalendar();    }
复制代码

fastTime

保存了一个时间戳表示时刻。最重要的参数。创建 Date 就是对这个值的赋值。

cdate

保存了时间相关内容,包括时区,语言等


    public static final int FIELD_UNDEFINED = -2147483648;    public static final long TIME_UNDEFINED = -9223372036854775808L;    private Era era;    private int year;    private int month;    private int dayOfMonth;    private int dayOfWeek;    private boolean leapYear;    private int hours;    private int minutes;    private int seconds;    private int millis;    private long fraction;    private boolean normalized;    private TimeZone zoneinfo;    private int zoneOffset;    private int daylightSaving;    private boolean forceStandardTime;    private Locale locale;
复制代码
defalutCenturyStart

这个值可以忽略,在过期方法中用到。


@Deprecated    public static long parse(String s) {   ... ...            // Parse 2-digit years within the correct default century.            if (year < 100) {                synchronized (Date.class) {                    if (defaultCenturyStart == 0) {                        defaultCenturyStart = gcal.getCalendarDate().getYear() - 80;                    }                }                year += (defaultCenturyStart / 100) * 100;                if (year < defaultCenturyStart) year += 100;            }            ... ...    }
复制代码

serialVersionUID

验证版本一致性的 UID

wtb

保存 toString 格式化用到的值

ttb

保存 toString 格式化用到的值

主要方法

java.util.Calendar

主要也是其中保存的毫秒值 time 字段,下面是我们常用的方法,用了默认的时区和区域语言:


public static Calendar getInstance() {        return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));    }
复制代码


国内环境默认 GregorianCalendar,但是 TH-th 用的 BuddhistCalendar 等一些坑:

set(int,int,int,int,int,int)方法

方法不能设置毫秒值,所以当用 getInstance 后即使用设置相同的值,最后毫秒值也是不一致的。所以如果有需要,将 MILLISECOND 清零。

set,add,get,roll

set 方法不会马上计算时间,指是修改了对应的成员变量,只有 get()、getTime()、getTimeInMillis()、add() 或 roll()的时候才会做调整


        //2000-8-31        Calendar cal1 = Calendar.getInstance();        cal1.set(2000, 7, 31, 0, 0 , 0);        //应该是 2000-9-31,也就是 2000-10-1        cal1.set(Calendar.MONTH, Calendar.SEPTEMBER);        //如果 Calendar 转化到 2000-10-1,那么现在的结果就该是 2000-10-30        cal1.set(Calendar.DAY_OF_MONTH, 30);        //输出的是2000-9-30,说明 Calendar 不是马上就刷新其内部的记录        System.out.println(cal1.getTime());
复制代码


也就是说多次设置的时候如果中间有需要调整的时间,但是实际是不会做调整的。所以尽量将无法确定的设置之后不要再进行其他调整,防止最后实际值与正常值不准。

add 方法会马上做时间修改

roll 与 add 类似,但是 roll 不会修改更大的字段的值。

java.text.SimpleDateFormat

创建设置 pattern 字符串,可以表示的格式如下:



日期格式是不同步的。建议为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须是外部同步的。


SimpleDateFormat 是线程不安全的类,其父类维护了一个 Calendar,调用相关方法有可能会修改 Calendar。一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。 正例:注意线程安全,使用 DateUtils。org.apache.commons.lang.time.DateUtils,也推荐如下处理:


private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {         @Override         protected DateFormat initialValue() {                 return new SimpleDateFormat("yyyy-MM-dd");         } };
复制代码

java.sql.Date/Time/Timestamp

这几个类都继承了 java.util.Date。


相当于将 java.util.Date 分开表示了。Date 表示年月日等信息。Time 表示时分秒等信息。Timestamp 多维护了纳秒,可以表示纳秒。


如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

jdk1.8 的时间类

1.8 增加了新的 date-time 包,遵循 JSR310。核心代码主要放在 java.time 包下。默认的日历系统用的 ISO-8601(基于格里高利历)。java.time 下主要内容包括:


java.time -主要包括,日期,时间,日期时间,时刻,期间,和时钟相关的类。


  • java.time.chrono -其他非 ISO 标准的日历系统可以用 java.time.chrono,里面已经定义了一部分年表,你也可以自定义。

  • java.time.format -格式化和解析日期时间的类

  • java.time.temporal -扩展 API,主要是提供给写框架和写库的人,允许日期时间相互操作,访问,和调整。字段和单位在这个包下定义。

  • java.time.zone -定义了时区,相对于时区的偏移量,时区规则等。


该包的 API 提供了大量相关的方法,这些方法一般有一致的方法前缀:


  • of:静态工厂方法。

  • parse:静态工厂方法,关注于解析。

  • get:获取某些东西的值。

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

  • with:不可变的 setter 等价物。

  • plus:加一些量到某个对象。

  • minus:从某个对象减去一些量。

  • to:转换到另一个类型。

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

相互转化和 Instant

可以看到老的时间日期类里面都有了 Instant 的转化。Instant 可以说是新旧转换的中转站。Instant 主要维护了秒和纳秒字段,可以表示纳秒范围。当然不支持的话会抛出异常。主要还是 java.util.Date 转换成新的时间类。

Clock

提供了访问当前时间的方法,也可以获取当前 Instant。Clock 是持有时区或者时区偏移量的。如果只是获取当前时间戳,推荐还是用 System.currentTimeMillis()

ZoneId/ZoneOffset/ZoneRules

zone id 主要包括两个方面,一个是相对于对于 UTC/Greenwich 的固定偏移量相当于一个大时区,另一个是时区内有特殊的相对于 UTC/Greenwich 偏移量的地区。通常固定偏移量部分可以用 ZoneOffset 表示,用 normalized()判断是否可以用 ZoneOffset 表示。判断主要用到了时区规则 ZoneRules。时区的真正规则定义在 ZoneRules 中,定义了什么时候多少偏移量。使用这种方式是因为 ID 是固定不变的,但是规则是政府定义并且经常变动。

LocalDateTime/LocalTime/LocalDate/ZoneDateTime

LocalDateTIme/LocalTime/LocalDate 都是没有时区概念的。这句话并不是说不能根据时区获取时间,而是因为这些类不持有表示时区的变量。而 ZoneDateTime 持有时区和偏移量变量。


这些类都可以对时间进行修改其实都是生成新对象。所以这里的时间类都是天然支持多线程的。


这些时间类中都提供了获取时间对象,修改时间获取新的时间对象,格式化时间等。

注意点

LocaDateTime 的 atZone 是调整本地时间的时区的。并不会改变时间。要使用其他时间需要获取的 LocalDateTime.now 的时候的就要传入时区变量。

DateTimeFormatter

时间对象进行格式化时间的需要用到格式化和解析日期和时间的时候需要用到 DateTimeFormatter。

扩展及思考

用 SimpleDateFormat 格式化的时候不要用 12 小时制即 hh,因为很容易导致上午下午不分,比如“2017-01-01 00:00:00“可能就变显示成”2017-01-01 12:00:00”::符号

LocalDateTime 的方法
public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) {    Objects.requireNonNull(formatter, "formatter");    return formatter.parse(text, LocalDateTime::from);}
复制代码
parse 调用的方法是
public <T> T parse(CharSequence text, TemporalQuery<T> query) {   ... ...}
复制代码
LocalDateTime::from 调用的方法是
public static LocalDateTime from(TemporalAccessor temporal) {    .... ...     }
复制代码


其中 temporal 是 LocalDateTime 的接口


这里其实大家都有一个疑问就是 LocalDateTime::from 到底代表什么意思。

LocalDateTime::from
//与下列表示相同x ->  LocalDateTime.from(x)//相当于new TemporalQuery<LocalDateTime>(){       @Override        public LocalDateTime queryFrom(TemporalAccessor temporal) {             return LocalDateTime.from(temporal);        }};
复制代码


发布于: 2 小时前阅读数: 9
用户头像

浩宇天尚

关注

🏆 InfoQ写作平台-签约作者 🏆 2020.03.25 加入

【个人简介】酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“ 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、APM专题及微服务/分布式体系等

评论

发布
暂无评论
☕【Java深层系列】「技术盲区」让我们一起完全吃透针对于时间和日期相关的API指南