Почему некоторые утверждают, что реализация дженериков в Java плохая?

115

Иногда я слышал, что с дженериками Java не справляется. (ближайшая ссылка здесь )

Простите за неопытность, но что могло бы сделать их лучше?

sdellysse
источник
23
Я не вижу причин закрывать это; Несмотря на всю эту чушь о дженериках, прояснение различий между Java и C # могло бы помочь людям, пытающимся избежать неосведомленного обмана.
Роб

Ответы:

146

Плохой:

  • Информация о типе теряется во время компиляции, поэтому во время выполнения вы не можете сказать, какой тип "предназначен" для
  • Не может использоваться для типов значений (это важно - например, в .NET a List<byte>действительно поддерживается byte[], и упаковка не требуется)
  • Синтаксис для вызова общих методов отстой (IMO)
  • Синтаксис ограничений может запутать
  • Подстановочные знаки обычно сбивают с толку
  • Различные ограничения в связи с вышеизложенным - кастинг и т. Д.

Хорошо:

  • Подстановочные знаки позволяют указывать ковариацию / контравариантность на вызывающей стороне, что очень удобно во многих ситуациях.
  • Лучше, чем ничего!
Джон Скит
источник
51
@Paul: Думаю, я бы предпочел временную поломку, но потом достойный API. Или выберите путь .NET и представьте новые типы коллекций. (Дженерики .NET в 2.0 не нарушили работу приложений 1.1.)
Джон Скит,
6
С большой существующей базой кода мне нравится то, что я могу начать изменять сигнатуру одного метода, чтобы использовать дженерики, не меняя всех, кто его вызывает.
Пол Томблин,
38
Но у этого есть огромные и постоянные недостатки. Есть большая краткосрочная победа и долгосрочная потеря, ИМО.
Джон Скит,
11
Джон: «Это лучше, чем ничего» субъективно :)
MetroidFan2002
6
@Rogerio: Вы утверждали, что мой первый «Плохой» предмет неверен. Моя первая «Плохая» запись говорит о том, что информация теряется во время компиляции. Чтобы это было неверно, вы должны сказать, что информация не теряется во время компиляции ... Что касается того, что сбивает с толку других разработчиков, вы, кажется, единственный, кого смущает то, что я говорю.
Джон Скит,
26

Самая большая проблема в том, что универсальные шаблоны Java используются только во время компиляции, и вы можете разрушить их во время выполнения. C # хвалят, потому что он больше проверяет во время выполнения. В этом посте есть действительно хорошее обсуждение , и оно связано с другими обсуждениями.

Пол Томблин
источник
На самом деле это не проблема, это никогда не создавалось для среды исполнения. Это как если бы вы сказали, что лодки - проблема, потому что они не могут взбираться на горы. Как язык, Java делает то, что нужно многим: выражает типы осмысленным образом. Для информации о типах времени выполнения люди все еще могут передавать Classобъекты.
Винсент Кантин
1
Он прав. ваша анология неверна, потому что люди, проектирующие лодку, ДОЛЖНЫ спроектировать ее для подъема в горы. Их цели проектирования были не очень хорошо продуманы для того, что было необходимо. Теперь мы все застряли на лодке и не можем взбираться на горы.
WiegleyJ
1
@VincentCantin - это определенно проблема. Следовательно, мы все на это жалуемся. Дженерики Java недоработаны.
Джош М.
17

Основная проблема заключается в том, что Java фактически не имеет универсальных шаблонов во время выполнения. Это функция времени компиляции.

Когда вы создаете универсальный класс в Java, они используют метод под названием «Type Erasure», чтобы фактически удалить все общие типы из класса и по существу заменить их на Object. Версия универсальных шаблонов высотой в милю состоит в том, что компилятор просто вставляет приведение типов в указанный универсальный тип всякий раз, когда он появляется в теле метода.

У этого есть много минусов. ИМХО, одна из самых больших проблем заключается в том, что вы не можете использовать отражение для проверки универсального типа. Типы на самом деле не являются универсальными в байтовом коде и, следовательно, не могут рассматриваться как универсальные.

Отличный обзор различий здесь: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

JaredPar
источник
2
Я не уверен, почему вас проголосовали против. Насколько я понимаю, вы правы, и эта ссылка очень информативна. Up-голосование.
Джейсон Джексон
@ Джейсон, спасибо. В моей первоначальной версии была опечатка, я полагаю, что за это мне отказали.
JaredPar
3
Err, потому что вы можете использовать отражение для просмотра универсального типа, подписи универсального метода. Вы просто не можете использовать отражение для проверки общей информации, связанной с параметризованным экземпляром. Например, если у меня есть класс с методом foo (List <String>), я могу рефлексивно найти «String»
oxbow_lakes
Есть много статей, в которых говорится, что стирание типа делает невозможным нахождение фактических параметров типа. Допустим: объект x = новый X [Y] (); можете ли вы показать, как при заданном x найти тип Y?
user48956 06
@oxbow_lakes - необходимость использовать отражение для поиска универсального типа почти так же плохо, как и неспособность этого сделать. Например, это плохое решение.
Джош М.
14
  1. Реализация во время выполнения (т.е. не стирание типа);
  2. Возможность использовать примитивные типы (это связано с (1));
  3. Хотя использование подстановочных знаков полезно, синтаксис и знание того, когда его использовать, ставят в тупик многих людей. и
  4. Нет улучшения производительности (из-за (1); дженерики Java являются синтаксическим сахаром для приведения объектов).

