时间旅行:Java 中的 Date 和 DateFormat,以及现代日期时间处理最佳实践

引言

时间,无疑是宇宙最神秘的艺术之一。在软件开发的世界中,时间是一种珍贵的资源,它如同沙漏中的细沙,不停地从我们手中溜走。当我们构建应用程序时,时间扮演着重要的角色,它贯穿于事件的发生、数据的记录, 甚至用户界面的呈现。
为了与时间打交道,Java 提供了 DateDateFormat 这两个关键工具。Date 类是我们的时间捕手,它能够获取当前日期和时间,甚至允许我们进行日期比较和格式化。而 DateFormat 则是时间的翻译官,它负责将日期时间转换为人类可读的字符串,并允许我们解析用户输入的日期。
然而,尽管它们是强大的工具,但它们也带来了许多陷阱和挑战。如何处理时区问题?如何在多线程环境中安全使用它们?如何实现国际化的日期时间格式?这就是我们将要探索的内容。
在这个时间之旅中,我们将不仅仅了解这两个类的使用方法,还将深入探讨它们的局限性,以及如何使用 Java 8 引入的 java.time 包来优雅地处理日期和时间。同时,我们还会揭示时间背后的一些最佳实践,包括如何处理日期范围和区间,以及时区的重要性。
让我们一起踏上这段有趣而富有挑战的时间之旅,探索时间的奥秘,解锁时间的力量,为我们的应用程序注入更多时间的价值。

一、Date 类

1. Date 类概述

Date 类用于在Java中表示日期和时间。它是Java标准库中用于处理日期和时间的基本类之一。

1.1 日期和时间的表示

日期和时间的表示通常涉及以下几个方面:

  • 日期:表示年、月、日,如 "2023-10-29" 表示2023年10月29日。
  • 时间:表示时、分、秒,如 "15:30:00" 表示下午3点30分。
  • 日期时间:同时包括日期和时间信息,如 "2023-10-29 15:30:00" 表示2023年10月29日下午3点30分。
  • 时区:由于不同地区使用不同的时区,同一时刻在不同时区可能对应不同的实际时间。

1.2 Java 中的 Date 类

Date 类是Java标准库中用于表示日期和时间的类,它包含了一个长整型数,表示自1970年1月07:30:00(以SGT即新加坡标准时区为准)以来的毫秒数。这个长整型数被称为"Unix时间"或"时间戳"。

1.3 Date 对象的创建

可以通过以下方式创建 Date 对象:

示例

java 复制代码
// 获取当前时间的时间戳
Date now = new Date();
System.out.println(now); // 输出当前日期和时间

// 创建特定日期时间的 Date 对象
long timestamp = 1603960800000L; // 2020-10-29 16:40:00的时间戳
Date specificDate = new Date(timestamp);
System.out.println(specificDate); // 输出指定日期和时间

运行结果如下

bash 复制代码
Sat Nov 04 09:57:48 SGT 2023
Thu Jan 01 07:30:00 SGT 1970

Sat Nov 04 09:57:48 SGT 2023即"星期六 11月 4号 9点57分48秒 新加坡标准时间 2023年" 中国话叫:2023年11月4号上午9点57分48秒,星期六。

1.4 日期和时间的时区问题

日期和时间的时区问题在全球化的背景下变得尤为重要。不同国家和地区使用不同的时区,而且一些地区还可能会在夏季采用夏令时。这使得处理日期和时间变得相当复杂。

在 Java 中,Date 类包含日期和时间信息以及默认的时区信息。这意味着 Date 对象会随着默认时区的更改而调整其内部时间表示,Date 类只会输出默认时区的时间。但也可以结合TimeZone类进行更精确的时区控制。

下面我们来看一个示例,展示了如何在不同的时区中处理日期和时间:

java 复制代码
import java.util.Date;
import java.util.TimeZone;

public class TimeZoneExample {
    public static void main(String[] args) {
        // 创建一个日期对象,表示特定的日期和时间
        Date date = new Date();

        // 保存当前系统的默认时区
        TimeZone defaultTimeZone = TimeZone.getDefault();

        // 指定目标时区
        TimeZone timeZone = TimeZone.getTimeZone("GMT");
        TimeZone.setDefault(timeZone);

        // 输出 GMT 时间
        System.out.println("GMT 时间: " + date);

        // 指定目标时区
        timeZone=TimeZone.getTimeZone("America/New_York");
        TimeZone.setDefault(timeZone);

        // 输出 纽约 时间
        System.out.println("在纽约的时间: " + date);

        // 恢复原来的默认时区
        TimeZone.setDefault(defaultTimeZone);
    }
}

运行结果如下:

bash 复制代码
GMT 时间: Sat Nov 04 02:15:59 GMT 2023
在纽约的时间: Fri Nov 03 22:15:59 EDT 2023

2. Date 对象操作

2.1 获取当前日期和时间

要获取当前日期和时间,可以使用 Date 类的无参构造函数,它会创建一个表示当前日期和时间的 Date 对象。示例如下:

java 复制代码
// 获取当前日期和时间
Date currentDate = new Date();
System.out.println(currentDate);

2.2 获取毫秒数

Date 对象内部保存了自1970年1月07:30:00 (以SGT即新加坡标准时区为准) 以来的毫秒数。

我们可以使用 getTime() 方法来获取这个毫秒数。

java 复制代码
// 获取 Date 对象的毫秒数
Date date = new Date();
long timestamp = date.getTime();
System.out.println("Unix时间戳: " + timestamp);

运行时间及结果: 2023/11/04 10:40 Unix时间戳 : 1699065624934

2.3 比较日期

可以使用 Date 对象的方法来比较日期,如比较两个日期是否相等、日期的先后顺序等。

示例

java 复制代码
// 创建两个 Date 对象
Date date1 = new Date();// 即当前时间: 2023-11-04 10:45:00
Date date2 = new Date(1603960800000L);//2020-10-29 16:40:00

// 比较两个日期是否相等
boolean isEqual = date1.equals(date2);
System.out.println("日期1和日期2是否相等: " + isEqual);

// 比较两个日期的先后顺序
int comparison = date1.compareTo(date2);
if (comparison < 0) {
    System.out.println("日期1在日期2之前");
} else if (comparison > 0) {
    System.out.println("日期1在日期2之后");
} else {
    System.out.println("日期1和日期2相等");
}

运行结果如下:

arduino 复制代码
日期1和日期2是否相等: false
日期1在日期2之后

2.4 日期的格式化和解析

为了将 Date 对象以人类可读的方式显示,或者将人类输入的日期字符串解析为 Date 对象,通常需要进行日期的格式化和解析。

java 复制代码
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.util.Date;

