是否有可能以某种方式延长标记为最终的 Joda-Time 类?

Is it somehow possible to extend Joda-Time classes marked final?

我真的很喜欢 Joda-Time,但是我 运行 遇到了一些我认为有问题的事情。我想扩展一些 classes,特别是 DateTime、LocalDate 和 LocalDateTime。但是它们被标记为 "final".

我发现了一个非常古老的线程,其中将其解释为一种确保 classes 保持不可变的方法。 http://osdir.com/ml/java-joda-time-user/2006-06/msg00001.html

我还在 SO 上找到了一个线程,其中讨论了将 Java class 标记为最终以确保不变性的必要性。 Why would one declare an immutable class final in Java?

无论如何,我发现无法扩展这些 classes 是一个主要限制。除了下载源文件并修改它们之外,还有什么可以用来创建这些 classes 的扩展版本吗?

编辑 - 讨论:

扩展 class 的能力是面向对象编程中最强大和有用的概念之一。这可以总是有用。 class 的作者不可能 100% 确定 his/her super-duper class 在扩展到涵盖没人能预见的用例时对某些程序员不会更有用。

Joda-Time classes 被标记为 "final" 的明显原因是确保某人不可能创建可变的扩展 class,并且将其与依赖于不可变的 Joda-Time 对象的现有程序一起使用。因此,在某种程度上,将这些 classes 标记为 "final" 是由于缺少允许将 classes 标记为 "immutable" 的 Java 语言机制, 因此它们可以被扩展,但前提是扩展的 class 也被标记为 "immutable"。

所以鉴于 Java 中缺少 "immutable" 关键字,我可以理解 Joda-Time 的作者想要避免这种情况。

以下解决方案是否可行?我们能否拥有一个结构,例如,LocalDate 派生自 LocalDateNonFinal? LocalDate 是一个空 class,标记为 "final"。所有功能都在 LocalDateNonFinal 中。

因此,如果您真的想扩展 LocalDate class,并且只打算在您自己的程序中使用扩展的 class,那么您可以改为扩展 LocalDateNonFinal,并将其称为 MyLocalDate。这不会将其他模块暴露给您可能犯的错误,因为它们仍然需要 LocalDate,并且不会接受 LocalDateNonFinal 或您的 MyLocalDate。

这可以与对想要扩展这些 classes 的程序员进行教育的尝试相结合,警告他们如果他们不小心创建了一个可变版本并且仍然将其视为不可变版本可能会出现的问题。并指出这些扩展的 classes 将不能与其他期望常规 ("final") classes.

的模块一起使用

PS。几天后,当我完全确定时,我将 post 我的变通解决方案。到目前为止,我已经对其中两个答案投了赞成票——感谢您的意见和建议。我目前倾向于按照 Dmitry Zaitsev 建议的类似包装器的解决方案。

如果您真的需要扩展 类 您可以从这里获取源代码: github link

分叉它们,将它们标记为非最终并扩展它们。

否则你不能扩展 final 类。

无法扩展任何标记为 final 的内容,并且分叉这些 classes 是不切实际的。除了您自己的代码中的对象将期望看到 Joda classes 并将验证那是他们得到的,您将无法传递您自己的版本。所以分叉的最佳情况是您将拥有一组对象供您自己使用,您必须将它们转换为 Joda 或 Java 8 才能将它们与其他代码一起使用。此外,您的分叉版本将无法受益于将来对原始 classes 所做的任何修复,除非您继续将修复复制到您自己的版本中。另一个问题可能是 Joda classes 和您自己的版本之间的比较可能无法传递,结果可能取决于调用它们的对象。

您可以创建一个实用程序 class,其中包含所有静态方法,这些方法将接受一个 Joda 对象,执行您想要的任何额外功能和 return 一个 Joda 对象。这将类似于 StringUtil classes,如在 apache-commons 中找到的那些或 java.lang.Math。这样你就避免了分叉的维护,并且你有一些东西可以直接与库或框架代码一起使用。

您可以将最终的 class 包装到您自己的 class 中,提供您想要的任何操作,并提供 "view" 方法,该方法将 return 原始的 Joda-time目的。像那样:

public class MyJodaExtension {

    private final DateTime dateTime;

    public MyJodaExtension(DateTime dateTime) {
        this.dateTime = dateTime;
    }

    public boolean myOperation() {
        return false;  // or whatever you need
    }

    public DateTime asDateTime() {
        return dateTime;
    }

}

使用这种方法,您甚至可以使 MyJodaExtension 可变,并根据需要提供 DateTime 的不同实例(但我希望您不要这样做,不可变的 class 很棒).

正如 Nathan Hughes 所说,您无法将此类 "inherited" class 传递给其他库或任何需要原始 Joda 时间的代码 class。

以防其他人感兴趣,这就是我最后做的事情。但我已将已接受的答案复选标记授予 Dmitry Zaitsev,因为他的答案对我最有用。

这是另外两个 class 使用的基础 class:

package com.Merlinia.MCopier_Main;

import org.joda.time.DateTime;


