开发者

Using generics in arguments of an Exception

I'm trying to store a collection of generic objects within an Exception and am having troubles figuring out the generics. Specifically, I'm using H开发者_如何学Goibernate Validator and would like to save the collected list of violations within an exception for processing in another layer of the application. Here's an example:

Set<ConstraintViolation<User>> violations = validator.validate(user);
if (violations.size() > 0) {
    throw new ValidationException("User details are invalid", violations);
}

In Eclipse, the throws line is showing the constructor undefined and is suggesting I change the constructor signature to ValidationException(String, Set<ConstraintViolation<User>>. Here's ValidationException:

public class ValidationException extends Exception {
    private Set<ConstraintViolation<?>> violations;

    public ValidationException() {
    }
    public ValidationException(String msg) {
        super(msg);
    }
    public ValidationException(String msg, Throwable cause) {
        super(msg, cause);
    }
    public ValidationException(String msg, Set<ConstraintViolation<?>> violations) {
        super(msg);
        this.violations = violations;
    }
    public Set<ConstraintViolation<?>> getViolations() {
        return violations;
    }
}

However, I want to keep ValidationException generic so that I can use it for more than just User validations. I've tried Set<ConstraintViolation<? extends Object>> as well, but get the same results.

Is there a way to accomplish what I'm trying to do?


You need to declare the violations set parameter as Set<? extends ConstraintViolation<?>>:

public ValidationException(String msg, 
                           Set<? extends ConstraintViolation<?>> violations) {
  super(msg);
  this.violations = Collections.unmodifiableSet(
      new HashSet<ConstraintViolation<?>>(violations));
}

Then everything should work as expected.

This has the additional benefit of defensively copying the Set you're given, ensuring that the exception's internal Set can't be changed.


One ugly approach would be to use unchecked cast:

public class ValidationException extends Exception {
    private Set<ConstraintViolation<?>> violations;

    @SuppressWarning("unchecked")
    public <T> ValidationException(String msg, Set<ConstraintViolation<T>> violations) {
        super(msg);
        this.violations = (Set<ConstraintViolation<?>>)(Set<?>) violations;
    } 
}

As far as I understand, unchecked cast is completely safe in this case, so that @SuppressWarning("unchecked") is absolutely legal.

From other hand, this constructor can't be called with Set<ConstraintViolation<?>> as a parameter.


Suppose the requirement is that the set must be homogeneous - violations must be of type Set<ConstraintViolation<X>> for some X.

The most natural way to do that is to make ValidationException generic:

public class ValidationException<T> extends Exception
    Set<ConstraintViolation<T>> violations;
    public ValidationException(String msg, Set<ConstraintViolation<T>> violations)

Of course, Java does not allow that for subtypes of Throwable, for reasons beyond type system. This is not our fault, so we are not guilty of inventing some workaround:

public class ValidationException extends Exception
{
    static class SetConstraintViolation<T> extends HashSet<ConstraintViolation<T>>
    {
        SetConstraintViolation(Set<ConstraintViolation<T>> violations)
        {
            super(violations);
        }
    }

    // this is homogeneous, though X is unknown
    private SetConstraintViolation<?> violations;

    public <T> ValidationException(String msg, Set<ConstraintViolation<T>> violations)
    {
        super(msg);
        this.violations = new SetConstraintViolation<T>(violations);
    }

    public <T> Set<ConstraintViolation<T>> getViolations()
    {
        return (Set<ConstraintViolation<T>>)violations;
    }
}

void test()
{
    Set<ConstraintViolation<User>> v = ...;
    ValidationException e = new <User>ValidationException("", v);
    Set<ConstraintViolation<User>> v2 = e.getViolations();
    Set<ConstraintViolation<Pswd>> v3 = e.getViolations();
    Set<? extends ConstraintViolation<?>> v4 = e.getViolations();
}

Note: the cast in getViolations() is only safe, if call site supplied correct T, as in the case of v2. In the case of v3, the cast is wrong - the compiler didn't warn us in vain.

The call site probably doesn't know, and doesn't care about, the exact T, as in the case of v4. The call site can cast the violations, a homogeneous collection of a certain unknown type, to a more general readonly type with wildcards. That is quite awkward. If case v4 is the most frequent use case, we should provide a method that simply returns Set<ConstraintViolation<?>>. We can't directly return violations, that's unsafe. A copy is required. If v4 is the only use case, the this solution really becomes the same solution proposed by previous responders.


I think the problem is that a Set<ConstraintViolation<User>> can only contain objects of type ConstraintViolation<User>, while your argument type Set<ConstraintViolation<?>> can contain violations of any type (so, it is a mixed collection). Thus, this is not a subtype. I think (did not try) you could declare the constructor as

public <T> ValidationException(String msg, Set<ConstraintViolation<T>> violations);

but then you still have the problem that the variable in your exception can't have such a type. You could copy the contents of the parameter in a new Set, use Collections.unmodifiedSet() to wrap it, or do the ugly cast mentioned by axtavt to get around this. Here my preferred way:

public <T> ValidationException(String msg, Set<ConstraintViolation<T>> violations) {
   this.violations = Collections.unmodifiableSet(violations);
}


I believe that a Java throwable cannot be made generic, because owing to type erasure a catch block cannot test the generic parameters of the exceptions that it catches. You will have to do it the old-fashioned way, with casts and whatnot.

You can add a method to automagically do the cast:

class ConstraintViolation extends SomeException {
  Constraint c;
  «T extends Constraint» T getConstraint() { return (T) c};
} 

…
UserConstraint uc = theViolation.getConstraint();

The compiler will warn you that it cannot actually perform the cast you are asking it to do, but that's cool.


You could do:

Set<ConstraintViolation<User>> violations = validator.validate(user);
if (violations.size() > 0) {
    throw new ValidationException("User details are invalid", (Set<ConstraintViolation<?>>) (Set<? extends ConstraintViolation<?>>) violations);
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