// 创建一个 SimpleDateFormat 对象用于日期格式化和解析
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// 日期格式化为字符串
Date date = new Date();
String formattedDate = dateFormat.format(date);
System.out.println("格式化后的日期: " + formattedDate);

// 字符串解析为日期
String dateString = "2023-10-29 15:30:00";
try {
    Date parsedDate = dateFormat.parse(dateString);
    System.out.println("解析后的日期: " + parsedDate);
} catch (ParseException e) {
    System.out.println("日期解析失败:" + e.getMessage());
}

运行结果如下:

bash 复制代码
格式化后的日期: 2023-11-04 10:48:31
解析后的日期: Sun Oct 29 15:30:00 SGT 2023

2.5 Date 的可变性和不可变性

Date 类是可变的,这意味着可以通过 set 方法修改 Date 对象的值。这可能会导致线程安全性问题,因此在多线程环境中使用时需要格外小心。

java 复制代码
// Date 对象的可变性示例
Date mutableDate = new Date();
System.out.println("可变日期:" + mutableDate);

// 修改 Date 对象的值
mutableDate.setTime(1603960800000L);
System.out.println("修改后的日期:" + mutableDate);

运行结果如下:

bash 复制代码
可变日期:Sat Nov 04 11:07:58 SGT 2023
修改后的日期:Thu Oct 29 16:40:00 SGT 2020

在实际应用中,为了避免线程安全问题,推荐使用不可变的日期和时间处理类,如 Java 8 中的 LocalDateLocalTimeLocalDateTime 等。

3. Date 类的问题

3.1 Date 类存在的问题

传统的 Date 类存在一些问题,这些问题主要包括:

  • 可变性 : Date 类是可变的,这意味着一旦创建,其值可以随时修改。这会导致潜在的线程安全问题和不可预测的行为。

  • 精度问题 : Date 类只能精确到毫秒级,不能表示更小的时间单位,例如纳秒。

  • 时区问题 : Date 类在处理时区时不够灵活,通常需要通过手动设置时区来进行处理,容易出错。

  • 命名问题 : Date 类中的一些方法的命名不够清晰,容易引起歧义。

3.2 时区问题的处理

Date 类在处理时区问题时存在困难。为了解决这个问题,可以使用 java.time 包中的新类,如 ZonedDateTimeZoneId。这些类提供了更灵活的时区支持,能够轻松处理不同时区的日期和时间。

示例

java 复制代码
// 使用 ZonedDateTime 处理时区问题
ZonedDateTime dateTimeInNewYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime dateTimeInTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

// 比较两个不同时区的日期和时间
if (dateTimeInNewYork.isEqual(dateTimeInTokyo)) {
    System.out.println("纽约和东京的时间相同。");
}else{
    System.out.println("纽约和东京的时间不同。");
}
System.out.println(dateTimeInNewYork);
System.out.println(dateTimeInTokyo);

运行结果如下:

bash 复制代码
纽约和东京的时间不同。
2023-11-03T23:08:53.777344300-04:00[America/New_York]
2023-11-04T12:08:53.783692300+09:00[Asia/Tokyo]

3.3 Date 类和多线程问题

由于 Date 类的可变性,它在多线程环境下容易引发线程安全问题。多个线程同时访问和修改同一个 Date 对象可能导致不一致的结果。为了解决这个问题,可以使用 java.time 包中的不可变类,如 LocalDateTimeLocalDateLocalTime,它们在多线程环境下更安全。

示例

java 复制代码
// 使用 LocalDateTime 类,它是不可变的
    
LocalDateTime currentDateTime = LocalDateTime.now();

// 在多线程环境下更安全

4. 替代方案 - java.time 包

4.1 为什么要使用 java.time

在 Java 8 中引入了 java.time 包,它提供了更加强大和灵活的日期和时间处理功能,相比于传统的 Date 类,它具有以下优势:

  • 不可变性 : java.time 包中的大多数类都是不可变的,这意味着一旦创建,它们的值不能被修改,这有助于避免意外的修改和副作用。

  • 丰富的方法 : java.time 包提供了丰富的方法来处理日期和时间,包括日期的创建、计算、格式化、解析等。这些方法使得日期和时间操作更加方便。

  • 清晰的命名 : java.time 包中的类和方法都有清晰的命名,使得代码更易理解和维护。

  • 处理时区 : java.time 包提供了更好的时区支持,可以轻松处理不同时区的日期和时间。

4.2 LocalDateTime、LocalDate 和其他类

java.time 包中引入了一系列新的日期和时间类,包括:

  • LocalDateTime: 表示日期和时间,不包含时区信息。
  • LocalDate: 表示日期,不包含时分秒。
  • LocalTime: 表示时间,不包含日期。
  • ZonedDateTime: 表示带时区的日期和时间。
  • ZoneId: 表示时区。

示例

java 复制代码
// 创建 LocalDateTime 对象表示当前日期和时间
LocalDateTime currentDateTime = LocalDateTime.now();

// 创建 LocalDate 对象表示当前日期
LocalDate currentDate = LocalDate.now();

// 创建 LocalTime 对象表示当前时间
LocalTime currentTime = LocalTime.now();

4.3 与 Date 类的比较

Date 类相比,java.time 包中的类更容易使用和理解。例如,要比较两个日期是否相等,使用 LocalDate 类的 isEqual 方法更加清晰和安全:

java 复制代码
LocalDate date1 = LocalDate.of(2023, 10, 1);
LocalDate date2 = LocalDate.of(2023, 10, 1);

if (date1.isEqual(date2)) {
    System.out.println("日期相等");
}

4.4 处理时区的新方法

java.time 包中的 ZonedDateTime 类允许处理带时区的日期和时间。它可以表示不同时区的日期和时间,并提供了时区的支持。通过 ZoneId 类,可以获取系统支持的时区列表。

示例

java 复制代码
// 创建一个带有时区信息的 ZonedDateTime 对象,表示当前美国纽约的时间
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("当前纽约时间: " + zonedDateTime);

// 获取系统支持的所有时区标识符
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();

// 遍历所有时区标识符并打印出来
availableZoneIds.forEach(System.out::println);

运行结果如下:

bash 复制代码
当前纽约时间: 2023-11-03T23:24:25.782088800-04:00[America/New_York]
Asia/Aden
America/Cuiaba
Etc/GMT+9
Etc/GMT+8
...(略)

java.time 包提供了更多的功能和方法,用于日期和时间的操作和计算。它是处理日期和时间的首选替代方案,特别适用于需要处理多时区、复杂日期计算和格式化的应用。

二、DateFormat 类

1. DateFormat 类概述

DateFormat 类是 Java 中用于日期和时间的格式化和解析的类。它允许将日期和时间格式化为字符串,也可以将字符串解析为日期和时间。以下是关于 DateFormat 类的一些概述:

1.1 日期和时间的格式化和解析

