使setter方法返回"this"是一种不好的编程实践吗?

274

在Java中,将setter方法返回“this”是好的还是坏的想法?

public Employee setName(String name){
   this.name = name;
   return this;
}

这个模式很有用,因为你可以像这样链接多个设置器:

list.add(new Employee().setName("Jack Sparrow").setId(1).setFoo("bacon!"));

不要这样做:

Employee e = new Employee();
e.setName("Jack Sparrow");
...and so on...
list.add(e);

......但这似乎违反了标准惯例。我认为这样做可能是值得的,因为它可以使setter执行其他有用的操作。我已经在一些地方看到了这种模式的使用(例如JMock、JPA),但它似乎并不常见,通常只用于非常明确定义API的情况下。

更新:

我描述的内容显然是有效的,但我真正想要的是一些关于是否普遍可接受的思考,以及是否存在任何陷阱或相关最佳实践。我知道生成器模式,但它比我描述的东西更复杂--如Josh Bloch所描述的那样,它需要一个关联的静态生成器类来创建对象。


2
自从我一段时间前看到这个设计模式后,我尽可能地在任何地方都使用它。如果一个方法不需要明确返回任何东西来完成它的工作,现在它就会返回 this。有时,我甚至改变函数的行为,使其不返回值,而是操作对象的成员,只是为了能够这样做。这太棒了。 :) - Inversus
7
对于返回self的可伸缩(telescopic)设置器和构建器,我更喜欢使用withName(String name)而不是setName(String name)。正如你所指出的,通常对于设置器的期望是返回void。"非标准"设置器可能无法与现有的框架(如JPA实体管理器、Spring等)良好协作。 - oᴉɹǝɥɔ
请在每个调用之前插入换行符 :) 如果您的IDE不支持此功能,请配置或获取一个合适的IDE。 - MauganRa
1
广泛使用的框架(例如Spring和Hibernate)将严格遵守void-setters约定(至少过去是这样)。 - Legna
请参见 https://dev59.com/UG025IYBdhLWcg3w4qEx - Pino
27个回答

115

这不是不良实践,而是一种越来越普遍的做法。大多数语言不要求您在不需要时处理返回的对象,因此它不会改变“常规”setter使用语法,但允许您将多个setter链接在一起。

这通常称为构建器模式或流畅接口

在Java API中也很常见:

String s = new StringBuilder().append("testing ").append(1)
  .append(" 2 ").append(3).toString();

34
通常在开发中会使用,但我不会说“这就是所谓的建造者模式”。 - Laurence Gonsalves
12
对我来说,有些人提出流畅接口的理由之一是使代码更易读,这让我觉得很有趣。我可以看到它更方便编写,但它似乎更难阅读。这是我唯一真正不赞同的地方。 - Brent Writes Code
34
这也被称为“火车失事反模式”。问题在于,当空指针异常堆栈跟踪包含像这样的一行时,您不知道哪个方法返回了null。这并不是说应该完全避免链接操作,但要注意糟糕的库(尤其是自制的库)。 - ddimitrov
19
只要你将其限制为返回 this,就不会有问题(只有第一次调用可能会抛出 NPE)。 - Stefan
4
假设您在代码需要多次重复使用 bla.foo.setBar1(...) ; bla.foo.setBar2(...) 这样的代码时,通过添加换行和缩进来提高可读性,则编写和阅读都更容易。这可以避免代码冗余,使得更复杂的调用或者类似于 10 个以上设置项的情况也能够轻松处理。请注意,由于无法在SO注释中使用换行符,因此您需要将 bla.foo 的调用放在新一行并进行缩进,然后在上一个 setter 下面添加 .setBar2(...) 这样的代码。 - Andreas Covidiot
显示剩余6条评论

97

总结一下:

  • 这被称为“流畅接口”或“方法链”。
  • 尽管现在在jQuery中很常见(非常好用),但这不是“标准”的Java。
  • 它违反了JavaBean规范,因此它会破坏各种工具和库,特别是JSP构建器和Spring。
  • 它可能会阻止JVM通常执行的一些优化。
  • 有些人认为它可以使代码更简洁,而其他人则认为它很可怕。