(1) приводит к очень странному поведению. Лучший пример, который я могу придумать, - это. Предполагать:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

затем объявите две переменные:

MyClass<T> m1 = ...
MyClass m2 = ...

Теперь звоните getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

У второго аргумент универсального типа удален компилятором, потому что это необработанный тип (то есть параметризованный тип не предоставляется), хотя он не имеет ничего общего с параметризованным типом.

Я также упомяну мою любимую декларацию из JDK:

public class Enum<T extends Enum<T>>

Помимо подстановочных знаков (что является смешанным), я просто думаю, что дженерики .Net лучше.

Клетус
источник
Но компилятор скажет вам, особенно с опцией rawtypes lint.
Том Хотин - tackline
Извините, почему последняя строка - ошибка компиляции? Я возился с ним в Eclipse и не могу заставить его выйти из строя - если я добавлю достаточно материала для компиляции остальной части.
oconnor0 04
public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop
@ oconnor0 Это не сбой компиляции, а предупреждение компилятора без видимой причины:The expression of type List needs unchecked conversion to conform to List<String>
artbristol
Enum<T extends Enum<T>>Сначала может показаться странным / избыточным, но на самом деле это довольно интересно, по крайней мере, в рамках ограничений Java / it generics. У перечислений есть статический values()метод, дающий массив их элементов, типизированных как перечисление, а не Enum, и этот тип определяется универсальным параметром, что означает, что вы хотите Enum<T>. Конечно, такая типизация имеет смысл только в контексте перечислимого типа, и все перечисления являются подклассами Enum, как вы хотите Enum<T extends Enum>. Однако Java не любит смешивать необработанные типы с обобщенными, таким образом, Enum<T extends Enum<T>>для согласованности.
JAB
10

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

Map someMap;

Теперь я должен объявить это как

Map<String, List<String>> someMap;

И каждый раз, когда я передаю его в какой-либо метод, мне приходится повторять это большое длинное объявление снова и снова. На мой взгляд, лишний набор текста отвлекает разработчика и выводит его из «зоны». Кроме того, когда код заполнен большим количеством мусора, иногда бывает трудно вернуться к нему позже и быстро просмотреть все ненужное, чтобы найти важную логику.

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

И что вы действительно покупаете за такое многословие? Сколько раз у вас действительно были проблемы, когда кто-то помещал целое число в коллекцию, которая должна содержать строки, или когда кто-то пытался вытащить строку из коллекции целых чисел? За мой 10-летний опыт работы над созданием коммерческих Java-приложений это никогда не было большим источником ошибок. Итак, я не совсем уверен, что вы получаете за лишнее многословие. Это действительно просто кажется мне лишним бюрократическим багажом.

Теперь я собираюсь стать действительно спорным. Что я считаю самой большой проблемой коллекций в Java 1.4, так это необходимость везде приводить типы. Я рассматриваю эти приведенные типы как лишнюю, многословную ерунду, имеющую многие из тех же проблем, что и родовые типы. Так, например, я не могу просто сделать

List someList = someMap.get("some key");

я должен сделать

List someList = (List) someMap.get("some key");

Причина, конечно, в том, что get () возвращает объект, который является супертипом List. Таким образом, присвоение не может быть выполнено без приведения типа. Опять же, подумайте, сколько на самом деле вам выгодно это правило. По моему опыту, немного.

Я думаю, что Java была бы намного лучше, если бы 1) она не добавляла дженерики, а 2) вместо этого разрешала неявное приведение от супертипа к подтипу. Позвольте неправильным приведениям быть обнаруженными во время выполнения. Тогда я мог бы легко определить

Map someMap;

а позже делая

List someList = someMap.get("some key");

весь мусор исчезнет, ​​и я действительно не думаю, что внесу в свой код новый большой источник ошибок.

Клинт Миллер
источник
15
Извините, я не согласен практически со всем, что вы сказали. Но я не собираюсь голосовать против, потому что вы это хорошо аргументируете.
Пол Томблин,
2
Конечно, есть множество примеров больших успешных систем, построенных на таких языках, как Python и Ruby, которые делают именно то, что я предложил в своем ответе.
Клинт Миллер,
Я думаю, что все мое возражение против такого типа идеи состоит не в том, что это плохая идея как таковая, а в том, что, поскольку современные языки, такие как Python и Ruby, облегчают и облегчают жизнь разработчиков, разработчики, в свою очередь, становятся интеллектуально самоуверенными и в конечном итоге их понимание собственного кода.
Дэн Тао
4
«И что вы действительно покупаете за такое излишнее многословие? ...» Это говорит бедному программисту по обслуживанию, что должно быть в этих коллекциях. Я обнаружил, что это проблема при работе с коммерческими приложениями Java.
richj
4
«Сколько раз у вас действительно были проблемы, когда кто-то помещал [x] в коллекцию, которая должна содержать [y] s?» - О мальчик, я сбился со счета! Кроме того, даже если ошибки нет, это убийца удобочитаемости. И сканирование многих файлов, чтобы узнать, что будет за объект (или отладка), действительно вытаскивает меня из зоны. Вам может понравиться Haskell - даже строгая типизация, но менее грубая (потому что типы выводятся).
Мартин Каподичи
8

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


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++