DateFormat 类主要用于以下两个任务:

  • 格式化:将日期和时间对象格式化为字符串,以便进行显示或存储。这将日期和时间表示为可读高的字符串。

  • 解析 :即格式化的逆过程。将日期和时间的字符串表示解析为日期和时间对象。这用于从用户输入或外部数据源中还原日期和时间。

1.2 Java 中的 DateFormat 类

在 Java 中,DateFormat 类是一个抽象类,它有多个实现类,如 SimpleDateFormat。这些实现类提供了不同的日期和时间格式化和解析功能,以满足各种需求。

1.3 不同国家和地区的日期格式

DateFormat 类支持不同国家和地区的日期和时间格式。它可以根据特定的 Locale(语言和地区)来格式化和解析日期。这意味着相同的日期和时间可以以不同的格式显示,根据不同的地区和语言习惯。

示例

java 复制代码
// 创建一个 DateFormat 对象,使用美国地区的日期格式
DateFormat dfUS = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US);
Date date = new Date();
String formattedDate = dfUS.format(date);
System.out.println("美国日期格式:" + formattedDate);

// 创建一个 DateFormat 对象,使用法国地区的日期格式
DateFormat dfFR = DateFormat.getDateInstance(DateFormat.SHORT, Locale.FRANCE);
String formattedDateFR = dfFR.format(date);
System.out.println("法国日期格式:" + formattedDateFR);

运行结果如下:

bash 复制代码
美国日期格式:11/4/23
法国日期格式:04/11/2023

1.4 使用不同日期样式

DateFormat 类提供了不同的日期和时间样式,包括 SHORTMEDIUMLONGFULL。每种样式代表了不同的日期和时间格式。通过选择不同的样式,可以获得不同粒度的日期和时间信息。

示例

java 复制代码
// 使用 MEDIUM 样式格式化日期和时间
DateFormat dfMedium = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,DateFormat.MEDIUM);
System.out.println("MEDIUM 样式:" + dfMedium.format(date));

// 使用 FULL 样式格式化日期和时间
DateFormat dfFull = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL);
System.out.println("FULL 样式:" + dfFull.format(date));

// 使用 Long 样式格式化日期和时间
DateFormat dfLong = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
System.out.println("Long 样式:" + dfLong.format(date));

运行结果如下:

bash 复制代码
MEDIUM 样式:2023年11月4日 下午2:42:43
FULL 样式:2023年11月4日星期六 新加坡标准时间 下午2:42:43
Long 样式:2023年11月4日 SGT 下午2:51:30

2. SimpleDateFormat 类

SimpleDateFormat 类是 Java 中用于日期和时间的格式化和解析的主要类。它是 DateFormat 类的一个具体实现,提供了更灵活的日期和时间格式化和解析功能。以下是关于 SimpleDateFormat 类的一些重要内容:

2.1 格式化日期和时间

SimpleDateFormat 主要用于将日期和时间对象格式化为字符串。它允许我们根据模式字符串定义日期和时间的显示格式。以下是一个简单的示例:

java 复制代码
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date now = new Date();
String formattedDate = sdf.format(now);
System.out.println("格式化后的日期:" + formattedDate);

运行结果如下:

bash 复制代码
格式化后的日期:2023-11-04 14:54:18

上述代码中,我们创建了一个 SimpleDateFormat 对象,使用模式字符串 "yyyy-MM-dd HH:mm:ss" 来指定日期和时间的格式。然后,我们将当前日期和时间对象格式化为指定格式的字符串。

2.2 自定义日期格式

SimpleDateFormat 允许我们根据需要自定义日期格式。模式字符串中包含不同的字符,它们代表日期和时间的不同部分。以下是一些常用的模式字符:

  • "yyyy":四位年份
  • "MM":月份
  • "dd":日期
  • "HH":小时(24 小时制)
  • "mm":分钟
  • "ss":秒

通过组合这些模式字符,可以创建自定义日期格式。例如:

java 复制代码
SimpleDateFormat customSdf = new SimpleDateFormat("dd/MM/yyyy HH:mm");
String customFormattedDate = customSdf.format(now);
System.out.println("自定义格式的日期:" + customFormattedDate);

运行结果如下:

bash 复制代码
自定义格式的日期:04/11/2023 14:57

2.3 解析日期和时间字符串

SimpleDateFormat 也可以用于将日期和时间的字符串表示解析为日期和时间对象。我们需要使用相同格式的模式字符串来告诉 SimpleDateFormat 如何解析字符串。以下是一个解析的示例:

java 复制代码
String dateString = "2023-10-30 15:30:00";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
    Date parsedDate = sdf.parse(dateString);
    System.out.println("解析后的日期:" + parsedDate);
} catch (ParseException e) {
    e.printStackTrace();
}

运行结果如下:

bash 复制代码
解析后的日期:Mon Oct 30 15:30:00 SGT 2023

上述代码中,我们创建了一个 SimpleDateFormat 对象,使用相同的模式字符串 "yyyy-MM-dd HH:mm:ss" 来指导解析。然后,我们尝试将日期和时间的字符串表示解析为日期对象。

2.4 SimpleDateFormat 的线程安全问题和解决方案

SimpleDateFormat 不是线程安全的类。如果多个线程同时访问同一个 SimpleDateFormat 实例,可能会导致解析和格式化错误。为了解决这个问题,可以采取以下两种方法之一:

  1. 使用线程局部变量(ThreadLocal ):每个线程都有自己的 SimpleDateFormat 实例,避免多线程竞争。

  2. 使用 java.time.format.DateTimeFormatterjava.time 包提供的日期时间 API 是线程安全的,可以考虑迁移到新的 API。

java 复制代码
ThreadLocal<SimpleDateFormat> threadLocalSdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
SimpleDateFormat sdf = threadLocalSdf.get();

或者,使用 java.time.format.DateTimeFormatter

java 复制代码
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String formattedDate = dtf.format(now);

3. DateFormat 类的线程安全问题

3.1 DateFormat 对象的线程安全性

DateFormat 类的对象在多线程环境中不是线程安全的。这是因为 DateFormat 类的实例包含了内部状态,用于格式化和解析日期和时间。多个线程同时访问同一个 DateFormat 对象可能导致竞争条件和不确定的结果。

3.2 SimpleDateFormat 的线程安全问题

SimpleDateFormatDateFormat 类的一个具体实现,它继承了 DateFormat 的线程不安全性。具体来说,多个线程同时访问同一个 SimpleDateFormat 实例进行格式化或解析操作可能导致混乱或错误的结果。

以下是一个简单的示例,展示了 SimpleDateFormat 的线程安全问题:

java 复制代码
// 3.2 
import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

public class DateFormatThreadSafetyExample {

