Используйте универсальный для хранения общего супертипа в Java

Предположим, у меня есть метод «mix», который принимает два списка, возможно, разных типов T и S, и возвращает один список, содержащий элементы обоих. В целях безопасности типов я хотел бы указать, что возвращаемый список имеет тип R, где R — это супертип, общий для T и S. Например:

List<Number> foo = mix(
    Arrays.asList<Integer>(1, 2, 3),
    Arrays.asList<Double>(1.0, 2.0, 3.0)
);

Чтобы указать это, я мог бы объявить метод как

static <R, T extends R, S extends R> List<R> mix(List<T> ts, List<S> ss)

Но что, если я хочу сделать mix методом экземпляра вместо статического в классе List2<T>?

<R, T extends R, S extends R> List<R> mix ...

затеняет <T> на экземпляре List2, так что это нехорошо.

<R, T extends S&T, S extends R> List<R> mix ...

решает проблему затенения, но не принимается компилятором

<R super T, S extends R> List<R> mix ...

отклоняется компилятором, поскольку подстановочные знаки с нижней границей нельзя хранить в именованной переменной (используется только в выражениях ? super X)

Я мог бы переместить аргументы в сам класс, например List2<R, T extends R, S extends R>, но информация о типе действительно не имеет значения на уровне экземпляра, потому что она используется только для одного вызова метода, и вам придется повторно приводить объект каждый раз, когда вы хотите для вызова метода с разными аргументами.

Насколько я могу судить, с помощью дженериков это сделать невозможно. Лучшее, что я могу сделать, это вернуть необработанное List2 и привести его к callsite, как до того, как были введены дженерики. У кого-нибудь есть лучшее решение?


person Matt G    schedule 18.08.2013    source источник
comment
Почему у вас S extends T вместо S extends R?   -  person Rohit Jain    schedule 19.08.2013
comment
Также было бы лучше, если бы вы могли показать тело вашего метода - mix. Что именно ты в нем делаешь? Вам действительно нужны эти параметры типа? Похоже, вместо этого вы можете использовать ограниченные подстановочные знаки.   -  person Rohit Jain    schedule 19.08.2013
comment
На самом деле, я не понимаю необходимости параметра типа R. Поскольку вы просто собираетесь вернуть List<Number>, просто укажите это как тип возвращаемого значения. Честно говоря, я не вижу здесь никакого применения универсального метода.   -  person Rohit Jain    schedule 19.08.2013
comment
упс Должно быть, S расширяет R, это была опечатка, которую скопировали.   -  person Matt G    schedule 19.08.2013
comment
часть List<Number> — это всего лишь пример. Я хочу, чтобы он мог смешивать любые два типа. Тривиальная реализация mix может быть просто ArrayList<R> result = new ArrayList<>(ts.size() + ss.size()); result.addAll(ts); result.addAll(ss); return result;   -  person Matt G    schedule 19.08.2013
comment
R должен быть неким надклассом, общим для T и S. Для Double и Integer это может быть Number. Для StringBuilder и String это может быть CharSequence. Для сканера и компаратора это может быть Object.   -  person Matt G    schedule 19.08.2013
comment
<R super T, S extends R> List<R> действительно имеет смысл. Связанный пост: Ограничение дженериков ключевым словом "super". Возможно, вам придется согласиться на сохранение статического метода.   -  person Paul Bellora    schedule 19.08.2013
comment
+1 Это хорошо. Я не думаю, что есть способ сделать это. Если вы не объявите отношение T к R в объявлении типа List2. Что бы просто переместить параметры типа в объявлении метода в объявление класса: class List2<R, T extends R, S extends R>, что, как я полагаю, не то, что вы хотели.   -  person Edwin Dalorzo    schedule 19.08.2013
comment
@ПолБеллора. R super T не поддерживается.   -  person Rohit Jain    schedule 19.08.2013
comment
@RohitJain Да, я знаю это. Пожалуйста, прочитайте весь мой комментарий.   -  person Paul Bellora    schedule 19.08.2013


Ответы (2)


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

<R super T, S extends R> List<R> mix(List<S> otherList)

Но, конечно, R super T не разрешено языком (обратите внимание, что ответ полигенных смазок на связанный пост неверен - есть варианты использования этого синтаксиса, как показывает ваш вопрос).

Здесь нет способа победить — у вас есть только один из нескольких обходных путей на выбор:

  • Прибегайте к использованию подписи с необработанными типами. Не делай этого.
  • Оставьте mix статическим методом. На самом деле это достойный вариант, если только он не должен быть частью интерфейса вашего класса по причинам, связанным с полиморфизмом, или вы планируете, что mix будет настолько широко используемым методом, что вы считаете, что сохранение его статичности неприемлемо.
  • Согласитесь с тем, что подпись mix является чрезмерно ограничительной, и задокументируйте, что определенные непроверенные приведения будут необходимы со стороны вызывающей стороны. Это похоже на то, что 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

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

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

К сожалению для вас, не всегда безопасно преобразовывать List2<? extends T> в List2<T>. Например, приведение List2<Integer> к List2<Number> может позволить добавить Double к чему-то, что должно содержать только Integer, и привести к неожиданным ошибкам во время выполнения. Исключение было бы, если бы List2 было неизменным (например, Optional), но это маловероятно.

Тем не менее, такие приведения можно было бы сойти с рук, если бы вы были осторожны и документировали небезопасный код с пояснениями. Предполагая, что mix имеет следующую подпись (и реализацию, ради интереса):

List<T> mix(final List<? extends T> otherList) {

    final int totalElements = (size() + otherList.size());
    final List<T> result = new ArrayList<>(totalElements);

    Iterator<? extends T> itr1 = iterator();
    Iterator<? extends T> itr2 = otherList.iterator();
    while (result.size() < totalElements) {
        final T next = (itr1.hasNext() ? itr1 : itr2).next();
        result.add(next);
        final Iterator<? extends T> temp = itr1;
        itr1 = itr2;
        itr2 = temp;
    }

    return result;
}

Тогда у вас может быть следующий сайт вызова:

final List2<Integer> ints = new List2<>(Arrays.asList(1, 2, 3));
final List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);

final List<Number> mixed;
// type-unsafe code within this scope
{
    @SuppressWarnings("unchecked") // okay because intsAsNumbers isn't written to
    final List2<Number> intsAsNumbers = (List2<Number>)(List2<?>)ints;
    mixed = intsAsNumbers.mix(doubles);
}

System.out.println(mixed); // [1, 1.5, 2, 2.5, 3, 3.5]

Опять же, установка статического mix будет чище и не будет риска для безопасности типов. Я бы удостоверился, что у меня есть очень веские причины, чтобы не продолжать в том же духе.

person Paul Bellora    schedule 20.08.2013

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

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

public <V extends Superclass> List<Superclass> mix(List<V> list1, List<V> list2) {
  List<Superclass> mixedList;

  mixedList.addAll(list1);
  mixedList.addAll(list2);
}

Последний случай намного сложнее. Я бы посоветовал вам переосмыслить свой дизайн, поскольку для метода микширования гораздо больше смысла находиться в суперклассе или в классе, который знает суперкласс и его подтипы, поскольку вы возвращаете список суперкласса.

Если вы действительно хотите это сделать, вам придется преобразовать List2 в List2 и сделать следующее:

public <R, V extends R> List<R> mix(List<V> list1, List<V> list2) {
    List<R> mixedList;

    mixedList.addAll(list1);
    mixedList.addAll(list2);

    return mixedList;
}
person Tarek    schedule 22.01.2014