java的时间处理

小TOT 创建于 2017-02-07

jdk时间处理

java.util.Date类用于封装日期及时间信息,一般用来表示时间,而不使用其进行日期操作。当我们要对日期进行操作时使用Callendar类。java.util.Calendar类用于封装日历信息,并对其进行操作,其主作用在于可以对时间分量进行运算。除了日期的表示和运算外,我们通常还需要将日期进行格式化输出,这时我们通常使用SimpleDateFormat。该类的作用在于将日期转换成字符串,或者将字符串转换成Date对象。

1. date 时间表示
Date对象用来表示时间,是时区非敏感的。使用new Date()创建当前系统时间,需要注意的是时间的完整描述不仅仅包括几月几日几点几分,还应该包含时区信息,因为只告诉年月日时分秒不告诉时区是在耍流氓。中国横跨两大时区,但是为了统一作息,我们统一使用了东八区即Asia/ShangHai。使用Date对象需要注意的是,getTime()方法获取的毫秒数为现在中时区(UTC)距1970-01-01:00:00:00的毫秒数,获得的long值被称为时间戳,在java中时间戳精确到毫秒级别,用long表示时间戳,在ruby,php等语言,精确到秒,用float表示。我们可以通过这个毫秒数进行年月日的换算。

  static void dateDemo(){
        Date date = new Date();
        System.out.println("new Date()的时间戳:"+date.getTime());
        System.out.println("现在系统的毫秒数:"+System.currentTimeMillis());
        //通过时间戳获取现在几点
        int millisOfOneDay = 1000*3600*24;
        int millisOfOneHour = 1000*3600;
        long dayNumberFrom_1970_01_01 = date.getTime()/millisOfOneDay;
        long todayTimeMillis = date.getTime() % millisOfOneDay;
        long hour = todayTimeMillis/millisOfOneHour;
        long theHourTimeMillis = todayTimeMillis % millisOfOneHour;
        long minutes = theHourTimeMillis / (60*1000);
        long theMinuteMillions = theHourTimeMillis % (60*1000);
        long seconds = theMinuteMillions / (1000);
        String timeDesc = hour+"点"+minutes+"分"+seconds + "秒";
        System.out.println("现在距离1970年一月一日的天数:"+dayNumberFrom_1970_01_01);
        System.out.println("现在时间是:"+ timeDesc);

        //使用java date自带方法获得现在时间
        System.out.println("date.getHours():"+date.getHours());
        System.out.println("date.getMinutes():"+date.getMinutes());
        System.out.println("date.getSeconds():"+date.getSeconds());
    }
    //运行得到类似结果
    new Date()的时间戳:1470326303201
    现在系统的毫秒数:1470326303202
    现在距离1970年一月一日的天数:17017
    现在时间是:155823date.getHours():23
    date.getMinutes():58
    date.getSeconds():23

时区图 由上面的示例可以看到,new Date()获取的时间戳和系统的时间戳是一样的(虽然输出结果差一毫秒),也就是说new Date()实际是使用的系统的时间戳进行构建的。再来看看,通过时间戳计算得来的时分秒,我们发现和系统显示的时间相差8个小时,正好时间是UTC(中时区)的现在时间。因此可以推断计算机存储的时间戳实际上是中时区的时间距离1970-01-01 00:00:00的毫秒数。在输出格式化的时候,将其转换成我们所在时区的时间。如果所有的操作系统的遵循这一点规范,系统的时间戳统一为UTC的时间戳,有一个好处在于,在不同时区传递时间时就没有必要传递时区信息了,因为默认都是UTC中时区。如果需要知道对方传递过来的时间在本地具体时间是多少时,只需要将这个时间进行简单的转换即可。

这里就深入来探讨web程序时间的处理,相信很多人在程序处理时间时,前端向服务端上传类似"2016-06-06 12:00:00"这样的字符串来表客户端的上传时间,然后服务器再将字符串转换成date对象,最后存到数据库。这样的好处在于通过参数能够很快知道具体时间表示。大多数程序都这样的处理时间,笔者在写这篇文章之前也是这么处理时间的。这样的时间处理有一个问题在于,程序是一个国际化程序时,就会遇到时区敏感问题。比如美国用户上传当地当时时间到服务器,而在中国看到的任然是美国的时间,而不是中国的当前时间,需要将其正确的表示成中国本地时间,需要对其进行转换。这样显然是不合理的。解决该问题,我们可以参照前人时间戳的设计,统一使用某个时区进行时间传递,遵循“数据的存储和显示相分离”的设计原则,我将表示绝对时间的时间戳(无论是Long型还是Float)存入数据库,在显示的时候根据用户设置的时区格式化为正确的字符串。这样无论是世界各个时区的用户看到美国用户上传的美国当时时间都是本时区的当前时间。

