Generic Runaway Builder in Java

I know there were similar questions. I did not see the answer to my question.

I will present what I want with some simplified code. Let's say I have a complex object, some of its values โ€‹โ€‹are common:

public static class SomeObject<T, S> { public int number; public T singleGeneric; public List<S> listGeneric; public SomeObject(int number, T singleGeneric, List<S> listGeneric) { this.number = number; this.singleGeneric = singleGeneric; this.listGeneric = listGeneric; } } 

I would like to build it using the free Builder syntax. I would like to do it elegantly. I would like this to work like this:

 SomeObject<String, Integer> works = new Builder() // not generic yet! .withNumber(4) // and only here we get "lifted"; // since now it set on the Integer type for the list .withList(new ArrayList<Integer>()) // and the decision to go with String type for the single value // is made here: .withTyped("something") // we've gathered all the type info along the way .create(); 

There are no dangerous throw warnings, and you do not need to specify generic upfront types (at the top where Builder is built).

Instead, we provide the type information explicitly, further down the chain โ€” along with the calls to withList and withTyped .

Now, what would be the most elegant way to achieve it?

I know the most common tricks, such as using recursive generics , but I played with it a bit and couldn't figure out how this relates to this use case.

Below is a mundane verbose solution that works in the sense of satisfying all requirements, but at the cost of a lot of verbosity - it introduces four builders (not related to inheritance), representing the four possible combinations of T and S types defined or not.

It works, but it is hardly a version to be proud of, and unattainable if we expect more general parameters than two.

 public static class Builder { private int number; public Builder withNumber(int number) { this.number = number; return this; } public <T> TypedBuilder<T> withTyped(T t) { return new TypedBuilder<T>() .withNumber(this.number) .withTyped(t); } public <S> TypedListBuilder<S> withList(List<S> list) { return new TypedListBuilder<S>() .withNumber(number) .withList(list); } } public static class TypedListBuilder<S> { private int number; private List<S> list; public TypedListBuilder<S> withList(List<S> list) { this.list = list; return this; } public <T> TypedBothBuilder<T, S> withTyped(T t) { return new TypedBothBuilder<T, S>() .withList(list) .withNumber(number) .withTyped(t); } public TypedListBuilder<S> withNumber(int number) { this.number = number; return this; } } public static class TypedBothBuilder<T, S> { private int number; private List<S> list; private T typed; public TypedBothBuilder<T, S> withList(List<S> list) { this.list = list; return this; } public TypedBothBuilder<T, S> withTyped(T t) { this.typed = t; return this; } public TypedBothBuilder<T, S> withNumber(int number) { this.number = number; return this; } public SomeObject<T, S> create() { return new SomeObject<>(number, typed, list); } } public static class TypedBuilder<T> { private int number; private T typed; private Builder builder = new Builder(); public TypedBuilder<T> withNumber(int value) { this.number = value; return this; } public TypedBuilder<T> withTyped(T t) { typed = t; return this; } public <S> TypedBothBuilder<T, S> withList(List<S> list) { return new TypedBothBuilder<T, S>() .withNumber(number) .withTyped(typed) .withList(list); } } 

Is there a smarter technique that I could apply?

+5
source share
1 answer

Well, thatโ€™s why a more traditional approach to creating a step would be like this.

Unfortunately, since we mix general and non-general methods, we have to reuse many methods. I do not think there is a good way.

