昨天在工作中,写了一个关于时间的工具类,其中一个函数的功能是判断当前时间是否为0点。本来想一天是86400秒,如果通过当前的unix时间与86400取余,且余数为0的话,那么肯定是0点了。所以我写了如下的代码:

public class DateUtil {

    public Boolean is0Clock(Long unixTimeStamp) {
        return unixTimeStamp % 86400 == 0;
    }

    public static void main(String[] args) {
        DateUtil dateUtil = new DateUtil();

        /*设置日期与时间 2018-06-14T00:00:00*/
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, 2017);
        /*Calendar类月份是0~11*/
        calendar.set(Calendar.MONTH, 5);
        calendar.set(Calendar.DATE, 14);
        calendar.set(Calendar.HOUR, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        System.out.println(dateUtil.is0Clock(calendar.getTimeInMillis() / 1000));
    }
}

但是结果却不是预测的true,如下:

Connected to the target VM, address: '127.0.0.1:50429', transport: 'socket'
false
Disconnected from the target VM, address: '127.0.0.1:50429', transport: 'socket'

Process finished with exit code 0

后来我查了unix时间的获取方式,发现是因为时区的原因导致的。java中的unix时间是没有时区概念的,计算方式是此刻0时区的时间距0时区1970-01-01 00:00:00秒数。上例中2018-06-14T00:00:00其实是东八区的0点,在0时区的对应的时间其实是2018-06-13T16:00:00,减去1970-01-01T00:00:00,得到时间戳1528905600,与86400取余不为0。到此,上述例子不过的原因已经明了。东八区判断时间是否为0点的逻辑应该如下:

public Boolean is0Clock(Long unixTimeStamp) {
    return (unixTimeStamp + 8 * 3600) % 86400 == 0;
}

Java8之前日期相关类

  • java.util.Date

java.util.Date这个类现在除了获取unix时间的getTime(),构造函数Date()、Date(long date) 、方法from(Instatnt instant)及toString()方法外,其他方法都以标记为@Deprecated。 且toString()使用了默认时区,方法返回的是英文版日期格式,如下:

public class DateTest {
    public void dateTest(){
        Date date0 = new Date();
        System.out.println(String.format("%d - %s", date0.getTime(), date0.toString()));

        Date date1 = new Date(1528943069 * 1000L);
        System.out.println(String.format("%d - %s", date1.getTime(), date1.toString()));

        date1.setTime(0L);
        System.out.println(String.format("%d - %s", date1.getTime(), date1.toString()));
    }

    public static void main(String[] args){
        DateTest dateTest = new DateTest();
        dateTest.dateTest();
    }
}

运行结果:

Connected to the target VM, address: '127.0.0.1:51417', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:51417', transport: 'socket'
1528944004363 - Thu Jun 14 10:40:04 CST 2018
1528943069000 - Thu Jun 14 10:24:29 CST 2018
0 - Thu Jan 01 08:00:00 CST 1970

Process finished with exit code 0
  • java.util.Calendar

java.util.Calendar 使用默认时区,除了获取 Unix 时间,也提供了诸多接口:“年、月、日、时、分、秒” 字段的获取与设置,时区设置,日期计算等接口。

public class CalendarTest {

    void printCalendar(Calendar calendar) {

         /*date 表示时间和日期,输出:
         1. Unix 时间(毫秒)
         2. 格式化输出 yyyy-MM-dd HH:mm:ss*/
        System.out.println(String.format("%d -> %04d-%02d-%02d %02d:%02d:%02d", calendar.getTime().getTime(),
                calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DATE),
                calendar.get(Calendar.HOUR), calendar.get(Calendar.MINUTE), calendar.get(Calendar.SECOND)));
    }

    public void calendarTest() {
        /*当前时间*/
        Calendar calendar = Calendar.getInstance();
        printCalendar(calendar);


        calendar.setTime(new Date(12 * 1000L));
        /*输出:12000 -> 1970-01-01 08:00:12 注意是8点,因为Calendar使用了默认时区*/
        printCalendar(calendar);


         /*设置日期与时间 2018-06-14 00:00:00*/
        calendar.set(2018, Calendar.JUNE, 14, 0, 0, 0);
        /*输出:1528905600 -> 2018-06-14 00:00:00*/
        printCalendar(calendar);

        /*设置时区*/
        calendar.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
        /*输出:东京: 1528905600000 - Thu Jun 14 00:00:00 CST 2018*/
        System.out.println(String.format("东京: %d - %s", calendar.getTime().getTime(), calendar.getTime().toString()));
        /*输出:1528905600000 -> 2018-06-14 01:00:00*/
        printCalendar(calendar);

        calendar.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
        /*输出:上海: 1528905600000 - Thu Jun 14 00:00:00 CST 2018*/
        /*输出:1528905600000 -> 2018-06-14 00:00:00*/
        System.out.println(String.format("上海: %d - %s", calendar.getTime().getTime(), calendar.getTime().toString()));
        printCalendar(calendar);

        calendar.set(Calendar.SECOND, 30);
        /*输出:1528905630000 - Thu Jun 14 00:00:30 CST 2018*/
        System.out.println(String.format("%d - %s", calendar.getTime().getTime(), calendar.getTime().toString()));
        /*输出:1528905630000 -> 2018-06-14 00:00:30*/
        printCalendar(calendar);

        /*日期计算*/
        /*加30秒*/
        calendar.add(Calendar.MINUTE, 30);
        /*输出:1528907430000 -> 2018-06-14 00:30:30*/
        printCalendar(calendar);

        calendar.roll(Calendar.DATE, 1);
        /*加1天*/
        /*输出:1528993830000 -> 2018-06-15 00:30:30*/
        printCalendar(calendar);
    }

    public static void main(String[] args){
        CalendarTest calendarTest = new CalendarTest();
        calendarTest.calendarTest();
    }
}
  • 格式化

