clone()方法真的有用吗?在getter/setter中采用防御性拷贝呢?

8
人们真的会用到防御性getter/setter吗?对我来说,99%的情况下,您打算将在另一个对象中设置的对象复制为相同的对象引用,并且您打算对其进行的更改也将在设置它的对象中进行。如果您setDate(Date dt)并稍后修改dt,谁在乎呢?除非我想要一些基本的不可变数据bean,它只具有原语和可能是简单的日期,否则我从不使用它。
至于克隆,存在深度或浅层副本的问题,因此似乎知道在克隆对象时会出现什么有点“危险”。我认为我只使用过一两次clone(),那是为了复制对象的当前状态,因为另一个线程(即另一个访问Session中的同一对象的HTTP请求)可能正在修改它。
编辑-我在下面发表的评论更多是问题:
但是,您确实更改了日期,所以这有点是您自己的错,因此整个“defensive”术语的讨论。如果这是您自己控制的所有应用程序代码,在一个小到中等规模的开发人员组中,只记录您的类是否足以替代制作对象副本吗?还是这不是必需的,因为在调用setter/getter时应始终假定某些东西没有复制?

1
你在这里提出了两个问题。你应该把它们分开。 - Welbog
它们可以说是“手牵手”前进的。 - GreenieMeanie
8个回答

12

来自Josh Bloch的《Effective Java》:

你必须以防御性编程的方式来假定你的类的客户端会尽力破坏它的不变式。如果有人试图破坏系统的安全性,这可能是真实的情况,但更可能的是,使用您的API的程序员的诚实错误导致意外行为,而您的类将不得不应对这些情况。无论哪种方式,写出能够在面对不良客户时保持强健的类都是值得花时间去做的。

条款24:需要时进行防御性拷贝


是的,这是一篇不错的文章。 另外,你的对象是否是深度不可变的?如果是,就不用担心clone()了。 如果不是,你需要重写clone()来复制那些可变的成员。 - Sorin Mocanu
我已经读了那本书,是一本不错的读物。然而,我更想要更多真实和实际的例子。例如,如果我们查看Java API的内部,是否有任何类实际上正在执行这个操作? - GreenieMeanie
我希望Java API可以做到这一点(Josh Block确实编写了许多java.util类)- 对于公共API来说,这真的非常重要。再次强调,大多数情况下,制作防御性副本所涉及的对象创建开销是可以忽略不计的。因此,如果制作防御性副本永远不会真正伤害您,但不这样做可能会引入非常严重的错误(相信我,这些是最糟糕的错误),那么这是一个明智的选择。 - bajafresh4life
是的,防御性拷贝很好...但我来这里是想知道使用clone()获取防御性拷贝是否安全。 - scottb

5

这是一个不太简单的问题。基本上,您需要考虑通过getter或调用另一个类的setter将给定给任何其他类的类的任何内部状态。例如,如果您执行以下操作:

Date now = new Date();
someObject.setDate(now);
// another use of "now" that expects its value to not have changed

如果你有一个类似于下面的方法:
  1. someObject 可能会改变 "now" 的值,这意味着当该方法后来使用该变量时,它可能会得到一个不同于预期的值。
  2. 如果在将 "now" 传递给 someObject 后您更改了其值,并且如果 someObject 没有进行防御性拷贝,则已更改 someObject 的内部状态。
您应该保护这两种情况,或者根据代码的客户端是谁,记录您所允许或禁止的期望。另一种情况是当一个类有一个 Map 并且您为 Map 本身提供了一个 getter 时。如果 Map 是对象的内部状态的一部分,并且该对象希望完全管理 Map 的内容,则永远不要让 Map 出去。如果您必须为地图提供一个 getter,则应返回 Collections.unmodifiableMap(myMap) 而不是 myMap。在这里,由于潜在成本,您可能不想进行克隆或防御性拷贝。通过返回包装后无法修改的 Map,您可以保护内部状态不被另一个类修改。
由于许多原因,clone() 通常不是正确的解决方案。一些更好的解决方案是:
  1. 对于getter:
    1. 不要返回Map,而是返回只针对keySet或Map.Entry或允许客户端代码执行所需操作的东西的迭代器。换句话说,返回的内容应该是你内部状态的只读视图;或
    2. 返回可变状态对象,但用类似于Collections.unmodifiableMap()的不可变包装进行封装。
    3. 提供一个get方法,它接受一个键并从地图中返回相应的值,而不是返回Map。如果所有客户端都要做的就是从Map中获取值,那么不要将Map本身放给客户端;相反,提供一个包装Map的get方法。
  2. 对于构造函数:
    1. 在对象构造函数中使用复制构造函数来复制任何可变的传入对象。
    2. 尽可能使用不可变数量作为构造函数参数,而不是可变数量。例如,有时候使用new Date().getTime()返回的长整型比使用Date对象更合理。
    3. 尽可能使你的状态是final的,但记住,final对象仍然是可变的,final数组仍然可以被修改。

