Использование дженериков в аргументах исключения

Я пытаюсь сохранить коллекцию общих объектов в Exception, и у меня возникают проблемы с выяснением генериков. В частности, я использую Hibernate Validator и хотел бы сохранить собранный список нарушений в рамках исключения для обработки на другом уровне приложения. Вот пример:

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

В Eclipse строка throws показывает конструктор undefined и предлагает мне изменить подпись конструктора на ValidationException(String, Set<ConstraintViolation<User>>. Здесь 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;
    }
}

Однако, я хочу сохранить ValidationException generic, чтобы я мог использовать его только для проверки User. Я также пробовал Set<ConstraintViolation<? extends Object>>, но получаю те же результаты.

Есть ли способ выполнить то, что я пытаюсь сделать?

Ответ 1

Вам нужно объявить параметр набора нарушений как Set<? extends ConstraintViolation<?>>:

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

Тогда все должно работать как ожидалось.

Это имеет дополнительное преимущество для защиты от копирования Set, предоставленного вам, гарантируя, что внутреннее исключение Set не может быть изменено.

Ответ 2

Один уродливый подход состоял бы в том, чтобы использовать непроверенный бросок:

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;
    } 
}

Насколько я понимаю, неконтролируемый бросок в этом случае полностью безопасен, так что @SuppressWarning("unchecked") абсолютно легален.

С другой стороны, этот конструктор нельзя вызывать с Set<ConstraintViolation<?>> в качестве параметра.

Ответ 3

Предположим, что требование должно быть однородным - violations должно быть типа Set<ConstraintViolation<X>> для некоторого X.

Самый естественный способ сделать это - сделать ValidationException generic:

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

Конечно, Java не допускает этого для подтипов Throwable по причинам, отличным от системы типов. Это не наша вина, поэтому мы не виновны в выработке какого-либо способа обхода:

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();
}

Примечание: листинг в getViolations() является только безопасным, если сайт звонка правильно поставлен T, как в случае v2. В случае v3, приведение не соответствует действительности - компилятор не предупредил нас напрасно.

Сайт вызова, вероятно, не знает и не заботится о точном T, как в случае с v4. Сайт вызова может передавать нарушения, однородную коллекцию определенного неизвестного типа, в более общий тип readonly с помощью подстановочных знаков. Это довольно неудобно. Если случай v4 является наиболее часто используемым случаем, мы должны предоставить метод, который просто возвращает Set<ConstraintViolation<?>>. Мы не можем напрямую вернуть violations, что небезопасно. Требуется копия. Если v4 является единственным вариантом использования, это решение действительно становится тем же самым решением, которое предлагалось предыдущими респондентами.

Ответ 4

Я думаю, проблема в том, что Set<ConstraintViolation<User>> может содержать только объекты типа ConstraintViolation<User>, тогда как ваш тип аргумента Set<ConstraintViolation<?>> может содержать нарушения любого типа (так что это смешанная коллекция). Таким образом, это не подтип. Я думаю (не пробовал) вы могли бы объявить конструктор как

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

но тогда у вас все еще есть проблема, что переменная в вашем исключении не может иметь такой тип. Вы можете скопировать содержимое параметра в новый Set, использовать Collections.unmodifiedSet(), чтобы обернуть его, или сделать уродливое литье, упомянутое axtavt, чтобы обойти это. Здесь мой предпочтительный способ:

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

Ответ 5

Я считаю, что Java throwable нельзя сделать универсальным, потому что из-за стирания типа блок catch не может проверить общие параметры исключений, которые он ловит. Вам придется сделать это старомодно, с кастами и еще много чего.

Вы можете добавить метод автоматического выполнения акта:

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

…
UserConstraint uc = theViolation.getConstraint();

Компилятор предупредит вас о том, что он не может выполнить бросок, который вы просите, но это круто.

Ответ 6

Вы можете сделать:

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