2. Calendar 时间运算
Calendar类主要用于时间的相关运算,即对时间进行修改操作等。需要注意的是,在Calendar中,月份的一月份用0表示,2月用1表示,以此类推11表示12月。同时需要注意的是,一周的开始时间为星期天,day_of_week值为1,以此类推,周一的day_of_week为2,周五的day_of_week为6。以上的设计导致java的时间api饱受诟病。
下面是Calendar的简单示例:

    static void calendarDemo(){
        Calendar calendar = Calendar.getInstance();
        System.out.println(calendar.getTimeZone());
        System.out.println("Calendar.getInstance()的时间毫秒 =》"+calendar.getTimeInMillis());
        System.out.println("系统时间戳=》"+System.currentTimeMillis());
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        simpleDateFormat.setCalendar(calendar);
        System.out.println("现在时间的格式化输出===》"+simpleDateFormat.format(calendar.getTime()));
        //利用calendar对时间各个分量进行计算
        //更多分量计算操作查看Calendar类的静态变量。

        System.out.println(calendar.get(Calendar.DAY_OF_WEEK));

        System.out.println("add one day before==>"+calendar.get(Calendar.DAY_OF_MONTH));
        calendar.add(Calendar.DAY_OF_MONTH,1);
        System.out.println("add one day after==>"+calendar.get(Calendar.DAY_OF_MONTH));
        //注意月份是从0开始,0->一月,1->二月,...,11->12月
        System.out.println("add one month before==>"+calendar.get(Calendar.MONTH));
        calendar.add(Calendar.MONTH,10);
        System.out.println("add 10 month after==>"+calendar.get(Calendar.MONTH));

    }

3. SimpleDateFormat 格式化数据
使用SimpleDateFormat可以对date对象进行格式化,方便阅读。simpledateFormat对象有一个timeZone属性,可以格式成指定的时区的时间表示。通过SimpleDateFormat时区示例我们也可以更好的理解时区的概念。

 static void simpleDateFormat(){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        TimeZone localTimeZone =  TimeZone.getDefault();
        sdf.setTimeZone(localTimeZone);
        System.out.println("default time zone==>"+sdf.format(new Date()));
        sdf.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
        System.out.println("GMT+8:00 time zone==>"+sdf.format(new Date()));

        sdf.setTimeZone(TimeZone.getTimeZone("GMT+7:00"));
        System.out.println("GMT+7:00 time zone==>"+sdf.format(new Date()));

        sdf.setTimeZone(TimeZone.getTimeZone("GMT+0:00"));
        System.out.println("GMT+0:00 time zone==>"+sdf.format(new Date()));
    }

    //运行结果
    default time zone==>2016-08-08 11:00:06
    GMT+8:00 time zone==>2016-08-08 11:00:06
    GMT+7:00 time zone==>2016-08-08 10:00:06
    GMT+0:00 time zone==>2016-08-08 03:00:06

使用joda对时间进行处理

正是由于java 时间api一些不合理的设计,导致api的使用体验非常的糟糕,于是有了第三方的时间处理包,推荐使用joda!
joda将时间设计成不可变对象,并且将时间的各个分量的操作,读取以及格式化都集中到一个对象,这样对时间的操作及格式化不用创建多个类。并且在时间的getDayOfMonth,getDayOfWeek(),getMonthOfYear()等时间分量也更具符合人类的阅读习惯。下面请看详细示例:

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;

import java.util.Locale;

/**
 * Created by guest on 16/8/5.
 */
public class JodaDemo {

    public static void main(String[] args) {
        jodaDemo();
    }

