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

Gre*_*nie 8 java defensive-programming clone java-ee

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

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

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

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

baj*_*ife 11

来自Josh Bloch的Effective Java:

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

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


Edd*_*die 5

这是一个非常重要的问题.基本上,你必须考虑通过getter或通过调用另一个类的setter给任何其他类的类的任何内部状态.例如,如果您这样做:

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

那么你可能有两个问题:

  1. someObject可能会改变" now" 的值,这意味着上面的方法在以后使用该变量时可能具有与预期不同的值,并且
  2. 如果在传递" now" 之后someObject改变了它的价值,如果someObject没有制作防御性副本,那么你就改变了内部状态someObject.

您应该针对这两种情况提供保护,或者您应该记录您对允许或禁止的内容的期望,具体取决于代码的客户端是谁.另一种情况是当一个类有一个Map并且你为自己提供一个吸气剂Map.如果Map是对象的内部状态的一部分并且对象期望完全管理内容Map,那么你永远不应该让它Map出来.如果你必须为地图提供一个getter,那么返回Collections.unmodifiableMap(myMap)而不是myMap.在这里,由于潜在的成本,您可能不希望制作克隆或防御性副本.通过返回Map包装以使其无法修改,您可以保护内部状态不被其他类修改.

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

  1. 对于吸气者:
    1. 而不是返回a Map,只返回Iterators到或者keySet或者Map.Entry允许客户端代码执行它需要做的事情.换句话说,返回一些本质上是内部状态的只读视图,或者
    2. 返回包含在类似于的不可变包装器中的可变状态对象 Collections.unmodifiableMap()
    3. 而不是返回a Map,提供一个get方法来获取键并从地图返回相应的值.如果所有客户都会使用Mapis get值,那么就不要给客户Map自己; 相反,提供一个包装的吸气剂Mapget()方法.
  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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

应该看到每个可运行的睡眠时间为500毫秒,但您会得到错误的时间信息.如果您更改构造函数以生成防御性副本:

    public MyRunnable(final Date date, final int count) {
      this.date  = new Date(date.getTime());
      this.count = count;
    }
Run Code Online (Sandbox Code Playgroud)

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

注意:无法正确管理状态的常见结果是ConcurrentModificationException迭代集合时.

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

  • 经过很长一段时间的编程,曾经在不同技能水平的不同团队中工作过,不止一次看过"演示成为产品",我总是提倡防御性编程,除非这样做的成本太高.当成本过高时,文件,文件,文件.或者以迭代器在JDK集合框架中的方式以快速失败的方式检查不允许的状态更改. (2认同)

Ste*_* B. 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... 
     }
}
Run Code Online (Sandbox Code Playgroud)

重点不是你总是想克隆,那会很愚蠢而且昂贵。重点不是要对何时适合进行此类事情做出笼统的声明。