返回

Java 高效生成年份日期: 掌握 LocalDate 与 Stream 方法

java

Java 生成指定年份全部日期的高效方法

一、问题来了:如何优雅地获取一年中的所有日期?

有时候,我们需要拿到某个特定年份里的每一天日期,比如生成报表、做数据填充或者处理时间序列数据。你可能写过类似下面这样的代码,用 java.util.Calendar 来实现:

// 目标年份
int year = 2024;

// 构建开始日期:年份的第一天
Calendar calStart = new GregorianCalendar(year, Calendar.JANUARY, 1);
Date startDate = calStart.getTime();

// 构建结束日期:下一年的第一天(作为循环边界)
Calendar calEnd = new GregorianCalendar(year + 1, Calendar.JANUARY, 1);
Date endDate = calEnd.getTime();

List<Date> dates = new ArrayList<>();
Calendar calendar = new GregorianCalendar();
calendar.setTime(startDate);

// 循环,直到当前日期不再早于结束日期
while (calendar.getTime().before(endDate)) {
    // 将当前日期添加到列表
    dates.add(calendar.getTime());
    // 日期加一天
    calendar.add(Calendar.DATE, 1);
}

// 打印部分结果验证
// for (int i = 0; i < 5; i++) {
//     System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(dates.get(i)));
// }
// System.out.println("总天数: " + dates.size());

这段代码确实能工作,它设置了开始日期(年份的第一天)和结束日期(下一年份的第一天),然后用一个 while 循环,不断给当前日期加一天,直到达到结束日期为止,并将每天的日期存入一个 List<Date>

但是,这种写法感觉有点啰嗦,对吧?Calendar API 用起来也不是那么直观,比如月份是从 0 开始计数的 (Calendar.JANUARY 实际是 0),Date 对象本身是可变的,容易引发一些难以察觉的问题。那有没有更方便、更现代化的方法呢?

二、老办法 (Calendar) 的小麻烦在哪?

在深入看新方法之前,稍微聊聊为什么大家觉得 CalendarDate 不够理想:

  1. API 设计不够友好Calendar 的月份是从 0 开始的,这和我们日常习惯(1-12月)不一样,很容易搞错。各种 setgetadd 方法用起来也感觉有点笨重。
  2. 可变性(Mutability)java.util.Date 对象是可变的。这意味着你把它传递给一个方法后,那个方法可能会意外地修改它,或者你在列表里拿到的 Date 对象被其他地方修改了,这会造成混淆和潜在的 bug。想象一下,你从列表里取出一个日期想看看,结果发现它被改成了别的时间,是不是很抓狂?
  3. 时区处理复杂Date 本身不包含时区信息(它内部存的是自 epoch 以来的毫秒数),而 Calendar 又依赖系统默认时区,处理不同时区的日期时间容易出错。
  4. 代码冗长 :就像上面看到的,即使是生成一个简单的日期列表,也需要不少设置和循环代码。

因为这些原因,Java 8 引入了全新的日期和时间 API (java.time 包),也叫 JSR-310。这个新 API 就是来解决这些老问题的。

三、新思路:拥抱 java.time (JSR-310)

java.time 包提供了一套设计更优良、线程安全(大部分核心类是不可变的)且功能更强大的日期时间处理工具。对于生成一年中所有日期这个需求,java.time 提供了更简洁、易读的方案。

方案一:使用 LocalDate 和循环迭代

这是最直接的改进,用 java.time.LocalDate 替代 java.util.DateCalendarLocalDate 只表示日期(年-月-日),不包含时间或时区信息,正好符合我们的需求,而且它是不可变的。

原理与作用:

  1. 确定目标年份。
  2. 获取该年份的第一天 (LocalDate)。
  3. 获取该年份的最后一天 (LocalDate)。可以直接用 Year.of(year).length() 获取该年天数,然后循环这么多次;或者直接获取结束日期,用 isBeforeisAfter 判断循环条件。
  4. 使用 LocalDateplusDays(1) 方法进行日期递增,这个方法会返回一个新的 LocalDate 对象,原对象不变。
  5. 将每天生成的 LocalDate 添加到列表中。

代码示例:

import java.time.LocalDate;
import java.time.Year;
import java.time.Month;
import java.util.ArrayList;
import java.util.List;

public class GenerateYearDatesIterative {

    public static List<LocalDate> getDatesOfYear(int year) {
        List<LocalDate> dates = new ArrayList<>();
        // 获取年份的第一天
        LocalDate date = LocalDate.of(year, Month.JANUARY, 1);
        // 获取这一年的天数,自动处理闰年
        int daysInYear = Year.of(year).length();

        // 循环添加每一天
        for (int i = 0; i < daysInYear; i++) {
            dates.add(date);
            date = date.plusDays(1); // 产生新的日期对象
        }
        return dates;
    }

