用下面的代码

long newDate = Date.parse("12 Oct 1960 13:25:00");
file.setLastModified(newDate);

我明白了 java.lang.IllegalArgumentException: Negative time

并用这段代码

public class FileTimes {

  static long getCalendarMillis(int year) {
    Calendar cal = Calendar.getInstance();
    cal.set(year, 10-1, 12, 13, 25, 00);
    return  cal.getTimeInMillis();
  }
  static long getDateMillis(int year) {
    return switch (year) {
      case 1960 -> Date.parse("12 Oct 1960 13:25:00");
      case 1980 -> Date.parse("12 Oct 1980 13:25:00");
      default -> (new Date()).getTime();
    };
  }
  static void touch(long millis) throws IOException {
    final File parent = new File("log/FileTimes"), file;
    parent.mkdirs();
    try (FileOutputStream out = new FileOutputStream(file = new File(parent, "FileTime"))) {
      out.close();
      Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(millis));
      System.out.println("Date of file:                  "+new Date(file.lastModified()));
    }
  }

  public static void main(String[] args) throws IOException, InterruptedException {
    for (int i=0; i<3; i++) {
      long millis = getDateMillis(1960);
      System.out.println("Millies from Date:             "+millis);
      touch(millis);
      millis = getCalendarMillis(1960);
      System.out.println("Millies from Calendar:         "+millis);
      touch(millis);
      millis = millis / 1000 * 1000 - 1000;
      System.out.println("Millies from Cal rounded down: "+millis);
      touch(millis);
      Thread.sleep(250);
      System.out.println();
    }
  }
}

我得到:

Millies from Date:             -290950500000
Date of file:                  Wed Oct 12 13:25:00 CET 1960
Millies from Calendar:         -290950499926
Date of file:                  Thu Jan 01 01:00:00 CET 1970
Millies from Cal rounded down: -290950500000
Date of file:                  Wed Oct 12 13:25:00 CET 1960

Millies from Date:             -290950500000
Date of file:                  Wed Oct 12 13:25:00 CET 1960
Millies from Calendar:         -290950499532
Date of file:                  Thu Jan 01 01:00:00 CET 1970
Millies from Cal rounded down: -290950500000
Date of file:                  Wed Oct 12 13:25:00 CET 1960

Millies from Date:             -290950500000
Date of file:                  Wed Oct 12 13:25:00 CET 1960
Millies from Calendar:         -290950499230
Date of file:                  Thu Jan 01 01:00:00 CET 1970
Millies from Cal rounded down: -290950500000
Date of file:                  Wed Oct 12 13:25:00 CET 1960

最后,我unknown date在 Linux 上使用 Nemo 文件管理器时使用:
Calendar cal ... cal.set(...);

请注意,每次调用的毫秒数略有不同!

但在使用 deprecated: 时我得到了正确的日期:
Date.parse(...);或者我对毫秒进行四舍五入。

对我来说,这看起来像是 Java 中的一个错误。你认为呢?

我在CET。

27

  • 1
    我们应该在哪个时区解释该日期和时间?


    – 

  • 1
    尝试FileTime ftime = FileTime.from(Instant.parse("1920-01-01T11:11:11Z"));Files.setAttribute(Path.of("/tmp/s"), "basic:lastModifiedTime", ftime);


    – 

  • 1
    @g00se"Ah, the strange ways of Windows" 你太客气了。


    – 


  • 2
    是的,根本不建议使用任何这些类。它们被取代java.time


    – 

  • 1
    我不得不说我相当同意斯蒂芬的观点——你应该这样做


    – 


4 个回答
4

不管 UNIX 纪元之前的时间戳如何表示,文件的最后修改时间 > 之前 < 1970 年有何意义?那时 UNIX/Linux 还不存在,因此也不存在文件。时间戳是针对文件的,而不是针对文件所代表的信息;例如它的内容。

您在这里试图表示的内容可以用其他方式更好地表示;即不是通过劫持文件系统时间戳来达到它们不想要的目的。

