替换老旧的java.util.Date

开发/后端 · 阅读 1171 · 点赞 0

Java中的日期核心概念

  1. 所有java.time对象都是不可变的
  2. 一个Instant是时间线上的一个点,功能上类似老的java.util.Date
  3. 在java的时间里,每天都是86400秒,没有闰秒
  4. 一个Duration就是两个时刻的间隔
  5. LocalDateTime没有时区信息
  6. TemporalAdjuster能处理常用的日历计算,例如查找某月的第一个星期二
  7. ZonedDateTime是指定时区的一个时间点,类似于GregorianCalendar
  8. 当处理带时区时间时,使用Period而不是Duration,以便更好的考虑夏时令变化
  9. 使用DateTimeFormatter来格式化并解析日期和时间

从计算机角度看待日期(时间线)

Java中,一个Instant表示时间线上一个点。原点,就是格林尼治时间1970年1月1号0点。UNIX/POSIX时间也使用同样约定。从原点开始按照每天86400秒精确的计算,向前向后都以纳秒计算。 Instant.MIN追溯到10亿年前,Instant.MAX是1000000000年的12月31日。
Instant.now()会返回当前时间点。
Duration.between可以计算时间点的时间差。

Instant start = Instant.now();
runAlgorithm();
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();

一个Duration就是两个瞬时时间点的时间量,时间量可以通过纳秒、毫秒、秒、分、小时、天等时间单位来计量。
下表是Instant和Duration的常用方法。

方法描述
plus, minus对当前的Instant 或者 Duration增加或者减少一段时间.
plusNanos
plusMillis
plusSeconds
plusMinutes
plusHours
plusDays
根据指定时间单位增加Instant或者Duration
minusNanos
minusMillis
minusSeconds
minusMinutes
minusHours
minusDays
根据指定时间单位减少Instant或者Duration
multipliedBy
dividedBy
negated
用给定的long值(或者-1)对Duration进行相乘或相除,从而对Duration进行缩放。
注意Instant是不能缩放的
isZero
isNegative
检查Duration是否为0或者负数

如果想检查一个算法的速度是否是另一个算法的10倍以上,可以这样:

Duration timeElapsed2 = Duration.between(start2, end2);
boolean overTenTimesFaster
= timeElapsed.multipliedBy(10).minus(timeElapsed2).isNegative();
// Or timeElapsed.toNanos() * 10 < timeElapsed2.toNanos()

Java8提供的常用的日期类

本地日期(LocalDate)

方法描述
now
of
ofInstant
从当前时间、指定的年月日、Instant或者ZonedId构造LocalDate对象
plusDays,
plusWeeks,
plusMonths,
plusYears
根据指定时间单位增加LocalDate
minusDays,
minusWeeks,
minusMonths,
minusYears
根据指定时间单位减少LocalDate
datesUntil产生一个Stream, 包含当前当前日期和终止日期及两者之间的所有日期
plus, minus增减一个 Duration(针对Instant)
或者 Period(针对LocalDate).
withDayOfMonth
withDayOfYear
withMonth
withYear
返回一个新的LocalDate对象,把年月日设定为指定的值
getDayOfMonth获取一个月中哪一天 ( 1 到 31).
getDayOfYear获取一年中哪一天 ( 1 and 366).
getDayOfWeek获取一周中哪一天, 返回 DayOfWeek 枚举值.
getMonth
getMonthValue
获取一年中哪个月,返回 Month 枚举, 或者 1 and 12.
getYear取得年份, between –999,999,999 and 999,999,999.
until获取一个 Period 对象, 或者指定单位(ChronoUnits)的日期差.
toEpochSecond给定一个LocalTime 和 ZoneOffset对象, 产生原点到指定时间点的秒数.
isBefore
isAfter
比较LocalDate
isLeapYear返回是否为闰年

程序员日是一年的第256天,我们很容易计算具体是哪一天:

LocalDate programmersDay = LocalDate.of(2014, 1, 1).plusDays(255);
// 九月13号, 闰年时是9月12号

时间线中两个时间点(Instant)的区间用Duration表示,对应的LocalDate的时间差异用Period来表示。

until方法会返回一个Period。

independenceDay.until(christmas); //返回独立日到圣诞节之间日期区间

java

independenceDay.until(christmas, ChronoUnit.DAYS) // 返回174 天

本地时间(LocalTime)

LocalTime表示本地时间,例如15:30:00。可以使用now或者of方法创建一个实例。

LocalTime now = LocalTime.now();
LocalTime bedTime = LocalTime.of(22, 30); //或者 LocalTime.of(22, 30, 0)

下表是LocalTime的常用方法。

