【问题标题】:Is it a good or bad practice to call instance methods from a java constructor?从 java 构造函数调用实例方法是好还是坏?
【发布时间】:2010-03-24 21:50:04
【问题描述】:

有几种不同的方法可以初始化复杂对象(使用注入的依赖项和所需的注入成员设置),看起来都很合理,但各有优缺点。我举个具体的例子:

final class MyClass {
  private final Dependency dependency;
  @Inject public MyClass(Dependency dependency) {
    this.dependency = dependency;
    dependency.addHandler(new Handler() {
      @Override void handle(int foo) { MyClass.this.doSomething(foo); }
    });
    doSomething(0);
  }
  private void doSomething(int foo) { dependency.doSomethingElse(foo+1); }
}

如您所见,构造函数做了 3 件事,包括调用实例方法。我被告知从构造函数调用实例方法是不安全的,因为它绕过了编译器对未初始化成员的检查。 IE。我可以在设置this.dependency 之前调用doSomething(0),这样可以编译但不起作用。重构它的最佳方法是什么?

  1. doSomething 设为静态并显式传入依赖项?在我的实际案例中,我有三个实例方法和三个成员字段,它们都相互依赖,所以这似乎需要大量额外的样板来使这三个都成为静态。

  2. addHandlerdoSomething 移动到@Inject public void init() 方法中。虽然与 Guice 一起使用是透明的,但它需要任何手动构造以确保调用init(),否则如果有人忘记,该对象将无法完全发挥作用。此外,这会暴露更多的 API,这两者似乎都是坏主意。

  3. 包装一个嵌套类以保持依赖关系,以确保它正常运行而不会暴露额外的 API:

    类 DependencyManager {
      私有最终依赖依赖;
      公共 DependecyManager(依赖依赖) { ... }
      公共 doSomething(int foo) { ... }
    }
    @Inject public MyClass(依赖依赖){
      DependencyManager manager = new DependencyManager(dependency);
      manager.doSomething(0);
    }
    这会将实例方法从所有构造函数中提取出来,但会生成一个额外的类层,当我已经有内部和匿名类(例如那个处理程序)时,它可能会变得混乱 - 当我尝试这个时,我被告知将 DependencyManager 移动到一个单独的文件,这也令人讨厌,因为它现在是多个文件来做一件事情。

那么处理这种情况的首选方法是什么?

【问题讨论】:

  • @Steve:我刚刚删除了第一个“pre”标签,以便代码使用颜色编码语法显示:)
  • 酷,不知道它是这样工作的。

标签: java constructor guice


【解决方案1】:

Effective Java 中的 Josh Bloch 建议使用静态工厂方法,尽管我找不到任何关于这种情况的论据。然而,Java Concurrency in Practice 中也有类似的情况,专门用于防止从构造函数中泄露对this 的引用。应用到这种情况下,它看起来像:

final class MyClass {
  private final Dependency dependency;

  private MyClass(Dependency dependency) {
    this.dependency = dependency;
  }

  public static createInstance(Dependency dependency) {
    MyClass instance = new MyClass(dependency);
    dependency.addHandler(new Handler() {
      @Override void handle(int foo) { instance.doSomething(foo); }
    });
    instance.doSomething(0);
    return instance;
  }
  ...
}

但是,这可能不适用于您使用的 DI 注释。

【讨论】:

  • 我正在使用 Guice(实际上是 Gin)——它不直接支持静态工厂,但它也不会阻止它们——我只需要添加一个GinModule 中工厂方法的额外副本。另一种选择是创建一个Provider<MyClass> 嵌套类并将MyClass 注释为@ProvidedBy(MyClass.MyProvider.class)。我一直试图避免使用静态工厂方法,因为静态可能会给测试带来很多麻烦,但我意识到构造函数实际上是一回事。
【解决方案2】:

它还与继承相混淆。如果你的构造函数在链中被调用来实例化你的类的子类,你可以调用一个在子类中被覆盖的方法,并且依赖于在子类构造函数运行之前不建立的不变量。

【讨论】:

  • 好点 - 我没想到!在这种情况下,这不是问题,因为 MyClassfinal,但需要牢记这一点。
【解决方案3】:

在构造函数中使用实例方法时要小心,因为该类尚未完全构造。如果被调用的方法使用了一个还没有被初始化的成员,那么不好的事情就会发生。

【讨论】:

    【解决方案4】:

    您可以使用静态方法来获取依赖项并构造并返回一个新实例,并将构造函数标记为 Friend。我不确定 Friend 虽然存在于 java 中(它是否受到包保护。)但这可能不是最好的方法。您还可以使用另一个类是工厂来创建 MyClass。

    编辑:哇,另一个帖子刚刚提出了同样的问题。看起来您可以在 Java 中将构造函数设为私有。你不能在 VB.NET 中做到这一点(不确定 C#)......非常酷......

    【讨论】:

      【解决方案5】:

      是的,它实际上是非法的,它甚至不应该编译(但我相信它会编译)

      改为考虑构建器模式(并倾向于不可变,在构建器模式术语中,这意味着您不能两次调用任何 setter,并且在对象被“使用”后也不能调用任何 setter——此时调用 setter点应该可能会引发运行时异常)。

      您可以在名为“Effective Java Reloaded: This Time It's for Real”的幻灯片演示文稿中找到 Joshua Bloch 关于(新)Builder 模式的幻灯片,例如:

      http://docs.huihoo.com/javaone/2007/java-se/

      【讨论】:

      • 它编译的事实使它不是“非法的”;不良做法/“不要这样做”和非法之间是有区别的
      • 你是对的,我可能使用了错误的词,但任何体面的 lint 程序都会将此显示为至少一个警告,也可能是一个错误——我不确定他们为什么允许它,但后来我仍然不确定他们为什么允许 Java 中的公共成员......这都是一个谜。
      猜你喜欢
      • 2015-05-23
      • 1970-01-01
      • 2010-10-17
      • 1970-01-01
      • 2014-07-23
      • 2012-09-16
      • 1970-01-01
      • 2011-01-09
      • 1970-01-01
      相关资源
      最近更新 更多