    private static final AtomicBoolean STOP = new AtomicBoolean(); // 创建一个原子布尔值,用于控制线程停止
    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-M-d"); // 创建一个日期格式化对象,格式为"yyyy-M-d"

    public static void main(String[] args) {
        Runnable runnable = () -> { // 创建一个Runnable对象,用于执行线程任务
            int count = 0; // 初始化计数器
            while (!STOP.get()) { // 当STOP值为false时,循环执行以下操作
                try {
                    FORMATTER.parse("2023-11-3"); // 尝试解析日期字符串"2023-11-3"
                } catch (Exception e) { // 如果解析失败,捕获异常
                    e.printStackTrace(); // 打印异常堆栈信息
                    if (++count > 3) { // 如果计数器大于3,将STOP值设为true,停止线程
                        STOP.set(true);
                    }
                }
            }
        };
        new Thread(runnable).start(); // 创建并启动一个新线程,执行runnable任务
        new Thread(runnable).start(); // 创建并启动另一个新线程,执行runnable任务
    }
}

运行结果如下:

bash 复制代码
java.lang.NumberFormatException: multiple points
        at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
        at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.base/java.lang.Double.parseDouble(Double.java:651)
        at java.base/java.text.DigitList.getDouble(DigitList.java:169)
        at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2202)
        at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2244)
        at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1545)
        at java.base/java.text.DateFormat.parse(DateFormat.java:397)
        at Test.DateFormatThreadSafetyExample.lambda$0(DateFormatThreadSafetyExample.java:16)
        at java.base/java.lang.Thread.run(Thread.java:833)
java.lang.NumberFormatException: multiple points
        at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
        at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.base/java.lang.Double.parseDouble(Double.java:651)
        at java.base/java.text.DigitList.getDouble(DigitList.java:169)
        at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2202)
        at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1937)
        at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1545)
        at java.base/java.text.DateFormat.parse(DateFormat.java:397)
        at Test.DateFormatThreadSafetyExample.lambda$0(DateFormatThreadSafetyExample.java:16)
        at java.base/java.lang.Thread.run(Thread.java:833)
java.lang.NumberFormatException: For input string: ""
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
        at java.base/java.lang.Long.parseLong(Long.java:721)
        at java.base/java.lang.Long.parseLong(Long.java:836)
        at java.base/java.text.DigitList.getLong(DigitList.java:195)
        at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2197)
        at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1937)
        at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1545)
        at java.base/java.text.DateFormat.parse(DateFormat.java:397)
        at Test.DateFormatThreadSafetyExample.lambda$0(DateFormatThreadSafetyExample.java:16)
        at java.base/java.lang.Thread.run(Thread.java:833)
java.lang.NumberFormatException: For input string: "E71"
        at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
        at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.base/java.lang.Double.parseDouble(Double.java:651)
        at java.base/java.text.DigitList.getDouble(DigitList.java:169)
        at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2202)
        at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1937)
        at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1545)
        at java.base/java.text.DateFormat.parse(DateFormat.java:397)
        at Test.DateFormatThreadSafetyExample.lambda$0(DateFormatThreadSafetyExample.java:16)
        at java.base/java.lang.Thread.run(Thread.java:833)
java.lang.NumberFormatException: For input string: "E7"
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
        at java.base/java.lang.Long.parseLong(Long.java:711)
        at java.base/java.lang.Long.parseLong(Long.java:836)
        at java.base/java.text.DigitList.getLong(DigitList.java:195)
        at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2197)
        at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1937)
        at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1545)
        at java.base/java.text.DateFormat.parse(DateFormat.java:397)
        at Test.DateFormatThreadSafetyExample.lambda$0(DateFormatThreadSafetyExample.java:16)
        at java.base/java.lang.Thread.run(Thread.java:833)
java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 19
        at java.base/java.text.DigitList.fitsIntoLong(DigitList.java:230)
        at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2195)
        at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1937)
        at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1545)
        at java.base/java.text.DateFormat.parse(DateFormat.java:397)
        at Test.DateFormatThreadSafetyExample.lambda$0(DateFormatThreadSafetyExample.java:16)
        at java.base/java.lang.Thread.run(Thread.java:833)
java.lang.NumberFormatException: For input string: ""
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
        at java.base/java.lang.Long.parseLong(Long.java:721)
        at java.base/java.lang.Long.parseLong(Long.java:836)
        at java.base/java.text.DigitList.getLong(DigitList.java:195)
        at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2197)
        at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1937)
        at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1545)
        at java.base/java.text.DateFormat.parse(DateFormat.java:397)
        at Test.DateFormatThreadSafetyExample.lambda$0(DateFormatThreadSafetyExample.java:16)
        at java.base/java.lang.Thread.run(Thread.java:833)

报错啦!!!

代码证明 SimpleDateFormat 类:线------程------不------安------全!

3.3 使用 ThreadLocal 解决线程安全问题

一种常见的解决 SimpleDateFormat 线程安全问题的方法是使用 ThreadLocal 。每个线程都拥有自己的 SimpleDateFormat 实例,避免了多线程竞争。以下是如何使用 ThreadLocal 来确保线程安全:

java 复制代码
public class ThreadLocalDateFormatExample {   
    // 创建一个静态的ThreadLocal变量threadLocalSdf,用于存储SimpleDateFormat对象。使用withInitial方法初始化为当前时间的格式化字符串"yyyy-MM-dd HH:mm:ss"
    private static ThreadLocal<SimpleDateFormat> threadLocalSdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) {    

        Runnable task = () -> {            // 定义一个Runnable任务
            SimpleDateFormat sdf = threadLocalSdf.get();            // 从threadLocalSdf中获取SimpleDateFormat对象
            Date now = new Date();            // 创建一个新的Date对象,表示当前时间
            String formattedDate = sdf.format(now);            // 使用SimpleDateFormat对象将当前时间格式化为字符串
            System.out.println("格式化的日期: " + formattedDate);        // 输出格式化后的日期字符串
        };        // 结束Runnable任务的定义
        Thread thread1 = new Thread(task);        // 创建一个新的线程,传入Runnable任务
        Thread thread2 = new Thread(task);        // 创建另一个新的线程,传入Runnable任务
        thread1.start();        // 启动第一个线程
        thread2.start();        // 启动第二个线程
    }
}  

运行结果如下:

bash 复制代码
格式化的日期: 2023-11-04 15:10:41
格式化的日期: 2023-11-04 15:10:41

在这个示例中,每个线程通过 ThreadLocal 获得自己的 SimpleDateFormat 实例,确保了线程之间的隔离,从而解决了线程安全问题。

使用 ThreadLocal 是一种常用的解决方案,但需要小心,以免导致内存泄漏。确保在不再需要时清理 ThreadLocal 变量。

4. 本地化和国际化

4.1 日期格式的本地化