    static void jodaDemo(){
        DateTime dateTime = new DateTime();
        System.out.println("dateTime base info=====>");
        printBaseInfo(dateTime);

        DateTime dateTime1 = new DateTime(DateTimeZone.forID("Etc/GMT+1"));
//        System.out.print("DateTimeZone.getAvailableIDs()"+DateTimeZone.getAvailableIDs());
        System.out.println("Etc/GMT+1 base info=====>");
        printBaseInfo(dateTime1);

        DateTime dateTime2 = dateTime1.withZone(DateTimeZone.forID("Etc/GMT+2"));
        System.out.println("Etc/GMT+2 base info=====>");
        printBaseInfo(dateTime2);
        LocalDate localDate = dateTime.toLocalDate();
        // 时间的运算
        System.out.println("datetime plusHours(5) before"+dateTime.toString("时区:Z HH:mm:ss",Locale.CHINESE));
        DateTime dateTime3 = dateTime.plusHours(5);
        System.out.println("datetime plusHours(5) after"+dateTime.toString("时区:Z HH:mm:ss",Locale.CHINESE));
        System.out.println("dateTime3 from dateTime.plusHours(5)"+dateTime3.toString("时区:Z HH:mm:ss",Locale.CHINESE));

    }

    static void printBaseInfo(DateTime dateTime){
        System.out.print("\n");
        System.out.println("the sys current millis==>" + System.currentTimeMillis());
        System.out.println("the dateTime.getMillis==>" + dateTime.getMillis());
        //日期的输出格式说明见SimpleDateFormat源码注释
        //Locale信息用于输入适合该地区格式
        System.out.println("datetime.toString(时区:Z yyy-MM-dd HH:mm:ss EEE)==>" + dateTime.toString("时区:Z yyy-MM-dd HH:mm:ss EEE", Locale.CHINESE));
        System.out.println("dateTime.getDayOfMonth()" + dateTime.getDayOfMonth());
        System.out.println("dateTime.getDayOfWeek()" + dateTime.getDayOfWeek());
        System.out.println("dateTime.getMonthOfYear()" + dateTime.getMonthOfYear());
        System.out.print("\n");

    }
}

下面是代码运行结果示例:

dateTime base info=====>

the sys current millis==>1470387207767
the dateTime.getMillis==>1470387207663
datetime.toString(时区:Z yyy-MM-dd HH:mm:ss EEE)==>时区:+0800 2016-08-05 16:53:27 星期五
dateTime.getDayOfMonth()5
dateTime.getDayOfWeek()5
dateTime.getMonthOfYear()8

Etc/GMT+1 base info=====>

the sys current millis==>1470387207784
the dateTime.getMillis==>1470387207784
datetime.toString(时区:Z yyy-MM-dd HH:mm:ss EEE)==>时区:-0100 2016-08-05 07:53:27 星期五
dateTime.getDayOfMonth()5
dateTime.getDayOfWeek()5
dateTime.getMonthOfYear()8

Etc/GMT+2 base info=====>

the sys current millis==>1470387207785
the dateTime.getMillis==>1470387207784
datetime.toString(时区:Z yyy-MM-dd HH:mm:ss EEE)==>时区:-0200 2016-08-05 06:53:27 星期五
dateTime.getDayOfMonth()5
dateTime.getDayOfWeek()5
dateTime.getMonthOfYear()8

datetime plusHours(5) before时区:+0800 16:53:27
datetime plusHours(5) after时区:+0800 16:53:27
dateTime3 from dateTime.plusHours(5)时区:+0800 21:53:27

从结果可以看出,当创建当前时间时,使用的时间戳是系统的时间戳。不论我们对时区进行如何修改,获取的时间戳是一样的,这和我们之前对时间戳定义的描述一致,使用中时区时间距离1970-01-01 00:00:00的毫秒数为时间戳,要对时间进行格式化时,再使用所在的时区进行时间换算最后输出可视化的时间。 同时我们也看到,使用joda对时间的操作较jdk的时间api要方便得多,从api的实际来看,joda设计更为合理。还需要注意的是joda中的时间对象设计为不可变对象,对其进行操作后源对象并不会产生改变,返回的新对象才是我们运行得到的结果对象。

java 8新的时间api。

在Java刚刚发布,也就是版本1.0的时候,对时间和日期仅有的支持就是java.util.Date类。大多数开发者对它的第一印象就是,它根本不代表一个“日期”。实际上,它只是简单的表示一个,从1970-01-01Z开始计时的,精确到毫秒的瞬时点。由于标准的toString()方法,按照JVM的默认时区输出时间和日期,有些开发人员把它误认为是时区敏感的(实际上date不是时区敏感的)。

