完全搞懂java中的时间戳,时区,日期格式

完全搞懂java中的时间戳,时区,日期格式_第1张图片

相关概念

完全搞懂java中的时间戳,时区,日期格式_第2张图片

【写在最前】
我们平时会接触各种计算机时间的概念,最常见的有GMT,UTC,CST等。
很多小白傻傻分不清楚他们之间的区别与联系,通过本文知识,让我们花5分钟时间彻底搞懂他,相信聪明的你,看完一定会有收获!

GMT

即:格林尼治时间(另有格林威治时间一说)
以本初子午线为基础,精确度相对低。
注意事项:
因为地球每天的自转是不规则的(正在缓慢减速)所以,格林尼治时间的精确度会越来越低。

UTC

即:世界协调时(Universal Time Coordinated的缩写)
以原子时钟长为基础,比GMT格林威治时更加科学更加精确。

UTC是国际无线电咨询委员会制定和推荐的,若与GMT时差大于0.9秒,则由位于巴黎的国际地球自转事务中央局发布闰秒,使UTC与地球自转周期一致。
UTC时间格式为:YYYY-MM-DDThh:mm:ssZ。例如,2014-11-11T12:00:00Z(为北京时间2014年11月11日20点0分0秒)
中国大陆、中国香港、中国澳门、中国台湾、蒙古国、新加坡、马来西亚、菲律宾、西澳大利亚州的时间与UTC的时差均为+8,也就是UTC+8。
注意事项:
1)目前UTC与GMT 相差为0.9秒,故二者可以基本视为一致。
我们一般认为GMT和UTC是一样的,都与英国伦敦的本地时相同。
2)早期的XP系统中,默认时间格式是GMT。而到了Win7之后,默认时间格式已经改成了UTC
3)阿里云API接口编程中,全部都是UTC

UNIX时间戳(timestamp)

计算机中的UNIX时间戳,是以GMT/UTC时间「1970-01-01T00:00:00」为起点,到当前具体时间的秒数(不考虑闰秒)。这样做的目的,主要是通过“整数计算”来简化计算机对时间操作的复杂度。
无论何种编程语言,基本都有获取unix时间戳的系统方法。
注意事项:
如果开发的软件系统仅仅在国内用,用timestamp没有太大问题(因为大家的linux服务器的时区是一样的)
如果软件系统需要跨国服务,则必须用UTC(比如阿里云API),否则就会因为服务器的UTC时区不同,导致timestamp结果值混乱

CST

这个代号缩写,并不是一个统一标准,目前,可以同时代表如下 4 个不同版本的时区概念(要根据上下文语义加以区分):
1)China Standard Time 中国标准时区 (UTC+8)
2)Cuba Standard Time 古巴标准时区 (UTC-4)
3)Central Standard Time (USA) 美国中央时区 (UTC-6)
4)Central Standard Time (Australia) 澳大利亚中央时区(UTC+9)

总结

我们一般都认为UTC和GMT时间是相同的,时间戳的含义是 从本初子午线1970年1月1日至今所过去的毫秒数,时间戳与时区无关,如果再不同的时区,先通过时间戳得到一个时间之后,再加上时区的偏移量

Date

构造

public Date() {
    this(System.currentTimeMillis());
}
public Date(long date) {
    fastTime = date;
    }

构造很简单,就是设置了一个long类型的时间戳
时间戳转换为日期
java.util.Date#normalize()

  private final BaseCalendar.Date normalize() {
    
            BaseCalendar cal = getCalendarSystem(fastTime);
            cdate = (BaseCalendar.Date) cal.getCalendarDate(fastTime,
                                                            TimeZone.getDefaultRef());
      return cdate;
    }

获取日历,要么是格林尼治日历,要么是儒略日历,我们用到的正常都还是格林尼治时间,补研究儒略日历

   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();
    }

