Jak zaimplementować idiom nazwanych parametrów w Javie? (szczególnie dla konstruktorów)
Szukam składni podobnej do Objective-C, a nie takiej używanej w JavaBeans.
Mały przykład kodu byłby w porządku.
Dzięki.
Jak zaimplementować idiom nazwanych parametrów w Javie? (szczególnie dla konstruktorów)
Szukam składni podobnej do Objective-C, a nie takiej używanej w JavaBeans.
Mały przykład kodu byłby w porządku.
Dzięki.
Odpowiedzi:
Najlepszym idiomem Java, jaki wydaje mi się do symulacji argumentów słów kluczowych w konstruktorach, jest wzorzec Builder, opisany w Effective Java 2nd Edition .
Podstawowym pomysłem jest posiadanie klasy Builder, która ma metody ustawiające (ale zwykle nie pobierające) dla różnych parametrów konstruktora. Jest też build()
metoda. Klasa Builder jest często (statyczną) zagnieżdżoną klasą klasy, z której została zbudowana. Konstruktor klasy zewnętrznej jest często prywatny.
Wynik końcowy wygląda mniej więcej tak:
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...
}
Aby utworzyć instancję Foo, napisz coś takiego:
Foo foo = Foo.builder()
.setColor(red)
.setName("Fred")
.setSize(42)
.build();
Główne zastrzeżenia to:
Możesz również sprawdzić ten wpis na blogu (nie mojego autorstwa).
.withFoo
raczej niż .setFoo
: newBuilder().withSize(1).withName(1).build()
zamiastnewBuilder().setSize(1).setName(1).build()
There's no compile-time checking that all of the parameters have been specified exactly once.
Ten problem można rozwiązać, zwracając interfejsy, Builder1
w BuilderN
których każdy obejmuje jeden z ustawiaczy lub build()
. Jest znacznie bardziej szczegółowy w kodzie, ale zawiera obsługę kompilatora dla DSL i sprawia, że autouzupełnianie jest bardzo przyjemne w użyciu.
Warto wspomnieć:
Foo foo = new Foo() {{
color = red;
name = "Fred";
size = 42;
}};
tak zwany inicjator podwójnego nawiasu . W rzeczywistości jest to klasa anonimowa z inicjatorem wystąpienia.
Możesz również spróbować skorzystać z porad tutaj: http://www.artima.com/weblogs/viewpost.jsp?thread=118828
int value; int location; boolean overwrite;
doIt(value=13, location=47, overwrite=true);
W witrynie wywołania jest gadatliwy, ale ogólnie daje najniższy narzut.
doIt( /*value*/ 13, /*location*/ 47, /*overwrite*/ true )
Styl 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()
zatrzymała mnie na tropie. Nigdy nie widziałem takiego stylu łańcucha lambda w Javie. Czy po raz pierwszy odkryłeś ten pomysł w innym języku z lambdami?
Java nie obsługuje nazwanych parametrów podobnych do celu w C dla konstruktorów lub argumentów metod. Co więcej, tak naprawdę nie jest to sposób działania języka Java. W Javie typowym wzorcem są werbalne nazwy klas i członków. Klasy i zmienne powinny być rzeczownikami, a nazwana metoda - czasownikami. Przypuszczam, że możesz wykazać się kreatywnością i odejść od konwencji nazewnictwa Javy i emulować paradygmat Objective-C w hakerski sposób, ale nie byłoby to szczególnie doceniane przez przeciętnego programistę Java odpowiedzialnego za utrzymanie twojego kodu. Pracując w dowolnym języku, powinieneś trzymać się konwencji języka i społeczności, zwłaszcza podczas pracy w zespole.
Jeśli używasz języka Java 6, możesz użyć parametrów zmiennych i zaimportować statyczne, aby uzyskać znacznie lepszy wynik. Szczegóły tego można znaleźć w:
http://zinzel.blogspot.com/2010/07/creating-methods-with-named-parameters.html
Krótko mówiąc, możesz mieć coś takiego:
go();
go(min(0));
go(min(0), max(100));
go(max(100), min(0));
go(prompt("Enter a value"), min(0), max(100));
Chciałbym zaznaczyć, że ten styl odnosi się zarówno do nazwanego parametru, jak i do właściwości bez prefiksu get i set, który ma inny język. To nie jest konwencjonalne w dziedzinie Java, ale jest prostsze, nietrudne do zrozumienia, szczególnie jeśli masz do czynienia z innymi językami.
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());
}
}
Co powiesz na
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");
Możesz użyć zwykłego konstruktora i metod statycznych, które nadają argumentom nazwę:
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;
}
}
Stosowanie:
import static Something.*;
Something s = new Something(name("pen"), size(20), weight(8.2));
Ograniczenia w porównaniu z rzeczywistymi nazwanymi parametrami:
/*name*/ "pen", /*size*/ 20, /*weight*/ 8.2)
)Jeśli masz wybór, spójrz na Scala 2.8. http://www.scala-lang.org/node/2075
not really better than a comment
... z drugiej strony ...;)
Używając lambd Java 8, możesz zbliżyć się jeszcze bardziej do rzeczywistych nazwanych parametrów.
foo($ -> {$.foo = -10; $.bar = "hello"; $.array = new int[]{1, 2, 3, 4};});
Zwróć uwagę, że prawdopodobnie narusza to kilka tuzinów „najlepszych praktyk Java” (jak wszystko, co wykorzystuje ten $
symbol).
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));
}
}
Plusy:
Cons:
$foo
nigdy nie ucieka do dzwoniącego (chyba że ktoś przypisze to do zmiennej w wywołaniu zwrotnym), więc dlaczego nie mogą być publiczne?
Możesz użyć adnotacji projektu Lombok @Builder do symulacji nazwanych parametrów w Javie. Spowoduje to wygenerowanie konstruktora, którego możesz użyć do tworzenia nowych instancji dowolnej klasy (zarówno klas, które napisałeś, jak i tych pochodzących z bibliotek zewnętrznych).
Oto jak włączyć to na zajęciach:
@Getter
@Builder
public class User {
private final Long id;
private final String name;
}
Następnie możesz użyć tego przez:
User userInstance = User.builder()
.id(1L)
.name("joe")
.build();
Jeśli chcesz stworzyć taki Builder dla klasy pochodzącej z biblioteki, utwórz metodę statyczną z adnotacjami w następujący sposób:
class UserBuilder {
@Builder(builderMethodName = "builder")
public static LibraryUser newLibraryUser(Long id, String name) {
return new LibraryUser(id, name);
}
}
Spowoduje to wygenerowanie metody o nazwie „builder”, którą można wywołać:
LibraryUser user = UserBuilder.builder()
.id(1L)
.name("joe")
.build();
Wydaje mi się, że „obejście problemu z komentarzami” zasługuje na własną odpowiedź (ukrytą w istniejących odpowiedziach i wymienioną w komentarzach tutaj).
someMethod(/* width */ 1024, /* height */ 768);
To jest wariant Builder
Wzorca, jak opisał Lawrence powyżej.
Często tego używam (w odpowiednich miejscach).
Główna różnica polega na tym, że w tym przypadku Budowniczy jest odporny na szczepienia . Ma to tę zaletę, że można go użyć ponownie i jest bezpieczny dla wątków.
Możesz więc użyć tego do stworzenia jednego domyślnego Konstruktora, a następnie w różnych miejscach, w których go potrzebujesz, możesz go skonfigurować i zbudować obiekt.
Ma to największy sens, jeśli budujesz ten sam obiekt w kółko, ponieważ wtedy możesz ustawić program budujący jako statyczny i nie musisz się martwić o zmianę jego ustawień.
Z drugiej strony, jeśli musisz budować obiekty ze zmieniającymi się parametrami, powoduje to pewne obciążenie. (ale hej, możesz łączyć generowanie statyczne / dynamiczne z niestandardowymi build
metodami)
Oto przykładowy kod:
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();
}
}
Każde rozwiązanie w Javie jest prawdopodobnie będzie dość rozwlekły, ale warto wspomnieć, że narzędzia takie jak Google AutoValues i Immutables wygeneruje konstruktora dla klasy automatycznie przy użyciu JDK czasie kompilacji przetwarzania adnotacji.
W moim przypadku chciałem użyć nazwanych parametrów w wyliczeniu Java, aby wzorzec konstruktora nie działał, ponieważ instancje wyliczenia nie mogą być tworzone przez inne klasy. Wymyśliłem podejście podobne do odpowiedzi @ deamon, ale dodaje sprawdzanie kolejności parametrów w czasie kompilacji (kosztem większej ilości kodu)
Oto kod klienta:
Person p = new Person( age(16), weight(100), heightInches(65) );
I realizacja:
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; }
}
Warto rozważyć idiom obsługiwany przez bibliotekę 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
odpowiedź, ale bez kodu wyjaśniającego.
Oto wzorzec Konstruktora sprawdzany przez kompilator. Ostrzeżenia:
.build()
metodyPotrzebujesz więc czegoś spoza klasy, co zakończy się niepowodzeniem, jeśli nie zostanie zaliczone Builder<Yes, Yes, Yes>
. Zobacz getSum
metodę statyczną jako przykład.
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);
}
}
Zastrzeżenia wyjaśnione . Dlaczego nie ma metody budowania? Problem w tym, że znajdzie się w Builder
klasie i zostanie sparametryzowany za pomocą K1, K2, K3
itp. Ponieważ sama metoda musi się kompilować, wszystko, co wywołuje, musi się kompilować. Zatem generalnie nie możemy umieścić testu kompilacji w metodzie samej klasy.
Z podobnego powodu nie możemy zapobiec podwójnemu przypisaniu za pomocą modelu konstruktora.
@irreputable wymyśliło fajne rozwiązanie. Jednak - może to pozostawić twoją instancję klasy w nieprawidłowym stanie, ponieważ nie nastąpi weryfikacja ani sprawdzanie spójności. Dlatego wolę łączyć to z rozwiązaniem Builder, unikając tworzenia dodatkowej podklasy, chociaż nadal będzie to podklasa klasy builder. Dodatkowo, ponieważ dodatkowa klasa konstruktora sprawia, że jest bardziej rozwlekły, dodałem jeszcze jedną metodę wykorzystującą lambdę. Dodałem kilka innych podejść do budowania dla kompletności.
Rozpoczynając od zajęć w następujący sposób:
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; }
}
Następnie za pomocą różnych metod:
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";
}
);
Wygląda na to, że po części jest to całkowite zdzierstwo z tego, co @LaurenceGonsalves już opublikował, ale zobaczysz małą różnicę w wybranej konwencji.
Zastanawiam się, gdyby JLS kiedykolwiek zaimplementował nazwane parametry, jak by to zrobili? Czy rozszerzyliby jeden z istniejących idiomów, zapewniając mu krótkie wsparcie? W jaki sposób Scala obsługuje nazwane parametry?
Hmmm - wystarczy do zbadania, a może nowe pytanie.