在升级Java到1.1期间,Date类被认为是无法修复的。由于这个原因,java.util.Calendar类被添加了进来。悲剧的是,Calendar类并不比java.util.Date好多少。它们面临的部分问题是:

  • 可变性。像时间和日期这样的类应该是不可变的。
  • 偏移性。Date中的年份是从1900开始的,而月份都是从0开始的。
  • 命名。Date不是“日期”,而Calendar也不真实“日历”。
  • 格式化。格式化只对Date有用,Calendar则不行。另外,它也不是线程安全的。
    大约在2001年,Joda-Time项目开始了。它的目的很简单,就是给Java提供一个高质量的时间和日期类库。尽管被耽搁了一段时间,它的1.0版还是被发布。很快,它就成为了广泛使用和流行的类库。随着时间的推移,有越来越多的需求,要在JDK中拥有一个像Joda-Time的这样类库。在来自巴西的Michael Nascimento Santos的帮助下,官方为JDK开发新的时间/日期API的进程:JSR-310,启动了。

新的日期/时间api随jdk一同推出,其实现了JSR-310所有规范,jsr-310参照了joda库很多的特性,将时间设计成了一个不可变对象。新api将时间清晰的分成,LocalDate,LocalTime,LocalDateTime,Instant,分别用来表示日期表示,时间表示,日期时间表示,以及Unix时间戳。与老api不同的是,Date/Time对象为不可变对象,本身带有了对时间的操作,而不需要额外的类似Calendar对时间分量进行操作,并且对于月份分量,周这些分类的输出结果也更加的符合人类阅读。下面就以LocalDateTime为例为大家简单的介绍下新的时间api的使用。


import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

/**
 * Created by 12613 on 2016/8/6.
 */
public class JDK_8_TIme_Demo {
    static void testDateTime(){
        //构造器
        /**
         LocalDateTime.now();
         LocalDateTime.of(args ..);
         LocalDateTime.ofInstant(Instant instance, ZoneId zoneId);
         */
        System.out.println("LocalDateTime.now()'s info====>");
        LocalDateTime localDateTime = LocalDateTime.now();
        printBaseInfo(localDateTime);

        System.out.print("localDate 时间操作===》");
        System.out.print("localDateTime.plusDays(10)===>"+localDateTime.plusDays(10));


    }


    static void printBaseInfo(LocalDateTime dateTime){
        System.out.print("\n");
        System.out.println("dateTime"+dateTime);
        System.out.println("dateTime.toInstant(ZoneOffset.ofHours(4)" + dateTime.toInstant(ZoneOffset.ofHours(4)));

        System.out.println("dateTime.toInstant(ZoneOffset.ofHours(4)).toEpochMilli()==>" + dateTime.toInstant(ZoneOffset.ofHours(4)).toEpochMilli());
        System.out.println("dateTime.toInstant(ZoneOffset.ofHours(7)).toEpochMilli()==>" + dateTime.toInstant(ZoneOffset.ofHours(7)).toEpochMilli());
        System.out.println("the sys current millis==>" + System.currentTimeMillis());

        //日期的输出格式说明见java.time.format.DateTimeFormatter源码注释
        //Locale信息用于输入适合该地区格式
        System.out.println(dateTime.format(DateTimeFormatter.ofPattern("yyy-MM-dd HH:mm:ss EEE ", Locale.CHINESE)));
        //        System.out.println("dateTime.getDayOfMonth()" + dateTime.getDayOfMonth());
        System.out.println("dateTime.getDayOfMonth()"+dateTime.getDayOfMonth());
        System.out.println("dateTime.getDayOfWeek()"+dateTime.getDayOfWeek());
        System.out.println("dateTime.getMonthValue()"+dateTime.getMonthValue());

    }

    public static void main(String[] args) {
        testDateTime();
    }
}

总结

本文简明扼要的介绍了使用jdk,joda,jdk8新的api对时间进行处理。同时也分享了一些系统中处理时间的一些经验。主要有以下两点。
1,对于时间的运算,如果你确实不能够仍受老版本时间api,如果你开发环境在jdk8以下,建议使用joda。若在jdk版本在8以上建议使用官方新的时间api。
2,对于系统的时间的设计,建议遵守“数据的存储和显示相分离”的设计原则,将表示绝对时间的时间戳(无论是Long型还是Float)存入数据库,在显示的时候根据用户设置的时区格式化为正确的字符串。这样无论是世界各个时区的用户看到美国用户上传的美国当时时间都是本时区的当前时间。

参考