参考文献

旧的日期和时间API的缺点

Date类的缺点

  • Date类中的大多数方法都已经过时,因为它们不是线程安全的,而且有一些设计缺陷.
  • Date类中的时间表示是基于1970年1月1日的毫秒数,这种表示方式不太方便处理日期和时间.

Calendar类的缺点

  • Calendar类不是线程安全的,如果多个线程同时访问一个Calendar实例,可能会导致不可预测的结果.
  • Calendar类中的月份是从0开始计数的,这种设计很容易导致错误.
  • Calendar类中的方法名不够直观,比如要设置月份要使用set(Calendar.MONTH, month),而不是setMonth(month).

DateFormat类的缺点

  • DateFormat类不是线程安全的,如果多个线程同时访问一个DateFormat实例,可能会导致不可预测的结果.
  • DateFormat类中的解析和格式化方法都很慢,因为它们需要进行字符串和日期之间的转换.

新的日期和时间API

  • java.time包的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
├── Clock.java
├── DateTimeException.java
├── DayOfWeek.java
├── Duration.java
├── Instant.java
├── LocalDate.java
├── LocalDateTime.java
├── LocalTime.java
├── Month.java
├── MonthDay.java
├── OffsetDateTime.java
├── OffsetTime.java
├── Period.java
├── Ser.java
├── Year.java
├── YearMonth.java
├── ZoneId.java
├── ZoneOffset.java
├── ZoneRegion.java
├── ZonedDateTime.java
├── chrono
│   ├── AbstractChronology.java
│   ├── ChronoLocalDate.java
│   ├── ChronoLocalDateImpl.java
│   ├── ChronoLocalDateTime.java
│   ├── ChronoLocalDateTimeImpl.java
│   ├── ChronoPeriod.java
│   ├── ChronoPeriodImpl.java
│   ├── ChronoZonedDateTime.java
│   ├── ChronoZonedDateTimeImpl.java
│   ├── Chronology.java
│   ├── Era.java
│   ├── HijrahChronology.java
│   ├── HijrahDate.java
│   ├── HijrahEra.java
│   ├── IsoChronology.java
│   ├── IsoEra.java
│   ├── JapaneseChronology.java
│   ├── JapaneseDate.java
│   ├── JapaneseEra.java
│   ├── MinguoChronology.java
│   ├── MinguoDate.java
│   ├── MinguoEra.java
│   ├── Ser.java
│   ├── ThaiBuddhistChronology.java
│   ├── ThaiBuddhistDate.java
│   ├── ThaiBuddhistEra.java
│   └── package-info.java
├── format
│   ├── DateTimeFormatter.java
│   ├── DateTimeFormatterBuilder.java
│   ├── DateTimeParseContext.java
│   ├── DateTimeParseException.java
│   ├── DateTimePrintContext.java
│   ├── DateTimeTextProvider.java
│   ├── DecimalStyle.java
│   ├── FormatStyle.java
│   ├── Parsed.java
│   ├── ResolverStyle.java
│   ├── SignStyle.java
│   ├── TextStyle.java
│   ├── ZoneName.java
│   └── package-info.java
├── package-info.java
├── temporal
│   ├── ChronoField.java
│   ├── ChronoUnit.java
│   ├── IsoFields.java
│   ├── JulianFields.java
│   ├── Temporal.java
│   ├── TemporalAccessor.java
│   ├── TemporalAdjuster.java
│   ├── TemporalAdjusters.java
│   ├── TemporalAmount.java
│   ├── TemporalField.java
│   ├── TemporalQueries.java
│   ├── TemporalQuery.java
│   ├── TemporalUnit.java
│   ├── UnsupportedTemporalTypeException.java
│   ├── ValueRange.java
│   ├── WeekFields.java
│   └── package-info.java
└── zone
├── Ser.java
├── TzdbZoneRulesProvider.java
├── ZoneOffsetTransition.java
├── ZoneOffsetTransitionRule.java
├── ZoneRules.java
├── ZoneRulesException.java
├── ZoneRulesProvider.java
└── package-info.java

