Java中的命名参数习语


81

如何在Java中实现命名参数习语?(特别是对于构造函数)

我在寻找一种类似于Objective-C的语法,而不是在JavaBeans中使用的一种语法。

一个小的代码示例就可以了。

谢谢。

Answers:


103

我似乎最喜欢在构造函数中模拟关键字参数的Java惯用语是Builder模式,有效Java 2nd Edition中对此进行了描述。

基本思想是拥有一个Builder类,该类具有用于不同构造函数参数的setter(但通常没有getter)。还有一种build()方法。Builder类通常是用于构建的类的(静态)嵌套类。外部类的构造函数通常是私有的。

最终结果如下所示:

public class Foo {
  public static class Builder {
    public Foo build() {
      return new Foo(this);
    }

    public Builder setSize(int size) {
      this.size = size;
      return this;
    }

    public Builder setColor(Color color) {
      this.color = color;
      return this;
    }

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

    // you can set defaults for these here
    private int size;
    private Color color;
    private String name;
  }

  public static Builder builder() {
      return new Builder();
  }

  private Foo(Builder builder) {
    size = builder.size;
    color = builder.color;
    name = builder.name;
  }

  private final int size;
  private final Color color;
  private final String name;

  // The rest of Foo goes here...
}

要创建Foo的实例,您可以编写以下内容:

Foo foo = Foo.builder()
    .setColor(red)
    .setName("Fred")
    .setSize(42)
    .build();

主要警告:

  1. 设置模式非常繁琐(如您所见)。除了计划在许多地方实例化的类以外,可能不值得。
  2. 没有编译时检查是否所有参数都只指定了一次。您可以添加运行时检查,也可以仅将其用于可选参数,并使必需参数成为Foo或Builder的构造函数的常规参数。(人们通常不必担心多次设置相同参数的情况。)

您可能还想查看此博客文章(不是我的)。


12
实际上,这不是Objective-C进行命名的方式。看起来更像是一个流畅的界面。真的不是同一回事。
Asaph

29
我喜欢使用.withFoo,而不是.setFoonewBuilder().withSize(1).withName(1).build()而不是newBuilder().setSize(1).setName(1).build()
notnoop 2010年

16
Asaph:是的,我知道。Java没有命名参数。这就是为什么我说这是“我似乎在模拟关键字参数时最好的Java习惯用法”。Objective-C的“命名参数”也不理想,因为它们强制执行特定的排序。它们不是像Lisp或Python中那样的真正关键字参数。至少对于Java Builder模式,您只需要记住名称(而不是顺序),就像真实的关键字参数一样。
劳伦斯·贡萨尔维斯

14
notnoop:我更喜欢“设置”,因为这些是会改变Builder的状态的设置器。是的,在将所有内容链接在一起的简单情况下,“ with”看起来不错,但在更复杂的情况下,您将Builder包含在其自己的变量中(也许是因为您有条件地设置属性),我喜欢前缀完全清楚地表明,在调用这些方法时,正在对Builder进行突变。“ with”前缀对我来说听起来有用,并且这些方法肯定不起作用。
劳伦斯·贡萨尔维斯

4
There's no compile-time checking that all of the parameters have been specified exactly once.通过将接口返回Builder1BuilderN每个覆盖setter或setter之一的位置,可以解决此问题build()。它的代码冗长得多,但是它附带了对DSL的编译器支持,使自动补全非常好用。
rsp 2010年

72

这值得一提:

Foo foo = new Foo() {{
    color = red;
    name = "Fred";
    size = 42;
}};

所谓的双括号初始化器。它实际上是带有实例初始化程序的匿名类。


26
有趣的技术,但似乎有点昂贵,因为每次我在代码中使用它时都会创建一个新类。
Red Hyena 2010年

6
除了自动格式化,子类化和序列化警告外,它实际上与基于属性的初始化的C#语法非常接近。但是,从4.0开始的C#也具有命名参数,因此程序员确实很喜欢选择,与Java程序员不同,后者必须模拟惯用语以防止他们日后陷入困境。
Distortum 2012年

3
很高兴看到这是可能的,但我不得不拒绝投票,因为正如Red Hyena所指出的那样,此解决方案非常昂贵。等不及Java像Python一样真正支持命名参数。
加特斯特

12
赞成。这以最易读,简洁的方式回答了问题。精细。这是“不表现”。我们在这里聊多少毫秒和几位?有人吗?十?不要误会我的意思-我不会使用它,因为我不希望被冗长的Java螺母(双关语意图)执行
Steve