在所有情况下,如果存在关于谁拥有可变状态的问题,请在getter、setter或构造函数上进行记录。无论如何,都要在某个地方记录下来。

以下是糟糕代码的一个微不足道的例子:

import java.util.Date;

public class Test {
  public static void main(String[] args) {
    Date now = new Date();
    Thread t1 = new Thread(new MyRunnable(now, 500));
    t1.start();
    try { Thread.sleep(250); } catch (InterruptedException e) { }
    now.setTime(new Date().getTime());   // BAD!  Mutating our Date!
    Thread t2 = new Thread(new MyRunnable(now, 500));
    t2.start();
  }

  static public class MyRunnable implements Runnable {
    private final Date date;
    private final int  count;

    public MyRunnable(final Date date, final int count) {
      this.date  = date;
      this.count = count;
    }

    public void run() {
      try { Thread.sleep(count); } catch (InterruptedException e) { }
      long time = new Date().getTime() - date.getTime();
      System.out.println("Runtime = " + time);
    }
  }
}

您应该看到每个可运行对象都会休眠500毫秒,但实际上您得到了错误的时间信息。如果您将构造函数更改为进行防御性复制:

    public MyRunnable(final Date date, final int count) {
      this.date  = new Date(date.getTime());
      this.count = count;
    }

然后,你就可以获得正确的时间信息。这只是一个简单的例子。您不希望调试一个复杂的例子。
注意:如果未正确管理状态,则ConcurrentModificationException(并发修改异常)在迭代集合时是常见的结果。
您应该进行防御性编码吗?如果您可以保证同一小组专业程序员始终是撰写和维护项目的人,他们将持续工作以保留项目细节的记忆,相同的人将在项目的生命周期内工作,并且该项目永远不会变得“大”,那么也许您可以不采取防御性措施。但是,除了极少数情况外,防御性编程的成本并不高--而且好处是巨大的。此外:防御性编码是一个好习惯。您不希望鼓励传递可变数据的不良习惯。这样做总有一天会对您造成困扰。当然,所有这些都取决于您的项目所需的正常运行时间。

我的观点是,(以你的一个例子为例)逻辑上循环论证是我的全部意思——如果您稍后修改“现在日期”是否真的很重要?换句话说,您的程序会崩溃还是会引入错误? - GreenieMeanie
1
这肯定会引入错误。 - Eddie
当然可以。如果您的setter执行任何验证,例如日期在过去,然后调用者将对象修改为将来的某个时间,则刚刚绕过了对象中的有效性检查。它现在处于未定义状态。 - sk.
但是,你确实改变了日期,所以这有点是你自己的错,因此整个“防御性”术语的讨论。如果所有应用程序代码都在你自己控制下的小到中等规模的开发人员组中,仅记录你的类是否足以替代制作对象副本?或者这不是必要的,因为当调用setter/getter时,你应该始终假设某些东西没有被复制? - GreenieMeanie
@GreenieMeanie:这取决于你是否愿意在半夜被叫醒处理出现的问题,以及你是否愿意花时间调试那些本可以轻松避免的错误。这就像是防御性驾驶一样——如果你因为没有采取防御措施而发生了本可以轻松避免的事故,那么你失去使用汽车的机会和责任与事故责任相同。 - Eddie
2
经过长时间的编程,参与了许多技能水平不同的团队,也见证了“演示成为产品”的情况不止一次,因此我总是主张在成本允许的情况下进行防御性编程。当成本过高时,就要进行文档记录。或者采用类似JDK集合框架中迭代器的方式,在禁止状态更改时进行快速失败检查。 - Eddie

3

对于这两个问题,关键是要明确控制状态。也许在大多数情况下,您可以“逃避”不考虑这些事情。随着应用程序变得越来越大,以及难以推理状态以及它如何在对象之间传播时,这往往不再成立。

