As you have noted, none of the 3 ideas in the question are supported (a constructor with a certain signature in an interface, an abstract class enforcing a certain constructor, or a static method within an interface or abstract class)
However, you can define an interface (or abstract class) that is a Factory for the type that you ultimately want.
public interface AnInterface {
int fancyComputation();
}
public interface IFooBarFactory<T extends AnInterface> {
T create(int magicNumber);
}
IFooBarFactory has 2 concrete implementations
public class BarFactory implements IFooBarFactory<Bar> {
public Bar create(int magicNumber) {
return new Bar(magicNumber);
}
}
public class FooFactory implements IFooBarFactory<Foo> {
public Foo create(int magicNumber) {
return new Foo(magicNumber);
}
}
Then use the strategy pattern (https://en.wikipedia.org/wiki/Strategy_pattern) to retrieve the correct factory. Then use this factory, which has a known interface, to manufacture your object with the correct value (and any additional values that are required to manufacture an object).
FooBarFactory fooBarFactory = new FooBarFactory();
IFooBarFactory<T> factory = fooBarFactory.createFactory(typeOfAnInterface);
T impl = factory.create(magicNumber);
With the conrete implementations
public class Bar implements AnInterface {
private final int magicInt;
public Bar(int magicInt) {
this.magicInt = magicInt;
}
public int fancyComputation() {
return magicInt + 2;
}
}
public class Foo implements AnInterface {
private final int magicInt;
public Foo(int magicInt) {
this.magicInt = magicInt;
}
public int fancyComputation() {
return magicInt + 1;
}
}
the following code:
public static void main(String ... parameters) {
test(Foo.class);
test(Bar.class);
}
private static <T extends AnInterface> void test(Class<T> typeOfAnInterface) {
T impl = createImplForAnInterface(typeOfAnInterface, 10);
System.out.println(typeOfAnInterface.getName() + " produced " + impl.fancyComputation());
}
private static <T extends AnInterface> T createImplForAnInterface(Class<T> typeOfAnInterface, int magicNumber) {
FooBarFactory fooBarFactory = new FooBarFactory();
IFooBarFactory<T> factory = fooBarFactory.createFactory(typeOfAnInterface);
T impl = factory.create(magicNumber);
return impl;
}
prints
Foo produced 11
Bar produced 12
This provides a number of benefits over a solution with introspection or static factories. The caller does not need to know how to manufacture any of the objects, nor is the caller required to know or care when method is the "correct" method to use in order to retrieve the correct type. All callers simply call the one public/known component, which returns the "correct" factory. This makes your callers cleaner because they are no longer tightly coupled to the concrete implementations of AnInterface for the types FooBar. They only need to be concerned with "I need an implementation of AnInterface, which consumes (or processes) this type." I know that this means you have two "factory" classes. One to retrieve the correct factory, and the other which is actually responsible for creating the concrete types Foo and Bar. However, you hide this implementation detail from the callers through an additional layer of abstraction (see the createImplForAnInterface method).
This approach will be particularly beneficial if you are generally using some form of dependency injection. My recommendation with correspond exactly to Guice's assisted inject (https://github.com/google/guice/wiki/AssistedInject) or a similar idea in Spring (Is it possible and how to do Assisted Injection in Spring?).
This means that you need to have several factory classes (or dependency injection binding rules for Guice) but each of these classes are small, simple, and easy to maintain. Then you write a small test that retrieves all classes that implement AnInterface and you verify that your component which implements the strategy-pattern has covered all cases (through reflection - I would use the Reflections class in org.reflections:reflections). This gives you a usable code-abstraction that simplifies the use of these objects by reducing redundant code, loosening a tight coupling of components, and not sacrificing polymorphism.