Home > Software design >  Implement an interface with a method signature whose generic parameters extend an interface
Implement an interface with a method signature whose generic parameters extend an interface

Time:01-19

Below is an example of what I'm trying to accomplish. I'm trying to create LibraryRunner which implements GenericRunner. However I'm getting an IDE warning that LibraryRunner does not implement the methods in the GenericRunner interface. Which makes sense, the method signature doesn't match, but how can I make the method signature match while still using the LibraryCalculator which extends the GenericCalculator?

public class LibraryRunner implements GenericRunner {
    @Override
    public <T> T run(LibraryCalculator<T> calculator) {
        return T;
    }
}

public interface GenericRunner {
    <T, A> T run(GenericCalculator<T, A> calculator);
}


public interface LibraryCalculator<T> extends GenericCalculator<T, LibraryAlgorithm> {
    T calc(LibraryAlgorithm algorithm) throws LibraryException;
}

public interface GenericCalculator<T, A> {
    T calc(A algorithm) throws Exception;
}


public class LibraryException extends Exception {
    
}

CodePudding user response:

The way the interfaces are defined, you cannot override the method and change its signature, even if LibraryCalculator is a subtype of GenericCalculator. The other answer explains why.

One way to solve that is to parameterize the GenericRunner interface with the calculator type:

public interface GenericRunner<T, A, C extends GenericCalculator<T, A>> {
    T run(C calculator);
}

public class LibraryRunner<T> implements GenericRunner<T, LibraryAlgorithm, LibraryCalculator<T>> {

    @Override
    public T run(LibraryCalculator<T> calculator) {
        ...
    }

}

But note that you cannot return an Object in the LibraryRunner method, as it does not match T. If you want to return a specific type like Object (which defeats the purpose of the generic LibraryCalculator), you have to specify it explicitly:

public class LibraryRunner implements GenericRunner<Object, LibraryAlgorithm, LibraryCalculator<Object>> {

    @Override
    public Object run(LibraryCalculator<Object> calculator) {
        return new Object();
    }

}

If that's not what you want, then it's up to the user of LibraryRunner to decide which type T should be bound to.

CodePudding user response:

When you override a method or implement it, you always need to satisfy the condition of the overridden method, for example you should still be able to call LibraryRunner.run with a parameter GenericCalculator. else it wont make sense to write:

LibraryRunner libRunner = new LibraryRunner();
GenericRunner genRunner = libRunner;
genRunner.run(new GenericCalculator<>());

Overriding the exception does work because when you call GenericCalculator.calc you are expecting any exception and calling LibraryCalculator.calc narrow it down to only LibraryException.

you can look on overriding methods as promise more information.

CodePudding user response:

I would argue that GenericRunner is not necessary. All it does is move the generic type arguments from the class level to the method level. Instead use Callable<T>. The problem with LibraryRunner as-is, as you are aware, is that it can not be used everywhere a GenericRunner can, since it's method accepts only a subset of objects that it 'should'. This argument is the problem. Some class needs to supply it, but the current interface higherarchy has pushed the responsibility one step too far I would argue. Typically you would have the implementation class construct, or accept in its constructor such arguments. Something like this:

public class LibraryRunner<T> implements Callable<T> {
    
    private final LibraryAlgorithm algorithm;
    private final Supplier<LibraryCalculator<T>> calculatorSupplier;
    
    public LibraryRunner(
            LibraryAlgorithm algorithm,
            Supplier<LibraryCalculator<T>> calculatorSupplier) {
        this.algorithm = algorithm;
        this.calculatorSupplier = calculatorSupplier;
    }

    @Override
    public T call() throws Exception {
        return calculatorSupplier.get().calcT(algorithm);
    }
}

Via the Supplier that you provide to the constructor, you have the ability to control what calculator implementation (restricted to LibraryCalculator) gets used by the LibraryRunner.

  •  Tags:  
  • Related