克隆()真的用过吗?在getter / setter中防御性复制怎么样?

时间:2009-05-29 14:52:07

标签: java java-ee clone defensive-programming

人们几乎都会使用防御性的吸气者/安装者吗?对我来说,99%的时间打算将您在另一个对象中设置的对象作为同一对象引用的副本,并且您打算对其所做的更改也在其设置的对象中进行。如果你setDate ( Date dt )并稍后修改dt,谁在乎呢?除非我想要一些基本的不可变数据bean,它只有原语,而且可能像Date一样简单,我从不使用它。

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

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

但是再一次,你DID改变日期,所以这是你自己的错,因此整个讨论术语“防守”。如果在中小型开发人员组中,您自己控制的是所有应用程序代码,那么仅仅记录您的类是否足以作为制作对象副本的替代方法?或者这不是必需的,因为你应该总是假设在调用setter / getter时没有被复制的东西?

8 个答案:

答案 0 :(得分:11)

来自Josh Bloch的Effective Java:

  

你必须假设你班级的客户会尽力摧毁其不变量,从而进行防御性编程。如果有人试图破坏您系统的安全性,这实际上可能是真的,但更有可能您的课程必须应对程序员使用您的API的诚实错误导致的意外行为。无论哪种方式,值得花时间编写一些在不良行为客户面前表现强大的课程。

Item 24: Make defensive copies when needed

答案 1 :(得分: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没有制作防御性副本,那么您已经更改了{{1}的内部状态}}。
  3. 您应该针对这两种情况提供保护,或者您应该记录您对允许或禁止的内容的期望,具体取决于代码的客户端是谁。另一种情况是,当一个类有一个someObject并且你为Map本身提供了一个getter。如果Map是对象内部状态的一部分,并且对象希望完全管理Map的内容,那么您应该永远Map退出。如果您必须为地图提供getter,请返回Map而不是Collections.unmodifiableMap(myMap)。在这里,由于潜在的成本,您可能不希望制作克隆或防御性副本。通过返回Map包装使其无法修改,您可以保护内部状态不被其他类修改。

    由于许多原因,myMap通常不是正确的解决方案。一些更好的解决方案是:

    1. 对于吸气者:
      1. 而不是返回clone(),只返回MapIteratorkeySet或任何允许客户端代码执行所需操作的内容。换句话说,返回基本上是内部状态的只读视图的内容,或
      2. 返回包含在类似于Map.Entry
      3. 的不可变包装器中的可变状态对象
      4. 不是返回Collections.unmodifiableMap(),而是提供一个Map方法,该方法接受一个键并从地图中返回相应的值。如果所有客户端都使用get来获取值,那么请不要向客户端提供Map本身;相反,提供一个包含Map的{​​{1}}方法的getter。
    2. 对于构造函数:
      1. 在对象构造函数中使用复制构造函数来复制传入的任何可变的内容。
      2. 设计在可能的情况下将不可变数量作为构造函数参数,而不是可变数量。例如,有时使用Map返回的长整数而不是get()对象是有意义的。
      3. 尽可能多地制作您的州new Date().getTime(),但请记住,Date对象仍然可以变更,并且仍然可以修改final数组。
    3. 在所有情况下,如果存在关于谁“拥有”可变状态的问题,请将其记录在getter或setter或构造函数上。把它记录在某处。

      以下是错误代码的一个简单示例:

      final

      应该看到每个runnable睡眠500毫秒,但是你得到错误的时间信息。如果您更改构造函数以生成防御性副本:

      final

      然后你会得到正确的时间信息。这是一个微不足道的例子。您不希望调试复杂的示例。

      注意:在迭代集合时,常见无法正确管理状态的结果为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); } } }

      你应该采取防御措施吗?如果您能保证同一个专业程序员团队将始终是编写和维护项目的人,他们将继续为此工作,以便他们保留对项目细节的记忆,同样的人会在项目的整个生命周期中对它进行研究,并且项目永远不会变得“大”,那么也许你可以逃避不这样做。但是防御性编程的成本并不大,除非是最罕见的情况 - 并且收益很大。另外:防守编码是一个好习惯。您不希望鼓励将可变数据传递到不应该拥有它的地方的坏习惯的发展。有一天咬你。当然,所有这些都取决于项目所需的正常运行时间。

答案 2 :(得分: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... 
     }
}

关键不在于你总是想要克隆,这将是愚蠢和昂贵的。关键是不要对何时合适的事情进行全面陈述。

答案 3 :(得分:1)

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

答案 4 :(得分:1)

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

答案 5 :(得分:0)

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

在我的工作中,我们并不担心防御性编程,尽管这是一个糟糕的习惯。但大部分时间都没问题,但我想我会仔细研究一下。

答案 6 :(得分:0)

我在“防御性复制讨论”中遗漏的一件事是表现方面。 嘲笑是恕我直言,性能与可读性/安全性/稳健性的完美结合。

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

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

答案 7 :(得分:0)

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

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

  2. 按如下方式创建可复制界面:

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

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

    Java支持covariant return type这一事实使得这项工作成为可能。派生类只是根据需要实现copy()方法,并为其特定类返回类型安全值。