方法描述
now,
of,
ofInstant
这些静态方法用于构造LocalTime对象
plusHours,
plusMinutes,
plusSeconds,
plusNanos
向当前的LocalTime对象增加小时、分、秒、纳秒
minusHours,
minusMinutes,
minusSeconds,
minusNanos
从当前的LocalTime对象减小小时、分、秒、纳秒
plus, minus增减Duration
withHour,
withMinute,
withSecond,
withNano
将LocalTime对象的小时、分钟、秒、纳秒设置为指定值,并返回一个新的LocalTime对象
getHour,
getMinute,
getSecond,
getNano
获得LocalTime的小时、分钟、秒、纳秒
toSecondOfDay,
toNanoOfDay
返回午夜零点到当前LocalTime对象之间相隔的秒数或者纳秒数
toEpochSecond把LocalTime对象转换成从原点(1970-01-01)开始的秒数
isBefore,
isAfter
比较LocalTime

日期调整器(TemporalAdjusters)

TemporalAdjusters类提供了一些常用的静态方法用于调整日期。

方法描述
next(weekday), previous(weekday)指定weekday 的前一天或后一天
nextOrSame(weekday),
previousOrSame(weekday)
从指定日期开始,指定weekday 的前一天或后一天
dayOfWeekInMonth(n, weekday)指定月份的第几个weekday
lastInMonth(weekday)指定月份的最后一个weekday
firstDayOfMonth(),
firstDayOfNextMonth(),
firstDayOfNextYear(),
lastDayOfMonth(),
lastDayOfPreviousMonth(),
lastDayOfYear()
不解释

格式化和解析(DateTimeFormatter)

DateTimeFormatter)提供了三种格式化日期/时间的方式:

  1. 预定义的标准格式化(这个看源码就行了,基本没有符合我们中国人习惯的预定格式)
  2. 本地化的日期和时间格式化
  3. 自定义模板的格式化(这是我们天朝程序员的最爱)
    仅凭DateTimeFormatter.ofPattern就可以仗剑走天涯了。

常见的日期问题

如何获取一天中的开始和结束时间

利用 LocalDate 对象

// atStartOfDay()
LocalDateTime startOfDay = localDate.atStartOfDay();
ZonedDateTime startOfDay = localDate.atStartOfDay(ZoneId.of("Europe/Paris"));
// of()
LocalDateTime startOfDay = LocalDateTime.of(localDate, LocalTime.MIDNIGHT);
// atTime()
LocalDateTime startOfDay = localDate.atTime(LocalTime.MAX);
// atDate()
LocalDateTime endOfDate = LocalTime.MAX.atDate(localDate);

利用 LocalDateTime 对象

// 常规做法
LocalDateTime localDateTime = LocalDateTime.parse("2018-06-23T05:55:55");
LocalDateTime endOfDate = localDateTime.toLocalDate().atTime(LocalTime.MAX);
// with()
LocalDateTime endOfDate = localDateTime.with(ChronoField.NANO_OF_DAY, LocalTime.MAX.toNanoOfDay());

利用 ZonedDateTime 对象

ZonedDateTime startofDay = zonedDateTime.with(ChronoField.HOUR_OF_DAY, 0);

获取日期序列

java7 的方式

public static List<Date> getDatesBetweenUsingJava7(
  Date startDate, Date endDate) {
  List<Date> datesInRange = new ArrayList<>();
  Calendar calendar = new GregorianCalendar();
  calendar.setTime(startDate);
  
  Calendar endCalendar = new GregorianCalendar();
  endCalendar.setTime(endDate);

  while (calendar.before(endCalendar)) {
      Date result = calendar.getTime();
      datesInRange.add(result);
      calendar.add(Calendar.DATE, 1);
  }
  return datesInRange;
}

java8 的方式

public static List<LocalDate> getDatesBetweenUsingJava8(
  LocalDate startDate, LocalDate endDate) { 

    long numOfDaysBetween = ChronoUnit.DAYS.between(startDate, endDate); 
    return IntStream.iterate(0, i -> i + 1)
      .limit(numOfDaysBetween)
      .mapToObj(i -> startDate.plusDays(i))
      .collect(Collectors.toList()); 
}

java9 的方式

public static List<LocalDate> getDatesBetweenUsingJava9(
  LocalDate startDate, LocalDate endDate) {

    return startDate.datesUntil(endDate)
      .collect(Collectors.toList());
}

两个日期之间相隔多少天(时/分/秒)

java.time.temporal.ChronoUnit

@Test
public void givenTwoDateTimesInJava8_whenDifferentiatingInSeconds_thenWeGetTen() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime tenSecondsLater = now.plusSeconds(10);

    long diff = ChronoUnit.SECONDS.between(now, tenSecondsLater);

    assertEquals(10, diff);
}

java.time.temporal.Temporal#until()

