如何在 Java 中实现命名参数习语?(特别是对于构造函数)
我正在寻找一种类似于 Objective-C 的语法,而不是 JavaBeans 中使用的语法。
一个小的代码示例就可以了。
我认为在构造函数中模拟关键字参数的最佳 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();
主要注意事项是:
您可能还想查看这篇博文(不是我写的)。
值得一提的是:
Foo foo = new Foo() {{
color = red;
name = "Fred";
size = 42;
}};
所谓的双括号初始化器。它实际上是一个带有实例初始化器的匿名类。
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);
}
}
您也可以尝试从这里遵循建议: http ://www.artima.com/weblogs/viewpost.jsp?thread=118828
int value; int location; boolean overwrite;
doIt(value=13, location=47, overwrite=true);
它在呼叫站点上很冗长,但总体而言开销最低。
我想指出,这种风格既解决了命名参数又解决了属性功能,而没有其他语言具有的get和set前缀。它在 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());
}
}
如果您使用的是 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 不支持构造函数或方法参数的类似 Objective-C 的命名参数。此外,这确实不是 Java 的做事方式。在 java 中,典型的模式是详细命名的类和成员。类和变量应该是名词,命名的方法应该是动词。我想您可以发挥创造力并偏离 Java 命名约定并以一种 hacky 方式模拟 Objective-C 范式,但是负责维护您的代码的普通 Java 开发人员不会特别欣赏这一点。在使用任何语言工作时,您都应该遵守语言和社区的约定,尤其是在团队工作时。
关于什么
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");
我觉得“评论解决方法”值得拥有自己的答案(隐藏在现有答案中并在此处的评论中提到)。
someMethod(/* width */ 1024, /* height */ 768);
您可以使用为参数命名的常用构造函数和静态方法:
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
使用 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));
}
}
优点:
缺点:
您可以使用 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();
这是一个经过编译器检查的 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
,等等。由于方法本身必须编译,它调用的所有东西都必须编译。所以,一般来说,我们不能在类本身的方法中进行编译测试。
出于类似的原因,我们无法防止使用构建器模型进行双重分配。
这是Builder
上面 Lawrence 描述的 Pattern 的一个变体。
我发现自己经常使用这个(在适当的地方)。
主要区别在于,在这种情况下, Builder 是immutable。这样做的好处是可以重复使用并且是线程安全的。
因此,您可以使用它来创建一个默认 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();
}
}
Java 中的任何解决方案都可能非常冗长,但值得一提的是,Google AutoValues和Immutables等工具将使用 JDK 编译时注释处理自动为您生成构建器类。
就我而言,我希望在 Java 枚举中使用命名参数,因此构建器模式不起作用,因为枚举实例不能被其他类实例化。我想出了一种类似于@deamon's answer的方法,但增加了参数排序的编译时检查(以更多代码为代价)
这是客户端代码:
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; }
}
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");
}
}
package org.xxx.lang;
/**
* A hack to work around the fact that java does not support
* named parameters in function calls.
*
* Its easy to swap a few String parameters, for example.
* Some IDEs are better than others than showing the parameter names.
* This will enforce a compiler error on an inadvertent swap.
*
* @param <T>
*/
public class Datum<T> {
public final T v;
public Datum(T v) {
this.v = v;
}
public T v() {
return v;
}
public T value() {
return v;
}
public String toString() {
return v.toString();
}
}
class Catalog extends Datum<String> {
public Catalog(String v) {
super(v);
}
}
class Schema extends Datum<String> {
public Schema(String v) {
super(v);
}
}
class Meta {
public void getTables(String catalog, String schema, String tablePattern) {
// pseudo DatabaseMetaData.getTables();
}
}
class MetaChecked {
public void getTables(Catalog catalog, Schema schema, String tablePattern) {
// pseudo DatabaseMetaData.getTables();
}
}
@Test
public void test() {
Catalog c = new Catalog("test");
assertEquals("test",c.v);
assertEquals("test",c.v());
assertEquals("test",c.value());
String t = c.v;
assertEquals("test",t);
}
public void uncheckedExample() {
new Meta().getTables("schema","catalog","%");
new Meta().getTables("catalog","schema","%"); // ooops
}
public void checkedExample() {
// new MetaChecked().getTables(new Schema("schema"),new Catalog("catalog"),"%"); // won't compile
new MetaChecked().getTables(new Catalog("catalog"), new Schema("schema"),"%");
}
现在我们都在使用 Java 17 ;-),使用记录是模仿这个习语的一种超级简单的方法:
public class OrderTemplate() {
private int tradeSize, limitDistance, backoffDistance;
public record TradeSize( int value ) {}
public record LimitDistance( int value ) {}
public record BackoffDistance( int value ) {}
public OrderTemplate( TradeSize t, LimitDistance d, BackoffDistance b ) {
this.tradeSize = t.value();
this.limitDistance = d.value();
this.backoffDistance = b.value();
}
}
然后你可以调用:
var t = new OrderTemplate( new TradeSize(30), new LimitDistance(182), new BackoffDistance(85) );
我发现它非常容易阅读,并且我已经完全停止混淆所有 int 参数(“它是大小优先还是距离......”)。
您可以模仿应用此模式的命名参数:
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
@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 是如何支持命名参数的?
嗯 - 足以研究,也许是一个新问题。