Як реалізувати ідіому іменованого параметра в Java? (особливо для конструкторів)
Я шукаю синтаксис, схожий на Objective-C, а не такий, який використовується в JavaBeans.
Невеликий приклад коду буде непоганим.
Дякую.
Як реалізувати ідіому іменованого параметра в Java? (особливо для конструкторів)
Я шукаю синтаксис, схожий на Objective-C, а не такий, який використовується в JavaBeans.
Невеликий приклад коду буде непоганим.
Дякую.
Відповіді:
Найкращою ідіомою 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();
Основні застереження:
Ви також можете переглянути цю публікацію в блозі (не я).
.withFoo
, а не .setFoo
: newBuilder().withSize(1).withName(1).build()
а неnewBuilder().setSize(1).setName(1).build()
There's no compile-time checking that all of the parameters have been specified exactly once.
Цю проблему можна подолати, повернувши інтерфейси Builder1
туди, BuilderN
де кожен охоплює або одного із установників, або build()
. Кодувати набагато детальніше, але він має підтримку компілятора для вашого DSL і робить з автозаповненням дуже приємно працювати.
Про це варто згадати:
Foo foo = new Foo() {{
color = red;
name = "Fred";
size = 42;
}};
так званий подвійний фігурний ініціалізатор . Це насправді анонімний клас з ініціалізатором екземпляра.
Ви також можете спробувати дотримуватися порад звідси: http://www.artima.com/weblogs/viewpost.jsp?thread=118828
int value; int location; boolean overwrite;
doIt(value=13, location=47, overwrite=true);
Це багатослівно на сайті дзвінків, але загалом дає найнижчі накладні витрати.
doIt( /*value*/ 13, /*location*/ 47, /*overwrite*/ true )
Стиль 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);
}
}
create()
зупинило мене на шляху. Я ніколи не бачив такого стилю лямбда-ланцюгів на Java. Ви вперше виявили цю ідею іншою мовою з лямбдами?
Java не підтримує подібні до Objective-C іменовані параметри конструкторів або аргументів методів. Більше того, це насправді не спосіб Java. У Java типовий шаблон - багатослівні імена класів та членів. Класи та змінні повинні бути іменниками, а названий метод - дієсловами. Я припускаю, що ви могли б проявити креативність і відхилитися від конвенцій про іменування Java та наслідувати парадигму Objective-C, але це не було б особливо оцінено пересічним розробником Java, відповідальним за підтримку вашого коду. Працюючи будь-якою мовою, вам слід дотримуватися умовних норм мови та спільноти, особливо під час роботи в команді.
Якщо ви використовуєте 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));
Я хотів би зазначити, що цей стиль звертається як до названого параметра, так і до властивостей без префікса get і set, який має інша мова. Це не звично в області 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());
}
}
Що стосовно
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");
Ви можете використовувати звичайний конструктор та статичні методи, які дають аргументам назву:
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
not really better than a comment
... з іншого боку ...;)
Використовуючи лямбди Java 8, ви можете наблизитись до реальних іменованих параметрів.
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));
}
}
Плюси:
Мінуси:
$foo
ніколи не втікає до абонента (якщо хтось не призначає його змінній всередині зворотного виклику), то чому вони не можуть бути загальнодоступними?
Ви можете використовувати анотацію @Builder проекту Lombok для моделювання названих параметрів у 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();
Я відчуваю, що "обхідний шлях" заслуговує на власну відповідь (прихований у існуючих відповідях і згаданий у коментарях тут).
someMethod(/* width */ 1024, /* height */ 768);
Це варіант 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();
}
}
Будь-яке рішення в 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; }
}
Можливо, варто розглянути ідіому, яку підтримує бібліотека 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
відповідь, але без коду, який би це пояснив.
Ось шаблон перевірки компілятора 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
і т. Д. Оскільки сам метод повинен компілювати, все, що він викликає, має скомпілювати. Отже, загалом ми не можемо поставити тест компіляції в метод самого класу.
З подібної причини ми не можемо запобігти подвійному призначенню, використовуючи модель конструктора.
@irreputable придумав гарне рішення. Однак - це може залишити ваш примірник класу в недійсному стані, оскільки перевірка та перевірка узгодженості не відбуватимуться. Тому я вважаю за краще поєднувати це з рішенням Builder, уникаючи створення додаткового підкласу, хоча це все одно буде підклас класу builder. Крім того, оскільки додатковий клас будівельника робить його більш багатослівним, я додав ще один метод, використовуючи лямбда. Для повноти я додав деякі інші підходи до будівництва.
Починаючи з класу наступним чином:
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 підтримує іменовані параметри?
Хммм - досить для дослідження, і, можливо, нове питання.