@Test
public void givenTwoDateTimesInJava8_whenDifferentiatingInSecondsUsingUntil_thenWeGetTen() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime tenSecondsLater = now.plusSeconds(10);

    long diff = now.until(tenSecondsLater, ChronoUnit.SECONDS);

    assertEquals(10, diff);
}

java.time.Duration and java.time.Period

@Test
public void givenTwoDateTimesInJava8_whenDifferentiating_thenWeGetSix() {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime sixMinutesBehind = now.minusMinutes(6);

    Duration duration = Duration.between(now, sixMinutesBehind);
    long diff = Math.abs(duration.toMinutes());

    assertEquals(6, diff);
}
@Test
public void givenTwoDatesInJava8_whenUsingPeriodGetDays_thenWorks()  {
    LocalDate aDate = LocalDate.of(2020, 9, 11);
    LocalDate sixDaysBehind = aDate.minusDays(6);

    Period period = Period.between(aDate, sixDaysBehind);
    int diff = Math.abs(period.getDays());

    assertEquals(6, diff);
}

注意:这个方法是有问题的,Period具有独立的年月日三个字段。

@Test
public void givenTwoDatesInJava8_whenUsingPeriodGetDays_thenDoesNotWork() {
    LocalDate aDate = LocalDate.of(2020, 9, 11);
    LocalDate sixtyDaysBehind = aDate.minusDays(60);

    Period period = Period.between(aDate, sixtyDaysBehind);
    int diff = Math.abs(period.getDays());

    assertEquals(60, diff); //实际值是29!!!!!!
}

修正的方法:

@Test
public void givenTwoDatesInJava8_whenUsingPeriod_thenWeGet0Year1Month29Days() {
    LocalDate aDate = LocalDate.of(2020, 9, 11);
    LocalDate sixtyDaysBehind = aDate.minusDays(60);
    Period period = Period.between(aDate, sixtyDaysBehind);
    int years = Math.abs(period.getYears());
    int months = Math.abs(period.getMonths());
    int days = Math.abs(period.getDays());
    assertArrayEquals(new int[] { 0, 1, 29 }, new int[] { years, months, days });
}

ZonedDateTime 和 OffsetDateTime 的区别

ZonedDateTime

  1. 存储了日期时间的所有字段,精度为纳秒,并且带有时区信息
  2. 里面的时区信息控制时区偏差值, 不能随意设置
  3. 支持夏令时(DST: Daylight Saving Time)

OffsetDateTime

  1. 存储了日期时间的所有字段,精度为纳秒,并且带有相对于格林尼治时间的时区偏差值信息
  2. 适合存储在数据库中和进行网络传输

与遗留代码共存

Java中, Instant是原先java.util.Date的替代类。Java8的Date类中增加了两个方法。
toIntant方法将Date对象转换为Instant对象,还有一个静态方法from正好相反。