还有两点没有提到:

  • 这违反了每个函数应该只做一个(且仅一个)事情的原则。你可以相信或不相信这一点,在Java中我相信它有效。

  • IDE默认情况下不会为您生成这些代码。

  • 最后,这里有一个真实的数据点。我使用过一个像这样构建的库时出现了问题。Hibernate的查询构建器就是一个现有库的例子。由于Query的set*方法返回查询,仅从签名上看就无法确定如何使用它。例如:

Query setWhatever(String what);
  • 这会引入一种歧义:方法是修改当前对象(即您的模式)还是Query可能真的是不可变的(这是一种非常流行和有价值的模式),并且该方法返回一个新的对象。这只会使库更难以使用,很多程序员也不会利用此功能。如果setter真正是setter,那么使用它就会更加清晰明了。


  • 1
    关于不可变性的最后一点非常重要。最简单的例子是String。Java开发人员期望在使用String方法时获得全新的实例,而不是相同但已修改的实例。使用流畅接口时,必须在方法文档中提到返回的对象是“this”而不是新实例。 - MeTTeO
    8
    虽然我总体上同意,但我不同意这违反了“只做一件事”的原则。返回“this”根本不是复杂的火箭科学。 :-) - user949300
    附加点:它还违反了命令查询分离原则。 - Marton_hun

    90

    我更喜欢使用“with”方法来实现这个:

    public String getFoo() { return foo; }
    public void setFoo(String foo) { this.foo = foo; }
    public Employee withFoo(String foo) {
      setFoo(foo);
      return this;
    }
    

    因此:

    list.add(new Employee().withName("Jack Sparrow")
                           .withId(1)
                           .withFoo("bacon!"));
    

    警告:这个使用withX语法通常用于为不可变对象提供“setter”,因此调用这些方法的人可能合理地期望它们创建新的对象而不是改变现有实例。也许更合理的措辞应该是:

    list.add(new Employee().chainsetName("Jack Sparrow")
                           .chainsetId(1)
                           .chainsetFoo("bacon!"));
    

    使用chainsetXyz()命名约定,几乎每个人都应该感到满意。


    22
    对于这个有趣的约定,我给出一个赞。虽然看起来现在对于每个类字段都需要有一个get、一个 set 和一个with,所以我不会在我的代码中采用它。尽管如此,这仍然是一个有趣的解决方案。 :) - Paul Manta
    1
    这取决于您调用setter的频率。我发现,如果经常调用这些setter,增加它们的额外麻烦是值得的,因为它可以简化其他任何地方的代码。但是,你的情况可能不同。 - qualidafial
    2
    如果你将这个添加到Project Lombok@Getter/@Setter注解中...那么对于链式调用来说,这将是非常棒的。 或者你可以使用类似于Kestrel组合器(https://github.com/raganwald/Katy)的东西,这是JQuery和Javascript狂热者使用的。 - Ehtesh Choudhury
    4
    我注意到Java 8中的java.time不可变类使用了这种模式,例如LocalDate.withMonth、withYear等。 - qualidafial
    4
    with 前缀是一种不同的约定,正如 @qualidafial 给出的例子一样。以 with 为前缀的方法不应该返回 this,而应该返回一个新的实例,就像当前实例但带有那个更改。这是为了使对象不可变。因此,当我看到以 with 为前缀的方法时,我认为我会得到一个新的对象,而不是同一个对象。 - tempcke
    显示剩余6条评论

    88

    我认为这种写法没有什么明显的问题,只是一种风格问题。当以下情况时使用它会很有用:

    • 你需要一次性设置多个字段 (包括在构造函数中)
    • 你在编写代码时已经知道需要设置哪些字段
    • 针对不同的字段组合,你需要设置大量的字段。

    替代该方法的其他方法可能包括:

    1. 一个巨型构造函数 (缺点: 可能会传递大量的null或默认值,并且很难知道哪个值对应于什么)
    2. 几个重载的构造函数 (缺点: 一旦有了超过几个构造函数,就会变得笨重)
    3. 工厂/静态方法 (缺点: 与重载的构造函数相同 - 一旦有了超过几个方法,就会变得笨重)

    如果你只打算一次性设置几个属性,那么我认为没有必要返回'this'。如果以后您决定返回其他内容,例如状态/成功指示器/消息,则此方法肯定会失败。


    2
    通常情况下,按照惯例,你不会从 setter 方法中返回任何值。 - Ken Liu
    18
    也许不是一开始就这样,但 setter 并不一定保持其原始目的。曾经是一个变量的东西可能会变成一个包含多个变量或具有其他副作用的状态。有些 setter 可能会返回先前的值,而另一些则可能会在异常情况下返回失败指示。这提出了另一个有趣的问题:如果你使用的工具/框架无法识别具有返回值的 setter,该怎么办? - Tom Clift
    13
    @Tom 说得好,这样做违反了Java bean对于getter和setter的约定。 - Andy White
    2
    @TomClift 打破“Java Bean”约定会引起任何问题吗?使用“Java Bean”约定的库是否只查看返回类型、方法参数和方法名称。 - Theo Briscoe
    3
    这就是为什么有建造者模式,设置器不应该返回任何东西,如果需要的话,创建一个建造者会更好看,代码也更少 :) - RicardoE
    显示剩余5条评论

    26

    如果你不想从setter返回'this',但也不想使用第二个选项,你可以使用以下语法来设置属性:

    list.add(new Employee()
    {{
        setName("Jack Sparrow");
        setId(1);
        setFoo("bacon!");
    }});
    

    补充一下,我认为用C#会稍微更加简洁:

    list.Add(new Employee() {
        Name = "Jack Sparrow",
        Id = 1,
        Foo = "bacon!"
    });
    

    17
    双括号初始化可能在使用等号时存在问题,因为它会创建一个匿名内部类;请参阅http://www.c2.com/cgi/wiki?DoubleBraceInitialization。 - Csaba_H
    @Csaba_H 显然,那个问题是由搞砸了“equals”方法的人造成的。如果你知道该怎么做,处理匿名类在“equals”中是非常干净的。 - AJMansfield
    1
    为了每个实例都创建一个新的(匿名)类?需要这样做吗? - user85421
    船长杰克·斯派罗 - pro100tom

    10

    它不仅打破了getter/setter的惯例,也破坏了Java 8方法引用框架。MyClass::setMyValue是一个BiConsumer<MyClass,MyValue>,而myInstance::setMyValue是一个Consumer<MyValue>。如果你让你的setter返回this,那么它就不再是一个有效的Consumer<MyValue>实例,而是一个Function<MyValue,MyClass>,并且会导致任何使用这些setter的方法引用(假设它们是void方法)出现错误。


    2
    如果Java有一种按返回类型重载的方式,而不仅仅是JVM,那将是非常棒的。你可以轻松地绕过许多这些破坏性的更改。 - Adowrath
    1
    你可以定义一个函数式接口,它同时扩展了 Consumer<A>Function<A,B>,并提供了 void accept(A a) { apply(a); } 的默认实现。这样它就可以轻松地用作任何一个接口,并且不会破坏需要特定形式的代码。 - Steve K
    4
    啊!这完全是错误的!这被称为void兼容性。返回一个值的setter可以作为Consumer使用。https://ideone.com/ZIDy2M - Michael

    9

    我不懂Java,但我用C++做过这个。 其他人说这会使代码行变得非常长且难以阅读, 但是我已经多次按照以下方式完成:

    list.add(new Employee()
        .setName("Jack Sparrow")
        .setId(1)
        .setFoo("bacon!"));
    

    这是更好的方法:
    list.add(
        new Employee("Jack Sparrow")
        .Id(1)
        .foo("bacon!"));
    

    至少我认为是这样。但如果您愿意,可以对我进行投票并称呼我为糟糕的程序员。而且我不知道在Java中是否允许这样做。


    “even better”这个词组并不适用于现代集成开发环境中可用的格式化源代码功能。很遗憾。 - Thorbjørn Ravn Andersen
    你可能是对的... 我使用过的唯一自动格式化程序是emacs的自动缩进。 - Carson Myers
    2
    源代码格式化程序可以通过在链中的每个方法调用后添加一个简单的“//”来强制执行。这会使您的代码有点丑陋,但不会像将垂直语句序列重新格式化为水平那样丑陋。 - qualidafial
    @qualidafial 如果你配置你的 IDE 不要合并已经分行的代码(例如:Eclipse > Properties > Java > Code Style > Formatter > Line Wrapping > Never join already wrapped lines),你就不需要在每个方法后面添加 // - DJDaveMark

    8

    这并不是一种坏的实践,但它与JavaBeans规范不兼容。

    而且有很多规范依赖于这些标准访问器。

    您始终可以使它们彼此共存。

    public class Some {
        public String getValue() { // JavaBeans
            return value;
        }
        public void setValue(final String value) { // JavaBeans
            this.value = value;
        }
        public String value() { // simple
            return getValue();
        }
        public Some value(final String value) { // fluent/chaining
            setValue(value);
            return this;
        }
        private String value;
    }
    

    现在我们可以将它们一起使用。
    new Some().value("some").getValue();
    

    这里是另一个不可变对象的版本。
    public class Some {
    
        public static class Builder {
    
            public Some build() { return new Some(value); }
    
            public Builder value(final String value) {
                this.value = value;
                return this;
            }
    
            private String value;
        }
    
        private Some(final String value) {
            super();
            this.value = value;
        }
    
        public String getValue() { return value; }
    
        public String value() { return getValue();}
    
        private final String value;
    }
    

    现在我们可以这样做。
    new Some.Builder().value("value").build().getValue();
    

    2
    我的编辑被拒绝了,但是你的Builder示例不正确。首先,.value() 不返回任何内容,也不设置 some 字段。其次,在 build() 中应添加保护措施并将 some 设置为 null,以使 Some 真正成为不可变对象,否则您可以在同一 Builder 实例上再次调用 builder.value()。最后,是的,你有一个 builder,但是你的 Some 仍然有一个公共构造函数,这意味着你并没有公开倡导使用 Builder,即用户除了尝试或搜索设置自定义 value 的方法外,不知道它的存在。 - Adowrath
    @Adowrath 如果答案不正确,你应该自己写一个答案,而不是试图修改别人的答案。 - CalvT
    @CalvT븃所以我应该在那里留下错误的信息,即使它只需要稍微编辑一下吗?这不是“编辑成形”,而是纠正他犯的小错误。此外,他的Builder示例是对普通答案的扩展,并不需要我单独回答。 - Adowrath
    1
    @JinKwon 很好,谢谢!如果我之前显得有些粗鲁,对不起。 - Adowrath
    1
    @Adowrath,请随时提出任何增强评论。顺便说一下,我不是拒绝你的编辑的那个人。 :) - Jin Kwon
    1
    我知道,我知道。^^ 谢谢你提供新版本,现在它提供了一个真正可变的“Immutable-Some”构建器。与我的编辑尝试相比,这是一个更聪明的解决方案,因为它不会使代码混乱。 - Adowrath

    7

    至少在理论上,它可以通过设置虚假的依赖关系来损坏JVM的优化机制。

    它被认为是语法糖,但实际上可能会在超级智能的Java 43虚拟机中产生副作用。

    这就是为什么我投反对票,不建议使用它。


    10
    有趣的... 能否再多讲一点? - Ken Liu
    3
    想一想超标量处理器如何处理并行执行。虽然程序员已知道要在第一个"set"方法执行后才能执行第二个"set"方法,但第二个"set"方法的执行对象仍然依赖于第一个"set"方法。 - Marian
    2
    我还是不太明白。如果你用两条单独的语句设置Foo和Bar,那么你为设置Bar的对象的状态与你为设置Foo的对象的状态不同。因此,编译器也无法并行执行这些语句。至少,在没有引入不必要的假设的情况下,我不知道它如何实现。(由于我对此一无所知,我不否认Java 43在某些情况下实际上会对其进行并行处理而在其他情况下不会,并且在一个情况下引入了不必要的假设但在另一个情况下则没有)。 - masonk
    13
    如果你不确定,就进行测试。-XX:+UnlockDiagnosticVMOptions -XX:+PrintInliningJava7 JDK会对链式方法进行内联优化,并且会将 void setters 标记为热点并进行内联,需要迭代相同数量的次数。我认为您低估了JVM的操作码修剪算法的威力;如果它知道您正在返回 this,则会跳过 jrs(Java 返回语句)操作码并将 this 保留在堆栈上。 - Ajax
    1
    Andreas,我同意你的观点,但当你有一层又一层低效代码时就会出现问题。99%的情况下,你应该编写清晰易懂的代码,这在这里经常被提到。但也有时候你需要现实一些,利用你多年的经验,在总体架构上进行预先优化。 - LegendLength
    显示剩余4条评论

    7

    因为它不返回void,所以它不再是有效的JavaBean属性setter。如果您是世界上使用可视化“Bean Builder”工具的七个人之一,或者是使用JSP-bean-setProperty元素的17个人之一,则可能很重要。


    1
    如果您使用像Spring这样的bean-aware框架,这也很重要。 - ddimitrov

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