You can achieve what you want with some changes and some help from functional programming ...
TL; DR
The basic idea is that the transform method does not receive any arguments. Instead, it will return an instance of some functional interface.
The implementation of this functional interface will consist of code that would be executed by the transform method if it had arguments.
To represent arguments of different types and / or a different number of arguments for each subclass of interface A we will use covariance in the return type of the transform method.
This means that the functional interface will be common (so that the type of arguments can be different for each subclass of A ) and that there will be subinterfaces that extend this functional interface, each of which will take a different number of arguments in one abstract method. This will allow you to return the value of the transform() method for arguments 1, 2, 3, ... etc.
To execute the code returned by the transform() method, we will do the following:
instanceOfB.transform().execute("hello"); instanceOfC.transform().execute(1, 'a'); instanceOfD.transform().execute(1, "hello");
Finally, in order to be able to execute code in a general way, the basic functional interface defines the varargs executeVariadic(Object... args) method, which will be implemented as the default method by each child functional interface, delegating its execute and casting arguments as necessary.
Now the long version ...
Let's start by renaming your A interface to something more visual. Since it defines a method called transform , name it Transformer .
Then we create a functional interface, which will be a transform method of the Transformer interface. Here he is:
@FunctionalInterface public interface Transformation { void executeVariadic(Object... args); }
This interface defines only one abstract method (SAM) that takes an argument of Object... varargs. It exists, so subinterfaces can override it.
Now create the Transformation1 functional interface, which extends the Transformation interface:
@FunctionalInterface public interface Transformation1<A> extends Transformation { void execute(A a); @Override @SuppressWarnings("unchecked") default void executeVariadic(Object... args) { this.execute((A) args[0]); } }
This Transformation1<A> functional interface is generic and defines a single abstract execute method that takes a single argument of type A The executeVariadic method executeVariadic overridden as the default method, which delegates its execution to the execute method, appropriately applying the first argument. This throw gives rise to a warning, but oh, well ... we better learn to live with it.
Now we will create a similar interface with two parameters of the general type and execute , which will receive two arguments whose types correspond to the parameters of the type type:
@FunctionalInterface public interface Transformation2<A, B> extends Transformation { void execute(A a, B b); @Override @SuppressWarnings("unchecked") default void executeVariadic(Object... args) { this.execute((A) args[0], (B) args[1]); } }
The idea is the same: the Transformation2 interface extends the Transformation interface, and we override the executeVariadic method executeVariadic that it is delegated to the execute method, appropriately discarding the arguments (and suppressing the annoying warning).
For completeness, we introduce the Transformation3 interface, which is similar to the previous TransformationX units:
@FunctionalInterface public interface Transformation3<A, B, C> extends Transformation { void execute(A a, B b, C c); @Override @SuppressWarnings("unchecked") default void executeVariadic(Object... args) { this.execute((A) args[0], (B) args[1], (C) args[2]); } }
Hope the sample is already clear. You should create as many TransformationX interfaces as possible as arguments that you want to support for the transform method of your Transformer ( A ) interface in your question, remember that I renamed it).
So far so good, I know this answer is long, but I needed to define the interfaces above so that now I could use them to connect all the parts.
Remember your interface A ? Let not only change your name to Transformer , but also the signature of its transform method:
@FunctionalInterface public interface Transformer { Transformation transform(); }
So now this is your basic interface. The transform method no longer has arguments, but returns Transformation instead.
See how to implement classes B , C and D But first let me rename them to TransformerB , TransformerC and TransformerD respectively.
Here's TransformerB :
public class TransformerB implements Transformer { @Override public Transformation1<String> transform() { return a -> System.out.println(a);
Important here is the use of covariance in the return type of the transform method. And I use the type Transformation1<String> , which is a subtype of Transformation and indicates that for the TransformerB class, the transform method returns a transformation that takes a single argument of type String . Since the Transformation1 interface is a SAM type, I use a lambda expression to implement it.
Here's how to call the code inside the TransformerB.transform method:
TransformerB b = new TransformerB(); b.transform().execute("hello");
b.transform() returns an instance of Transformation1 , the execute method is immediately called with the String argument that it expects.
Now consider the implementation of TransformerC :
public class TransformerC implements Transformer { @Override public Transformation2<Integer, Character> transform() { return (a, b) -> System.out.println(a + b); } }
Again, covariance in the return type of the transform method allows you to return a specific Transformation , in this case Transformation2<Integer, Character> .
Using:
TransformerC c = new TransformerC(); c.transform().execute(1, 'A');
In the TransformerD example, I used a transformation with three arguments:
public class TransformerD implements Transformer { public Transformation3<Integer, Double, String> transform() { return (a, b, c) -> System.out.println(a + b + c); } }
Using:
TransformerD d = new TransformerD(); d.transform().execute(12, 2.22, "goodbye");
This is all type safe because generic types can be specified in the TransformationX return type of each particular transform implementation method. However, primitive types cannot be used because primitive types cannot be used as parameters of a type type.
As for the way the transform method is invoked in a general way, this is simple:
void callTransf(Transformer a, Object... args) { a.transform().executeVariadic(args); }
This is why the executeVariadic method executeVariadic . And it is redefined in every TransformationX interface, so it can be used polymorphically, as in the code above.
Calling the callTransf method callTransf also simple:
callTransf(b, "hello"); callTransf(c, 1, 'A'); callTransf(d, 12, 2.22, "goodbye");