继续向下看,获取timeZone后进入

  public CalendarDate getCalendarDate(long var1, CalendarDate var3) {
        int var4 = 0;
        int var5 = 0;
        int var6 = 0;
        long var7 = 0L;
        TimeZone var9 = var3.getZone();
        if (var9 != null) {
            int[] var10 = new int[2];
            if (var9 instanceof ZoneInfo) {
                var5 = ((ZoneInfo)var9).getOffsets(var1, var10);
            } else {
                var5 = var9.getOffset(var1);
                var10[0] = var9.getRawOffset();
                var10[1] = var5 - var10[0];
            }

            var7 = (long)(var5 / 86400000);
            var4 = var5 % 86400000;
            var6 = var10[1];
        }

        var3.setZoneOffset(var5);
        var3.setDaylightSaving(var6);
        var7 += var1 / 86400000L;
        var4 += (int)(var1 % 86400000L);
        if (var4 >= 86400000) {
            var4 -= 86400000;
            ++var7;
        } else {
            while(var4 < 0) {
                var4 += 86400000;
                --var7;
            }
        }

        var7 += 719163L;
        this.getCalendarDateFromFixedDate(var3, var7);
        this.setTimeOfDay(var3, var4);
        var3.setLeapYear(this.isLeapYear(var3));
        var3.setNormalized(true);
        return var3;
    }

在这里插入图片描述

初始日期为全0
依次求出时间戳的总天数 = 时间戳/86400000(一天的毫秒数)
除去天出以外的时间毫秒数时间戳%86400000+偏移时区的毫秒数,例如上海是东八区,那就是再加上8100060*60 = 28800000,这样小时的时间就加上了时区的偏移量。
这样就根据时间戳得到了 年月日时分秒时区偏移后的日期。
toString
在这里插入图片描述

最终打印结果

TimeZone

时区类,能够根据系统设置的时区获取偏移量(相对于本初子午线的0偏移)

一个错误用法

目的时获取当天十天的0时0分0秒,比如现在时间时间是2022-07-07 18:13:33
那么结果应该是2022-07-07 00:00:00

    public static void main(String[] args) {
        long current=System.currentTimeMillis();//当前时间毫秒数
        //获取今天0时的时间
        long zero=current/86400000*86400000-TimeZone.getDefault().getRawOffset();
        Date date = new Date(zero);
        System.out.println(date);
    }
  1. 获取到的当前时间戳
  2. 通过除86400000再乘86400000得到 整天的时间戳,时分秒的时间戳被舍弃,但是我们知道,时间戳的0时0分0秒,在中国的东八区会在加上8小时,这里他又减去了这偏移的8小时毫秒。看起来似乎没什么问题
  3. 但是他忘了,在0点-8点之间,对于时间戳来说,其本初子午线时间仍然在前一天,那么这个时候,他直接舍弃时分秒,再减去时区偏移8小时得到的时前一天的0时0分0秒,这种情况下的结果时错误的

Calendar

日历类

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

创建日历类,这里我们看格林尼治日历,可以对日期进行设置操作。针对的是日期
构造

 GregorianCalendar(TimeZone zone, Locale locale, boolean flag) {
        super(zone, locale);
        gdate = (BaseCalendar.Date) gcal.newCalendarDate(getZone());
    }

父类构造

    protected Calendar(TimeZone zone, Locale aLocale)
    {
        fields = new int[FIELD_COUNT];
        isSet = new boolean[FIELD_COUNT];
        stamp = new int[FIELD_COUNT];

        this.zone = zone;
        setWeekCountData(aLocale);
    }