2
完善!只需要公共/受保护的字段。这绝对是最好的解决方案,其开销要比生成器少得多。Hyena / Gattster:请(1)阅读JLS和(2)检查生成的字节码,然后再写评论。
JonatanKaźmierczak17年

21

您也可以尝试从此处遵循建议:http : //www.artima.com/weblogs/viewpost.jsp?thread=118828

int value; int location; boolean overwrite;
doIt(value=13, location=47, overwrite=true);

它在呼叫站点上很冗长,但总体开销最低。


3
造成低开销的原因是好的,但是感觉很扎实。在有许多参数的情况下,我可能会使用Builder()方法。
加特斯特

23
我认为这完全错过了命名参数的要点。(其中有些东西将名称与值相关联)。没有迹象表明任何如果颠倒顺序。我不建议这样做,只是建议添加评论:doIt( /*value*/ 13, /*location*/ 47, /*overwrite*/ true )
Scheintod

20

Java 8样式:

public class Person {
    String name;
    int age;

    private Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    static PersonWaitingForName create() {
        return name -> age -> new Person(name, age);
    }

    static interface PersonWaitingForName {
        PersonWaitingForAge name(String name);
    }

    static interface PersonWaitingForAge {
        Person age(int age);
    }

    public static void main(String[] args) {

        Person charlotte = Person.create()
            .name("Charlotte")
            .age(25);

    }
}
  • 命名参数
  • 固定参数顺序
  • 静态检查->没有无名人士
  • 很难偶然切换相同类型的参数(例如在Telescop构造函数中可能)

3
很好 它没有可变的参数顺序是多么可惜。(但不是要说我会用这个...)
Scheintod

1
这是一个绝妙的主意。定义使create()我停滞不前。我从未见过Java中的这种lambda链接样式。您是不是首先用lambdas用另一种语言发现了这个主意?
kevinarpe18年

2
称为currying:en.wikipedia.org/wiki/Currying。顺便说一句:也许这是一个聪明的主意,但我不推荐这种命名参数样式。我在带有许多参数的真实项目中对其进行了测试,这导致难以阅读和难以导航代码。
亚历克斯(Alex)

最终,将为Java提供Visual Basic样式的命名参数。Java之前没有,因为C ++没有。但是我们最终会到达那里。我会说90%的Java多态性只是围绕可选参数。
调谐的

7

Java不为构造函数或方法参数支持类似于Objective-C的命名参数。此外,这实际上不是Java的处理方式。在Java中,典型的模式是详细命名的类和成员。类和变量应为名词,命名方法应为动词。我想您可以变得富于创造力,并且可以偏离Java命名约定,并以一种怪诞的方式模仿Objective-C范例,但是负责维护您的代码的普通Java开发人员不会对此特别赞赏。使用任何一种语言工作时,都应该遵守该语言和社区的约定,尤其是在团队中工作时。


4
+1-有关坚持当前使用的语言习语的建议。请考虑其他需要阅读您的代码的人!
斯蒂芬·C

3
我支持您的回答,因为我认为您说的很对。如果我不得不猜测你为什么会投票,那可能是因为这不能回答问题。问:“如何使用Java命名参数?” 答:“你不要”
Gattster

12
我投反对票,是因为我认为您的答案与问题无关。详细名称并不能真正解决参数排序问题。是的,您可以使用名称进行编码,但这显然不可行。提出不相关的范例并不能解释为什么不支持一个范例。
Andreas Mueller 2013年

7

如果使用的是Java 6,则可以使用变量参数并导入static以产生更好的结果。有关详细信息,请参见:

http://zinzel.blogspot.com/2010/07/creating-methods-with-named-parameters.html

简而言之,您可能会遇到以下情况:

go();
go(min(0));
go(min(0), max(100));
go(max(100), min(0));
go(prompt("Enter a value"), min(0), max(100));

2
我喜欢它,但仍然只能解决一半问题。在Java中,如果不丢失对所需值的编译时检查,就无法防止参数意外转置。
cdunn2001

没有类型安全性,这比简单的注释要糟糕。
彼得·戴维斯

7

我想指出的是,该样式既解决了命名参数又解决了特性功能,而没有其他语言拥有的getset前缀。它不是Java领域中的常规方法,而是其更简单,不难理解的方法,特别是在您处理其他语言的情况下。

public class Person {
   String name;
   int age;

   // name property
   // getter
   public String name() { return name; }

   // setter
   public Person name(String val)  { 
    name = val;
    return this;
   }

