Подтвердить что ты не робот

Почему параметр типа Java не может иметь нижнюю границу?

Я понимаю, что вы не можете привязать параметр типа generics Java к нижней границе (т.е. используя ключевое слово super). Я читал, что Часто задаваемые вопросы по Angelika Langer Generics по этому вопросу. Они говорят, что это в основном сводится к тому, что нижняя граница бесполезна ( "не имеет никакого смысла" ).

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

public static ArrayList<String> createArrayListFullOfEmptyStrings(int i);

Но это излишне ограничительно для ваших клиентов. Почему они не могут вызвать ваш метод следующим образом:

//should compile
List<Object> l1 = createArrayListFullOfEmptyStrings(5); 
List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
List<String> l3 = createArrayListFullOfEmptyStrings(5);

//shouldn't compile
List<Integer> l4 = createArrayListFullOfEmptyStrings(5);

В этот момент у меня возникнет соблазн попробовать следующее определение:

public static <T super String> List<T> createArrayListFullOfEmptyStrings(int size) {
  List<T> list = new ArrayList<T>(size);
  for(int i = 0; i < size; i++) {
     list.add("");
  }
  return list;
}

Но он не будет компилироваться; ключевое слово super является незаконным в этом контексте.

Мой пример выше плохого примера (игнорируя то, что я говорю ниже)? Почему здесь нет нижней границы? И если это было бы полезно, какова реальная причина, по которой в Java не разрешена?

P.S.

Я знаю, что лучшая организация может быть примерно такой:

public static void populateListWithEmptyStrings(List<? super String> list, int size);

List<CharSequence> list = new ArrayList<CharSequence>();
populateListWithEmptyStrings(list, 5);

Можем ли мы с этой целью притворяться, что по требованию нам нужно выполнить обе операции в одном вызове метода?

Изменить

@Tom G (справедливо) спрашивает, какое преимущество имеет List<CharSequence> для List<String>. Во-первых, никто не сказал, что возвращаемый список неизменен, поэтому здесь одно преимущество:

List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
l2.add(new StringBuilder("foo").append("bar"));
4b9b3361

Ответ 1

В принципе, его недостаточно полезно.

Я думаю, что ваш пример указывает на единственное преимущество нижней границы, функцию, которую часто задает: Restricted Instantiation:

Суть заключается в следующем: все, что "супер" связало бы вы купили бы ограничение, что только супертипы Number могут использоваться как аргументы type.....

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

Из-за характера полиморфизма и специализации верхние границы гораздо полезнее нижних границ, как описано в FAQ (Доступ к нестационарным членам и стирание типов). Я подозреваю, что сложность, введенная нижними границами, не стоит его ограниченного значения.


OP: Я хочу добавить, я думаю, вы показали, что это полезно, просто недостаточно полезно. Придумайте неопровержимые примеры использования убийц, и я верну JSR.: -)

Ответ 2

спецификация говорит о нижних границах параметров типа, например

4.10.2

переменная типа является прямым супертипом ее нижней границы.

5.1.10

переменная нового типа... нижняя граница которой

Похоже, что переменная типа имеет только (ненулевую) нижнюю границу, если она является синтетической в ​​результате захвата подстановочных знаков. Что делать, если язык допускает нижние границы для всех параметров типа? Вероятно, это не вызывает больших проблем, и он исключил только для того, чтобы сохранить общие методы проще (ну...) Обновить. Говорят, что теоретическое исследование параметров нижнего ограниченного типа не проводится тщательно.

Обновление: документ, утверждающий нижние границы, в порядке: "Недопустимость типа Java-типа: можем ли мы его исправить" Daniel Smith

RETRACT: следующий аргумент неверен. Пример OP является законным.

Ваш конкретный пример не очень убедителен. Сначала это не безопасно. Возвращенный список действительно является List<String>, он небезопасен для просмотра его как другого типа. Предположим, что ваш код компилируется:

    List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

то мы можем добавить к нему не-String, что неверно

    CharSequence chars = new StringBuilder();
    l2.add(chars); 

Ну а List<String> нет, но несколько напоминает список CharSequence. Ваша потребность может быть решена с помощью шаблона:

public static  List<String> createArrayListFullOfEmptyStrings(int size)  

// a list of some specific subtype of CharSequence 
List<? extends CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

// legal. can retrieve elements as CharSequence
CharSequence chars = l2.get(0);

// illegal, won't compile. cannot insert elements as CharSequence
l2.add(new StringBuilder());

Ответ 3

Больше, чем ответ, это другой вариант (возможно, убийца?). У меня есть помощник ModelDecorator. Я хочу, чтобы у него был следующий публичный API