共有17个字段


    public final static int ERA = 0;

    public final static int YEAR = 1;


    public final static int MONTH = 2;


    public final static int WEEK_OF_YEAR  = 3;

 
    public final static int WEEK_OF_MONTH = 4;


    public final static int DATE = 5;

   
    public final static int DAY_OF_MONTH = 5;


    public final static int DAY_OF_YEAR = 6;

  
    public final static int DAY_OF_WEEK = 7;

  
    public final static int DAY_OF_WEEK_IN_MONTH = 8;

    public final static int AM_PM = 9;

   
    public final static int HOUR = 10;

  
    public final static int HOUR_OF_DAY = 11;

   
    public final static int MINUTE = 12;

   
    public final static int SECOND = 13;

  
    public final static int MILLISECOND = 14;

  
    public final static int ZONE_OFFSET = 15;

  
    public final static int DST_OFFSET = 16;
 private void setWeekCountData(Locale desiredLocale)
    {
        /* try to get the Locale data from the cache */
        int[] data = cachedLocaleData.get(desiredLocale);
        if (data == null) {  /* cache miss */
            data = new int[2];
            data[0] = CalendarDataUtility.retrieveFirstDayOfWeek(desiredLocale);
            data[1] = CalendarDataUtility.retrieveMinimalDaysInFirstWeek(desiredLocale);
            cachedLocaleData.putIfAbsent(desiredLocale, data);
        }
        firstDayOfWeek = data[0];
        minimalDaysInFirstWeek = data[1];
    }

默认,firstDayOfWeek和minimalDaysInFirstWeek都是1

   public final static int SUNDAY = 1;
    public final static int MONDAY = 2;
    public final static int TUESDAY = 3;
    public final static int WEDNESDAY = 4;
    public final static int THURSDAY = 5;
    public final static int FRIDAY = 6;
    public final static int SATURDAY = 7;

默认一周的第一天为SUNDAY周日

firstDayOfWeek和minimalDaysInFirstWeek含义

getFirstDayOfWeek()
获取一星期的第一天;例如,在美国,这一天是 SUNDAY,而在法国,这一天是 MONDAY
getMinimalDaysInFirstWeek
getMinimalDaysInFirstWeek()
获取一年中第一个星期所需的最少天数,例如,如果定义第一个星期包含一年第一个月的第一天,则此方法将返回 1。
以上是JDK的解释,getFirstDayOfWeek还好理解,即,一周的第一天是周日,还是周一的问题。
getMinimalDaysInFirstWeek就显示比较难懂了。它的意思是指,一周里的天数。
来看个例子吧,比如我想得到,2017年的第3周的周一,是哪一天。好了,看代码吧。

import java.text.SimpleDateFormat;

import java.util.Calendar;

public class CalendarTest02 {

    static final String[] weeks = new String[] { "星期天", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", };

    /**

     * @param args

     */

    public static void main(String[] args) {

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

        Calendar cale = Calendar.getInstance();

        System.out.println("现在是" + sdf.format(cale.getTime()));

        System.out.println("getFirstDayOfWeek默认值-->" + cale.getFirstDayOfWeek());

        System.out.println("getFirstDayOfWeek默认值-->" + weeks[cale.getFirstDayOfWeek() - 1]);

        //切换年,保持年月日时分秒不变
        cale.set(Calendar.YEAR, 2017);
       //切换周,周几不会变
        cale.set(Calendar.WEEK_OF_YEAR, 3);

        cale.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);

        System.out.println(sdf.format(cale.getTime()));

        System.out.println("--------------------------我是分割线------------------------");

/**

 * #####################################

 */

        Calendar cale2 = Calendar.getInstance();

        System.out.println("现在是" + sdf.format(cale2.getTime()));

        cale2.setFirstDayOfWeek(Calendar.MONDAY); // 星期一

        System.out.println("getFirstDayOfWeek值-->" + weeks[cale2.getFirstDayOfWeek() - 1]);

        cale2.set(Calendar.YEAR, 2017);

        cale2.set(Calendar.WEEK_OF_YEAR, 3);

        cale2.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);

        System.out.println(sdf.format(cale2.getTime()));

        System.out.println("--------------------------我是分割线------------------------");

/**

 * #####################################

 */

        Calendar cale3 = Calendar.getInstance();

        System.out.println("现在是" + sdf.format(cale3.getTime()));

