Как я могу использовать параметры метода и типа класса в одном ограничении?

Я попытаюсь проиллюстрировать свою проблему в следующем упрощенном примере:

public class DataHolder<T> {
  private final T myValue;

  public DataHolder(T value) {
    myValue = value;
  }

  public T get() {
    return myValue;
  }

  // Won't compile
  public <R> DataHolder<R super T> firstNotNull(DataHolder<? extends R> other) {
    return new DataHolder<R>(myValue != null ? myValue : other.myValue);      }

  public static <R> DataHolder<R> selectFirstNotNull(DataHolder<? extends R> first,
                                                     DataHolder<? extends R> second) {
    return new DataHolder<R>(first.myValue != null ? first.myValue : second.myValue);
  }
}

Здесь я хочу написать общий метод firstNotNull, который возвращает DataHolder, параметризованный общим супертипом параметра типа T аргумента this и other, поэтому позже я мог бы написать, например.

DataHolder<Number> r = new DataHolder<>(3).firstNotNull(new DataHolder<>(2.0));

или

DataHolder<Object> r = new DataHolder<>("foo").firstNotNull(new DataHolder<>(42));

Проблема заключается в том, что это определение firstNotNull отклоняется компилятором с сообщением о том, что super T часть ограничения типа является незаконной (синтаксически). Однако без этого ограничения определение также неверно (очевидно), потому что в этом случае T и R не связаны друг с другом.

Интересно, что определение аналогичного статического метода selectFirstNotNull является правильным, и последнее работает так, как ожидалось. Можно ли достичь такой же гибкости с помощью нестатических методов в системе типа Java?

Ответ 1

Это невозможно сделать. Авторы Guava столкнулись с той же проблемой: Optional.or. Из этой документации метода:

Примечание о дженериках: подпись public T or(T defaultValue)чрезмерно ограничительный. Однако идеальная сигнатура public <S super T> S or(S) не является законной Java. В результате, некоторые разумные операции с подтипами являются ошибки компиляции:

Optional<Integer> optionalInt = getSomeOptionalInt();
Number value = optionalInt.or(0.5); // error

FluentIterable<? extends Number> numbers = getSomeNumbers();   
Optional<? extends Number> first = numbers.first();
Number value = first.or(0.5); // error

В качестве обходного пути всегда можно Optional<? extends T> to Optional<T>. Литье любого из вышеперечисленных пример Optional экземпляры Optional<Number> (где Number - это желаемый тип вывода) решает проблему:

Optional<Number> optionalInt = (Optional) getSomeOptionalInt();   
Number value = optionalInt.or(0.5); // fine

FluentIterable<? extends Number> numbers = getSomeNumbers();   
Optional<Number> first = (Optional) numbers.first();
Number value = first.or(0.5); // fine

Так как DataHolder является неизменным, как Optional, вышеописанное обходное решение будет работать и для вас.

См. также: Ответ Rotsor на ограничение общих типов с ключевым словом 'super'

Ответ 2

Я не думаю, что есть простой и безопасный способ сделать это. Я пробовал несколько подходов, но единственный рабочий подход, который я нашел, - это начать с типичного экземпляра типа super и сделать этот метод довольно простым:

public DataHolder<T> firstNotNull(DataHolder<? extends T> other) {
    return new DataHolder<T>(myValue != null ? myValue : other.myValue);
}

Теперь вам нужно изменить свой вызов на:

DataHolder<Number> r = new DataHolder<Number>(3).firstNotNull(new DataHolder<>(2.0));

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

Ответ 3

Попробуйте изменить свой метод следующим образом:

      public <R> DataHolder<R> firstNotNull(DataHolder<? super T> other) {
        return new DataHolder<R>((R)(this.myValue != null ? myValue : other.myValue));
     }

ПРЕДУПРЕЖДЕНИЕ. Это компилирует и дает внешний вид правильной проверки по большей части, но не идеально. Он будет ограничивать входные параметры, но не выход. Это невозможно сделать отлично. В некотором смысле вам может быть лучше делать это неконтролируемым, а не давать иллюзию проверки. Вот несколько примеров:

      DataHolder<BigDecimal> a = new DataHolder<>(new BigDecimal(34.0));
      DataHolder<Number> b = new DataHolder<>(new Integer(34));
      DataHolder<String> c = new DataHolder<>("");
      DataHolder<Number> p = a.firstNotNull(b); // WORKS (good)
      DataHolder<BigDecimal> q =  b.firstNotNull(a); // FAILS (good)
      DataHolder<BigDecimal> r =  b.firstNotNull(c); // FAILS (good)
      DataHolder<String> s =  a.firstNotNull(b); // WORKS (not good!!!)