本地化是根据特定地区或文化习惯来适应不同的语言和格式。在日期格式化中,本地化通常涉及到日期的显示方式、月份和星期的命名等。

4.2 DateFormat 对不同语言的支持

DateFormat 类和其子类提供了对不同语言的支持。它们可以根据不同的 Locale 对象来格式化和解析日期和时间,以适应不同的语言和文化。

例如,下面是如何在不同语言环境下使用 SimpleDateFormat

java 复制代码
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class DateFormatLocalizationExample {
    public static void main(String[] args) {
        Date now = new Date();
        
        // 适应美国英语环境 格式化日期和时间
        SimpleDateFormat sdfUS = new SimpleDateFormat("MMMM dd, yyyy", Locale.US);
        // 适应法国环境 格式化日期和时间
        SimpleDateFormat sdfFR = new SimpleDateFormat("dd MMMM yyyy", Locale.FRENCH);
        
        System.out.println("Date in US format: " + sdfUS.format(now));
        System.out.println("Date in French format: " + sdfFR.format(now));
    }
}

运行结果如下:

bash 复制代码
Date in US format: November 04, 2023
Date in French format: 04 novembre 2023

在上面的示例中,我们创建了两个 SimpleDateFormat 实例,一个使用美国英语环境,另一个使用法国环境。这导致了不同的日期格式,分别使用不同的日期和月份名称。

4.3 使用 ResourceBundle 进行国际化

除了 DateFormat ,在实现国际化时,还可以使用 ResourceBundle 类来管理应用程序的本地化资源。这包括日期和时间格式、消息文本、标签和其他文本内容。

ResourceBundle 允许将不同的本地化资源存储在属性文件中,根据当前 Locale 动态加载适当的资源。

以下是一个简单的示例,演示如何使用 ResourceBundle 进行国际化:

java 复制代码
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date; 
import java.util.ResourceBundle; 

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

         // 获取当前日期
        Date currentDate = new Date();

        // 使用DateFormat格式化日期
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String formattedDate = dateFormat.format(currentDate);
        // 将格式化后的日期字符串按"-"分割成数组
        String date[]=formattedDate.split("-"); 
        
    
        // 使用ResourceBundle加载资源文件,设置为中文资料
        ResourceBundle bundle = ResourceBundle.getBundle("messages_CH");
        // 从资源文件中获取翻译
        String translatedDate = bundle.getString("date").replace("%year", date[0]).replace("%month", date[1]).replace("%date", date[2]);
        // 输出结果
        System.out.println("咱们中国人这么说:\n" + bundle.getString("hello")+"\n" +translatedDate);

        // 切换到美国英语资源文件
        bundle = ResourceBundle.getBundle("messages_US"); 
        // 从资源文件中获取翻译
        translatedDate = bundle.getString("date").replace("%year", date[0]).replace("%month", date[1]).replace("%date", date[2]);
        // 输出结果
        System.out.println("人家老美得这么说:\n" + bundle.getString("hello")+"\n" +translatedDate);
    }
} 

需要注意的是我们需要创建两个资源包文件,即messages_CH.propertiesmessages_US.properties,它们和ResourceBundleExample.java的相对位置如下:

bash 复制代码
my_project:
│  messages_CH.properties
│  messages_US.properties
└─src
    ResourceBundleExample.java

messages_CH.propertiesmessages_US.properties的文件内容分别如下:

bash 复制代码
hello=你好呀!
date=今天是%year年%month月%date日.
bash 复制代码
hello=Hello!
date=Today is %month,%date,%year.

最后,程序的运行结果如下:

bash 复制代码
咱们中国人这么说:
你好呀!
今天是2023年11月04日.
人家老美得这么说:
Hello!
Today is 11,04,2023.

4.4 示例:日期格式的本地化

下面是一个示例,演示了如何在不同本地化环境下格式化日期。在这个示例中,我们将使用 Locale 设置不同的语言环境并对日期进行本地化:

java 复制代码
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

public class DateLocalizationExample {
    public static void main(String[] args) {
    // 创建一个Date对象,表示当前日期和时间
    Date now = new Date();

    // 创建一个Locale对象,表示美国英语、法国法语
    Locale usLocale = new Locale("en", "US");
    Locale frLocale = new Locale("fr", "FR");

    // 分别使用美国英语、法国法语的Locale对象创建一个DateFormat对象,用于格式化日期和时间
    DateFormat usDateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, usLocale);

    DateFormat frDateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, frLocale);

    System.out.println("Date and time in US English: " + usDateFormat.format(now));

    System.out.println("Date and time in French: " + frDateFormat.format(now));
    }
}

运行结果如下:

bash 复制代码
Date and time in US English: November 4, 2023 at 3:24:03 PM SGT
Date and time in French: 4 novembre 2023 à 15:24:03 SGT

在这个示例中,我们使用 DateFormat 类的不同本地化设置,根据美国英语和法国法语环境分别格式化日期和时间。这会导致不同的日期和时间显示格式,以适应不同的语言和文化要求。

三、最佳实践

1. 处理日期范围和区间

在处理日期和时间时,处理日期范围和区间是一个常见但重要的任务。以下是一些最佳实践来处理日期范围和区间:

使用合适的数据结构: 为了表示日期范围和区间,可以使用 Java 中的 Date 对象或更现代的 LocalDate 对象。 LocalDate 对象是 Java 8 引入的 java.time 包的一部分,它提供了更好的支持。例如,你可以使用 LocalDate 来表示一个日期范围的开始和结束。

java 复制代码
LocalDate startDate = LocalDate.of(2023, 1, 1);
LocalDate endDate = LocalDate.of(2023, 12, 31);

处理时区差异: 当涉及到跨越不同时区的日期和时间时,要特别小心。最好使用 ZonedDateTime 来表示具有时区信息的日期和时间。这将确保你的日期范围考虑了时区的变化。

java 复制代码
ZoneId newYorkZone = ZoneId.of("America/New_York");
ZonedDateTime startDateTime = ZonedDateTime.of(2023, 1, 1, 0, 0, 0, 0, newYorkZone);
ZonedDateTime endDateTime = ZonedDateTime.of(2023, 12, 31, 23, 59, 59, 999, newYorkZone);

避免使用字符串表示: 避免在应用程序中使用字符串来表示日期范围,因为这可能导致格式化和解析错误。优选使用日期对象来处理日期范围。

2. 理解时区的重要性

时区在处理日期和时间时至关重要。以下是一些理解时区重要性的最佳实践:

存储时间戳而非字符串: 存储日期和时间时,最好使用时间戳(例如 InstantZonedDateTime)而不是字符串。时间戳是相对于时区的不变值,可以轻松地转换为不同的时区。

显示本地时间: 当向用户显示日期和时间时,通常应将其转换为用户所在地区的本地时间。这可以通过 ZoneIdZonedDateTime 来实现。

