Java8 新的日期/时间API操作和示例

目录

1、Java8 新的日期/时间API操作

(1)LocalDate、LocalTime 和 LocalDateTime 时间类

(2)Instant、Duration 和 Period 时间间隔类

(3)Temporal、TemporalField 和 ChronoField

(4)时间、日期设置和修改

(5)时间、日期解析和格式化

(6)不同时区的时间处理

2、日期/时间 API 使用示例

(1)通过 Java 的基本语法来实现万年历

(2)Java 获取一年中所有的周六和周日


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 对象对日期进行灵活处理:

        TemporalAdjusterJava 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_DATEISO 格式的日期,例如:"2023-12-31"
  • DateTimeFormatter.ISO_LOCAL_TIMEISO 格式的时间,例如:"15:30:00"
  • DateTimeFormatter.ISO_LOCAL_DATE_TIMEISO 格式的日期时间,例如:"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]

        至此,全文结束。