当某些系统上的外部工具(lsLinux 文件管理器)对纪元前的文件时间戳表现奇怪时,可能是因为所述工具的设计者将这些时间戳视为坏数据。你可能对此无能为力。


如前所述,Java 可以处理纪元之前的时间戳。如果您使用“自纪元以来的秒数”表示,只需使用负整数。

您尝试使用File.setLastModified.失败是因为该方法对负时间戳进行了硬连线检查。 (根据 OpenJDK Java 11 源代码。)

在更新的问题中,您声称此行为是 Java 中的错误。好吧,如果您在 1997 年针对 Java 1.0 报告了这个问题,也许就有可能修复它。然而,java.io.File它是一个遗留类,充满了不一致和/或未记录的行为,如果不破坏过去近 30 年编写的“数百万”现有 Java 应用程序,就无法修复这些行为。如果您想要一致的、有据可查的行为,请使用PathFiles。这些和其他 NIO 类于 2011 年添加到 Java 7 中,以帮助 Java“很好地发挥”各种依赖于操作系统的文件系统功能。

你应该改用Files.setLastModifiedTime。它的 javadoc 是这么说的。

public static Path setLastModifiedTime(Path path,
                                       FileTime time)
                                throws IOException

更新文件的上次修改时间属性。文件时间转换为文件系统支持的纪元和精度。从较细粒度转换为较粗粒度会导致精度损失。当文件系统不支持或超出底层文件存储支持的范围时尝试设置上次修改时间时,此方法的行为未定义。它可能会或不会因抛出IOException.

这意味着如果文件系统支持 UNIX 纪元之前的时间戳,那么setLastModifiedTime应该可以工作。

(再次)查看 UNIX / Linux 情况下的 OpenJDK Java 11 源代码,它将尝试设置您给出的修改时间。如果设置时间戳的系统调用失败并出现EINVAL,并且时间戳为负数,它将再次尝试将时间戳设置为零。 (请注意,此行为没有记录,因此可以想象它可能会在 Java 版本之间发生变化。)

3

  • 我有一堆六十年代及以前的照片,是从幻灯片上扫描的。对我来说,直接在文件管理器中查看原始日期是有意义的。但除此之外,事实证明,Files.setLastModifiedTime()在使用推荐的 class 计算毫秒时,将文件时间设置为 1970 年 1 月 1 日Calendar。请参阅上面我编辑的代码。


    – 


  • 1
    从你的角度来看,也许这是有道理的。从操作系统工具的角度来看,事实并非如此。例如,如果您使用增量文件系统备份/恢复软件,将文件修改时间戳设置为与磁盘上文件的最后修改不匹配的值通常会导致它们无法备份。馊主意。如果要记录这种元数据,最好使用扩展属性、数据库或其他东西。


    – 


  • 另外….不要使用Calendar.自 Java 7 发布以来,它是一个遗留类。使用java.time类来代替。同样,java.util.Datejava.io.File等都是遗产。


    – 


太长了;博士

计算 1970 年之前的毫秒数。

long epochMilli =
        LocalDateTime.parse(
                        "12 Oct 1960 13:25:00" ,
                        DateTimeFormatter.ofPattern( "d MMM uuuu HH:mm:ss" ).withLocale( Locale.US )
                )
                .atZone(
                        ZoneId.of( "Europe/Amsterdam" )
                )
                .toInstant( )
                .toEpochMilli( );

java.time

其他人则解决文件系统问题。我将解决您的时间计算问题。

您正在使用有严重缺陷的日期时间类,这些类在几年前已被JSR 310 中定义的现代java.time类所取代。

LocalDateTime

将日期与时间字符串解析为LocalDateTime.定义格式模式以匹配您传入的字符串。您必须指定 aLocale来解析月份名称。

String input = "12 Oct 1960 13:25:00";
Locale locale = Locale.US;
DateTimeFormatter f = DateTimeFormatter.ofPattern( "d MMM uuuu HH:mm:ss" ).withLocale( locale );
LocalDateTime ldt = LocalDateTime.parse( input , f );

