从构造函数调用 setter

从构造函数调用 mutator (如果有的话)的优缺点是什么

例如:

public MyConstructor(int x) {
this.x = x;
}

对比:

public MyConstructor(int x) {
setX(x);
}


public void setX(int x) {
this.x = x;
}

你有什么偏好吗?(这不是家庭作业,只是看看我们的编码标准文档,其中提到在构造函数中设置实例变量时,总是调用 mutator,我并不总是这样做)

43111 次浏览

这取决于你如何对待 setter。如果您认为可以从该类派生,则可以允许这样的调用,以便在其他地方重写该行为。否则,您可以将其设置为。

当然,允许显式调用 setter 还可以提供在设置期间注入其他行为的方法(比如在需要时应用安全性)。

就个人而言,我会直接在 大部分的情况下设置变量。

方法 通常期望实例在被调用时已经完全形成。特别是,从构造函数调用重写的方法会导致难以理解的代码和难以发现的错误。

尽管如此,我还是经常尝试使类不可变,在这种情况下,不仅没有 setter,而且还需要 从构造函数(或变量初始化器)设置最终变量:)

在属性具有逻辑的情况下,setter 逻辑通常是验证,有时会将传播更改为观察者。我通常希望在方法开始时显式检查构造函数参数,而且在实例完全创建之前不会发生 想要任何更改传播。

除非您的 setter 正在执行比 this.x = x更复杂的操作,否则我将直接设置变量。这个参数直接指向你的实例变量对我来说更能说明问题。

我很少这样做,我从来没有因为这个问题。它也可以有意外后果,特别是如果你的 setter 在子类中被覆盖,或者你的 setter 在初始化过程中触发了额外的方法调用。

我也不介意,我也没有强烈的偏好[也不会坚持一些严格的标准] ,只要用你的判断。但是如果使用 w/setter,那么重写该类的任何人都必须知道对象可能没有完全创建。也就是说,要确保 setter 中没有额外的代码可以发布 this引用。

Setters 对于标准范围检查等非常有用,因此不需要额外的代码来验证输入。同样,使用 setter 是一种稍微肮脏一点的方法。

使用该字段有明显的好处(比如确保子类中的任何人都不能恶意地改变行为) ,而且它通常是首选的,尤其是。在图书馆。

实际上,c-tor 是创建对象的三种可能方式之一(序列化 + 克隆是另外两种方式) ,可能需要在 c-tor 之外处理一些瞬态状态,因此您仍然需要考虑字段与 setter 之间的区别。(编辑,还有另一种方法: 不安全,但这是序列化使用的方法)

出于以下几个原因,我倾向于直接在构造函数中设置它们。首先,像 this.x = x;这样的东西就像调用一个单独的方法做同样的事情一样清楚,如果不是更清楚的话。其次,除非标记为 final,否则该方法可能已经被重写,从构造函数调用可能被重写的方法在我的书中是一个大禁忌。第三,大多数方法通常假设对象在执行时已经完成,而不是构造到一半。虽然在这种简单的情况下,这不会引起任何问题,但在更复杂的情况下,它可能会导致需要很长时间才能追踪到的非常微妙的 bug。

在任何地方使用 setter/getter 的主要原因是,这意味着只需在3个地方更改字段名、定义、 getter/setter 方法,就可以重命名字段,所有这些都应该能够编译并且正常运行。在我看来,这个参数现在是无效的,因为任何像样的现代 IDE 都会用一个简单的快捷键重命名这个字段的所有匹配项。

我们的编码标准要求使用访问器(也可能是私有的)只有一个原因。 如果您想快速查找正在更改字段的代码,只需在 Eclipse 中打开 setter 的调用层次结构即可!

对于一个代码库来说,这是一个巨大的时间节省,因为代码库已经达到了200万行代码,而且还简化了重构。

我遵循两条关于构造函数的规则来最小化问题,这就是为什么我不使用 mutator 方法的原因:

(非 final 类的)构造函数应该只调用 final 或 private 方法 。如果您决定忽略此规则并让构造函数调用非 final/非 private 方法,那么:

  • 这些方法和它们可能调用的任何方法都必须小心,不要假定实例已经完全初始化,并且
  • 覆盖这些方法的子类(甚至可能不知道超类构造函数调用这些方法的子类)不能假定子类构造函数和超类的构造函数已经完全执行。在继承层次结构的更深处,带有“邪恶”构造函数的超类变得更糟。

那些额外的认知包袱值得吗?对于那些只给实例变量赋值的简单变异函数,你可以允许一个异常,因为这样做没有什么好处,甚至看起来也不值得。

[[@Jon Skeet 在他的回答中提到: “ ... ... 特别是,从构造函数调用重写的方法会导致难以理解的代码和难以发现的 bug。”但我认为这个问题的后果压力还不够大。]

在实例完全初始化之前,构造函数应该小心不要泄漏 this。虽然前面的规则是关于访问 ivar 的类和子类内部的方法,但是在完全初始化 this之前,还必须注意(甚至是 final/private)将 this传递给其他类和实用程序函数的方法。构造函数调用的非私有、可重写的方法越多,泄漏 this的风险就越大。


关于构造函数调用非 final、非私有方法的一些引用:

Https://www.securecoding.cert.org/confluence/display/java/met05-j.+ensure+that+constructors+do+not+call+overridable+methods

Http://www.javaworld.com/article/2074669/core-java/java-netbeans——overridable-method-call-in-constructor.html

Http://www.javaspecialists.eu/archive/issue210.html

在构造函数中调用任何 publicstaticnon-final方法都取决于你,但是最好的做法是永远不要在构造函数中调用这些方法,因为这些方法可以在子类中被重写,实际上只有这些方法的重写版本才会被调用(如果你使用多态行为)。

例如:

public class Foo {


public Foo() {
doSmth(); // If you use polymorphic behavior this method will never be invoked
}


public void doSmth() {
System.out.println("doSmth in super class");
}


public static void main(String[] args) {
new Bar(200);
}
}


class Bar extends Foo {


private int y;;


public Bar(int y) {
this.y = y;
}


@Override
public void doSmth() { // This version will be invoked even before Barr object initialized
System.out.println(y);
}


}

它将打印0。

更多细节请阅读 Bruce Eckel“ Thinking in Java”章“多态性”