8

人们实际上曾经使用过防御性吸气剂/二传手吗?对我来说,99% 的时间你打算让你在另一个对象中设置的对象成为同一个对象引用的副本,并且你打算对它所做的更改也在它设置的对象中进行。如果你setDate ( Date dt )和以后修改 dt,谁在乎呢?除非我想要一些基本的不可变数据 bean,它只有原语并且可能像 Date 这样简单,否则我从不使用它。

就克隆而言,副本的深度或浅度存在问题,因此知道克隆对象时会出现什么似乎有点“危险”。我想我只使用clone()过一次或两次,那就是复制对象的当前状态,因为另一个线程(即另一个访问 Session 中相同对象的 HTTP 请求)可能正在修改它。

编辑 - 我在下面发表的评论更多的是问题:

但是话又说回来,您确实更改了日期,所以这是您自己的错,因此整个讨论了“防御性”一词。如果所有应用程序代码都在中小型开发人员群体中由您自己控制,那么仅记录您的类就足以替代制作对象副本了吗?或者这不是必需的,因为您应该始终假设在调用 setter/getter 时没有复制某些内容?

4

8 回答 8

12

来自 Josh Bloch 的 Effective Java:

您必须在假设您的类的客户将尽最大努力破坏其不变量的情况下进行防御性编程。如果有人试图破坏您的系统的安全性,这实际上可能是正确的,但您的类更有可能不得不处理由于使用您的 API 的程序员的诚实错误而导致的意外行为。无论哪种方式,花时间编写在面对行为不端的客户时仍然健壮的类是值得的。

第 24 条:在需要时制作防御性副本

于 2009-05-29T15:47:32.357 回答
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并且你为它自己提供一个getter Map。如果Map是对象内部状态的一部分,并且对象希望完全管理 的内容Map,那么你永远不应该泄露Map出去。如果您必须为地图提供 getter,则返回Collections.unmodifiableMap(myMap)而不是myMap. 由于潜在的成本,您可能不想在此处制作克隆或防御性副本。通过返回您的 Map 以使其无法修改,您可以保护您的内部状态不被另一个类修改。

由于许多原因,clone()往往不是正确的解决方案。一些更好的解决方案是:

  1. 对于吸气剂:
    1. 与其返回 a Map,不如仅Iterator将 s 返回到 thekeySet或 theMap.Entry或任何允许客户端代码执行其需要执行的操作。换句话说,返回本质上是您内部状态的只读视图的内容,或者
    2. 返回包装在不可变包装器中的可变状态对象,类似于Collections.unmodifiableMap()
    3. 与其返回 a ,不如Map提供一个get方法,该方法接受一个键并从映射中返回相应的值。如果所有客户都想Map从中获取价值,那么不要给客户Map本身;相反,提供一个包装Map'get()方法的 getter。
  2. 对于构造函数:
    1. 在对象构造函数中使用复制构造函数来复制传入的任何可变内容。
    2. 尽可能设计为将不可变数量作为构造函数参数,而不是可变数量。例如,有时将 long 返回的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结果是迭代集合时。

你应该防御性地编码吗?如果你能保证同样的专家程序员团队永远是编写和维护你的项目的人,他们会不断地工作,这样他们就可以记住项目的细节,同样的人会为它工作项目的生命周期,并且项目永远不会变得“大”,那么也许你可以不这样做。但是除了极少数情况外,防御性编程的成本并不高——而且收益很大。另外:防御性编码是一个好习惯。您不想鼓励养成将可变数据传递到不应该有它的地方的坏习惯。这有一天咬你。当然,所有这些都取决于项目所需的正常运行时间。

于 2009-05-29T17:02:29.137 回答
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... 
     }
}

关键不是你总是想克隆,那将是愚蠢和昂贵的。关键是不要笼统地声明这种事情何时合适。

于 2009-05-29T15:28:49.197 回答
1

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

于 2009-05-29T19:37:43.047 回答
1

我使用 Clone() 在用户会话中保存对象状态,以允许在编辑期间撤消。我也在单元测试中使用过它。

于 2009-05-29T15:57:31.627 回答
0

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

  1. 在您的类中创建复制构造函数,但要保护它们。原因是使用 new 运算符创建对象可能会在使用派生对象时导致各种问题。

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

     公共接口可复制<T> {
            公共T复制();
     }

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

Java 支持协变返回类型这一事实使这项工作得以实现。派生类只需适当地实现 copy() 方法,并为其特定类返回一个类型安全的值。

于 2010-04-08T23:13:59.073 回答
0

我不喜欢 clone() 方法,因为总是需要类型转换。出于这个原因,我大部分时间都使用复制构造函数。它更清楚地说明了它的作用(新对象),并且您可以很好地控制它的行为方式或副本的深度。

在我的工作中,我们不担心防御性编程,尽管这是一个坏习惯。但大多数时候一切正常,但我想我会仔细看看。

于 2009-05-29T19:27:32.990 回答
0

在“防御性副本讨论”中,我总是缺少一件事是性能方面。恕我直言,这是性能与可读性/安全性/健壮性的完美示例。

防御副本非常适合稳健的颂歌。但是,如果您在应用程序的时间关键部分使用它,它可能是一个主要的性能问题。我们最近进行了这个讨论,其中数据向量将其数据存储在 double[] 值中。getValues() 返回 values.clone()。在我们的算法中,为许多不同的对象调用了 getValues()。当我们想知道为什么这段简单的代码需要这么长时间执行时,我们检查了代码 - 用返回值替换了 return values.clone(),突然我们的总执行时间降低到不到原来的 1/10价值。好吧 - 我不需要说我们选择跳过防守。

注意:我不是一般的防御副本。但是在 clone() 时使用你的大脑!

于 2009-09-07T08:45:04.993 回答