java8之前,可以使用java.text.SimpleDateFormat,快速地进行日期与时间的格式化输出和字符串解析。

public class SimpleDateFormatTest {

    public static void main(String[] args) {
        try {
            SimpleDateFormatTest simpleDateFormatTest = new SimpleDateFormatTest();
            simpleDateFormatTest.simpleDateFormatTest();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void simpleDateFormatTest() throws ParseException {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

        DateFormat dateFormatWithZone = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss XXX");

        /*格式化*/
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date(0));
        /*输出:1970-01-01T08:00:00 +08:00*/
        System.out.println(dateFormatWithZone.format(calendar.getTime()));
        /*输出:1970-01-01T08:00:00*/
        System.out.println(dateFormat.format(calendar.getTime()));

        /*解析*/
        String timeWithZone = "2017-11-30T00:00:00 +00:00";
        /*转化为date对象*/
        Date dateWithZone = dateFormatWithZone.parse(timeWithZone);
        /*转化为String输出,输出:2017-11-30T08:00:00 +08:00*/
        System.out.println(dateFormatWithZone.format(dateWithZone));

        String time = "2017-11-30T00:00:00";
        /*转化为date对象*/
        Date date = dateFormat.parse(time);
        /*转化为String输出,输出:2017-11-30T08:00:00*/
        System.out.println(dateFormat.format(date));
    }
}

综上,可以看出,使用Date类来进行时间操作是不合理的,因为Date类大多数方法都已经过期了,无法使用Date类的方法进行时间操作,另外Date不支持时区指定。Calendar可以进行时间操作,且可以指定时区,但是使用起来也有一定的不方便之处,比如Calendar的月份是0~11。用来格式化和解析的SimpleDateFormat也非线程安全类。所以Java8之后,提供了另外一套时间类。分别是Instant、LocalDateTime、LocalDate、LocalTime、ZonedDateTime,及用来格式化的DateTimeFormat。

Java8之后日期相关类

  • Instant

java.time.Instant为纳秒级精度unix时间,其toString()方法基于 ISO-8601 进行格式化。Instant更多用作Date和LocalDateTime、LocalDate、LocalTime转换的中间工具。如下:

public class InstantTest {
    public void instantTest(){
        Instant now = Instant.now();
        /*输出:2018-06-14T04:21:35.411Z*/
        System.out.println(now);

        Instant instant = Instant.ofEpochSecond(1528905600L);
        /*输出:2018-06-13T16:00:00Z*/
        System.out.println(instant);

        instant = Instant.ofEpochMilli(1528905600000L);
        /*输出:2018-06-13T16:00:00Z*/
        System.out.println(instant);

        instant = Instant.ofEpochSecond(1528905600L, 999);
        /*2018-06-13T16:00:00.000000999Z*/
        System.out.println(instant);
    }

    public static void main(String[] args){
        InstantTest instantTest = new InstantTest();
        instantTest.instantTest();
    }
}
  • LocalDateTime、LocalDate、LocalTime

LocalDateTime本地日期时间,LocalDate本地日期,LocalTime本地时间。LocalDate实例与LocalTime实例能够共同构建LocalDateTime实例,由LocalDateTime实例能够获取LocalDate实例与LocalTime实例。如下:

public class LocalDateTimeTest {
    public void localDateTimeTest(){
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);

        /*设置到分*/
        LocalDateTime dateTime = LocalDateTime.of(2018,6,14,0,0);
        /*输出:2018-06-14T00:00*/
        System.out.println(dateTime);

        /*设置毫秒*/
        dateTime = LocalDateTime.of(2018,6,14,0,0,30);
        /*输出:2018-06-14T00:00:30*/
        System.out.println(dateTime);

        /*设置到纳秒*/
        dateTime = LocalDateTime.of(2018,6,14,0,0,30, 999);
        /*输出:2018-06-14T00:00:30.000000999*/
        System.out.println(dateTime);