LocalDate,LocalTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* A date without a time-zone in the ISO-8601 calendar system,
* such as {@code 2007-12-03}.
* <p>
* {@code LocalDate} is an immutable date-time object that represents a date,
* often viewed as year-month-day. Other date fields, such as day-of-year,
* day-of-week and week-of-year, can also be accessed.
* For example, the value "2nd October 2007" can be stored in a {@code LocalDate}.
* <p>
* This class does not store or represent a time or time-zone.
* Instead, it is a description of the date, as used for birthdays.
* It cannot represent an instant on the time-line without additional information
* such as an offset or time-zone.
* <p>
* The ISO-8601 calendar system is the modern civil calendar system used today
* in most of the world. It is equivalent to the proleptic Gregorian calendar
* system, in which today's rules for leap years are applied for all time.
* For most applications written today, the ISO-8601 rules are entirely suitable.
* However, any application that makes use of historical dates, and requires them
* to be accurate will find the ISO-8601 approach unsuitable.
*
* <p>
* This is a <a href="{@docRoot}/java/lang/doc-files/ValueBased.html">value-based</a>
* class; use of identity-sensitive operations (including reference equality
* ({@code ==}), identity hash code, or synchronization) on instances of
* {@code LocalDate} may have unpredictable results and should be avoided.
* The {@code equals} method should be used for comparisons.
*
* @implSpec
* This class is immutable and thread-safe.
*
* @since 1.8
*/
public final class LocalDate
implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
// ...略
}
/**
* A time without a time-zone in the ISO-8601 calendar system,
* such as {@code 10:15:30}.
* <p>
* {@code LocalTime} is an immutable date-time object that represents a time,
* often viewed as hour-minute-second.
* Time is represented to nanosecond precision.
* For example, the value "13:45.30.123456789" can be stored in a {@code LocalTime}.
* <p>
* This class does not store or represent a date or time-zone.
* Instead, it is a description of the local time as seen on a wall clock.
* It cannot represent an instant on the time-line without additional information
* such as an offset or time-zone.
* <p>
* The ISO-8601 calendar system is the modern civil calendar system used today
* in most of the world. This API assumes that all calendar systems use the same
* representation, this class, for time-of-day.
*
* <p>
* This is a <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a>
* class; use of identity-sensitive operations (including reference equality
* ({@code ==}), identity hash code, or synchronization) on instances of
* {@code LocalTime} may have unpredictable results and should be avoided.
* The {@code equals} method should be used for comparisons.
*
* @implSpec
* This class is immutable and thread-safe.
*
* @since 1.8
*/
public final class LocalTime
implements Temporal, TemporalAdjuster, Comparable<LocalTime>, Serializable
// ...略
}
  • LocalDate是一个不可变对象,它只提供简单的日期,并不包含当天的时间信息.此外它也不附带任何与时区相关的信息.
  • LocalTime是一个不可变对象,它只提供简单的时间,并不包含当天的日期信息.此外它也不附带任何与时区相关的信息.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Test
public void testLocalDateAndLocalTime() {
final LocalDate nowLocalDate = LocalDate.now();
log.info("当前日期: " + nowLocalDate);
final LocalDate date = LocalDate.of(2023, 4, 1);
log.info("当前日期: " + date);
final int year = date.getYear();
final Month month = date.getMonth();
final int dayOfYear = date.getDayOfYear();
final int dayOfMonth = date.getDayOfMonth();
final DayOfWeek dayOfWeek = date.getDayOfWeek();
final int lengthOfMonth = date.lengthOfMonth();
final int lengthOfYear = date.lengthOfYear();
final boolean leapYear = date.isLeapYear();
log.info("当前日期所属年份: {},月份:{},位于一年中的第几天:{},位于一个月中的第几天:{},位于一周中的周几:{}",
year, month, dayOfYear, dayOfMonth, dayOfWeek);
log.info("当前月份一共多少天:{},当前年一共多少天:{},是否是闰年:{}",
lengthOfMonth, lengthOfYear, leapYear);

final int year2 = date.get(ChronoField.YEAR);
final int month2 = date.get(ChronoField.MONTH_OF_YEAR);
final int day = date.get(ChronoField.DAY_OF_MONTH);
log.info("通过TemporalField实现子类获取年月日:{},{},{}",
year2, month2, day);

final LocalTime nowLocalTime = LocalTime.now();
final LocalTime time = LocalTime.of(16, 38, 5);
final int hour = nowLocalTime.getHour();
final int minute = nowLocalTime.getMinute();
final int second = nowLocalTime.getSecond();
log.info("当前时间:{},hour:{},minute:{},second:{}", nowLocalTime, hour, minute, second);

// 通过parse解析
final LocalDate parseLocalDate = LocalDate.parse("2023-04-01");
final LocalTime parseLocalTime = LocalTime.parse("16:38:05");
log.info("解析后的日期:{},解析后的时间:{}", parseLocalDate, parseLocalTime);
}