        cale3.setFirstDayOfWeek(Calendar.MONDAY); // 星期一

        cale3.setMinimalDaysInFirstWeek(7); // 7天为一周

        System.out.println("getFirstDayOfWeek值-->" + weeks[cale3.getFirstDayOfWeek() - 1]);

        cale3.set(Calendar.YEAR, 2017);

        cale3.set(Calendar.WEEK_OF_YEAR, 3);

        cale3.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);

        System.out.println(sdf.format(cale3.getTime()));

    }

}

现在是2022-07-08
getFirstDayOfWeek默认值-->1
getFirstDayOfWeek默认值-->星期天
2017-01-16
--------------------------我是分割线------------------------
现在是2022-07-08
getFirstDayOfWeek值-->星期一
2017-01-09
--------------------------我是分割线------------------------
现在是2022-07-08
getFirstDayOfWeek值-->星期一
2017-01-16

对着日历看看就明白了
完全搞懂java中的时间戳,时区,日期格式_第3张图片

对FirstDayOfWeek和MinimalDaysInFirstWeek有一个认识这十分重要
有了这个个认识看几个很容易混淆的字段

1、Calendar.MONTH

月份从0-11,获取之后需要加1才能得到真正的月份

2、Calendar.DAY_OF_WEEK

本周的第几天,从星期天开始算,因为其对应的就是一周七天的枚举,无论FirstDayOfWeek设置成什么,周日都返回1

   public final static int SUNDAY = 1;

   public final static int MONDAY = 2;


   public final static int TUESDAY = 3;

  
   public final static int WEDNESDAY = 4;

  
   public final static int THURSDAY = 5;

  
   public final static int FRIDAY = 6;


   public final static int SATURDAY = 7;

3、Calendar.WEEK_OF_MONTH和Calendar.DAY_OF_WEEK_IN_MONTH的区别

DAY_OF_WEEK_IN_MONTH:第一周从1号开始算,1-7号为第一周,8-14号为第二周…
WEEK_OF_MONTH:第一周不按1号算,1号该是周几就是周几

4、Calendar.HOUR和Calendar.HOUR_OF_DAY的区别

HOUR:12小时制
HOUR_OF_DAY24小时制
回到刚刚的构造函数

GregorianCalendar(TimeZone zone, Locale locale, boolean flag) {
        super(zone, locale);
        gdate = (BaseCalendar.Date) gcal.newCalendarDate(getZone());
    }

初始化一个日期,初始为为0000-00-00T00:00:00.000Z
后再build方法中设置时间为当前时间戳然后填充GregorianCalendar的字段。实际上就是当前时间

setTime

   public final void setTime(Date date) {
        setTimeInMillis(date.getTime());
    }
  public void setTimeInMillis(long millis) {
        // If we don't need to recalculate the calendar field values,
        // do nothing.
        if (time == millis && isTimeSet && areFieldsSet && areAllFieldsSet
            && (zone instanceof ZoneInfo) && !((ZoneInfo)zone).isDirty()) {
            return;
        }
        time = millis;
        isTimeSet = true;
        areFieldsSet = false;
        computeFields();
        areAllFieldsSet = areFieldsSet = true;
    }

与初始化相同,就又根据时间戳生成日期并填充Calendar的各个字段

set字段方法

 public final void set(int year, int month, int date, int hourOfDay, int minute,
                          int second)
    {
        set(YEAR, year);
        set(MONTH, month);
        set(DATE, date);
        set(HOUR_OF_DAY, hourOfDay);
        set(MINUTE, minute);
        set(SECOND, second);
    }

      calendar.set(Calendar.YEAR,1998);
   final void internalSet(int field, int value)
    {
        fields[field] = value;
    }

设置字段,简单的设置对应字段

get

   calendar.get(Calendar.YEAR)
protected final int internalGet(int field)
    {
        return fields[field];
    }

直接从数组中取值

