cannot infer type-variable(s) - Wildcard vs Generic

76 Views Asked by At

I have created a ValidatorUtils to validate collections based on the jakarta validation applied on the type to validate:

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidatorUtils {

    private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
    
    /**
     * Validate all the elements of the collection, then if at least one was not valid, a ConstraintViolationException is thrown with all the violations encountered.
     */
    public static <TO_VALIDATE> void validateAll(Collection<TO_VALIDATE> collectionToValidate) {
        Set<ConstraintViolation<TO_VALIDATE>> allViolations = new HashSet<>();
        for (TO_VALIDATE toValidate : collectionToValidate) {
            Set<ConstraintViolation<TO_VALIDATE>> violations = VALIDATOR.validate(toValidate);
            allViolations.addAll(violations);
        }
        if (!allViolations.isEmpty()) {
            throw new ConstraintViolationException(allViolations);
        }
    }

}

I have an interface CollectionValidator with a validate method, that by default validate a collection using the ValidatorUtils.validateAll method:

public interface CollectionValidator<REQUEST extends Collection<?>> {

    public default void validate(REQUEST request) {
        ValidatorUtils.validateAll(request);
    }
    
}

when building the project, I have got the following error:

[ERROR] Compilation failure
[ERROR] CollectionValidator.java:[11,31] method validateAll in class ValidatorUtils cannot be applied to given types;
[ERROR] required: java.util.Collection<TO_VALIDATE>
[ERROR] found: REQUEST
[ERROR] reason: cannot infer type-variable(s) TO_VALIDATE
[ERROR]         (argument mismatch; REQUEST cannot be converted to java.util.Collection<TO_VALIDATE>)

to solve it, I have added TYPE_TO_VALIDATE as parameterized type on CollectionValidator:

public interface CollectionValidator<TYPE_TO_VALIDATE, REQUEST extends Collection<TYPE_TO_VALIDATE>> extends Validator<REQUEST> {

    @Override
    public default void validate(REQUEST request) {
        ValidatorUtils.validateAll(request);
    }
    
}

But why with wildcard is not working?

1

There are 1 best solutions below

0
On BEST ANSWER

I will simplify your code a bit and just consider:

static <U> void f(Collection<U> c) {}

static <T extends Collection<?>> void g(T t) {
    f(t); // error
}

f is basically your ValidatorUtils.validateAll, and g is CollectionValidator.validate.

What type is the type parameter U, when you call f(t)?

There is no answer to that question, if t is of type T. That is why you get the error "cannot infer type-variable(s)".

If you want to know where in the spec this is specified, you can start with Invocation Applicability Inference, where the applicability of f is determined. At this step, a constraint formula ‹tCollection<α>› is generated, where α is an inference variable representing the type parameter U that we are trying to infer.

‹t → α› then gets reduced to ‹TCollection<α>›, ‹T <: Collection<α>›, and finally ‹? <= α›. You can follow the reduction steps in the Reduction section. At this point, ‹? <= α› gets reduced to "false", causing the inference to fail.

A constraint formula of the form ‹S <= T›, where S and T are type arguments (§4.5.1), is reduced as follows:

  • If T is a type:

    • If S is a type, the constraint reduces to ‹S = T›.

    • If S is a wildcard, the constraint reduces to false.

(S is the wildcard ? and T is the inference variable α, in this case.)


If you write g like this:

static <E, T extends Collection<E>> void g(T t) {
    f(t);
}

Then it is fine, because type inference can infer that U should be E. You can follow the inference steps above, and see that you get a ‹E <= α› constraint this time, which gets reduced to ‹E = α›, and becomes the bound E = α.


Bonus: why do these compile?

static <T extends Collection<?>> void g(T t) {
    Collection<?> temp = t;
    f(temp); // OK
    f((Collection<?>)t); // OK
}

This is because of capture conversion. Capture conversion replaces the wildcards in a parameterised type with fresh type variables. For example, it can convert Collection<?> to Collection<CAP#1>, where CAP#1 is a fresh type variable (U and T are also type variables, but CAP#1 is a brand new type). Note that you cannot actually write this new type variable in Java code. I'm using CAP#1 here because that is what javac usually calls them.

Both temp and (Collection<?>)t undergo capture conversion. See Cast Expressions and Simple Expression Names.

If the expression name appears in an assignment context, invocation context, or casting context, then the type of the expression name is the declared type of the field, local variable, or parameter after capture conversion.

The type of a cast expression is the result of applying capture conversion to this target type.

So the expressions temp and (Collection<?>)t are actually of type Collection<CAP#1> and Collection<CAP#2>. There is no wildcards! CAP#1 and CAP#2 are both real types! U can be inferred to be CAP#1 and CAP#2 in the two cases respectively, in the same way that it is inferred to be E.

t in the original code is also subject to capture conversion, because it is a simple expression name. However, capture conversion only converts parameterised types like Collection<?>, not a type variable like T. In fact, it would be disastrous if type variables are also converted to Collection<CAP#1> - you wouldn't be able to do something as simple as:

class C<T extends Collection<?>> {
    void one(T a) { }
    void two(T b) { 
        // b would be type Collection<CAP#1> here, and you would not be able to call one
        one(b); 
    }
}

Finally, your validateAll doesn't actually need to be generic.

static void validateAll(Collection<?> collectionToValidate) {
    Set<ConstraintViolation<?>> allViolations = new HashSet<>();
    for (var toValidate : collectionToValidate) {
        var violations = VALIDATOR.validate(toValidate);
        allViolations.addAll(violations);
    }
    if (!allViolations.isEmpty()) {
        throw new ConstraintViolationException(allViolations);
    }
}

Note that I deliberately avoided writing the types of toValidate and violations, because their types contain type variables created by capture conversion.