ISO 8601

顺便说一句,我建议您向输入数据的发布者介绍用于将日期时间值交换为文本的标准格式。您的标准格式输入将是1960-10-12T13:25. java.time在生成和解析文本时默认使用 ISO 8601 格式 – 因此无需为标准输入定义任何格式模式。

ZonedDateTime

你说:

我在 CEST。

所以我猜测这意味着您想要解释从该时区看到的该日期下午 1:25 的输入。

实时时区

不幸的是,CEST不是实时时区。此类伪区域用于向用户进行本地化显示,但不适用于编程逻辑。那些 2-4 个字符的伪区域不是标准化的,甚至不是唯一的!像 CEST 这样的伪时区涵盖了许多实时时区,每个时区都有自己的规则。说“我的时区是 CEST”就像说“这笔钱是”——美元、加元、澳元、巴哈马元、圭亚那元,谁知道呢?

相反,请以 格式指定Continent/Region。也许您确实想要一个时区,例如Europe/Amsterdam

ZoneId z = ZoneId.of( "Europe/Amsterdam" ) ;

将该时区应用于我们LocalDateTime来确定某个时刻,即对象中时间轴上的特定点

ZonedDateTime zdt = ldt.atZone( z ) ;

zdt.toString() = 1960-10-12T13:25+01:00[欧洲/阿姆斯特丹]

Instant

调整以通过 UTC 的挂钟/日历查看同一时刻(与 UTC 时间子午线的偏移为零时-分-秒)。我们可以通过提取.根据定义,对象Instant始终采用 UTC。

Instant instant = zdt.toInstant() ;

instant.toString() = 1960-10-12T12:25:00Z

自纪元以来的毫秒数

显然,您的文件系统 API 期望自 UTC 1970 年第一时刻的纪元参考(1970-01-01T00:00Z)以来的毫秒数。那一刻是在常数 中定义的

long epochMilli = instant.toEpochMilli() ;

纪元 = -290950500000

请注意负数如何表示 1970 UTC 之前的时刻。


我不知道 -290950500000L 值是否适用于您的 Linux 文件系统。

1

  • 如果你有例如,Instant instant = Instant.ofEpochSecond(-290950500L, 12340000L);你仍然会遇到错误Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(instant.toEpochMilli()));


    – 

如果您从外部代码中获取Date,CalendarInstant对象,这里有一个解决方法:milliseconds != 0

long millis = date.getTime();
// or:
long millis = calendar.getTimeInMillis();
// or:
long millis = instant.toEpochMilli();

// round down *absolutely* – for positive and negative values – to seconds:
millis = Math.floorDiv(millis, 1000) * 1000;

Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(millis));

我相信这是不可能的,因为文件系统使用的 UNIX 时间戳是用 1970 年以来的秒数表示的(对于大多数 Linux 文件系统(如 EXT),实际上是毫秒)。

这意味着低于 1970 年的文件时间实际上会回绕并成为未来的一年,该年份取决于文件系统中存储的文件戳的整数大小。如果 UNIX 时间戳以 32 位存储,则 1970 – 1 将是 2038 年。如果是 64 位,则会下溢到更大的年份。

可以通过将其设置为有符号整数来使 Java 保持沉默,以确保该值不是负数,但在传递给 setLastModified 函数之前会自动下溢。这样,如果您尝试将文件时间设置为 1969,由于整数下溢到 32 位整数限制,文件最终的实际文件时间将设置为 2038。

2

  • 2
    JavaDocpublic static FileTime fromMillis(long value)表示:value - the value, in milliseconds, since the epoch (1970-01-01T00:00:00Z); can be negative,因此负值(即 1970 年之前)应该有效。


    – 

  • 3
    根据:“Unix 时间通常被编码为有符号整数。”因此 1970 年之前的时刻可以表示为负数。整数环绕和溢出在这里不应成为问题。


    –