您已经提到了需要控制状态的主要原因 - 能够在另一个线程访问数据时安全使用它。但是也很容易犯以下错误:

class A {
   Map myMap;
}


class B {
   Map myMap;
   public B(A a)
   {
        myMap = A.getMap();//returns ref to A's myMap
   }
    public void process (){ // call this and you inadvertently destroy a
           ... do somethign destructive to the b.myMap... 
     }
}

重点不是你总是想要克隆,那会很愚蠢且昂贵。重点不是对于何时适合这种操作做出笼统的陈述。

通常在 API 文档中,getter/setter 方法只会被简单地描述为 "获取或设置某个东西",因此你并不知道它的真正作用是什么。 - GreenieMeanie
如果我正在获取地图,我不会假设可以安全修改它,除非Javadoc明确说明它是安全的。你似乎在寻找“正确”的方法 - 正确的方法是意识到问题和方法副作用,并采取适当的措施。 - Steve B.
1
一个好习惯是,如果你依赖于一个你“拥有”(创建并包含)的对象的状态,要么将其设置为不可变,永远不要返回它,要么返回一个副本。永远不要相信任何人不会破坏你的对象,不要给他们这个能力!(这也是绝对不要传递没有包装类的集合的一个非常好的理由)。 - Bill K
是的,虽然需要一些防御措施,但不应该复制两次同样的东西。不必要的复制会严重影响性能。 - iny

1

我在用户会话中使用了Clone()来保存对象状态,以便在编辑过程中进行撤销。我还在单元测试中使用了它。


1

我可以想到一种情况,其中克隆比复制构造函数更可取。如果您有一个函数,它接受类型为X的对象,然后返回修改后的副本,如果您想保留内部的非X相关信息,则使用克隆作为该副本可能更可取。例如,通过5小时增加Date的函数即使传递了SpecialDate类型的对象也可能很有用。尽管如此,大多数情况下,使用组合而不是继承可以完全避免这些问题。


0

我已经开始使用以下做法:

  1. 在你的类中创建复制构造函数,但将它们设置为受保护。这样做的原因是使用 new 运算符创建对象时,当与派生对象一起使用时可能会导致各种问题。

  2. 创建一个 Copyable 接口,如下所示:

     public interface Copyable<T> {
            public T copy();
     }

让实现 Copyable 接口的类的 copy 方法调用受保护的复制构造函数。派生类可以调用 super.Xxx(obj_to_copy); 来利用基类的复制构造函数,并根据需要添加其他功能。

Java 支持 covariant return type 的事实使得这项工作成为可能。派生类只需适当地实现 copy() 方法并为其特定类返回类型安全的值即可。


0

在“防御性复制讨论”中,我总是觉得缺少一个方面,那就是性能方面。 在我看来,这种讨论是性能与可读性/安全性/稳健性之间的完美例证。

防御性复制对于稳健代码非常有用。但如果您将其用于应用程序的时间关键部分,则可能会成为主要的性能问题。我们最近进行了这项讨论,在其中数据向量将其数据存储在 double[] 值中。getValues() 返回 values.clone()。 在我们的算法中,getValues() 被调用了很多不同的对象。当我们在想为什么这么简单的代码执行起来如此缓慢时,我们检查了代码-使用 return values 代替 return values.clone() 后,我们的总执行时间降低到原来的不到1/10。好吧-我不需要说我们选择跳过防御性。

注意:我并不反对一般的防御性复制。但是在 clone() 时请使用大脑!


0

我不喜欢clone()方法,因为它总是需要类型转换。因此,我大多数时候使用复制构造函数。它更清楚地说明了它所做的事情(创建新对象),并且您可以更好地控制它的行为或复制的深度。

在我的工作中,我们不担心防御性编程,尽管这是一个坏习惯。但大多数时候都还好,但我认为我会更仔细地看待它。


如果您不知道要复制/克隆的对象的类类型怎么办?如果我给您一只想要复制的动物,您会使用哪个类的复制构造函数?从动物扩展出哺乳动物、狗、猫等。clone()的目的是让您可以获得一个副本,而无需确定其类类型。 - Steve Kuo
在JDK 1.5+中,您可以使用克隆方法并将返回类型更改为您的类(尽管几乎没有人这样做,因为他们习惯了旧的限制)。但是,由于其他原因,克隆已经失效。 - Yishai

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