Calendar的防御性拷贝

18

一直在尝试找到最佳方法来实现一个可以对日历对象进行防御性复制的方法。

例如:

public void setDate(Calendar date) {
    // What to do.... 
}

我尤其关注的是在线程检查空输入并进行复制时的交错问题,或者我是否忽略了什么非常明显的东西?


2
克隆有什么问题吗?(我非常感兴趣) - zeller
好的观点,如果对象本身不可扩展,克隆是一个好选择,但不幸的是,Calendar(GregorianCalendar)是可扩展的,但如果我确定对象是日历,并且没有其他淘气子类,那么可以使用克隆。实际上,我在同一类的getter方法中使用克隆方法,因为我确信我的内部日历实际上就是那个。 - JJ Vester
我明白了,你说得对。但是如果对象属于一个你不想支持的类,你可以禁止它的复制,对吧? - zeller
公正的观点,我担心这种方法可能会变得有些臃肿,仅仅为了一个setter :) 但是这是一个有效的观点。 - JJ Vester
1
如果你担心同步和交错问题,那么请在setDate()方法中添加“synchronize”。你关于“检查空值”的评论听起来有点像双重检查锁定问题(http://en.wikipedia.org/wiki/Double-checked_locking)- 如果是这种情况,再次同步就是答案。 - Paul Jowett
显示剩余2条评论
7个回答

28

现在的受众对象可能略有不同了...

如果我真的必须使用Calendar(而不是Joda Time),我会使用clone()。您在评论中争辩说您担心“淘气子类”-您如何提议在任何方案中解决这个问题? 如果您不知道涉及的子类,也不信任它们,那么您就无法保留特定于类型的数据。如果您不相信子类不会弄乱事情,那么您一般会面临更大的问题。当进行日期/时间计算时,您如何相信它将为您提供正确的结果? clone()是克隆对象的预期方法:我期望一个明智的子类在其中钩入任何需要的特定于类型的行为。您不需要知道哪些状态位是相关的-您只需让类型自己处理即可。

与使用Calendar.getInstance()并自己设置属性相比的优势:
  • 您将保留相同的日历类型
  • 您不需要担心遗忘属性:这是该类型的责任
  • 您明确表示要做的什么,并让实现来处理如何,这总是很好的。您的代码精确地表达了您的意图。

编辑:关于原始问题担心的“线程交错”:无论其他线程做什么,date参数的值都不会更改 。但是,如果另一个线程在您进行防御性复制时修改对象的内容,则可能非常容易导致问题。如果这是一个风险,那么您基本上面临着更大的问题。


18

最简单的方法是:

copy = Calendar.getInstance(original.getTimeZone());
copy.setTime(original.getTime());

我强烈建议(尽可能地)在Java中使用JodaTime来表示时间和日期。它有可变和不可变的类。


2
我认为这并不能确保日历处于同一时区。 - zeller
是的,我喜欢这个方向,我担心在检查空值并随后将副本设置为原始时间戳时,并发线程之间的交错问题,我认为这是一个我必须面对的问题。是的,我听说JodaTime非常好,虽然我必须承认我没有使用过它 :) 我会去看看的。 - JJ Vester
3
@zeller 您关于时区的观点是正确的。我已经编辑了这个例子。 - Luka Klepec
1
这并不保证它会是完全相同类型的日历。 - Jon Skeet

2

我知道这已经过时了,但我想发表一下我的看法。

如果您按照合同编写程序,则对象不负责另一个对象的错误。日历实现了Cloneable,这意味着子类也会实现!如果Calendar的子类违反了Cloneable合同,则需要纠正子类,而不是调用clone的类。

在面向对象编程中,一个对象只应关心它所涉及的类和合同。当您问“如果子类违反了它怎么办?”时,它会大大地复杂化设计,因数而异。每当对象将对象作为参数传递时,永远存在该对象是子类且会破坏所有内容的可能性。在调用getX()时,为了子类不抛出ArithmeticException异常,您是否进行防御性编程?

Jon Skeet也提供了一个非常好的答案,比我的更好,但我认为此问题的潜在触发者可能从听到“按合同设计”这个小小的点中受益匪浅。虽然方法已经接近死亡,但方法论对我的设计有很大的帮助。


1
只需将您的日历对象包装到ThreadLocal中。这将确保每个日历实例仅由一个线程使用。就像这样:
public class ThreadLocalCalendar
{
    private final static ThreadLocal <Calendar> CALENDAR =
        new ThreadLocal <Calendar> ()
        {
            @Override
            protected Calendar initialValue()
            {
                GregorianCalendar calendar = new GregorianCalendar();

                // Configure calendar here.  Set time zone etc.

                return calendar;
            }
        };

    // Called from multiple threads in parallel
    public void foo ()
    {
        Calendar calendar = CALENDAR.get ();

        calendar.setTime (new Date ());
        // Use calendar here safely, because it belongs to current thread
    }
}

1
以下是什么意思?
public synchronized void setDate(Calendar date) {
    // What to do.... 
    Calendar anotherCalendar = Calendar.getInstance();
    anotherCalendar.setTimeInMillis(date.getTimeInMillis());
}

在编程中,synchronized 的正确使用取决于您的用例。


虽然也可以用,但我更喜欢克隆的答案,在一个完美世界里,使用不可变类型时,克隆应该是更好的方法。不过,你的回答也是一种有效的尝试。 - JJ Vester

0

这是不可能保证的!

线程安全:除非您了解从引用来源实施的安全方案,否则无法保证线程安全。该方可能会给您提供一个新的参考,这种情况下,您可以直接使用参考。该方可能已经针对该日历参考发布了他们的安全方案,在这种情况下,您可以遵循相同的方案(有时不可能)来检查非空引用、类型,然后通过使用getInstance()进行防御性复制。如果没有了解这些,我认为无法确保线程安全。

Calendar的防御性复制:如果您不信任引用来源的地方,则克隆不是一种选择!Calendar不支持任何使用现有Calendar对象创建新对象的构造函数!

简而言之,没有解决问题的方法。JodaTime是前进的最佳方式。


-2
我建议在这里使用“同步块”。

同步化不会对获取防御性副本做任何事情。 - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接