        /*LocalDate & LocalTime -> LocalDateTime*/
        LocalDate date = LocalDate.of(2018,6,14);
        LocalTime time = LocalTime.of(1, 0, 0, 30);

        dateTime =  date.atTime(time);
        /*输出:2018-06-14T01:00:00.000000030*/
        System.out.println(dateTime);

        dateTime = date.atTime(1,59,59);
        /*输出:2018-06-14T01:59:59*/
        System.out.println(dateTime);

        dateTime = date.atStartOfDay();
        /*输出:2018-06-14T00:00*/
        System.out.println(dateTime);

        dateTime = time.atDate(date);
        /*输出:2018-06-14T01:00:00.000000030*/
        System.out.println(dateTime);


        /*LocalDateTime -> LocalDate & LocalTime*/
        dateTime = LocalDateTime.of(2018,6,14,0,0,30, 999);

        date = dateTime.toLocalDate();
        /*输出:2018-06-14*/
        System.out.println(date);

        time = dateTime.toLocalTime();
        /*输出:00:00:30.000000999*/
        System.out.println(time);
    }

    public static void main(String[] args){
        LocalDateTimeTest localDateTimeTest = new LocalDateTimeTest();
        localDateTimeTest.localDateTimeTest();
    }
}
  • java.time.ZonedDateTime

java.time.ZonedDateTime用于表示位于特定 “时区” 的 “日期与时间”,通常用于 “Unix 时间” 和特定时区时间的本地时间调整。如下:

public class ZonedDateTimeTest {
    public void zonedDateTimeTest(){
        LocalDateTime localDateTime = LocalDateTime.of(2018, 6, 14,0,0,0);
        /*localDateTime设置为2018-06-14 00:00:00*/
        System.out.println(localDateTime);

        /*localDateTime的东八区本地时间*/
        ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("Asia/Shanghai"));
        /*输出:2018-06-14T00:00+08:00[Asia/Shanghai]*/
        System.out.println(zonedDateTime);
        /*东八区本地时间转unix时间,输出:1528905600*/
        System.out.println(zonedDateTime.toInstant().getEpochSecond());

        /*由zonedDateTime获取相同的Unix时间下,东九区的zoneDateTime*/
        zonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
        /*输出:2018-06-14T01:00+09:00[Asia/Tokyo]*/
        System.out.println(zonedDateTime);
        /*东九区时间转unix时间,输出:1528905600*/
        System.out.println(zonedDateTime.toInstant().getEpochSecond());

        /*由东九区zoneDateTime,获取相同本地时间东八区的zoneDateTime*/
        zonedDateTime = zonedDateTime.withZoneSameLocal(ZoneId.of("Asia/Shanghai"));
        /*输出:2018-06-14T01:00+08:00[Asia/Shanghai]*/
        System.out.println(zonedDateTime);
        /*东八区unix时间,输出:1528909200*/
        System.out.println(zonedDateTime.toInstant().getEpochSecond());
    }

    public static void main(String[] args){
        ZonedDateTimeTest zonedDateTimeTest = new ZonedDateTimeTest();
        zonedDateTimeTest.zonedDateTimeTest();
    }
}
  • java.time.format.DateTimeFormatter

java.time.format.DateTimeFormatter 能够进行LocalDate、LocalTime、LocalDateTime、ZonedDateTime的格式化输出。同时,LocalDate、LocalTime、LocalDateTime、ZonedDateTime 提供了静态的 parse 方法,能够进行字符串解析。

public class DateTimeFormatTest {
    public void dateTimeFormatTest(){
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒");

        ZonedDateTime zonedDateTime = Instant.ofEpochSecond(1528905600).atZone(ZoneId.of("Asia/Shanghai"));
        /*输出:2018 年 06 月 14 日 00 时 00 分 00 秒*/
        System.out.println(formatter.format(zonedDateTime));

        zonedDateTime = Instant.ofEpochSecond(1528905600).atZone(ZoneId.of("Asia/Tokyo"));
        /*输出:2018 年 06 月 14 日 01 时 00 分 00 秒*/
        System.out.println(formatter.format(zonedDateTime));

        LocalDateTime localDateTime = LocalDateTime.parse("2018 年 06 月 14 日 01 时 00 分 00 秒", formatter);
        /*输出:2018-06-14T01:00*/
        System.out.println(localDateTime);
    }

    public static void main(String[] args){
        DateTimeFormatTest dateTimeFormatTest = new DateTimeFormatTest();
        dateTimeFormatTest.dateTimeFormatTest();
    }
}

另外,LocalDate、LocalTime、LocalDateTime、ZonedDateTime的toString方法也可以格式化输出,预格式如下:

类型

默认格式示例

Instant

2017-11-23T10:15:30.00Z

LocalDate

2017-11-23

LocalTime

10:15:30

LocalDateTime

2017-11-23T10:15:30

ZonedDateTime

2017-11-23T10:15:30+01:00[Asia/Shanghai]