转换为遗留类从遗留类转换为新类
Instant ↔
java.util.Date
Date.from(instant)data.toInstant()
ZonedDateTime ↔
java.util.GregorianCalendar
GregorianCalendar
.from(zonedDateTime)
cal.toZonedDateTime()
Instant ↔
java.sql.Timestamp
TimeStamp.from(instanttimestamp.toInstant()
LocalDateTime ↔
java.sql.Timestamp
Timestamp.valueOf(localDateTime)timestamp.toLocalDateTime()
LocalDate ↔ java.sql.DateDate.valueOf(localDate)date.toLocalDate()
LocalTime ↔ java.sql.TimeTime.valueOf(localTime)time.toLocalTime()
DateTimeFormatter →
java.text.DateFormat
formatter.toFormat()
java.util.TimeZone → ZoneIdTimezone.getTimeZone(id)timeZone.toZoneId()
java.nio.file.attribute.FileTime
→ Instant
FileTime.from(instant)fileTime.toInstant()

新旧日期互转的一些例子

Java Date to LocalDate

常用的两种方式:

1、利用 Date.toInstant() 方法

// 注意要有时区转换
public static LocalDate convertDateToLocalDateUsingInstant(Date date) {
     return date.toInstant()
             .atZone(ZoneId.systemDefault())
             .toLocalDate();
 }

 // Intant.ofEpochMilli(date.getTime())能获取Instant对象
 public static LocalDate convertDateToLocalDateUsingOfEpochMilli(Date date) {
     return Instant.ofEpochMilli(date.getTime())
             .atZone(ZoneId.systemDefault())
             .toLocalDate();
 }

2、利用 java.sql.Date

// java.sql.Date提供了一个方便的转换方法
public static LocalDate convertDateToLocalDateUsingSQLDate(Date date) {
    return new java.sql.Date(date.getTime()).toLocalDate();
}

Java LocalDate to Date

与上面类似, 还是两种方式: 利用Instant 和 java.sql.Date

1、利用Instant的例子

public static Date convertToDateUsingInstant(LocalDate date) {
    return java.util.Date.from(date.atStartOfDay()
            .atZone(ZoneId.systemDefault())
            .toInstant());
}

2、利用java.sql.Date

public static Date convertToDateUsingDate(LocalDate date) {
     return java.sql.Date.valueOf(date);
 }

Java LocalDateTime to Date

利用 Instant 对象 或者 Timestamp

package com.sptan;

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

public class LocalDateTimeToDateMain {

    public static void main(String[] args) {
        LocalDateTime ldt = LocalDateTime.now();
        Date dt1=convertLocalDateTimeToDateUsingInstant(ldt);
        System.out.println(dt1);
        System.out.println("=====================");
        Date dt2=convertLocalDateTimeToDateUsingTimestamp(ldt);
        System.out.println(dt2);
    }

    public static Date convertLocalDateTimeToDateUsingInstant(LocalDateTime localDateTime) {
        return Date
          .from(localDateTime.atZone(ZoneId.systemDefault())
          .toInstant());
    }

    public static Date convertLocalDateTimeToDateUsingTimestamp(LocalDateTime localDateTime) {
        return Timestamp.valueOf(localDateTime);
    }
}

Convert LocalDateTime to Timestamp in Java

package com.sptan;

import java.sql.Timestamp;
import java.time.LocalDateTime;

public class ConvertLocalDataTimeToTimestamp {
    public static void main(String[] args) {

        LocalDateTime current_date_time = LocalDateTime.now();
        //returns time and date object of today's date.
        //printing the time and date
        System.out.println("Local Date Time : " + current_date_time);

        //Timestamp object
        Timestamp timestamp_object = Timestamp.valueOf(current_date_time);
        System.out.println("Time stamp : " + timestamp_object);
    }
}

输出结果类似于:

Local Date Time : 2021-09-01T23:49:33.175092900
Time stamp : 2021-09-01 23:49:33.1750929

一些常见问题新旧日期的不同处理方法示例

取得当前时间

// Old
Date now = new Date();

// New
ZonedDateTime now = ZonedDateTime.now();

表示指定时间

// Old
Date birthDay = new GregorianCalendar(1990, Calendar.DECEMBER, 15).getTime();

// New
LocalDate birthDay = LocalDate.of(1990, Month.DECEMBER, 15);

提取特定字段

// Old
int month = new GregorianCalendar().get(Calendar.MONTH);

// New
Month month = LocalDateTime.now().getMonth();

对时间进行增减运算

// Old
GregorianCalendar calendar = new GregorianCalendar();
calendar.add(Calendar.HOUR_OF_DAY, -5);
Date fiveHoursBefore = calendar.getTime();

// New
LocalDateTime fiveHoursBefore = LocalDateTime.now().minusHours(5);

设置指定的字段

// Old
GregorianCalendar calendar = new GregorianCalendar();
calendar.set(Calendar.MONTH, Calendar.JUNE);
Date inJune = calendar.getTime();

// New
LocalDateTime inJune = LocalDateTime.now().withMonth(Month.JUNE.getValue());

截取

截取是指将指定的时间字段重置。

比如下面的例子分钟重置到0。

// Old
Calendar now = Calendar.getInstance();
now.set(Calendar.MINUTE, 0);
now.set(Calendar.SECOND, 0);
now.set(Calendar.MILLISECOND, 0);
Date truncated = now.getTime();

// New
LocalTime truncated = LocalTime.now().truncatedTo(ChronoUnit.HOURS);

时区转换

// CET为欧洲中部时区
// Old
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTimeZone(TimeZone.getTimeZone("CET"));
Date centralEastern = calendar.getTime();

// New
ZonedDateTime centralEastern = LocalDateTime.now().atZone(ZoneId.of("CET"));

取得时间跨度

// Old
GregorianCalendar calendar = new GregorianCalendar();
Date now = new Date();
calendar.add(Calendar.HOUR, 1);
Date hourLater = calendar.getTime();
long elapsed = hourLater.getTime() - now.getTime();

// New
LocalDateTime now = LocalDateTime.now();
LocalDateTime hourLater = LocalDateTime.now().plusHours(1);
Duration span = Duration.between(now, hourLater);

时间的格式化与解析

// Old
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date now = new Date();
String formattedDate = dateFormat.format(now);
Date parsedDate = dateFormat.parse(formattedDate);

// New
LocalDate now = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String formattedDate = now.format(formatter);
LocalDate parsedDate = LocalDate.parse(formattedDate, formatter);

一月之中的天数计数

// Old
Calendar calendar = new GregorianCalendar(1990, Calendar.FEBRUARY, 20);
int daysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);

// New
int daysInMonth = YearMonth.of(1990, 2).lengthOfMonth();