java 复制代码
ZoneId userZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime userDateTime = startDateTime.withZoneSameInstant(userZone);

考虑夏令时: 夏令时是时区的一部分,它会导致时间的变化。在处理涉及夏令时的日期和时间时要特别小心。

使用现代日期时间库: 如果可能的话,尽量使用 Java 8 引入的 java.time 包,它提供了更强大和准确的日期和时间处理功能,尤其在处理时区和夏令时方面。

总之,日期和时间的处理需要小心考虑时区和夏令时的问题。使用合适的数据结构和现代日期时间库,可以更容易地处理日期范围和区间,同时避免出现常见的日期和时间错误。

2. 时间戳和时区信息

2.1 使用时间戳表示日期和时间

在Java中,你可以使用时间戳来表示日期和时间,同时使用DateDateFormat类来进行操作和格式化。下面是一个完整的示例,演示如何使用时间戳表示日期和时间,以及如何使用DateDateFormat类进行相关操作:

java 复制代码
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TimestampExample {
    public static void main(String[] args) {
        // 获取当前时间的时间戳
        long currentTimestamp = System.currentTimeMillis();

        // 使用时间戳创建Date对象
        Date date = new Date(currentTimestamp);

        // 使用SimpleDateFormat格式化日期
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String formattedDate = dateFormat.format(date);

        System.out.println("当前时间的时间戳: " + currentTimestamp);
        System.out.println("时间戳转换为日期时间: " + formattedDate);

        // 创建一个特定的日期时间的时间戳
        String dateStr = "2023-10-15 14:30:00";
        try {
            DateFormat parseFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date parsedDate = parseFormat.parse(dateStr);
            long customTimestamp = parsedDate.getTime();
            System.out.println("自定义日期时间的时间戳: " + customTimestamp);
        } catch (Exception e) {
            System.err.println("日期解析出错: " + e.getMessage());
        }
    }
}

运行结果如下:

bash 复制代码
当前时间的时间戳: 1699088456494
时间戳转换为日期时间: 2023-11-04 17:00:56
自定义日期时间的时间戳: 1697351400000

2.2 存储和检索时区信息

java 复制代码
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TimeZoneInfoExample {
    public static void main(String[] args) {
        // 创建一个自定义日期格式
        SimpleDateFormat customDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");

        // 设置时区信息
        customDateFormat.setTimeZone(java.util.TimeZone.getTimeZone("America/New_York"));

        // 获取当前时间
        Date currentTime = new Date();

        // 格式化并输出带时区信息的日期时间
        String formattedDate = customDateFormat.format(currentTime);
        System.out.println("当前时间(纽约时区): " + formattedDate);

        // 更改时区到柏林
        customDateFormat.setTimeZone(java.util.TimeZone.getTimeZone("Europe/Berlin"));

        // 格式化并输出带时区信息的日期时间
        formattedDate = customDateFormat.format(currentTime);
        System.out.println("当前时间(柏林时区): " + formattedDate);

        // 更改时区到东京
        customDateFormat.setTimeZone(java.util.TimeZone.getTimeZone("Asia/Tokyo"));

        // 格式化并输出带时区信息的日期时间
        formattedDate = customDateFormat.format(currentTime);
        System.out.println("当前时间(东京时区): " + formattedDate);
     }
}

运行结果如下:

bash 复制代码
当前时间(纽约时区): 2023-11-04 03:30:38 EDT
当前时间(柏林时区): 2023-11-04 08:30:38 CET
当前时间(东京时区): 2023-11-04 16:30:38 JST

在这个示例中,我们首先创建一个SimpleDateFormat对象,用于自定义日期时间格式,包括时区信息。然后,我们使用setTimeZone方法来设置所需的时区,例如"America/New_York"、"Europe/Berlin"和"Asia/Tokyo"。

接下来,我们获取当前时间(默认时区),然后使用SimpleDateFormat对象将其格式化为包含所选时区信息的日期时间字符串。这样,你可以存储和检索不同时区的日期时间信息。

四、实际应用

示例应用 - 日程管理系统(概念版)

Java 复制代码
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.Scanner;