jdkoftinoff
источник
6

Дженерики Java проверяются на правильность во время компиляции, а затем вся информация о типах удаляется (процесс называется стиранием типа . Таким образом, родовой типList<Integer> будет сокращен до своего необработанного типа , неуниверсального List, который может содержать объекты произвольного класса.

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

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");
Антон Гоголев
источник
5

Я бы хотел, чтобы это была вики, чтобы я мог добавлять что-то другим ... но ...

Проблемы:

  • Тип Erasure (время выполнения недоступно)
  • Нет поддержки примитивных типов
  • Несовместимость с аннотациями (они оба были добавлены в 1.5, я до сих пор не уверен, почему аннотации не позволяют использовать дженерики, кроме спешки с функциями)
  • Несовместимость с массивами. (Иногда мне действительно хочется сделать что-то вроде Class <? Extends MyObject> [], но мне это не разрешено)
  • Необычный синтаксис и поведение подстановочных знаков
  • Тот факт, что общая поддержка несовместима между классами Java. Они добавили его в большинство методов коллекций, но время от времени вы сталкиваетесь с экземпляром, где его нет.
Лапли Андерсон
источник
5

Игнорируя весь беспорядок стирания типов, указанные универсальные шаблоны просто не работают.

Это компилирует:

List<Integer> x = Collections.emptyList();

Но это синтаксическая ошибка:

foo(Collections.emptyList());

Где foo определяется как:

void foo(List<Integer> x) { /* method body not important */ }

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

натуральный
источник
3
Непоследовательный вывод javac - чушь.
М.П.
3
Я предполагаю, что причина того, что последняя форма отклонена, заключается в том, что может быть более одной версии "foo" из-за перегрузки метода.
Джейсон Крейтон
2
эта критика больше не применима, поскольку в Java 8 появился улучшенный вывод целевого типа
oldrinb
4

Внедрение универсальных шаблонов в Java было сложной задачей, поскольку архитекторы пытались сбалансировать функциональность, простоту использования и обратную совместимость с унаследованным кодом. Как и следовало ожидать, пришлось пойти на компромисс.

Некоторые также считают, что реализация универсальных шаблонов в Java повысила сложность языка до неприемлемого уровня (см. Статью Кена Арнольда « Обобщения, признанные вредными »). Часто задаваемые вопросы по обобщениям Анжелики Лангер дают довольно хорошее представление о том, насколько сложными могут быть вещи.

Зак Скривена
источник
3

Java не применяет Generics во время выполнения, только во время компиляции.

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

Powerlord
источник
1
Компилятор скажет вам, что вы облажались, если вам это удастся.
Tom Hawtin - tackline
@Tom - не обязательно. Есть способы обмануть компилятор.
Пол Томблин
2
Вы не слушаете, что вам говорит компилятор ?? (см. мой первый комментарий)
Том Хотин - tackline
1
@Tom, если у вас есть ArrayList <String> и вы передаете его методу старого стиля (скажем, устаревшему или стороннему коду), который принимает простой список, этот метод может затем добавить в этот список все, что угодно. Если это будет выполняться во время выполнения, вы получите исключение.
Пол Томблин,
1
static void addToList (список список) {list.add (1); } Список <String> list = новый ArrayList <String> (); addToList (список); Это компилируется и даже работает во время выполнения. Единственный раз, когда вы видите проблему, это когда вы удаляете из списка, ожидая строку, и получаете int.
Лапли Андерсон
3

Дженерики Java создаются только во время компиляции и компилируются в неуниверсальный код. В C # фактический скомпилированный MSIL является общим. Это имеет огромное значение для производительности, поскольку Java по-прежнему выполняет приведение типов во время выполнения. Подробнее см. Здесь .

Шимон Розга
источник
2

Если вы послушаете Java Posse # 279 - Интервью с Джо Дарси и Алексом Бакли , они говорят об этой проблеме. Здесь также есть ссылка на сообщение в блоге Нила Гафтера под названием Reified Generics for Java, в котором говорится:

Многие люди недовольны ограничениями, вызванными тем, как дженерики реализованы в Java. В частности, они недовольны тем, что параметры универсального типа не уточняются: они недоступны во время выполнения. Универсальные типы реализуются с использованием стирания, при котором параметры универсального типа просто удаляются во время выполнения.

Это сообщение в блоге ссылается на более старую запись Puzzling Through Erasure: answer section , в которой подчеркивается пункт о совместимости миграции в требованиях.

Целью было обеспечить обратную совместимость как исходного, так и объектного кода, а также совместимость миграции.

Кевин Хакансон
источник