Java中的命名参数习语
Posted
技术标签:
【中文标题】Java中的命名参数习语【英文标题】:Named Parameter idiom in Java 【发布时间】:2010-12-31 13:48:09 【问题描述】:如何在Java中实现命名参数习语? (尤其是构造函数)
我正在寻找一种类似于 Objective-C 的语法,而不是 JavaBeans 中使用的语法。
一个小的代码示例就可以了。
【问题讨论】:
另见:jaksa.wordpress.com/2019/10/28/named-parameters-in-java 【参考方案1】:我认为在构造函数中模拟关键字参数的最佳 Java 习惯是 Builder 模式,在 Effective Java 2nd Edition 中进行了描述。
基本思想是拥有一个 Builder 类,该类具有用于不同构造函数参数的设置器(但通常没有获取器)。还有一个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();
主要注意事项是:
-
设置模式非常冗长(如您所见)。可能不值得,除了您计划在许多地方实例化的类。
没有编译时检查所有参数是否只指定了一次。您可以添加运行时检查,也可以仅将其用于可选参数,并将必需参数设为 Foo 或 Builder 的构造函数的普通参数。 (人们一般不会担心同一个参数被多次设置的情况。)
您可能还想查看this blog post(不是我)。
【讨论】:
这真的不是Objective-C那样的命名参数。这看起来更像是一个流畅的界面。这真的不是一回事。 我喜欢使用.withFoo
,而不是.setFoo
:newBuilder().withSize(1).withName(1).build()
而不是newBuilder().setSize(1).setName(1).build()
阿萨夫:是的,我知道。 Java 没有命名参数。这就是为什么我说这是“我为 simulating 关键字参数看到的最好的 Java 习惯用法”。 Objective-C 的“命名参数”也不太理想,因为它们强制特定的顺序。它们不是像 Lisp 或 Python 那样的真正的关键字参数。至少对于 Java Builder 模式,您只需要记住名称,而不是顺序,就像真正的关键字参数一样。
notnoop:我更喜欢“set”,因为这些是改变 Builder 状态的 setter。是的,“with”在您将所有内容链接在一起的简单情况下看起来不错,但在更复杂的情况下,您将 Builder 放在自己的变量中(可能是因为您有条件地设置属性)我喜欢这个集合前缀清楚地表明,当调用这些方法时,Builder 正在发生变化。 “with”前缀对我来说听起来很实用,而这些方法显然不起作用。
There's no compile-time checking that all of the parameters have been specified exactly once.
这个问题可以通过返回接口Builder1
到BuilderN
来解决,其中每个接口都覆盖了一个setter 或build()
。它的代码更加冗长,但它为您的 DSL 提供了编译器支持,并且使自动完成非常好用。【参考方案2】:
这个值得一提:
Foo foo = new Foo()
color = red;
name = "Fred";
size = 42;
;
所谓的双大括号初始化器。它实际上是一个带有实例初始化器的匿名类。
【讨论】:
有趣的技术,但似乎有点贵,因为每次我在代码中使用它都会创建一个新类。 除了自动格式化、子类化和序列化警告之外,这实际上非常接近基于属性的初始化的 C# 语法。但是,从 4.0 开始的 C# 也有命名参数,所以程序员真的被宠坏了,不像 Java 程序员必须模拟成语以防止他们日后自取其辱。 很高兴看到这是可能的,但我不得不投反对票,因为正如 Red Hyena 指出的那样,这种解决方案很昂贵。不能等到 Java 像 Python 一样真正支持命名参数。 点赞。这以最易读、最简洁的方式回答了这个问题。美好的。它“不高效”。我们在这里谈论多少额外的毫秒和比特?其中一个?十?不要误会我的意思——我不会使用它,因为我讨厌被一个冗长的 Java nut 执行(双关语;) 完美!只需要公共/受保护的字段。这绝对是最好的解决方案,比 builder 造成的开销要少得多。 Hyena/Gattster:请 (1) 阅读 JLS 并 (2) 在编写 cmets 之前检查生成的字节码。【参考方案3】: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 构造函数中一样)
【讨论】:
不错。可惜它没有可变的参数顺序。 (但不是说我会用这个……) 这是一个绝妙的主意。create()
的定义让我停下了脚步。我从未在 Java 中看到过这种风格的 lambda 链接。你是不是第一次用 lambdas 用另一种语言发现了这个想法?
这叫做柯里化:en.wikipedia.org/wiki/Currying。顺便说一句:也许这是一个聪明的主意,但我不推荐这种命名参数风格。我在一个带有许多参数的真实项目中对其进行了测试,它导致代码难以阅读和导航。
最终,Java 将被赋予 Visual Basic 风格的命名参数。 Java 以前没有,因为 C++ 没有。但我们最终会到达那里。我会说 90% 的 Java 多态性只是在修改可选参数。【参考方案4】:
您也可以尝试听从这里的建议: http://www.artima.com/weblogs/viewpost.jsp?thread=118828
int value; int location; boolean overwrite;
doIt(value=13, location=47, overwrite=true);
它在调用站点上很冗长,但总体而言开销最低。
【讨论】:
很好,因为开销很低,但感觉很hackish。对于有很多参数的情况,我可能会使用 Builder() 方法。 我认为这完全忽略了命名参数的意义。 (它具有将名称与值相关联的东西)。如果您颠倒订单,则没有任何指示whatsoever。我建议不要这样做,而是简单地添加评论:doIt( /*value*/ 13, /*location*/ 47, /*overwrite*/ true )
【参考方案5】:
我想指出的是,这种样式在没有 get 和 set 的情况下解决了 named parameter 和 properties 功能 其他语言有的前缀。它在 Java 领域不是传统的,但它更简单、更短,尤其是在您处理过其他语言的情况下。
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 Adane").age(3);
// addresses property style
System.out.println(jacobi.name());
System.out.println(jacobi.age());
// updates property values
jacobi.name("Lemuel Jacobi Adane");
jacobi.age(4);
System.out.println(jacobi.name());
System.out.println(jacobi.age());
【讨论】:
【参考方案6】:Java 不支持构造函数或方法参数的类似 Objective-C 的命名参数。此外,这确实不是 Java 的做事方式。在 java 中,典型的模式是详细命名的类和成员。类和变量应该是名词,命名的方法应该是动词。我想您可以发挥创造力并偏离 Java 命名约定并以一种 hacky 的方式模拟 Objective-C 范式,但是负责维护您的代码的普通 Java 开发人员不会特别欣赏这一点。使用任何语言工作时,您都应该遵守语言和社区的约定,尤其是在团队工作时。
【讨论】:
+1 - 关于坚持使用您当前使用的语言的成语的建议。请考虑其他需要阅读您的代码的人! 我赞成你的回答,因为我认为你的观点很好。如果我不得不猜测您为什么投反对票,那可能是因为这不能回答问题。问:“我如何在 Java 中做命名参数?”答:“你没有” 我投了反对票,因为我认为您的回答与问题无关。冗长的名称并不能真正解决参数排序问题。是的,您可以在名称中对它们进行编码,但这显然是不切实际的。提出一个不相关的范式并不能解释为什么不支持一个范式。【参考方案7】:如果您使用的是 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));
【讨论】:
我喜欢它,但它仍然只解决了一半的问题。在 Java 中,如果不丢失对所需值的编译时检查,就无法防止意外转置参数。 没有类型安全,这比简单的 //cmets 更糟糕。【参考方案8】:怎么样
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");
【讨论】:
Builder 更好,因为您可以检查 build() 上的一些约束。但我也更喜欢没有 set/with 前缀的较短参数。 此外,构建器模式更好,因为它允许您使构建的类(在本例中为 Tiger)不可变。【参考方案9】:我觉得“评论解决方法”值得拥有自己的答案(隐藏在现有答案中并在此处的 cmets 中提及)。
someMethod(/* width */ 1024, /* height */ 768);
【讨论】:
【参考方案10】:您可以使用通常的构造函数和为参数命名的静态方法:
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
【讨论】:
这种方法的一个缺点是您必须以正确的顺序获取参数。上面的代码可以让你写:Something s = new Something(name("pen"), size(20), size(21));此外,这种方法并不能帮助您避免输入可选参数。 我会赞成这个分析:not really better than a comment
...另一方面... ;)【参考方案11】:
使用 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));
优点:
比我目前看到的任何构建器模式都要短得多 适用于方法和构造函数 完全键入安全 它看起来非常接近其他编程语言中的实际命名参数 它与典型的构建器模式一样安全(可以多次设置参数)缺点:
你的老板可能会为此对你处以私刑 很难判断发生了什么【讨论】:
缺点:字段是公开的,不是最终的。如果您对此感到满意,为什么不使用 setter 呢?它对方法有什么作用? 可以使用二传手,但这有什么意义呢?它只会使代码更长,并且会消除这样做的好处。赋值是无副作用的,setter 是黑盒子。$foo
永远不会转义给调用者(除非有人确实将它分配给回调内部的变量)所以为什么不能它们是公开的?
【参考方案12】:
您可以使用 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();
【讨论】:
Google auto/value 具有类似的目的,但使用了注解处理框架,它比 Lombocks 字节码操作项目更安全(升级 JVM 后仍然可以工作)。 我认为与使用 Lombok 相比,Google 自动/价值需要多花一点时间。 Lombok 的方法或多或少与传统的JavaBean 编写兼容(例如,您可以通过new 实例化,字段在调试器中正确显示等)。我也不是 Lombok 使用的字节码操作 + IDE 插件解决方案的忠实粉丝,但我不得不承认,在实践中它工作正常。到目前为止,JDK版本更改、反射等都没有问题。 是的,这是真的。对于 auto/value,您需要提供一个抽象类,然后将其实现。 Lombok 需要更少的代码。所以需要比较利弊。【参考方案13】:这是上面 Lawrence 描述的 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();
【讨论】:
【参考方案14】:Java 中的任何解决方案都可能非常冗长,但值得一提的是,Google AutoValues 和 Immutables 等工具将使用 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;
【讨论】:
【参考方案15】:这是一个经过编译器检查的 Builder 模式。注意事项:
这不能防止参数的双重赋值 你不能有一个好的.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
等参数化。由于方法本身必须编译,所以它调用的所有内容都必须编译。所以,一般来说,我们不能在类本身的方法中进行编译测试。
出于类似的原因,我们无法使用构建器模型来防止重复赋值。
【讨论】:
【参考方案16】: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");
【讨论】:
这似乎和R Casha
的回答基本一样,只是没有代码解释。【参考方案17】:
您可以模仿应用此模式的命名参数:
public static class CarParameters
// to make it shorter getters and props are omitted
public ModelParameter setName(String name)
this.name = name;
return new ModelParameter();
public class ModelParameter
public PriceParameter setModel(String model)
CarParameters.this.model = model;
return new PriceParameter();
public class PriceParameter
public YearParameter setPrice(double price)
CarParameters.this.price = price;
return new YearParameter();
public class YearParameter
public ColorParameter setYear(int year)
CarParameters.this.year = year;
return new ColorParameter();
public class ColorParameter
public CarParameters setColor(Color color)
CarParameters.this.color = color;
return new CarParameters();
然后你可以将它传递给你的方法:
factory.create(new CarParameters()
.setName("Ford")
.setModel("Focus")
.setPrice(20000)
.setYear(2011)
.setColor(BLUE));
您可以在这里阅读更多内容https://medium.com/@ivorobioff/named-parameters-in-java-9072862cfc8c
【讨论】:
【参考方案18】:@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 是如何支持命名参数的?
嗯 - 足以研究,也许是一个新问题。
【讨论】:
以上是关于Java中的命名参数习语的主要内容,如果未能解决你的问题,请参考以下文章
将命名空间从 java 传递给 xslt,并使用 java 中的参数作为 xslt 中的节点