Java中的命名参数习语

时间:2010-01-01 06:18:33

标签: java idioms

如何在Java中实现命名参数习语? (特别是对于建设者)

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

一个小代码示例没问题。

感谢。

18 个答案:

答案 0 :(得分:90)

我在构造函数中模拟关键字参数的最佳Java习惯是Builder模式,在Effective 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的构造函数。 (人们通常不担心多次设置相同参数的情况。)
  3. 您可能还想查看this blog post(不是我)。

答案 1 :(得分:63)

值得一提的是:

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

所谓的双支撑初始化器。它实际上是一个带有实例初始化程序的匿名类。

答案 2 :(得分:19)

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

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

呼叫网站上的详细信息,但整体上的开销最低。

答案 3 :(得分:16)

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构造函数中一样)

答案 4 :(得分:8)

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

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));

答案 5 :(得分:7)

我想指出,此样式同时解决了命名参数属性功能,但未设置获取 其他语言的前缀。它在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 :(得分:7)

这是Joshua Bloch的Effective Java中给出的技术的一点变化。在这里,我尝试使客户端代码更具可读性(或者可能更加DSLish)。

/**
 * Actual class for which we want to implement a 
 * named-parameter pseudo-constructor
 */
class Window{
    protected int x, y, width, height;
    protected boolean isResizable;
    protected String title;

    public void show(){
        // Show the window
        System.out.printf("Window \"%s\" set visible.%n",title);
    }

    /**
     * This class is only used to set the parameter values
     */
    static class HavingProperties extends Window{

        public HavingProperties x(int value){
            this.x=value;
            return this;
        }

        public HavingProperties y(int value){
            this.y=value;
            return this;
        }

        public HavingProperties width(int value){
            this.width=value;
            return this;
        }

        public HavingProperties height(int value){
            this.height=value;
            return this;
        }

        public HavingProperties resizable(boolean value){
            this.isResizable=value;
            return this;
        }

        public HavingProperties title(String value){
            this.title=value;
            return this;
        }
    }
}

public class NamedParameterIdiomInAction {
    public static void main(String... args){
        Window window=new Window.HavingProperties().x(10).y(10).width(100).
                height(100).resizable(true).title("My App");
        window.show();
    }
}

请注意,通过此变体,您还可以为伪构造函数指定有意义的名称。

答案 7 :(得分:6)

Java不支持构造函数或方法参数的类似Objective-C的命名参数。此外,这实际上不是Java的做事方式。在java中,典型的模式被详细命名为类和成员。类和变量应该是名词,命名的方法应该是动词。我想你可以创造性地偏离Java命名约定并以hacky方式模拟Objective-C范例,但是负责维护代码的普通Java开发人员不会特别赞赏这一点。在使用任何语言工作时,您应该坚持语言和社区的惯例,尤其是在团队合作时。

答案 8 :(得分: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");

答案 9 :(得分:3)

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

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

请注意,这可能违反了几十个最佳实践" (就像使用$符号的任何东西一样)。

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));
  }
}

优点:

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

缺点:

  • 你的老板可能会因此而责备你
  • 很难说出发生了什么

答案 10 :(得分: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));

与实际命名参数相比的限制:

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

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

答案 11 :(得分:2)

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; }
}

答案 12 :(得分:2)

您可以使用项目Lombok的@Builder annotation在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();

答案 13 :(得分:2)

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

{{1}}

答案 14 :(得分:1)

karg library支持的习语可能值得考虑:

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");
    }
}

答案 15 :(得分:1)

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

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

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

因此,您可以使用它来创建一个默认构建器,然后在您需要它的各个位置,您可以配置它并构建您的对象。

如果您反复构建同一个对象,这是最有意义的,因为这样您就可以使构建器保持静态,而不必担心更改它的设置。

另一方面,如果你必须用改变的参数来构建对象,那么这会有一些开销。 (但是,嘿,你可以将静态/动态生成与自定义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();

    }
}

答案 16 :(得分:1)

这是一个经过编译器检查的构建器模式。注意事项:

  • 这无法防止参数的双重赋值
  • 没有好的.build()方法
  • 每个字段一个通用参数

所以你需要在课堂之外的东西,如果没有通过Builder<Yes, Yes, Yes>就会失败。以 getSum 静态方法为例。

class No {}
class Yes {}

class Builder<K1, K2, K3> {
  int arg1, arg2, arg3;

  Builder() {}

  static Builder<No, No, No> make() {
    return new Builder<No, No, No>();
  }

  @SuppressWarnings("unchecked")
  Builder<Yes, K2, K3> arg1(int val) {
    arg1 = val;
    return (Builder<Yes, K2, K3>) this;
  }

  @SuppressWarnings("unchecked")
  Builder<K1, Yes, K3> arg2(int val) {
    arg2 = val;
    return (Builder<K1, Yes, K3>) this;
  }

  @SuppressWarnings("unchecked")
  Builder<K1, K2, Yes> arg3(int val) {
    this.arg3 = val;
    return (Builder<K1, K2, Yes>) this;
  }

  static int getSum(Builder<Yes, Yes, Yes> build) {
    return build.arg1 + build.arg2 + build.arg3;
  }

  public static void main(String[] args) {
    // Compiles!
    int v1 = getSum(make().arg1(44).arg3(22).arg2(11));
    // Builder.java:40: error: incompatible types:
    // Builder<Yes,No,Yes> cannot be converted to Builder<Yes,Yes,Yes>
    int v2 = getSum(make().arg1(44).arg3(22));
    System.out.println("Got: " + v1 + " and " + v2);
  }
}

警告说明。为什么没有构建方法?麻烦的是,它将在Builder 类中,并且会用K1, K2, K3 等参数化。由于方法本身必须编译,因此它调用的所有内容都必须编译。所以,一般情况下,我们不能在类本身的方法中进行编译测试。

出于类似的原因,我们无法防止使用构建器模型进行双重赋值。

答案 17 :(得分:0)

@irreputable提出了一个很好的解决方案。但是 - 它可能会使您的Class实例处于无效状态,因为不会进行验证和一致性检查。因此,我更喜欢将它与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如何支持命名参数?

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