LocalDateTime

  • LocalDateTimeLocalDateLocalTime的合并类,它同时表示了日期和时间,但不带有时区信息.
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testLocalDateTime() {
final LocalDateTime now = LocalDateTime.now();
final LocalDate date = LocalDate.now();
final LocalTime time = LocalTime.now();
LocalDateTime dt1 = LocalDateTime.of(2013, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
log.info("now: {}\ndt1: {}\ndt2: {}\ndt3: {}\ndt4: {}\ndt5: {}", now,dt1, dt2, dt3, dt4, dt5);
}

Instant机器的日期和时间格式

  • 基本上它是以Unix元年时间(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的秒数进行计算
1
2
3
4
5
6
7
8
9
10
11
@Test
public void testInstant() {
final Instant instant = Instant.ofEpochSecond(3);
final Instant instant2 = Instant.ofEpochSecond(3, 0);
final Instant instant3 = Instant.ofEpochSecond(2, 1_000_000_000);
final Instant now = Instant.now();
log.info("now:{}", now);
log.info("instant1: {},instant2: {},instant3: {}", instant, instant2, instant3);
// 下面的是错误示例 会抛出 java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
final int i = now.get(ChronoField.DAY_OF_MONTH);
}

DurationPeriod时间间隔

  • Duration类表示时间的持续时间,通常用于表示两个时间点之间的时间间隔.它可以精确地表示秒、纳秒和毫秒之间的时间间隔.

  • Period类表示时间的周期,通常用于表示两个日期之间的时间间隔,如年、月、日等.它可以精确地表示年、月、日之间的时间间隔.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testDuration() {
final Duration durationTime = Duration.between(LocalTime.parse("16:38:05"), LocalTime.parse("20:35:05"));
final Duration durationInstant = Duration.between(Instant.ofEpochSecond(5), Instant.ofEpochSecond(3));
log.info("durationTime:{},durationInstant:{}", durationTime, durationInstant);
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes2 = Duration.of(3, ChronoUnit.MINUTES);
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
log.info("threeMinutes:{},threeMinutes2:{}", threeMinutes, threeMinutes2);
log.info("tenDays:{},threeWeeks:{},twoYearsSixMonthsOneDay:{}", tenDays, threeWeeks, twoYearsSixMonthsOneDay);
}

操纵日期

  • 使用withAttribute方法操作时间日期,withAttribute方法会创建对象的一个副本,并按照需要修改它的属性.
  • 注意点: 使用withAttribute方法不会修改原来的对象.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void userDateTime() {

// 使用withAttribute方法
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.withYear(2011);
LocalDate date3 = date2.withDayOfMonth(25);
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9);
log.info("d1:{},d2:{},d3:{},d4:{}", date1, date2, date3, date4);

LocalDate date5 = LocalDate.of(2014, 3, 18);
// 增加一周
LocalDate date6 = date1.plusWeeks(1);
// 减去三年
LocalDate date7 = date2.minusYears(3);
// 增加三个月
LocalDate date8 = date3.plus(6, ChronoUnit.MONTHS);
log.info("d5:{},d6:{},d7:{},d8:{}", date5, date6, date7, date8);
}
方法名 是否是静态方法 描述
from 依据传入的 Temporal对象创建对象实例
now 依据系统时钟创建 Temporal对象
of 由 Temporal对象的某个部分创建该对象的实例
parse 由字符串创建 Temporal对象的实例
atOffset 将 Temporal对象和某个时区偏移相结合
atZone 将 Temporal对象和某个时区相结合
format 使用某个指定的格式器将 Temporal对象转换为字符串(Instant类不提供该方法)
get 读取 Temporal对象的某一部分的值
minus 创建 Temporal对象的一个副本,通过将当前 Temporal对象的值减去一定的时长 创建该副本
plus 创建 Temporal对象的一个副本,通过将当前 Temporal对象的值加上一定的时长 创建该副本
with 以该 Temporal对象为模板,对某些状态进行修改创建该对象的副本

使用TemporalAdjusters

  • TemporalAdjuster的作用是提供一种通用的、灵活的方式来调整日期时间对象,可以实现各种日期时间调整的需求,比如调整到下一个工作日、下个月的第一天、下一个星期六等等.
1
2
3
4
5
6
7
8
9
10
import static java.time.temporal.TemporalAdjusters.*;

@Test
public void testTemporalAdjuster() {
LocalDate date1 = LocalDate.of(2023, 3, 18);
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(lastDayOfMonth());

log.info("d1:{},d2:{},d3{}", date1, date2, date3);
}
方法名 描述
dayOfWeekInMonth(int ordinal) 返回指定月份中第几个星期几,例如第2个星期五.
firstDayOfMonth() 返回当前日期所在月份的第一天日期.
firstDayOfNextMonth() 返回当前日期所在月份的下一个月的第一天日期.
firstDayOfNextYear() 返回当前日期所在年份的下一年的第一天日期.
firstDayOfYear() 返回当前日期所在年份的第一天日期.
firstInMonth(DayOfWeek dayOfWeek) 返回当前日期所在月份中第一个指定星期几的日期.
lastDayOfMonth() 返回当前日期所在月份的最后一天日期.
lastDayOfNextMonth() 返回当前日期所在月份的下一个月的最后一天日期.
lastDayOfNextYear() 返回当前日期所在年份的下一年的最后一天日期.
lastDayOfYear() 返回当前日期所在年份的最后一天日期.
lastInMonth(DayOfWeek dayOfWeek) 返回当前日期所在月份中最后一个指定星期几的日期.
next(DayOfWeek dayOfWeek) 返回当前日期之后的第一个指定星期几的日期.
previous(DayOfWeek dayOfWeek) 返回当前日期之前的第一个指定星期几的日期.
nextOrSame(DayOfWeek dayOfWeek) 返回当前日期之后的第一个指定星期几的日期,如果当前日期已经是指定星期几,则返回当前日期.
previousOrSame(DayOfWeek dayOfWeek) 返回当前日期之前的第一个指定星期几的日期,如果当前日期已经是指定星期几,则返回当前日期.
1
2
3
4
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	// 自定义TemporalAdjuster 
public class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek dow =DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
}
}
// <==>
date = date.with(temporal -> {
DayOfWeek dow =
DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
else if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
// <==>
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
temporal -> {
DayOfWeek dow =
DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
date = date.with(nextWorkingDay);

DateTimeFormatter格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
public void testDateFormat() {
LocalDate date = LocalDate.of(2023, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
log.info("s1:{}\ns2:{}", s1, s2);

LocalDate date1 = LocalDate.parse("20230318",
DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2023-03-18",
DateTimeFormatter.ISO_LOCAL_DATE);
log.info("s1:{}\ns2:{}", date1, date2);

// 创建自定义时间格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date3 = LocalDate.of(2023, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date4 = LocalDate.parse(formattedDate, formatter);
log.info("s1:{}\nformattedDate:{}\ns2:{}", date3, formattedDate,date4);

// 通过构造器来创建时间格式
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral("%")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral("%")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
}

ZoneId处理不同时区

  • 新的java.time.ZoneId类是老版java.util.TimeZone的替代品
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    @Test
public void testZoneId() {
ZoneId romeZone = ZoneId.of("Asia/Shanghai");
// 老的API转换为新的API
ZoneId zoneId = TimeZone.getDefault().toZoneId();

LocalDate date = LocalDate.of(2023, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2023, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
// 2023-03-18T13:45+08:00[Asia/Shanghai]
log.info("z1:{},z2:{},z3:{}", zdt1, zdt2, zdt3);
}
// 17:52:24.296 [main] INFO cn.holelin.test.jdk8.NewDateAndTimeTest -- z1:2023-03-18T00:00+08:00[Asia/Shanghai],z2:2023-03-18T13:45+08:00[Asia/Shanghai],z3:2023-04-01T17:52:24.295820+08:00[Asia/Shanghai]

新老API转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Date to LocalDateTime
* @param date
* @return
*/
public static LocalDateTime dateToLocalDateTime(Date date) {
return date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}

/**
* LocalDateTime to Date
* @param localDateTime
* @return
*/
public static Date localDateTimeToDate(LocalDateTime localDateTime) {
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}

/**
* LocalDate to Date
* @param localDate
* @return
*/
public static Date localDateToDate(LocalDate localDate) {
return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
}

/**
* Date to LocalDate
* @param date
* @return
*/
public static LocalDate dateToLocalDate(Date date) {
return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDate();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TimeUtilTest {

@Test
void dateToLocalDateTime() {
System.out.println(TimeUtil.dateToLocalDateTime(new Date()));
}

@Test
void localDateTimeToDate() {
System.out.println(TimeUtil.localDateTimeToDate(LocalDateTime.now()));
}

@Test
void localDateToDate() {
System.out.println(TimeUtil.localDateToDate(LocalDate.now()));
}

@Test
void dateToLocalDate() {
System.out.println(TimeUtil.dateToLocalDate(new Date()));
}
}

LocalDate -> LocalDateTime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LocalDate localDate = LocalDate.parse("2019-01-04");

//Beginning of the day
LocalDateTime localDateTime1 = localDate.atStartOfDay();
System.out.println(localDateTime1);

//Current time
LocalDateTime localDateTime2 = localDate.atTime(LocalTime.now());
System.out.println(localDateTime2);

//Specific time
LocalDateTime localDateTime3 = localDate.atTime(04, 30, 56);
System.out.println(localDateTime3);


LocalDateTime startOfDay = localDate.atStartOfDay(); // 获取当日开始时间
LocalDateTime endOfDay = startOfDay.plusDays(1).minusNanos(1); // 计算当日结束时间

LocalDateTime -> LocalDate

1
2
3
4
5
LocalDateTime localDateTime = LocalDateTime.now();

LocalDate localDate = localDateTime.toLocalDate();

System.out.println(localDate);