问题
I want to write a generic builder class which wraps around any java class and providing setter functions of a specific style. I am not sure if this could be called "dynamically generated functions".
When I have a beanish Pojo class i.e.
class Pojo {
public void setValue(int value) {...}
public void setName(String name) {...}
}
My Maker
class should be usable like this:
Pojo p = Builder<Pojo>.create(new Pojo())
.setName("Funny")
.setValue(123)
.build();
As you can see, the work it does should be similar to
class PojoBuilder {
private Pojo pojo;
PojoBuilder(Pojo pojo) { this.pojo = pojo; }
public static PojoMaker create(Pojo p) { return new PojoBuilder(p); }
public PojoBuilder setName(String name) { pojo.setName(name); return this; }
public PojoBuilder setValue(int val) { pojo.setValue(val); return this; }
public Pojo make() { return pojo; }
}
Only, I would like Maker
to be generic. Obviously, the "setXyz"-Methods depend on the generic argument. How to do that?
Of course, functionally equivalent but syntactically different approach is also fine.
I'd like to do it without annotations: With annotations I gather I'd need a second javac-pass over my source code, generating the wrapper code. That seems to be what Limbok does or how some JPA wrappers work. But when I work with Mockito it seems that this pass is not necessary. So, How can I do it with Generics?
回答1:
The following code shows that it is possible up to a certain point (I did not test for corner case such as primitive type, etc).
It use Java 8, lamdba and type erasure.
Since in Java 8 you can reference constructor using X::new, and method using the same syntax, it works by stacking into a map each method and their parameter, so that we don't rely on a particular instance (so that build() can create new instance of Foobar).
package foobar;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
public class Maker<T> {
private final Supplier<? extends T> supplier;
/**
* We need to store the instance since build must return a new instance.
* <p>
* Sadly, we need to rely on type erasure (hence BiConsumer, not BiConsumer<T,V>).
*/
@SuppressWarnings("rawtypes")
private final Map<BiConsumer, Object> values = new HashMap<>();
public Maker(final Supplier<? extends T> supplier) {
this.supplier = supplier;
}
public static <T> Maker<T> create(final Supplier<? extends T> builder) {
return new Maker<>(builder);
}
public <U> Maker<T> set(final BiConsumer<T, U> consumer, final U value) {
values.put(consumer, value);
return this;
}
@SuppressWarnings("unchecked")
public T create() {
final T instance = supplier.get();
values.forEach((key, value) -> {
key.accept(instance, value);
});
return instance;
}
public static void main(final String[] args) {
final Maker<Foobar> maker = Maker.create(Foobar::new).set(Foobar::setName, "Name");
final AtomicInteger generator = new AtomicInteger(0);
Arrays.asList("Alpha", "Beta", "Gamma").forEach(name -> {
final Integer id = generator.incrementAndGet();
maker.set(Foobar::setName, name);
maker.set(Foobar::setId, id);
final Foobar foobar = maker.create();
if (!name.equals(foobar.getName())) {
throw new AssertionError("expected " + name + ", got " + foobar.getName());
}
if (!id.equals(foobar.getId())) {
throw new AssertionError("expected " + id + ", got " + foobar.getId());
}
System.out.println(foobar);
});
}
}
With the Foobar class:
public class Foobar {
private Integer id;
private String name;
public Integer getId() {return id;}
public void setId(final Integer id) {this.id = id;}
public String getName() {return name;
public void setName(final String name) {this.name = name;}
@Override public String toString() {
return "Foobar [id=" + id + ", name=" + name + "]";
}
}
来源:https://stackoverflow.com/questions/24936150/how-to-implement-a-builder-class-using-generics-not-annotations