    // 或者用 while 循环判断结束日期
    public static List<LocalDate> getDatesOfYearAlternative(int year) {
        List<LocalDate> dates = new ArrayList<>();
        LocalDate startDate = LocalDate.of(year, Month.JANUARY, 1);
        // 获取该年的最后一天
        LocalDate endDate = LocalDate.of(year, Month.DECEMBER, 31);
        // 注意:如果是 year + 1 的第一天,循环条件是 isBefore
        // LocalDate endDateBoundary = LocalDate.of(year + 1, Month.JANUARY, 1);

        LocalDate currentDate = startDate;
        // 循环直到当前日期超过了该年的最后一天
        while (!currentDate.isAfter(endDate)) {
            dates.add(currentDate);
            currentDate = currentDate.plusDays(1);
        }
        return dates;
    }

    public static void main(String[] args) {
        int targetYear = 2024; // 2024 是闰年,有 366 天
        List<LocalDate> yearDates = getDatesOfYear(targetYear);

        System.out.println(targetYear + "年的第一天: " + yearDates.get(0));
        System.out.println(targetYear + "年的最后一天: " + yearDates.get(yearDates.size() - 1));
        System.out.println(targetYear + "年总天数: " + yearDates.size());

        List<LocalDate> yearDatesAlt = getDatesOfYearAlternative(targetYear);
         System.out.println(targetYear + "年总天数 (alternative): " + yearDatesAlt.size());
    }
}

代码说明:

  • LocalDate.of(year, Month.JANUARY, 1) 创建指定年份 1 月 1 日的日期对象。注意这里用 Month.JANUARY 枚举,比数字 0 更清晰。
  • Year.of(year).length() 直接返回该年的总天数,自动判断是否是闰年(如 2024 年返回 366)。
  • date.plusDays(1) 每次迭代将日期加一天。因为 LocalDate 是不可变的,这会返回一个新的 LocalDate 实例。
  • 整个过程非常清晰,代码量也减少了。

安全与使用建议:

  • 不可变性 : LocalDate 是不可变的,这意味着你得到的日期列表中的每个元素都是独立的,不会互相影响,也不会被外部代码意外修改,这让代码更健壮、易于推理。
  • 无时区 : LocalDate 不关心时区,它就是一个纯粹的日期。如果你需要处理带时区的日期时间,应该使用 ZonedDateTime。对于只关心某一年有哪些天来说,LocalDate 非常合适。

进阶使用技巧:

  • 如果你只需要处理日期,不关心时间,优先考虑 LocalDate
  • 可以结合 java.time.format.DateTimeFormatter 方便地将 LocalDate 格式化为各种字符串形式。
// import java.time.format.DateTimeFormatter;
// DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
// String formattedDate = yearDates.get(0).format(formatter); // "2024/01/01"

方案二:使用 java.time 和 Stream API (Java 8+)

对于喜欢函数式编程风格的同学,Java 8 引入的 Stream API 提供了一种更紧凑、表达力更强的方式。

原理与作用:

  1. 确定年份的开始日期 (LocalDate)。
  2. 确定年份的总天数或者结束日期。
  3. 使用 Stream.iterate 方法生成一个无限的日期流,起始于第一天,每次递增一天。
  4. 使用 limit 操作限制流的长度为该年的总天数。
  5. 使用 collect 操作将流中的日期收集到一个 List 中。
  6. (Java 9+): 可以使用 LocalDatedatesUntil 方法更直接地生成日期范围流。

代码示例 (Java 8 - 使用 Stream.iteratelimit):

import java.time.LocalDate;
import java.time.Year;
import java.time.Month;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class GenerateYearDatesStreamJava8 {

    public static List<LocalDate> getDatesOfYearUsingStream(int year) {
        LocalDate startDate = LocalDate.of(year, Month.JANUARY, 1);
        long daysInYear = Year.of(year).length(); // 获取该年天数

        // 生成日期流
        List<LocalDate> dates = Stream.iterate(startDate, date -> date.plusDays(1)) // 从 startDate 开始,每次加一天
                                     .limit(daysInYear) // 限制流包含的天数
                                     .collect(Collectors.toList()); // 收集到 List

        return dates;
    }

    public static void main(String[] args) {
        int targetYear = 2023; // 2023 是平年,有 365 天
        List<LocalDate> yearDates = getDatesOfYearUsingStream(targetYear);

        System.out.println(targetYear + "年的第一天: " + yearDates.get(0));
        System.out.println(targetYear + "年的最后一天: " + yearDates.get(yearDates.size() - 1));
        System.out.println(targetYear + "年总天数: " + yearDates.size());
    }
}