   // age property
   // getter
   public int age() { return age; }

   // setter
   public Person age(int val) {
     age = val;
     return this;
   }

   public static void main(String[] args) {

      // Addresses named parameter

      Person jacobi = new Person().name("Jacobi").age(3);

      // Addresses property style

      println(jacobi.name());
      println(jacobi.age());

      //...

      jacobi.name("Lemuel Jacobi");
      jacobi.age(4);

      println(jacobi.name());
      println(jacobi.age());
   }
}

6

关于什么

public class Tiger {
String myColor;
int    myLegs;

public Tiger color(String s)
{
    myColor = s;
    return this;
}

public Tiger legs(int i)
{
    myLegs = i;
    return this;
}
}

Tiger t = new Tiger().legs(4).color("striped");

5
Builder更好,因为您可以检查build()上的一些约束。但我也更喜欢没有set / with前缀的较短参数。
rkj 2012年

4
同样,构建器模式更好,因为它允许您使内置类(在这种情况下为老虎)不可变。
杰夫·奥尔森

2

您可以使用通常的构造函数和静态方法来给参数命名:

public class Something {

    String name;
    int size; 
    float weight;

    public Something(String name, int size, float weight) {
        this.name = name;
        this.size = size;
        this.weight = weight;
    }

    public static String name(String name) { 
        return name; 
    }

    public static int size(int size) {
        return size;
    }

    public float weight(float weight) {
        return weight;
    }

}

用法:

import static Something.*;

Something s = new Something(name("pen"), size(20), weight(8.2));

与真实命名参数相比的局限性:

  • 参数顺序是相关的
  • 单个构造函数无法使用可变参数列表
  • 每个参数都需要一个方法
  • 并不比评论好(new Something(/*name*/ "pen", /*size*/ 20, /*weight*/ 8.2)

如果可以选择,请查看Scala 2.8。http://www.scala-lang.org/node/2075


2
这种方法的一个缺点是您必须以正确的顺序获取参数。上面的代码可以让您编写:Something s = new Something(name(“ pen”),size(20),size(21)); 同样,这种方法也不能帮助您避免输入可选参数。
马特·奎尔

1
我对此表示赞同,以进行分析:not really better than a comment...另一方面……;)
Scheintod

2

使用Java 8的lambda,您可以更接近真实的命名参数。

foo($ -> {$.foo = -10; $.bar = "hello"; $.array = new int[]{1, 2, 3, 4};});

请注意,这可能违反了几十个“ java最佳实践”(例如使用该$符号的任何东西)。

public class Main {
  public static void main(String[] args) {
    // Usage
    foo($ -> {$.foo = -10; $.bar = "hello"; $.array = new int[]{1, 2, 3, 4};});
    // Compare to roughly "equivalent" python call
    // foo(foo = -10, bar = "hello", array = [1, 2, 3, 4])
  }

  // Your parameter holder
  public static class $foo {
    private $foo() {}

    public int foo = 2;
    public String bar = "test";
    public int[] array = new int[]{};
  }

  // Some boilerplate logic
  public static void foo(Consumer<$foo> c) {
    $foo foo = new $foo();
    c.accept(foo);
    foo_impl(foo);
  }

  // Method with named parameters
  private static void foo_impl($foo par) {
    // Do something with your parameters
    System.out.println("foo: " + par.foo + ", bar: " + par.bar + ", array: " + Arrays.toString(par.array));
  }
}

优点:

  • 比目前为止我见过的任何构建器模式都要短
  • 适用于方法和构造函数
  • 完全安全
  • 它看起来与其他编程语言中的实际命名参数非常接近
  • 它与典型的构建器模式一样安全(可以多次设置参数)

缺点:

  • 你的老板可能会为此向你私刑
  • 很难说是怎么回事

1
缺点:字段是公开的,不是最终的。如果您对此表示满意,为什么不只使用二传手?它对方法如何起作用?
亚历克斯(Alex)

可以使用二传手,但这有什么意义呢?它只会使代码更长,而这将消除这样做的好处。分配是无副作用的,二传手是黑匣子。$foo从来没有逃脱给调用者(除非有人做它分配给回调的变量中),那么,为什么不能他们是公众?
Vic

2

您可以使用项目Lombok的@Builder批注来模拟Java中的命名参数。这将为您生成一个生成器,您可以使用该生成器来创建任何类的新实例(既包括您编写的类,又包括来自外部库的类)。

这是在类上启用它的方法:

