7

I want to make a method that accepts any class T that implements any interface I.

Then do something with the class and return the interface I that is implemented.

Here's what I've tried:

class MyLibrary {
    
    public static <I, T extends I> I registerImplementation(Class<T> classImpl) {
        I interfaceImpl = (I) classImpl.newInstance();
        return interfaceImpl;
    }
}

I'm then creating an interface and a class which implements that interface:

interface UserInterface {
    void doSomethingDefined();
}

class UserClass_v1_10_R2 implements UserInterface {

    @Override
    public void doSomethingDefined() {
        System.out.println("What this does is well-defined");
    }

    public void doVersionSpecificStuff() {
        System.out.println("This is not in the interface, so don't really depend on this");
    }
}

However, when I'm calling the method referencing UserClass, it returns the same class type T instead of the interface type I, allowing all the class methods which are not declared in the interface to be called.

I.e. in the statement below, even though registerImplementation() is declared to return a reference to UserInterface, the compiler sees the reference as pointing to an instance of UserClass_v1_10_R2, allowing access to the implementation's methods that are not in the interface.

MyLibrary.registerImplementation(UserClass_v1_10_R2.class).doVersionSpecificStuff();

Contrast this with the supposedly identical

UserInterface uiObj = MyLibrary.registerImplementation(UserClass_v1_10_R2.class);

uiObj.doVersionSpecificStuff(); // This does not compile
fuggerjaki61
  • 822
  • 1
  • 11
  • 24
user7401478
  • 1,372
  • 1
  • 8
  • 25
  • What happens if you explicitly specify the generic type parameters with the following: `MyLibrary.registerImplementation(UserClass_v1_10_R2.class).doVersionSpecificStuff();` – Jacob G. Nov 28 '20 at 20:09
  • Can you do `UserClass_v1_10_R2 r = MyLibrary.registerImplementation(UserClass_v1_10_R2.class);`? – daniu Nov 28 '20 at 20:11
  • @JacobG. Compile error, cannot resolve symbol "registerImplementation" – user7401478 Nov 28 '20 at 20:13
  • @daniu This works as the return value of this function is UserClass, however I need to return the interface that it implements without specifically declaring which one, as its name is provided by the user. – user7401478 Nov 28 '20 at 20:14
  • @user7401478 “Cannot resolve symbol” sounds like an Intellij-specific error - could you compile the suggested code with `javac` directly to see if that fixes the issue? – Jacob G. Nov 28 '20 at 20:33
  • Curiously, if you first assign the result of `registerImplementation()` to a reference of type `UserInterface` then of course the extra implementation method isn't visible, as expected. This will be one for the language lawyers. – Jim Garrison Nov 28 '20 at 20:52
  • Very interesting question, I added a bit for extra clarity, as it wasn't immediately obvious what you were asking. – Jim Garrison Nov 28 '20 at 20:59
  • @JacobG. Unfortunately, this is not much different compared to asking the user of such library to then manually cast the returned `UserClass` to his desired interface and defeats the whole idea of using generics. – user7401478 Nov 28 '20 at 21:12
  • @user7401478 Understandable. My guess is that the compiler must be inferring `I` as `UserClass_v1_10_R2`, and I'm currently looking over the Java Language Specification to see if I can find a section that explains it. – Jacob G. Nov 28 '20 at 21:14
  • I'm also curious, what version of Java do you happen to be compiling with? – Jacob G. Nov 28 '20 at 21:18
  • @JacobG. I've used both Java 8 and 11, and both compilers produce the same result. – user7401478 Nov 28 '20 at 21:22
  • Based on your usage: do you need to specify them programmatically or can they be loaded automatically (see [list all implementations of interface](https://stackoverflow.com/questions/347248/how-can-i-get-a-list-of-all-the-implementations-of-an-interface-programmatically)) – fuggerjaki61 Nov 28 '20 at 22:12
  • Am I the only one here who thinks what you are doing does not make much sense? Why should the compiler favour the inference that `I` is `UserInterface`, over anything else, without any extra information? `I` could be `UserClass_v1_10_R2`, or `Object` as well. And what if `UserClass_v1_10_R2` implements multiple interfaces? The compiler can only possibly be designed to choose `Object` or `T` as the type of `I` when no more information is present. And it seems the latter is the case... – Sweeper Nov 29 '20 at 00:53

2 Answers2

1

That's just how the compiler infers the type. If you compile with a special (undocumented) flag:

javac --debug=verboseResolution=all  ...

you will see that:

  Note: Deferred instantiation of method <I,T>registerImplementation(Class<T>)
  MyLibrary.registerImplementation(UserClass_v1_10_R2.class).doVersionSpecificStuff();
                                    ^
  instantiated signature: (Class<UserClass_v1_10_R2>)UserClass_v1_10_R2

Look at the instantiated signature: (Class<UserClass_v1_10_R2>)UserClass_v1_10_R2

So you need to add another argument, to take the interface:

public static <I, T extends I> I registerImplementation(Class<I> interfaceClass, Class<T> implClass) {
    // do whatever checks you want if this is an interface or if a constructor can be called....
    I interfaceImpl = null;
    try {
        interfaceImpl = interfaceClass.cast(implClass.newInstance());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return interfaceImpl;
}

Of course this raises the question if you have multiple interfaces, but that is something for you to think about.

Eugene
  • 117,005
  • 15
  • 201
  • 306
0

It's possible to achieve this by passing the desired interface class as another argument to the method. This also allows to verify that the generic type class is an interface, as well as adding extra checks to make sure that the class implementing the interface can be instantiated.

public class MyLibrary {
    public static <I> I registerImplementation(Class<I> interfaceClass, Class<? extends I> implClass) {
        if (!interfaceClass.isInterface()) {
            throw new IllegalArgumentException("Must be an interface class");
        }

        // Check constructor accessibility, etc...
        if (Modifier.isAbstract(implClass.getModifiers())) {
            throw new IllegalArgumentException("Must be instantiable");
        }

        return implClass.newInstance();
    }
}

This returns an implementation of the interface passed as the first parameter. The second parameter's bounded type Class<? extends I> means an explicit cast is not required:

// This returns UserInterface
MyLibrary.registerImplementation(UserInterface.class, UserClass_v1_10_R2.class);
user7401478
  • 1,372
  • 1
  • 8
  • 25