代码示例 (Java 9+ - 使用 LocalDate.datesUntil):

LocalDate 在 Java 9 中增加了一个非常有用的方法 datesUntil(endDate),可以直接生成一个从当前日期到指定结束日期(不含)的 Stream<LocalDate>

import java.time.LocalDate;
import java.time.Month;
import java.util.List;
import java.util.stream.Collectors;

// 注意:以下代码需要 Java 9 或更高版本
public class GenerateYearDatesStreamJava9 {

    public static List<LocalDate> getDatesOfYearUsingDatesUntil(int year) {
        LocalDate startDate = LocalDate.of(year, Month.JANUARY, 1);
        // 结束日期应该是下一年的一月一日,因为 datesUntil 不包含结束日期
        LocalDate endDateExclusive = LocalDate.of(year + 1, Month.JANUARY, 1);

        // 生成日期流并收集
        List<LocalDate> dates = startDate.datesUntil(endDateExclusive)
                                        .collect(Collectors.toList());

        return dates;
    }

    public static void main(String[] args) {
        // 确保你的运行环境是 Java 9+
        try {
            Class.forName("java.time.LocalDate").getMethod("datesUntil", LocalDate.class); // 简单检查方法是否存在
            
            int targetYear = 2024;
            List<LocalDate> yearDates = getDatesOfYearUsingDatesUntil(targetYear);
    
            System.out.println(targetYear + "年的第一天: " + yearDates.get(0));
            System.out.println(targetYear + "年的最后一天: " + yearDates.get(yearDates.size() - 1));
            System.out.println(targetYear + "年总天数: " + yearDates.size());

        } catch (ReflectiveOperationException e) {
            System.out.println("此代码需要 Java 9 或更高版本才能运行 datesUntil 方法。");
        }

    }
}

代码说明:

  • Java 8 版本: Stream.iterate(startDate, date -> date.plusDays(1)) 创建一个从 startDate 开始,后续元素是前一个元素加一天的无限流。limit(daysInYear) 截断流,只取该年份天数那么多个元素。最后 collect(Collectors.toList()) 把流中的日期收集起来。
  • Java 9+ 版本: startDate.datesUntil(endDateExclusive) 直接创建了从 startDate (包含) 到 endDateExclusive (不包含) 的所有日期的流。代码更简洁!注意 endDateExclusive 设置为下一年的一月一日。

安全与使用建议:

  • 和方案一一样,基于不可变的 LocalDate,线程安全。
  • Stream API 写法通常更紧凑,特别是 Java 9+ 的 datesUntil,代码可读性很强。
  • 对于大数据量处理,Stream 可以提供潜在的并行处理能力(虽然对于一年366天来说,并行优化意义不大,甚至可能有开销)。

进阶使用技巧:

  • 如果你不需要把所有日期都存到列表里,而是想对每一天执行某个操作,可以直接在 Stream 上进行处理,避免创建中间列表,更节省内存。
// 比如,直接打印出每一天
// (Java 9+ 版本示例)
// LocalDate startDate = LocalDate.of(2024, Month.JANUARY, 1);
// LocalDate endDateExclusive = LocalDate.of(2025, Month.JANUARY, 1);
// startDate.datesUntil(endDateExclusive)
//          .forEach(System.out::println);
  • 结合其他 Stream 操作,可以进行更复杂的日期筛选、转换等。例如,只获取某个年份的所有周一:
import java.time.DayOfWeek;
// (Java 9+ 版本示例)
// LocalDate startDate = LocalDate.of(2024, Month.JANUARY, 1);
// LocalDate endDateExclusive = LocalDate.of(2025, Month.JANUARY, 1);
// List<LocalDate> mondays = startDate.datesUntil(endDateExclusive)
//                                    .filter(date -> date.getDayOfWeek() == DayOfWeek.MONDAY)
//                                    .collect(Collectors.toList());

四、小结

生成指定年份所有日期的需求,虽然用老的 java.util.Calendar 也能实现,但代码显得有点笨重且易错。

Java 8 引入的 java.time API (LocalDate, Year, Stream) 提供了更现代化、更简洁、也更安全的解决方案:

  1. LocalDate 迭代法 :清晰直观,容易理解,代码量适中,性能良好。适合大多数场景。
  2. Stream API 法 :代码紧凑,尤其 Java 9+ 的 datesUntil 方法非常优雅,体现了函数式编程的风格。如果熟悉 Stream,这是个不错的选择。

通常推荐优先使用 java.time API (方案一或方案二),它们是 Java 处理日期时间的标准方式,能让你的代码更健壮、更易读、更易维护。