@Getter
@Builder
public class User {
    private final Long id;
    private final String name;
}

之后,您可以通过以下方式使用它:

User userInstance = User.builder()
    .id(1L)
    .name("joe")
    .build();

如果要为来自库的类创建此类Builder,请创建一个带注释的静态方法,如下所示:

class UserBuilder {
    @Builder(builderMethodName = "builder")
    public static LibraryUser newLibraryUser(Long id, String name) {
        return new LibraryUser(id, name);
    }
  }

这将生成一个名为“ builder”的方法,可以通过以下方法调用:

LibraryUser user = UserBuilder.builder()
    .id(1L)
    .name("joe")
    .build();

Google auto / value的目的类似,但使用的是注释处理框架,它比项目Lombocks字节码操作安全得多(东西在JVM升级后仍然可以工作)。
勒内(René)

1
我认为,与使用Lombok相比,Google的auto / value需要多一点的路程。Lombok的方法或多或少与传统JavaBean编写兼容(例如,您可以通过new实例化,字段可以在调试器中正确显示,等等)。我也不是Lombok使用字节码编译+ IDE插件解决方案的忠实拥护者,但是我不得不承认,实际上它可以正常工作。没有问题,到目前为止与JDK版本的变化,反射等
伊什特万Devai

是的,那是真的。对于自动/值,您需要提供一个抽象类,然后将其实现。龙目岛需要更少的代码。因此,需要比较优缺点。
勒内

2

我觉得“评论解决方法”应有其自己的答案(隐藏在现有答案中,并在此处的评论中提到)。

someMethod(/* width */ 1024, /* height */ 768);

1

这是Builder劳伦斯在上面描述的模式的一种变体。

我发现自己经常使用它(在适当的地方)。

主要区别在于,在这种情况下,Builder是不可变的。这样做的好处是可以重用并且是线程安全的。

因此,您可以使用它制作一个默认的Builder,然后在需要它的各个地方配置它并构建您的对象。

如果您要一遍又一遍地构建相同的对象,这是最有意义的,因为这样您就可以使构建器成为静态的,而不必担心更改其设置。

另一方面,如果您必须使用变化的参数来构建对象,则这会降低一些开销。(但是,您可以将静态/动态生成与自定义build方法结合使用)

这是示例代码:

public class Car {

    public enum Color { white, red, green, blue, black };

    private final String brand;
    private final String name;
    private final Color color;
    private final int speed;

    private Car( CarBuilder builder ){
        this.brand = builder.brand;
        this.color = builder.color;
        this.speed = builder.speed;
        this.name = builder.name;
    }

    public static CarBuilder with() {
        return DEFAULT;
    }

    private static final CarBuilder DEFAULT = new CarBuilder(
            null, null, Color.white, 130
    );

    public static class CarBuilder {

        final String brand;
        final String name;
        final Color color;
        final int speed;

        private CarBuilder( String brand, String name, Color color, int speed ) {
            this.brand = brand;
            this.name = name;
            this.color = color;
            this.speed = speed;
        }
        public CarBuilder brand( String newBrand ) {
            return new CarBuilder( newBrand, name, color, speed );
        }
        public CarBuilder name( String newName ) {
            return new CarBuilder( brand, newName, color, speed );
        }
        public CarBuilder color( Color newColor ) {
            return new CarBuilder( brand, name, newColor, speed );
        }
        public CarBuilder speed( int newSpeed ) {
            return new CarBuilder( brand, name, color, newSpeed );
        }
        public Car build() {
            return new Car( this );
        }
    }

    public static void main( String [] args ) {

        Car porsche = Car.with()
                .brand( "Porsche" )
                .name( "Carrera" )
                .color( Color.red )
                .speed( 270 )
                .build()
                ;

        // -- or with one default builder

        CarBuilder ASSEMBLY_LINE = Car.with()
                .brand( "Jeep" )
                .name( "Cherokee" )
                .color( Color.green )
                .speed( 180 )
                ;

        for( ;; ) ASSEMBLY_LINE.build();

        // -- or with custom default builder:

        CarBuilder MERCEDES = Car.with()
                .brand( "Mercedes" )
                .color( Color.black )
                ;

        Car c230 = MERCEDES.name( "C230" ).speed( 180 ).build(),
            clk = MERCEDES.name( "CLK" ).speed( 240 ).build();

    }
}

1

Java中的任何解决方案都可能非常冗长,但是值得一提的是,诸如Google AutoValuesImmutables之类的工具将使用JDK编译时注释处理为您自动生成构建器类。