public class ScheduleApp {
    private static ScheduleManager scheduleManager = new ScheduleManager(); // 创建日程管理器
    private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.CHINA); // 日期格式

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in); // 创建一个输入扫描器
        int choice;

        do {
            // 显示菜单选项
            System.out.println("日程管理应用菜单:");
            System.out.println("1. 添加事件");
            System.out.println("2. 查看事件");
            System.out.println("3. 删除事件");
            System.out.println("4. 编辑事件");
            System.out.println("5. 查找和过滤");
            System.out.println("6. 按日历查找");
            System.out.println("7. 清空事件");
            System.out.println("8. 随机测试");
            System.out.println("9. 退出");
            System.out.print("请输入您的选择:");
            choice = scanner.nextInt(); // 获取用户选择

            switch (choice) {
                case 1:
                    addEvent(scanner); // 调用添加事件的方法
                    break;
                case 2:
                    viewEvents(); // 调用查看事件的方法
                    break;
                case 3:
                    deleteEvent(scanner); // 调用删除事件的方法
                    break;
                case 4:
                    editEvent(scanner); // 调用编辑事件的方法
                    break;
                case 5:
                    findAndFilter(scanner); // 调用查找和过滤的方法
                    break;
                case 6:
                    calendarQuery(scanner); // 调用按日历查询的方法
                    break;
                case 7:
                    scheduleManager.clearEvents(); // 清空所有事件
                    System.out.println("已清空所有事件。");
                    break;
                case 8:
                    randomTest(5); // 进行随机测试,生成5个随机事件
                    break;
                case 9:
                    System.out.println("正在退出...");
                    break;
                default:
                    System.out.println("无效选项。请重试。");
            }
        } while (choice != 9);

        scanner.close();
    }

    // 添加事件
    private static void addEvent(Scanner scanner) {
        System.out.print("请输入事件标题: ");
        scanner.nextLine();
        String title = scanner.nextLine();
        System.out.print("请输入事件描述(以 /end 结束输入):\n");

        StringBuilder descriptionBuilder = new StringBuilder();
        String line;
        while (true) {
            line = scanner.nextLine();
            if ("/end".equals(line)) {
                break;
            }
            descriptionBuilder.append(line).append("\n");
        }
        String description = descriptionBuilder.toString();

        System.out.print("请输入事件的开始时间 (yyyy-MM-dd HH:mm): ");
        Date startTime = null;
        try {
            startTime = dateFormat.parse(scanner.nextLine());
        } catch (ParseException e) {
            System.out.println("无效的日期格式。");
            return;
        }

        System.out.print("请输入事件的结束时间 (yyyy-MM-dd HH:mm): ");
        Date endTime = null;
        try {
            endTime = dateFormat.parse(scanner.nextLine());
        } catch (ParseException e) {
            System.out.println("无效的日期格式。");
            return;
        }

        Event event = new Event(title, description, startTime, endTime);
        System.out.print("是否设置提醒时间 (Y/N)?: ");
        String setReminder = scanner.nextLine();

        if (setReminder.equalsIgnoreCase("Y")) {
            System.out.print("请输入提前提醒的时间段(30分钟/1小时/1天): ");
            String reminderPeriod = scanner.nextLine();

            Date reminderTime = calculateReminderTime(startTime, reminderPeriod);
            event.setReminderTime(reminderTime);
        }

        scheduleManager.addEvent(event);
        System.out.println("事件添加成功.");
    }

    // 计算提醒时间
    private static Date calculateReminderTime(Date startTime, String reminderPeriod) {
        long timeDifferenceMillis = 0;
        if (reminderPeriod.equals("30分钟")) {
            timeDifferenceMillis = 30 * 60 * 1000;
        } else if (reminderPeriod.equals("1小时")) {
            timeDifferenceMillis = 60 * 60 * 1000;
        } else if (reminderPeriod.equals("1天")) {
            timeDifferenceMillis = 24 * 60 * 60 * 1000;
        }
        return new Date(startTime.getTime() - timeDifferenceMillis);
    }

    // 查看事件
    private static void viewEvents() {
        List<Event> events = scheduleManager.getEvents();

        if (events.isEmpty()) {
            System.out.println("没有安排的事件.");
        } else {
            System.out.println("已安排的事件:");
            int i = 0;
            for (Event event : events) {
                System.out.println("索引值: " + i++);
                System.out.println(event.toString());
            }
        }
    }

    // 日历查询事件
    private static void calendarQuery(Scanner scanner) {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA); // 日期格式
        System.out.println("按日历查询事件:");
        System.out.print("请输入起始日期 (yyyy-MM-dd): ");
        String startDateStr = scanner.next();

        System.out.print("请输入结束日期 (yyyy-MM-dd): ");
        String endDateStr = scanner.next();

        try {
            Date startDate = dateFormat.parse(startDateStr);
            Date endDate = dateFormat.parse(endDateStr);

            List<Event> eventsInRange = scheduleManager.getEventsInRange(startDate, endDate);

            if (eventsInRange.isEmpty()) {
                System.out.println("在该日期范围内没有事件.");
            } else {
                System.out.println("在该日期范围内的事件:");
                for (Event event : eventsInRange) {
                    System.out.println(event.toString());
                }
            }
        } catch (ParseException e) {
            System.out.println("无效的日期格式.");
        }
    }

    // 删除事件
    private static void deleteEvent(Scanner scanner) {
        System.out.print("请输入要删除的事件索引: ");
        int index = scanner.nextInt();
        if (index >= 0 && index < scheduleManager.getEvents().size()) {
            scheduleManager.deleteEvent(index);
            System.out.println("事件删除成功.");
        } else {
            System.out.println("无效的索引. 未删除任何事件.");
        }
    }

    // 编辑事件
    private static void editEvent(Scanner scanner) {
        System.out.print("请输入要编辑的事件索引: ");
        int index = scanner.nextInt();
        if (index >= 0 && index < scheduleManager.getEvents().size()) {
            Event event = scheduleManager.getEvents().get(index);
            System.out.println("当前事件信息:");
            System.out.println(event.toString());
            System.out.print("请输入新的事件标题 (或按 Enter 保留不变): ");
            scanner.nextLine(); // 消耗换行字符
            String newTitle = scanner.nextLine();
            if (!newTitle.isEmpty()) {
                event.setTitle(newTitle);
            }
            System.out.print("请输入新的事件描述 (或按 Enter 保留不变): ");
            String newDescription = scanner.nextLine();
            if (!newDescription.isEmpty()) {
                event.setDescription(newDescription);
            }
            System.out.print("请输入新的事件开始时间 (yyyy-MM-dd HH:mm) (或按 Enter 保留不变): ");
            String newStartTime = scanner.nextLine();
            if (!newStartTime.isEmpty()) {
                try {
                    Date startTime = dateFormat.parse(newStartTime);
                    event.setStartTime(startTime);
                } catch (ParseException e) {
                    System.out.println("无效的日期格式. 事件开始时间未修改.");
                }
            }

            System.out.print("请输入新的事件结束时间 (yyyy-MM-dd HH:mm) (或按 Enter 保留不变): ");
            String newEndTime = scanner.nextLine();
            if (!newEndTime.isEmpty()) {
                try {
                    Date endTime = dateFormat.parse(newEndTime);
                    event.setEndTime(endTime);
                } catch (ParseException e) {
                    System.out.println("无效的日期格式. 事件结束时间未修改.");
                }
            }

            System.out.print("请输入新的提醒时间 (30分钟/1小时/1天) (或按 Enter 保留不变): ");
            String newReminder = scanner.nextLine();
            if (!newReminder.isEmpty()) {
                Date reminderTime = calculateReminderTime(event.getStartTime(), newReminder);
                event.setReminderTime(reminderTime);
            }

            System.out.println("事件编辑成功.");
        } else {
            System.out.println("无效的索引. 事件未修改.");
        }
    }

    // 查找和过滤事件
    private static void findAndFilter(Scanner scanner) {
        System.out.print("请输入要查找的关键字: ");
        scanner.nextLine();
        String keyword = scanner.nextLine();

        List<Event> filteredEvents = scheduleManager.findEventsByKeyword(keyword);

        if (filteredEvents.isEmpty()) {
            System.out.println("未找到包含关键字的事件.");
        } else {
            System.out.println("包含关键字的事件:");
            for (Event event : filteredEvents) {
                System.out.println(event.toString());
            }
        }
    }

    // 随机测试
    private static void randomTest(int nums) {
        Random random = new Random();
        int numRandomEvents = nums; // 生成 nums 个随机事件

        for (int i = 0; i < numRandomEvents; i++) {
            String title = "随机事件 " + (i + 1);
            String description = "这是一个随机生成的事件";

            // 生成随机的开始时间和结束时间
            Calendar calendar = Calendar.getInstance();
            long currentTimeMillis = System.currentTimeMillis();
            calendar.setTimeInMillis(currentTimeMillis + random.nextInt(30) * 60 * 1000); // 开始时间在未来30分钟内
            Date startTime = calendar.getTime();
            calendar.setTimeInMillis(currentTimeMillis + random.nextInt(60) * 60 * 1000); // 结束时间在未来60分钟内
            Date endTime = calendar.getTime();

            Event event = new Event(title, description, startTime, endTime);

            // 随机设置提醒时间
            if (random.nextBoolean()) {
                calendar.setTime(startTime);
                calendar.add(Calendar.MINUTE, -random.nextInt(60)); // 提前0-60分钟提醒
                Date reminderTime = calendar.getTime();
                event.setReminderTime(reminderTime);
            }

            // 将生成的事件添加到日程管理器
            scheduleManager.addEvent(event);
        }

        System.out.println("生成了 " + numRandomEvents + " 个随机事件。");
    }
}
java 复制代码
// Event.java
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class Event {
    private String title;
    private String description;
    private Date startTime;
    private Date endTime;
    private Date reminderTime;

    private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.CHINA);

    public Event(String title, String description, Date startTime, Date endTime) {
        this.title = title;
        this.description = description;
        this.startTime = startTime;
        this.endTime = endTime;
        this.reminderTime = null; // 默认提醒时间为空
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public void setStartTime(Date startTime) {
        this.startTime = startTime;
    }

    public void setEndTime(Date endTime) {
        this.endTime = endTime;
    }

    public void setReminderTime(Date reminderTime) {
        this.reminderTime = reminderTime;
    }

    public String getTitle() {
        return title;
    }

    public String getDescription() {
        return description;
    }

    public Date getStartTime() {
        return startTime;
    }

    public Date getEndTime() {
        return endTime;
    }

    public Date getReminderTime() {
        return reminderTime;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("标题: ").append(title).append("\n");
        sb.append("描述: ").append(description).append("\n");
        sb.append("开始时间: ").append(dateFormat.format(startTime)).append("\n");
        sb.append("结束时间: ").append(dateFormat.format(endTime)).append("\n");
    
        // 计算距离事件开始时间的时间差
        long startTimeMillis = startTime.getTime();
        long currentTimeMillis = System.currentTimeMillis();
        long timeDifferenceMillis = startTimeMillis - currentTimeMillis;
    
        if (timeDifferenceMillis < 0) {
            // 事件已经开始,计算已开始多长时间
            timeDifferenceMillis = currentTimeMillis - startTimeMillis;
            sb.append("已开始: ");
        } else {
            sb.append("距离开始时间还有: ");
        }
    
        long days = timeDifferenceMillis / (24 * 60 * 60 * 1000);
        long hours = (timeDifferenceMillis % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000);
        long minutes = (timeDifferenceMillis % (60 * 60 * 1000)) / (60 * 1000);
    
        if (days > 0) {
            sb.append(days).append(" 天 ");
        }
        if (hours > 0) {
            sb.append(hours).append(" 小时 ");
        }
        sb.append(minutes).append(" 分钟\n");
    
        return sb.toString();
    }
        
}
Java 复制代码
// ScheduleManager.java

