Home > Blockchain >  Java Generic Method to enforce type invariance between parameter type and return type
Java Generic Method to enforce type invariance between parameter type and return type

Time:10-12

I have tried a number of different variations of the code below and cannot find any solution which doesn't rely on an unsafe cast or cause another other compiler warning. I am confident the goal is possible, but maybe not?

To put it simply, the goal is that I have derived types which are related to each other and have invariant relationship, which can be enforced by a generic method.

AlphaTask always returns AlphaTaskResult. AlphaTask is a concrete implementation of ITask<T>, where T is String. AlphaTaskResult extends the base class of TaskResult<T>, where again T is String.

Everything checks out until it comes to writing a generic method which take any Task and get back the corresponding TaskResult type.

The error is:

Required type: List<U>
Provided:      List<TaskResult<T>>
no instance(s) of type variable(s) exist so that TaskResult<T> conforms to U inference variable T has incompatible bounds: equality constraints: U lower bounds: TaskResult<T>
package com.adobe.panpipe;

import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;


interface ITask<T>{
    TaskResult<T> make();
}

class TaskResult<T>{
    T value;
}

class AlphaTaskResult extends TaskResult<String> {
    AlphaTaskResult(String value){
        this.value = value;
    }
}

class BetaTaskResult extends TaskResult<Integer> {
    BetaTaskResult(Integer value){
        this.value = value;
    }
}

class AlphaTask implements ITask<String> {
    public AlphaTaskResult make(){
        return new AlphaTaskResult("alphaTask");
    }
}

class BetaTask implements ITask<Integer> {
    public BetaTaskResult make(){
        return new BetaTaskResult(9001);
    }
}

public class Main <T>{

    public static <T, U extends TaskResult<T>, V extends ITask<T>> List<U> run(List<V> tasks){

        List<U> results =  tasks
                .stream()
                .map(ITask::make)
                .collect(Collectors.toList());

        return results;
    }

    public static void main(String[] args) {

        List<AlphaTaskResult> alphaResults = run(Arrays.asList(new AlphaTask(), new AlphaTask()));
        List<BetaTaskResult> betaResults = run(Arrays.asList(new BetaTask(), new BetaTask()));

    }
}

CodePudding user response:

There is no connection between ITask and TaskResult. Sure, the task implementations override make with their respective return types, but this information can't be used in the declaration of the run method.

You can make the connection by adding an additional type parameter to ITask:

interface ITask<T, TResult extends TaskResult<T>>{
    TResult make();
}

class TaskResult<T>{
    T value;
}

Change the task implementations and run accordingly:

class AlphaTask implements ITask<String, AlphaTaskResult> {
    public AlphaTaskResult make(){
        return new AlphaTaskResult("alphaTask");
    }
}

class BetaTask implements ITask<Integer, BetaTaskResult> {
    public BetaTaskResult make(){
        return new BetaTaskResult(9001);
    }
}

public class Main { // Main doesn't need a type parameter
    public static <T, U extends TaskResult<T>, V extends ITask<T, U>> List<U> run(List<V> tasks){

        List<U> results =  tasks
            .stream()
            .map(ITask::make)
            .collect(Collectors.toList());

        return results;
    }
    public static void main(String[] args) {
        List<AlphaTaskResult> alphaResults = run(Arrays.asList(new AlphaTask(), new AlphaTask()));
        List<BetaTaskResult> betaResults = run(Arrays.asList(new BetaTask(), new BetaTask()));
    }

}

CodePudding user response:

Just add the type R extends TaskResult<T> of the result to the type ITask of the task. The following compiles just fine:

import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;


interface ITask<T, R extends TaskResult<T>>{
    R make();
}

class TaskResult<T>{
    T value;
}

class AlphaTaskResult extends TaskResult<String> {
    AlphaTaskResult(String value){
        this.value = value;
    }
}

class BetaTaskResult extends TaskResult<Integer> {
    BetaTaskResult(Integer value){
        this.value = value;
    }
}

class AlphaTask implements ITask<String, AlphaTaskResult> {
    public AlphaTaskResult make(){
        return new AlphaTaskResult("alphaTask");
    }
}

class BetaTask implements ITask<Integer, BetaTaskResult> {
    public BetaTaskResult make(){
        return new BetaTaskResult(9001);
    }
}

public class Main <T>{

    public static <T, R extends TaskResult<T>> List<R> run(List<ITask<T, R>> tasks){

        List<R> results =  tasks
                .stream()
                .map(ITask::make)
                .collect(Collectors.toList());

        return results;
    }

    public static void main(String[] args) {
        List<AlphaTaskResult> alphaResults = run(Arrays.asList(new AlphaTask(), new AlphaTask()));
        List<BetaTaskResult> betaResults = run(Arrays.asList(new BetaTask(), new BetaTask()));

    }
}

The reason why your version does not compile is that the exact type of the result is not included in the type of the task, and since there are no type refinements in Java, there is nothing from which the type U could be inferred.

  • Related