class ModelDecorator<T>{
    public static <T> ModelDecorator<T> create(Class<T> clazz);
    public <SUPER> T from(SUPER fromInstance);
}

Итак, заданные классы A, B расширяет A, его можно использовать следующим образом:

A a = new A();
B b = ModelDecorator.create(B.class).from(a);

Но я хочу иметь ограничения на T и SUPER, поэтому я убеждаюсь, что с помощью API могут быть созданы только подклассы. В этот момент я могу сделать:

C c = new C();
B b = ModelDecorator.create(B.class).from(c);

Где B не наследуется от C.

Очевидно, если бы я мог:

    public <SUPER super T> T from(SUPER fromInstance);

Это решит мою проблему.

Ответ 4

Какое преимущество при наборе списка дает вам в этот момент? Когда вы перебираете возвращаемую коллекцию, вы все равно можете сделать следующее:

for(String s : returnedList) {
CharSequence cs = s;
//do something with your CharSequence
}

Ответ 5

Изменение: я принес хорошие новости. Есть способ получить большую часть того, что вы хотите.

public static <R extends List<? super String>> R createListFullOfEmptyString(IntFunction<R> creator, int size)
{
  R list = creator.apply(size);
  for (int i = 0; i < size; i++)
  {
    list.add("");
  }
  return list;
}

List<Object> lis1 = createListFullOfEmptyString(ArrayList::new, 5); // works
ArrayList<String> lis2 = createListFullOfEmptyString(ArrayList::new, 5); // works
List<Integer> lis3 = createListFullOfEmptyString(ArrayList::new, 5) // breaks 

Недостатком является то, что клиентам необходимо предоставить экземпляр R для мутации или какие-то средства для создания R. Другого способа безопасно создать его нет.

Я сохраню свой оригинальный ответ ниже для информационных целей.


В итоге:

Нет веской причины, просто это не было сделано.

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

А) Принять или создать параметризованную структуру данных

B) Записать вычисленные (не переданные) значения в эту структуру данных

C) Вернуть эту структуру данных

Запись/принятие значений - это как раз тот случай, когда применяется контравариантность, что означает, что параметр типа в структуре данных должен быть ограничен снизу типом значения, записываемого в структуру данных. Единственный способ выразить это в Java в настоящее время - использовать подстановочный знак с нижним ограничением в структуре данных, например, List<? супер T>.


Если мы разрабатываем такой API, как OP, который, естественно, (но не юридически) может быть выражен как:

// T is the type of the value(s) being computed and written to the data structure

// Method creates the data structure
<S super T> Container<S> create()

// Method writes to the data structure
<S super T> Container<S> write(Container<S> container)

Тогда доступные нам варианты:

A) Используйте подстановочный знак с нижним ограничением и заставьте абонентов разыграть вывод:

// This one is actually useless - there is no type the caller can cast to that is both read- and write-safe.
Container<? super T> create()

// Caller must cast result to the same type they passed in.
Container<? super T> write(Container<? super T> container)

Б) Чрезмерно ограничьте параметр типа в структуре данных, чтобы он соответствовал типу записываемого значения, и вынудите вызывающих абонентов преобразовывать ввод и вывод:

// Caller must accept as-is; cannot write values of type S (S super T) into the result.
Container<T> create()

// Caller must cast Container<S> (S super T) to Container<T> before calling, then cast the result back to Container<S>.
Container<T> write(Container<T> container)

C) Используйте новый параметр типа и выполните наше собственное небезопасное приведение внутри:

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> create()

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> write(Container<S> container)

Выбрать свой яд.

Ответ 6

Хм, хорошо, пусть работает с этим. Вы определяете метод:

public static <T super String> List<T> createArrayListFullOfEmptyStrings(int size) {

Что это значит? Это означает, что если я вызову ваш метод, я верну список суперкласса String. Возможно, он возвращает список String. Возможно, он возвращает список объектов. Я не знаю.

Круто.

List<Object> l1 = createArrayListFullOfEmptyStrings(5);

По вашему мнению, это должно скомпилировать. Но это не так! Я могу поместить Integer в список Object - l1.add(3). Но если вы возвращаете список String, то выполнение этого должно быть незаконным.

List<String> l3 = createArrayListFullOfEmptyStrings(5);

По вашему мнению, это должно скомпилировать. Но это не так! l3.get(1) должен всегда возвращать String... но этот метод мог бы вернуть список объектов, что означает, что l3.get(1) может быть, возможно, целым.

Единственное, что работает, это

List<? super String> l5 = createArrayListFullOfEmptyStrings(5);

Все, что я знаю, это то, что я могу безопасно позвонить l4.put("foo"), и я могу спокойно получить Object o = l4.get(2).