// 导入 ArrayList 和 List 类,以支持事件列表的管理
import java.util.Date;
import java.util.ArrayList;
import java.util.List;

// ScheduleManager 类用于管理事件列表
public class ScheduleManager {
    // 存储事件的列表
    private List<Event> events = new ArrayList<>();

    // 添加事件到事件列表
    public void addEvent(Event event) {
        events.add(event);
    }

    // 获取事件列表
    public List<Event> getEvents() {
        return events;
    }

    // 删除指定索引位置的事件
    public boolean deleteEvent(int index) {
        if (index >= 0 && index < events.size()) {
            events.remove(index);
            return true; // 删除成功
        }
        return false; // 删除失败,索引无效
    }

    // 获取指定索引位置的事件
    public Event getEvent(int index) {
        if (index >= 0 && index < events.size()) {
            return events.get(index);
        }
        return null; // 返回空值,索引无效
    }
    
    // 根据关键字查找事件
    public List<Event> findEventsByKeyword(String keyword) {
        List<Event> filteredEvents = new ArrayList<>();
        for (Event event : events) {
            // 如果事件的标题或描述包含关键字,则将其添加到过滤事件列表中
            if (event.getTitle().contains(keyword) || event.getDescription().contains(keyword)) {
                filteredEvents.add(event);
            }
        }
        return filteredEvents; // 返回包含关键字的事件列表
    }

    // 清空方法
    public void clearEvents() {
        events.clear();
    }


    // 获取日期范围内的事件
    public List<Event> getEventsInRange(Date startDate, Date endDate) {
        List<Event> eventsInRange = new ArrayList<>();
        for (Event event : events) {
            if (event.getStartTime().after(startDate) && event.getEndTime().before(endDate)) {
                eventsInRange.add(event);
            }
        }
        return eventsInRange;
    }
}

演示

最后的小牢骚

在 Java 编程中,处理日期和时间就像是在穿越时间迷宫。我们的首站是 Date 类,它曾是日期时间的王者,但后来被证明有点靠不住。Date 类让我们能够获取当前日期和时间,但它是可变的,这就像在双刃剑上跳舞,很容易割伤自己。而且,不同国家和地区的日期格式也不能被满足。试图在多线程中使用 Date 类?那简直是自找麻烦。
然后,Java 8 出现了救世主 - java.time 包。它引入了一堆新朋友,比如 LocalDateLocalDateTime,它们都是不可变的。这些新朋友让我们处理日期和时间变得更加简单和安全。所以,从现在开始,不要再用老掉牙的 Date 类了,转而拥抱 java.time 包的时代。
如果你想了解更多关于java.time 包的知识,关注我!不仅是关于java.time 包,C++AndroidJava、数据结构等知识我都将持续输出!🫡🫡🫡
但别急,如果你还需要和老代码打交道,有 DateFormat 类来帮忙。DateFormat 能够格式化和解析日期时间字符串,它非常聪明,可以适应不同国家和地区的日期格式,而且可以让日期时间在不同语言之间自由切换。
然而,要小心,DateFormat 也有它的问题,尤其是在多线程环境中,你可能会发现它有点暴躁。不过,聪明的 Java 程序员总会找到解决方法,比如使用 ThreadLocal 来确保每个线程都有自己的 DateFormat 实例。
最后,如果你还需要应对国际化的挑战,ResourceBundle 是你的好朋友。它可以让你的日期格式适应不同语言和文化,让你的应用程序更受欢迎。
在穿越 Java 的时间迷宫中,不仅要了解这些工具,还要牢记最佳实践,特别是在处理日期范围和区间时要懂得时区的重要性。所以,朋友们,准备好了吗?一起在时间的大舞台上留下自己的足迹吧!

求点赞👍求关注❤️求转发💕

相关推荐
P.H. Infinity40 分钟前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天43 分钟前
java的threadlocal为何内存泄漏
java
caridle1 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^1 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋31 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花1 小时前
【JAVA基础】Java集合基础
java·开发语言·windows
小松学前端1 小时前
第六章 7.0 LinkList
java·开发语言·网络
Wx-bishekaifayuan1 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
全栈开发圈1 小时前
新书速览|Java网络爬虫精解与实践
java·开发语言·爬虫