如果构造函数的输入参数无效,如何创建一个防御性拷贝

4
在Josh Bloch的优秀著作Effective Java中,第39条建议如下:
“在检查参数的有效性之前,制作防御性副本,并且对副本而不是原件执行有效性检查。”
给出的示例如下:
public Period(Date start, Date end) {
   this.start = new Date(start.getTime());
   this.end = new Date(end.getTime());

   if(this.start.compareTo(this.end) > 0)
      throw new IllegalArgumentException("...");
   }
}

在进行防御性拷贝之后再进行有效性检查的问题在于,无效的参数会导致拷贝的创建失败。例如,如果你为`start`或`end`传入`null`,则上述类将抛出`NullPointerException`。如果我在防御性拷贝之前进行有效性检查,则会面临布洛赫提到的时间检查/使用攻击的风险,这也是进行防御性拷贝的原因。我的问题是如何解决这个问题?我不相信我是第一个看到这个问题的人(尽管该书的勘误表没有提到),所以可能我只是忽略了什么。
4个回答

1

防御性复制是一种好的策略,但它有其前提条件...其中之一是必须有可以真正复制的东西...

在我看来,这意味着在复制之前必须检查null,如果失败则抛出适当的异常...


1

你不需要对测试指针的有效性感到“防御”。指针不能更改为null或不同的对象,只有它所指向的内容可以更改。

在制作“防御性副本”时,您需要使用一种“棍子”来测试地形,然后再踏上它——在使用每个指针之前检查其有效性,限制检查边界值等。这并不难,只是繁琐,并且需要一些细节思维。

[此外,简单地允许NullPointerExceptions“冒泡”也没有太大的危害。]


1

正如其他人所说,您在复制参数之前检查null

如果我将有效性检查移动到防御性副本之前,我就会容易受到时间检查/使用攻击的影响,这是Bloch引用进行防御性副本的原因。

不,黑客无法将实际实例的引用更改为null引用或反之亦然。 复制是为了避免来自另一个线程对参数的内部状态进行更改。


我认为这部分解决了我之前遗漏的部分,但让我问一个澄清问题。如果我使用非空值调用 Period p = new Period(someStart, someEnd),然后在构造函数中的空检查之后和防御性复制之前将 someStart=null,那么当它到达 start.getTime() 调用时,它不会抛出 NullPointerException 吗? - stand
someStart只是一个对象的引用(假设该对象在0x123处)。当您使用参数someStart调用构造函数时,您正在将其start参数指向实际对象0x123。无论其他线程中是否设置了someStart = null,构造函数都只知道对象0x123。危险的是,如果从另一个线程调用someStart.changeState(5),这确实会更改对象0x123的状态。 - toto2
Java总是通过复制参数来传递它们 -- "按值调用"。例如,整数直接被复制。对于对象引用,引用中的地址被复制。一旦你在方法的参数列表中有了那个地址,对象就不会消失。如果调用者使用异步操作清除他的引用副本,则对你复制的内容没有影响。即使Java采用了"按引用调用"并且指针可以被改变,你只需要先复制指针的本地副本,然后再检查它是否为空即可。 - Hot Licks
我理解你的意思,空指针检查可以安全地执行,谢谢。我也在试着考虑更一般的情况,即因为无效的输入参数(不仅仅是空指针)而进行防御性复制可能会抛出异常。虽然我承认我想不到任何例子,所以我认为这不是个问题。 - stand

0

我不理解这个问题。异常框架已经考虑了无效参数。


可能有些情况下,即使你有空参数,也会构建一个对象,并使用一些默认值。但我承认这将是一个糟糕的设计。 - toto2

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