Java8 新的日期/时间API操作和示例
目录
(1)LocalDate、LocalTime 和 LocalDateTime 时间类
(2)Instant、Duration 和 Period 时间间隔类
(3)Temporal、TemporalField 和 ChronoField
1、Java8 新的日期/时间API操作
Java 旧的日期时间类 java.util.Date 和 java.util.Calendar 存在可变性,导致在多线程环境下使用时会存在线程安全问题。在新的 API 中,几乎所有的类都是不可变的,从而保证了线程安全性。此外,旧的 API 命名不清晰,使得日期时间处理相对困难。新的 API 使用了更清晰和直观的命名,使得代码更易读、更易写。
(1)LocalDate、LocalTime 和 LocalDateTime 时间类
LocalDate 和 LocalTime 是 Java 中 java.time 包下的两个日期时间类,用于分别表示日期和时间,它们不包含时区信息,仅仅表示日期或时间部分。//时间和日期可以进行分开
LocalDate:用于表示日期,包含年、月、日,但不包含时、分、秒和时区信息,可以使用 now() 方法获取当前日期,或者使用 of() 方法指定特定的年、月、日创建实例。
// 获取当前日期
LocalDate currentDate = LocalDate.now();
// 创建特定日期
LocalDate specificDate = LocalDate.of(2023, 12, 31);
// 获取年、月、日
int year = currentDate.getYear();
int month = currentDate.getMonthValue();
int day = currentDate.getDayOfMonth();
LocalTime:用于表示时间,包含时、分、秒和纳秒,但不包含日期和时区信息。也可以使用 now() 方法获取当前时间,或者使用 of() 方法指定特定的时、分、秒创建实例。
// 获取当前时间
LocalTime currentTime = LocalTime.now();
// 创建特定时间
LocalTime specificTime = LocalTime.of(12, 30, 0);
// 获取时、分、秒
int hour = currentTime.getHour();
int minute = currentTime.getMinute();
int second = currentTime.getSecond();
LocalDateTime:是 LocalDate 和 LocalTime 的合体。它同时表示了日期和时间,但不带有时区信息,你可以直接创建,也可以通过合并日期和时间对象构造,如下所示。
// 获取当前日期时间
LocalDateTime currentDateTime = LocalDateTime.now();
// 创建特定日期时间
LocalDateTime specificDateTime = LocalDateTime.of(2023, 12, 31, 12, 30, 0);
// 使用 LocalDate 和 LocalTime 构建 LocalDateTime
LocalDate localDate = LocalDate.of(2023, 12, 31);
LocalTime localTime = LocalTime.of(12, 30);
LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
(2)Instant、Duration 和 Period 时间间隔类
Instant、Duration 和 Period 是 Java 中用于处理时间间隔和持续时间的类。
Instant:用于表示时间线上的一个具体点,通常表示自 1970-01-01T00:00:00Z(即协调世界时)开始经过的秒数和纳秒数。可以使用 now() 方法获取当前的时间点,或者使用 ofEpochSecond()、ofEpochMilli() 等方法指定特定的秒数或毫秒数创建实例。
// 获取当前时间点
Instant currentInstant = Instant.now();
// 创建特定时间点
Instant specificInstant = Instant.ofEpochSecond(1630452000); // 2022-09-01T00:00:00Z
需要特别强调的一点,Instant 的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。所以,它无法处理那些我们非常容易理解的时间单位。比如下面这段语句:
int hour = Instant.now().get(ChronoField.HOUR_OF_DAY);
它会抛出如下异常:
Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: HourOfDay
Duration:用于表示时间段的持续时间,包含了以秒和纳秒为单位的时间间隔。可以使用 between() 方法计算两个时间点之间的持续时间,或者使用 ofSeconds()、ofMinutes() 等方法指定特定的时间间隔创建实例。
// 计算两个时间/时间点之间的持续时间:不支持使用LocalDate入参
Duration instantDuration = Duration.between(startInstant, endInstant);
Duration timeDuration = Duration.between(startTime, endTime);
Period:用于表示日期之间的时间段,以年、月、日为单位。可以使用 between() 方法计算两个日期之间的时间段,或者使用 of() 方法指定特定的年、月、日创建实例。
// 计算两个日期之间的时间段:不支持时间
Period period = Period.between(startDate, endDate);
// 创建特定时间段
Period specificPeriod = Period.ofYears(2); // 2年的时间段
(3)Temporal、TemporalField 和 ChronoField
对于一套新的 API,我们常常会为一些陌生的类而感到困惑,比如 Temporal、TemporalField 和 ChronoField。
Temporal 是 Java Date Time API 中的一个接口,它是所有日期和时间类的基本接口,定义了对日期时间操作的通用方法。//定义一个通用接口的好处是,它的类型可以对所有的子类类型进行抽象,使一套模板方法具有普适性
TemporalField 是一个接口,它定义了如何访问 Temporal 对象某个字段的值。ChronoField 枚举实现了这一接口,所以你可以很方便地使用 get 方法得到枚举元素的值,如下所示://ChronoField 是一个枚举类
// 通过 TemporalField 获取字段值
int month = LocalDate.now().get(ChronoField.MONTH_OF_YEAR);
int hour = LocalTime.now().get(ChronoField.CLOCK_HOUR_OF_DAY);
int week = LocalDateTime.now().get(ChronoField.ALIGNED_WEEK_OF_MONTH);
(4)时间、日期设置和修改
在 Java 中,可以使用 LocalDateTime 类来修改日期时间的各个部分。LocalDateTime 是不可变的,因此修改操作会返回一个新的实例,而不是修改原始实例。
以下是一些常见的 LocalDateTime 时间修改操作示例:
增减年、月、日、时、分、秒、纳秒:
LocalDateTime currentDateTime = LocalDateTime.now();
// 增加或减少年、月、日、时、分、秒、纳秒
LocalDateTime futureDateTime = currentDateTime.plusYears(1)
.minusMonths(3)
.plusDays(7)
.plusHours(2)
.minusMinutes(30)
.plusSeconds(15)
.minusNanos(500000000);
// 或者使用带有时间单位的方法
LocalDateTime modifiedDateTime = currentDateTime.plus(1, ChronoUnit.YEARS)
.minus(3, ChronoUnit.MONTHS)
.plus(7, ChronoUnit.DAYS)
.plus(2, ChronoUnit.HOURS)
.minus(30, ChronoUnit.MINUTES)
.plus(15, ChronoUnit.SECONDS)
.minus(500000000, ChronoUnit.NANOS);
设置特定的年、月、日、时、分、秒、纳秒:
LocalDateTime currentDateTime = LocalDateTime.now();
// 设置特定的年、月、日、时、分、秒、纳秒
LocalDateTime newDateTime = currentDateTime.withYear(2025)
.withMonth(10)
.withDayOfMonth(15)
.withHour(18)
.withMinute(45)
.withSecond(30)
.withNano(0);
使用 TemporalAdjuster 对象对日期进行灵活处理:
TemporalAdjuster 是 Java Date Time API 中的一个接口,用于根据特定的规则调整日期时间,例如,将日期调整为下一个工作日、月末、下个星期等。通过 TemporalAdjusters 类的静态工厂方法可以使用大量预定义的 TemporalAdjuster,代码如下所示:
LocalDateTime now = LocalDateTime.now();
LocalDateTime dayOfWeek = now.with(TemporalAdjusters.dayOfWeekInMonth(1, DayOfWeek.SUNDAY));
LocalDateTime lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth());
使用自定义的 TemporalAdjuster 对象:
正如我们看到的,使用 TemporalAdjuster 可以进行更加复杂的日期操作,那么如果没有找到符合要求的 TemporalAdjuster,我们也可以创建自定义的 TemporalAdjuster,方法很简单,只需要实现 TemporalAdjuster 接口即可,代码如下所示:
public class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int daysToAdd = 1;
if (dayOfWeek == DayOfWeek.FRIDAY) {
daysToAdd = 3; //星期五至星期一,加3天
} else if (dayOfWeek == DayOfWeek.SATURDAY) {
daysToAdd = 2; //星期六至星期一,加2天
}
//其他时间都是+1天
return temporal.plus(daysToAdd, ChronoUnit.DAYS);
}
public static void main(String[] args) {
LocalDate date = LocalDate.now();
TemporalAdjuster adjuster = new NextWorkingDay();
LocalDate nextWorkingDay = date.with(adjuster);
}
}
TemporalAdjuster 接口只声明了单一的一个方法:adjustInto,因此它也是一个函数式接口。
(5)时间、日期解析和格式化
DateTimeFormatter 是 Java Date Time API 中用于格式化和解析日期时间对象的类。
格式化日期时间对象为字符串:
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = dateTime.format(formatter);
解析字符串为日期时间对象:
String strDateTime = "2023-12-31 15:30:00";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime parsedDateTime = LocalDateTime.parse(strDateTime, formatter);
预定义的 DateTimeFormatter:
Java 提供了一些预定义的 DateTimeFormatter,方便进行常见格式化和解析操作:
- DateTimeFormatter.ISO_LOCAL_DATE:ISO 格式的日期,例如:"2023-12-31"
- DateTimeFormatter.ISO_LOCAL_TIME:ISO 格式的时间,例如:"15:30:00"
- DateTimeFormatter.ISO_LOCAL_DATE_TIME:ISO 格式的日期时间,例如:"2023-12-31T15:30:00"
- DateTimeFormatter.BASIC_ISO_DATE:基本的 ISO 格式日期,例如:"20231231"
使用示例:
//格式化
String format = localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
//解析
LocalDateTime parse = LocalDateTime.parse("2023-12-27T13:39:07", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
(6)不同时区的时间处理
时区的处理是新版日期和时间 API 新增加的重要功能,使用新版日期和时间 API 时区的处理被极大地简化了。新的 java.time.zoneId 类是老版 java.util.Timezone 的替代品。zoneId 类跟其他日期类一样,也是无法修改的。
获取默认时区和所有可用的时区:
//获取默认时区
ZoneId defaultZone = ZoneId.systemDefault(); // Asia/Shanghai
//获取所有可用的时区
Set<String> allZoneIds = ZoneId.getAvailableZoneIds();
每个特定的 zoneId 对象都是一个地区 ID 标识,地区 ID 都为 “{区域/城市}” 的格式,如:"Asia/Shanghai",这些地区集合的设定都由英特网编号分配机构(IANA)的时区数据库提供。
创建特定的时区实例:
ZoneId newYorkZone = ZoneId.of("America/New_York");
ZoneId londonZone = ZoneId.of("Europe/London");
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
将时区应用到日期时间对象:
LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime newYorkDateTime = localDateTime.atZone(newYorkZone);
ZonedDateTime londonDateTime = localDateTime.atZone(londonZone);
ZonedDateTime tokyoDateTime = localDateTime.atZone(tokyoZone);
//默认时区
ZoneId defaultZone = ZoneId.systemDefault();
ZonedDateTime defaultZoneTime = localDateTime.atZone(defaultZone);
//defaultZoneTime内容:
2023-12-28T09:29:30.277+08:00[Asia/Shanghai]
在不同时区之间转换时间:
ZonedDateTime tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
ZonedDateTime londonTime = tokyoTime.withZoneSameInstant(ZoneId.of("Europe/London"));
ZoneId 可以用于将本地日期时间转换为特定时区的日期时间,并且允许在不同时区之间进行时间转换。它是处理全球时区的重要工具,在跨越不同时区的应用程序中特别有用。
在示例中,带时区信息的时间使用 ZonedDateTime 进行表示,那么 ZonedDateTime、LocaleDate、LocalTime、LocalDateTime 以及 ZoneId 之间有什么差异呢?
下边一张图,可以清晰的解释它们之间的区别:
2、日期/时间 API 使用示例
(1)通过 Java 的基本语法来实现万年历
通过年份和月份的天数进行万年历计算:
import java.util.Scanner;
/**
* @author swadian2008
*/
public class DateUtils {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入年:");
int year = sc.nextInt();
System.out.println("请输入月份:");
int month = sc.nextInt();
//1.计算1900.1.1到输入年的天数
int dayOfYear = 0;
for (int i = 1900; i < year; i++) {
if (i % 4 == 0 && i % 100 != 0 || i % 400 == 0) { // 闰年
dayOfYear += 366;
} else {
dayOfYear += 365;
}
}
//2.计算1月到输入月的天数
int dayOfMonth = 0;
for (int i = 1; i < month; i++) {
switch (i) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
dayOfMonth += 31;
break;
case 4:
case 6:
case 9:
case 11:
dayOfMonth += 30;
break;
case 2:
if ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)) {
dayOfMonth += 29;
} else {
dayOfMonth += 28;
}
break;
}
}
//3.获取输入月的天数
int day = 0;
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
day = 31;
break;
case 4:
case 6:
case 9:
case 11:
day = 30;
break;
case 2:
if ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)) {
day = 29;
} else {
day = 28;
}
break;
}
//4.计算星期
int allDay = dayOfYear + dayOfMonth + 1;
int week = allDay % 7; // 计算余数在星期中的位置
int count = 0;// 计数器,记录日期的空格
System.out.println("星期日\t星期一\t星期二\t星期三\t星期四\t星期五\t星期六");
//5.打印空格
for (int i = 1; i <= week; i++) {
System.out.print("\t\t\t");
count++;
}
//6. 打印日历
for (int i = 1; i <= day; i++) {
if (i < 10) { // 为了格式化
System.out.print(i + "\t\t\t");
} else {
System.out.print(i + "\t\t");
}
count++;
//若记录数是七的倍数,换行输出
if (count % 7 == 0) {
System.out.println();
}
}
}
}
输出结果图示:
(2)Java 获取一年中所有的周六和周日
下边的示例中使用了一个Map来收集一年当中的周六和周日的日期,统计年份可以根据需要进行调节,下边代码也适合用来处理自定义节假日的需求:
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;
import static java.time.temporal.TemporalAdjusters.firstInMonth;
/**
* @author swadian2008
*/
public class WeekDay {
public static void main(String[] args) {
//收集2023年所有的周六和周日
int year = 2023;
int month = 1;
int day = 1;
//按月份统计周六和周日
Map<Integer, List<Integer>> weekMap = new HashMap<>(12);
//创建代表一年中第一天的LocalDate对象
LocalDate localDate = LocalDate.of(year, month, day);
//收集所有星期六
collectDayOfWeek(weekMap, localDate, DayOfWeek.SATURDAY, year);
//收集所有星期日
collectDayOfWeek(weekMap, localDate, DayOfWeek.SUNDAY, year);
Map<Integer, List<Integer>> sortWeekMap = weekMap.entrySet().stream().collect(Collectors.toMap(Entry::getKey, r -> r.getValue().stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList())));
for (Entry<Integer, List<Integer>> map : sortWeekMap.entrySet()) {
System.out.println(map.getKey() + "月:" + map.getValue().toString());
}
}
private static void collectDayOfWeek(Map<Integer, List<Integer>> weekMap, LocalDate localDate, DayOfWeek dayOfWeek, int year) {
//获取一年中的第一个指定日期(星期六或星期日)
LocalDate weekday = localDate.with(firstInMonth(dayOfWeek));
while (weekday.getYear() == year) {
int month = weekday.get(ChronoField.MONTH_OF_YEAR);
int day = weekday.getDayOfMonth();
List<Integer> integers = weekMap.get(month);
if (Objects.nonNull(integers) && !integers.isEmpty()) {
integers.add(day);
} else {
integers = new ArrayList<>();
integers.add(day);
weekMap.put(month, integers);
}
//向后迭代一个星期
weekday = weekday.plus(Period.ofDays(7));
}
}
}
输出结果:
1月:[1, 7, 8, 14, 15, 21, 22, 28, 29]
2月:[4, 5, 11, 12, 18, 19, 25, 26]
3月:[4, 5, 11, 12, 18, 19, 25, 26]
4月:[1, 2, 8, 9, 15, 16, 22, 23, 29, 30]
5月:[6, 7, 13, 14, 20, 21, 27, 28]
6月:[3, 4, 10, 11, 17, 18, 24, 25]
7月:[1, 2, 8, 9, 15, 16, 22, 23, 29, 30]
8月:[5, 6, 12, 13, 19, 20, 26, 27]
9月:[2, 3, 9, 10, 16, 17, 23, 24, 30]
10月:[1, 7, 8, 14, 15, 21, 22, 28, 29]
11月:[4, 5, 11, 12, 18, 19, 25, 26]
12月:[2, 3, 9, 10, 16, 17, 23, 24, 30, 31]
至此,全文结束。