add

   calendar.add(Calendar.YEAR,1);

按照日历的规则,给指定字段添加或者减少时间

getTime

 public final Date getTime() {
        return new Date(getTimeInMillis());
    }
public long getTimeInMillis() {
        if (!isTimeSet) {
            updateTime();
        }
        return time;
    }

毫秒为单位,返回该日历的时间值。

SimpleDateFormat

日期和时间格式由 日期和时间模式字符串 指定。在 日期和时间模式字符串 中,未加引号的字母 ‘A’ 到 ‘Z’ 和 ‘a’ 到 ‘z’ 被解释为模式字母,用来表示日期或时间字符串元素。文本可以使用单引号 (') 引起来,以免进行解释。所有其他字符均不解释;只是在格式化时将它们简单复制到输出字符串

白话文的讲:这些A——Z,a——z这些字母(不被单引号包围的)会被特殊处理替换为对应的日期时间,其他的字符串还是原样输出。
日期和时间模式(注意大小写,代表的含义是不同的)

yyyy:年
MM:月
dd:日
hh:1~12小时制(1-12)
HH:24小时制(0-23)
mm:分
ss:秒
S:毫秒
E:星期几
D:一年中的第几天
F:一月中的第几个星期(会把这个月总共过的天数除以7)
w:一年中的第几个星期
W:一月中的第几星期(会根据实际情况来算)
a:上下午标识
k:和HH差不多,表示一天24小时制(1-24)K:和hh差不多,表示一天12小时制(0-11)。
z:表示时区  

import java.text.SimpleDateFormat;
import java.util.Date;
 
/**
 * Created by lxk on 2016/11/4
 */
public class Format {
    public static void main(String[] args) {
        Date ss = new Date();
        System.out.println("一般日期输出:" + ss);
        System.out.println("时间戳:" + ss.getTime());
        SimpleDateFormat format0 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String time = format0.format(ss.getTime());//这个就是把时间戳经过处理得到期望格式的时间
        System.out.println("格式化结果0:" + time);
        SimpleDateFormat format1 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
        time = format1.format(ss.getTime());
        System.out.println("格式化结果1:" + time);
    }
}

前后端日期传递


首先简单介绍下常见的几种时间:
CST
北京时间,China Standard Time,又名中国标准时间

中部标准时间(北美洲),Central Standard Time (USA) UT-6:00
澳州中部时间,Central Standard Time (Australia) UT+9:30
中国时间,China Standard Time UT+8:00
古巴标准时间,Cuba Standard Time UT-4:00

CST可以表示美国。澳大利亚,中国。古巴四个国家的时间

示例:
Sun Aug 30 2020 23:07:43 GMT+0800 (中国标准时间)

GMT
格林尼治标准时间,Greenwich Mean Time

示例:
Sun, 30 Aug 2020 15:09:23 GMT

UTC
国际协调时间,Coordinated Universal Time

ISO
标准时间

示例:
2020-08-30T15:09:23.786Z

CST 中国标准时间(东八区)= UTC/GMT + 8小时


一般前端传值可能传的时GMT时间格式,也可能时ISO格式,也可能是常见的yyyy-MM-dd HH:mm:ss这种格式。那么就要求后端接收不同的格式都处理成CST时间(东八区时间)
那么后端要能保证对不同日期格式都进行正确处理

以springMvc处理日期类型为例

 @PostMapping("/register")
    @ApiOperation(value = "用户注册接口", httpMethod ="POST" , response = UserRegisterResponse.class, notes = "....")
    @ResponseBody
    @ResponseJsonFormat
    public UserRegisterResponse register(@RequestBody UserRegisterRequest userRegisterRequest){//用户信息要用aop拦截,登录肯定不用拦截了。
        if(userRegisterRequest.getPassward() != null){
            userRegisterRequest.setPassward(passwordEncoder.encode(userRegisterRequest.getPassward()));
        }
        return  userReadFacade.register(userRegisterRequest);
    }

实体类字段birthday为Date类型

@Data
@ApiModel
public class UserRegisterRequest implements Serializable {

    private Long id;

    private String userName;

    private String nickName;

    private Boolean gender;

    private Date birthday;

    private String mobile;

    private String email;

    private String passward;
}

springMvc再处理请求绑定到实体类时将日期字符串转换成Date对象
学过springMvc处理流程的知道对于注解@RequestBody对类,将会由RequestResponseBodyMethodProcessor进行处理 json格式的请求会交给MappingJackson2HttpMessageConverter处理
对于Date类型由DateDeserializer处理····
上面都不清楚也没关系。直接看重要的代码
com.fasterxml.jackson.databind.DeserializationContext#parseDate

 public Date parseDate(String dateStr) throws IllegalArgumentException
    {
        try {
            DateFormat df = getDateFormat();
            return df.parse(dateStr);
        } catch (ParseException e) {
            throw new IllegalArgumentException(String.format(
                    "Failed to parse Date value '%s': %s", dateStr,
                    ClassUtil.exceptionMessage(e)));
        }
    }
    

com.fasterxml.jackson.databind.util.StdDateFormat#_parseDate

protected Date _parseDate(String dateStr, ParsePosition pos) throws ParseException
    {
        if (looksLikeISO8601(dateStr)) { // also includes "plain"
            return parseAsISO8601(dateStr, pos);
        }
        // Also consider "stringified" simple time stamp
        int i = dateStr.length();
        while (--i >= 0) {
            char ch = dateStr.charAt(i);
            if (ch < '0' || ch > '9') {
                // 07-Aug-2013, tatu: And [databind#267] points out that negative numbers should also work
                if (i > 0 || ch != '-') {
                    break;
                }
            }
        }
        if ((i < 0)
            // let's just assume negative numbers are fine (can't be RFC-1123 anyway); check length for positive
                && (dateStr.charAt(0) == '-' || NumberInput.inLongRange(dateStr, false))) {
            return _parseDateFromLong(dateStr, pos);
        }
        // Otherwise, fall back to using RFC 1123. NOTE: call will NOT throw, just returns `null`
        return parseAsRFC1123(dateStr, pos);
    }

looksLikeISO8601就是国标的情况2022-07-09T07:18:00.144Z这种格式
默认一共能处理这几种格式

protected final static String[] ALL_FORMATS = new String[] {
        DATE_FORMAT_STR_ISO8601,
        "yyyy-MM-dd'T'HH:mm:ss.SSS", // ISO-8601 but no timezone
        DATE_FORMAT_STR_RFC1123,
        DATE_FORMAT_STR_PLAIN
    };
  //国标,日期时间最全毫秒,时区偏移量都有
   public final static String DATE_FORMAT_STR_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
  //年月日
    protected final static String DATE_FORMAT_STR_PLAIN = "yyyy-MM-dd";
   
    protected final static String DATE_FORMAT_STR_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";

前端请求示例
完全搞懂java中的时间戳,时区,日期格式_第4张图片

经测试这几种格式如果不指定时区都认为前端传来的时间时区在偏移为0的本初子午线,后端转换后都加了8小时
经过debug找到了原因。
原来jackson在将字符串处理成日期时SimpleDateFormat使用的时区偏移为0
完全搞懂java中的时间戳,时区,日期格式_第5张图片

处理完毕之后,在使用new Date转换对象,这里因为new Date使用本机时区而后又加了8小时

public final Date getTime() {
        return new Date(getTimeInMillis());
    }

因此我这里因为前端使用是的是本机的东八区,到了后端按照0时区处理,而后又转成本地时区,导致时间快了八个小时。
这种情况要么前端传递时间时-8小时,要么前后端统一时区进行处理

除了默认的几种格式还可以利用jackson自定义日期格式和时区

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

你可能感兴趣的:(java日期处理,java,开发语言,日期,Date)