/**
 * This common base class is used to provide a (mutable) wrapper for the (immutable) Joda-Time
 * DateTime and LocalDateTime classes. It is used as the base class (super class, in Java
 * terminology) for the DateTimeLocal and DateTimeUtc classes. This provides a somewhat kludgy way
 * of extending the DateTime and LocalDateTime classes, since they can't be directly extended
 * because they are marked "final".
 *
 * The only service provided by this class is that it contains a field which can contain the .Net
 * DateTime "ticks" value that was used to create this object via MCopier deserialization. This is
 * then used by MCopier serialization to provide an identical result if the object is round-tripped
 * from .Net to Java and back again, although only if the associated DateTime or LocalDateTime
 * object has not been updated. (If the DateTime or LocalDateTime object is updated then this field
 * is set to Long.MIN_VALUE, and is no longer considered valid.) Sending an identical result back to
 * .Net simplifies the testing program, as well as avoiding unnecessary loss of precision. (Joda-
 * Time is only precise to the nearest millisecond. .Net DateTime ticks can, in theory, be precise
 * to the nearest 100 nanoseconds.)
 */
public abstract class DateTimeCommon {

   // See here: 
   private static final long CTicksAtEpoch = 621355968000000000L;
   private static final long CTicksPerMillisecond = 10000;


   private long _dotNetDateTimeTicks;  // Long.MIN_VALUE means not valid


   // Constructor for new object, not due to MCopier deserialization
   public DateTimeCommon() {
      _dotNetDateTimeTicks = Long.MIN_VALUE;
   }

   // Copy constructor
   public DateTimeCommon(DateTimeCommon copyFrom) {
      _dotNetDateTimeTicks = copyFrom._dotNetDateTimeTicks;
   }

   // Constructor used by MCopier deserialization
   public DateTimeCommon(long dotNetDateTimeTicks) {
      _dotNetDateTimeTicks = dotNetDateTimeTicks;
   }


   protected void indicateDotNetTicksNotValid() {
      _dotNetDateTimeTicks = Long.MIN_VALUE;
   }


   // Method used by MCopier deserialization to compute the number of milliseconds in Java notation
   // that corresponds to a long int containing a .Net DateTime value in "ticks". But note that
   // although Java millis are normally always based on the UTC time zone, that the millis returned
   // by this method in the case of a .Net DateTimeLocal value are not UTC-based; they are
   // independent of time zone and represent a different instant in time for different time zones.
   // See also here:
   // 
   protected static long convertTicksToMillis(long dotNetDateTimeTicks) {

      return
         (dotNetDateTimeTicks - CTicksAtEpoch + CTicksPerMillisecond / 2) / CTicksPerMillisecond;
   }


   // Method used by MCopier serialization
   protected long getAsDotNetTicks(DateTime jodaDateTime) {

      if (_dotNetDateTimeTicks != Long.MIN_VALUE) {
         return _dotNetDateTimeTicks;
      }
      return (jodaDateTime.getMillis() * CTicksPerMillisecond) + CTicksAtEpoch;
   }
}

这是从基数 class:

派生的 classes 之一(更复杂的)
package com.Merlinia.MCopier_Main;

import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;


/**
 * This class provides a (mutable) wrapper for the (immutable) Joda-Time LocalDateTime class. See
 * comments on the DateTimeCommon base class.
 *
 * All manipulation of this object should consist of a call to getJodaLocalDateTime() followed by
 * manipulation of the LocalDateTime object, producing a new (immutable) LocalDateTime object,
 * followed by a call to setJodaLocalDateTime().
 *
 * When doing MCopier serialization and deserialization from/to .Net DateTime "ticks" we do
 * something a bit sneaky: We pretend that the ticks represent a UTC time, and we pretend that the
 * associated Joda-Time LocalDateTime object also represents UTC time. Both of these pretences are
 * (normally) false, but the end result is that it works. See also here:
 * 
 */
public class DateTimeLocal extends DateTimeCommon {

   private LocalDateTime _jodaLocalDateTime;


   // Constructor for new object, not due to MCopier deserialization
   public DateTimeLocal(LocalDateTime jodaLocalDateTime) {
      super();
      _jodaLocalDateTime = jodaLocalDateTime;
   }

   // Copy constructor
   public DateTimeLocal(DateTimeLocal copyFrom) {
      super(copyFrom);
      _jodaLocalDateTime = copyFrom._jodaLocalDateTime;
   }

   // Constructor used by MCopier deserialization
   public DateTimeLocal(long dotNetDateTimeTicks) {
      super(dotNetDateTimeTicks);
      _jodaLocalDateTime = new LocalDateTime(
                        DateTimeCommon.convertTicksToMillis(dotNetDateTimeTicks), DateTimeZone.UTC);
   }


   public LocalDateTime getJodaLocalDateTime() {
      return _jodaLocalDateTime;
   }

   public void setJodaLocalDateTime(LocalDateTime jodaLocalDateTime) {
      _jodaLocalDateTime = jodaLocalDateTime;
      super.indicateDotNetTicksNotValid();
   }


   // Method used by MCopier serialization
   public long getAsDotNetTicks() {
      return super.getAsDotNetTicks(_jodaLocalDateTime.toDateTime(DateTimeZone.UTC));
   }
}