The basic idea is this: define each step on the interface, and then implement them all in a private class. We can do this using common interfaces, inheriting their original types. It is ugly, but it works.

 public interface NumberStep { NumberStep withNumber(int number); } public interface NeitherDoneStep extends NumberStep { @Override NeitherDoneStep withNumber(int number); <T> TypeDoneStep<T> withTyped(T type); <S> ListDoneStep<S> withList(List<S> list); } public interface TypeDoneStep<T> extends NumberStep { @Override TypeDoneStep<T> withNumber(int number); TypeDoneStep<T> withTyped(T type); <S> BothDoneStep<T, S> withList(List<S> list); } public interface ListDoneStep<S> extends NumberStep { @Override ListDoneStep<S> withNumber(int number); <T> BothDoneStep<T, S> withTyped(T type); ListDoneStep<S> withList(List<S> list); } public interface BothDoneStep<T, S> extends NumberStep { @Override BothDoneStep<T, S> withNumber(int number); BothDoneStep<T, S> withTyped(T type); BothDoneStep<T, S> withList(List<S> list); SomeObject<T, S> create(); } @SuppressWarnings({"rawtypes","unchecked"}) private static final class BuilderImpl implements NeitherDoneStep, TypeDoneStep, ListDoneStep, BothDoneStep { private final int number; private final Object typed; private final List list; private BuilderImpl(int number, Object typed, List list) { this.number = number; this.typed = typed; this.list = list; } @Override public BuilderImpl withNumber(int number) { return new BuilderImpl(number, this.typed, this.list); } @Override public BuilderImpl withTyped(Object typed) { // we could return 'this' at the risk of heap pollution return new BuilderImpl(this.number, typed, this.list); } @Override public BuilderImpl withList(List list) { // we could return 'this' at the risk of heap pollution return new BuilderImpl(this.number, this.typed, list); } @Override public SomeObject create() { return new SomeObject(number, typed, list); } } // static factory public static NeitherDoneStep builder() { return new BuilderImpl(0, null, null); } 

Since we donโ€™t want people to turn to an ugly implementation, we make it confidential and force everyone to go through the static method.

Otherwise, it works almost the same as your own idea:

 SomeObject<String, Integer> works = SomeObject.builder() .withNumber(4) .withList(new ArrayList<Integer>()) .withTyped("something") .create(); 

// we could return 'this' at the risk of heap pollution

What is it? Ok, so here is the problem in general, and like this:

 NeitherDoneStep step = SomeObject.builder(); BothDoneStep<String, Integer> both = step.withTyped("abc") .withList(Arrays.asList(123)); // setting 'typed' to an Integer when // we already set it to a String step.withTyped(123); SomeObject<String, Integer> oops = both.create(); 

If we did not create copies, now we will have 123 masquerades around as String .

(If you use only the builder as a free set of calls, this cannot be.)

Although we do not need to make a copy for withNumber , I just took an extra step and made the builder unchanged. We create more objects than necessary, but in reality there is no other good solution. If everyone uses the constructor correctly, we can make it mutable and return this .


Since we are interested in new generic solutions, here is the implementation of the builder in one class.

The difference is that we do not save the types typed and list if we refer to one of their setters a second time. This is actually not the most disadvantage, it is just different, I think. This means that we can do this:

 SomeObject<Long, String> = SomeObject.builder() .withType( new Integer(1) ) .withList( Arrays.asList("abc","def") ) .withType( new Long(1L) ) // <-- changing T here .create(); 
 public static class OneBuilder<T, S> { private final int number; private final T typed; private final List<S> list; private OneBuilder(int number, T typed, List<S> list) { this.number = number; this.typed = typed; this.list = list; } public OneBuilder<T, S> withNumber(int number) { return new OneBuilder<T, S>(number, this.typed, this.list); } public <TR> OneBuilder<TR, S> withTyped(TR typed) { // we could return 'this' at the risk of heap pollution return new OneBuilder<TR, S>(this.number, typed, this.list); } public <SR> OneBuilder<T, SR> withList(List<SR> list) { // we could return 'this' at the risk of heap pollution return new OneBuilder<T, SR>(this.number, this.typed, list); } public SomeObject<T, S> create() { return new SomeObject<T, S>(number, typed, list); } } // As a side note, // we could return eg <?, ?> here if we wanted to restrict // the return type of create() in the case that somebody // calls it immediately. // The type arguments we specify here are just whatever // we want create() to return before withTyped(...) and // withList(...) are each called at least once. public static OneBuilder<Object, Object> builder() { return new OneBuilder<Object, Object>(0, null, null); } 

The same goes for making copies and fouling heaps.


Now we are very romantic. The idea here is that we can โ€œdisableโ€ each method, causing a capture conversion error.

A bit hard to explain, but the main idea:

  • Each method somehow depends on a type variable declared in the class.
  • "Disable" this method, specifying the type of the return value, enter a type variable ? .
  • This results in a capture conversion error if we try to call a method for this return value.

The difference between this example and the previous example is that if we try to call the setter a second time, we get a compiler error:

 SomeObject<Long, String> = SomeObject.builder() .withType( new Integer(1) ) .withList( Arrays.asList("abc","def") ) .withType( new Long(1L) ) // <-- compiler error here .create(); 

Thus, we can only call one setter once.

The two main drawbacks here are that you:

  • cannot call setters a second time for legitimate reasons.
  • and can call setters a second time with the null literal.

I think this is a pretty interesting proof of concept, even if it's a little impractical.

 public static class OneBuilder<T, S, TCAP, SCAP> { private final int number; private final T typed; private final List<S> list; private OneBuilder(int number, T typed, List<S> list) { this.number = number; this.typed = typed; this.list = list; } public OneBuilder<T, S, TCAP, SCAP> withNumber(int number) { return new OneBuilder<T, S, TCAP, SCAP>(number, this.typed, this.list); } public <TR extends TCAP> OneBuilder<TR, S, ?, SCAP> withTyped(TR typed) { // we could return 'this' at the risk of heap pollution return new OneBuilder<TR, S, TCAP, SCAP>(this.number, typed, this.list); } public <SR extends SCAP> OneBuilder<T, SR, TCAP, ?> withList(List<SR> list) { // we could return 'this' at the risk of heap pollution return new OneBuilder<T, SR, TCAP, SCAP>(this.number, this.typed, list); } public SomeObject<T, S> create() { return new SomeObject<T, S>(number, typed, list); } } // Same thing as the previous example, // we could return <?, ?, Object, Object> if we wanted // to restrict the return type of create() in the case // that someone called it immediately. // (The type arguments to TCAP and SCAP should stay // Object because they are the initial bound of TR and SR.) public static OneBuilder<Object, Object, Object, Object> builder() { return new OneBuilder<Object, Object, Object, Object>(0, null, null); } 

Again, about making copies and polluting the heap.


Anyway, I hope this gives you some ideas to drown your teeth. :)

If you are interested in such things at all, I recommend studying with annotation processing , because you can generate such things much easier than writing them manually. As we said in the comments, manually recording such things becomes unrealistic pretty quickly.

+5
source

Source: https://habr.com/ru/post/1247652/


All Articles