就我而言,我想在Java枚举中使用命名参数,因此构建器模式将不起作用,因为枚举实例不能被其他类实例化。我想出了一种类似于@deamon答案的方法,但增加了对参数顺序的编译时检查(以更多代码为代价)

这是客户代码:

Person p = new Person( age(16), weight(100), heightInches(65) );

并执行:

class Person {
  static class TypedContainer<T> {
    T val;
    TypedContainer(T val) { this.val = val; }
  }
  static Age age(int age) { return new Age(age); }
  static class Age extends TypedContainer<Integer> {
    Age(Integer age) { super(age); }
  }
  static Weight weight(int weight) { return new Weight(weight); }
  static class Weight extends TypedContainer<Integer> {
    Weight(Integer weight) { super(weight); }
  }
  static Height heightInches(int height) { return new Height(height); }
  static class Height extends TypedContainer<Integer> {
    Height(Integer height) { super(height); }
  }

  private final int age;
  private final int weight;
  private final int height;

  Person(Age age, Weight weight, Height height) {
    this.age = age.val;
    this.weight = weight.val;
    this.height = height.val;
  }
  public int getAge() { return age; }
  public int getWeight() { return weight; }
  public int getHeight() { return height; }
}

0

karg库支持的惯用语可能值得考虑:

class Example {

    private static final Keyword<String> GREETING = Keyword.newKeyword();
    private static final Keyword<String> NAME = Keyword.newKeyword();

    public void greet(KeywordArgument...argArray) {
        KeywordArguments args = KeywordArguments.of(argArray);
        String greeting = GREETING.from(args, "Hello");
        String name = NAME.from(args, "World");
        System.out.println(String.format("%s, %s!", greeting, name));
    }

    public void sayHello() {
        greet();
    }

    public void sayGoodbye() {
        greet(GREETING.of("Goodbye");
    }

    public void campItUp() {
        greet(NAME.of("Sailor");
    }
}

这似乎与R Casha答案基本相同,但是没有解释它的代码。
Scheintod

-1

@irreputable提出了一个很好的解决方案。但是,这可能会使您的Class实例处于无效状态,因为不会进行任何验证和一致性检查。因此,我更喜欢将其与Builder解决方案结合起来,避免创建额外的子类,尽管它仍然会子类化builder类。另外,由于额外的构建器类使它更加冗长,因此我使用了lambda添加了另一种方法。为了完整性,我添加了其他一些构建器方法。

从一个类开始,如下所示:

public class Foo {
  static public class Builder {
    public int size;
    public Color color;
    public String name;
    public Builder() { size = 0; color = Color.RED; name = null; }
    private Builder self() { return this; }

    public Builder size(int size) {this.size = size; return self();}
    public Builder color(Color color) {this.color = color; return self();}
    public Builder name(String name) {this.name = name; return self();}

    public Foo build() {return new Foo(this);}
  }

  private final int size;
  private final Color color;
  private final String name;

  public Foo(Builder b) {
    this.size = b.size;
    this.color = b.color;
    this.name = b.name;
  }

  public Foo(java.util.function.Consumer<Builder> bc) {
    Builder b = new Builder();
    bc.accept(b);
    this.size = b.size;
    this.color = b.color;
    this.name = b.name;
  }

  static public Builder with() {
    return new Builder();
  }

  public int getSize() { return this.size; }
  public Color getColor() { return this.color; }  
  public String getName() { return this.name; }  

}

然后使用此方法应用不同的方法:

Foo m1 = new Foo(
  new Foo.Builder ()
  .size(1)
  .color(BLUE)
  .name("Fred")
);

Foo m2 = new Foo.Builder()
  .size(1)
  .color(BLUE)
  .name("Fred")
  .build();

Foo m3 = Foo.with()
  .size(1)
  .color(BLUE)
  .name("Fred")
  .build();

Foo m4 = new Foo(
  new Foo.Builder() {{
    size = 1;
    color = BLUE;
    name = "Fred";
  }}
);

Foo m5 = new Foo(
  (b)->{
    b.size = 1;
    b.color = BLUE;
    b.name = "Fred";
  }
);

看起来部分是@LaurenceGonsalves已经发布的内容的全部抄袭,但是您会看到选择的约定中的细微差别。

我想知道,如果JLS是否会实现命名参数,它们将如何实现?通过提供简短形式的支持,它们是否可以扩展到现有的习惯用法之一?Scala还如何支持命名参数?

嗯-足以研究,也许是一个新问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.