Что не так с дженериками Java? [закрыто]

49

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

Майкл К
источник
2
Смотрите здесь: stackoverflow.com/questions/355060/c-vs-java-generics
Роберт Харви
1
См. Также stackoverflow.com/questions/31693/…
Билл Мичелл,
как сказал Клинтон Бегин («парень из iBatis»), «они не работают ...»
комат

Ответы:

54

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

ChaosPandion
источник
4
Просто добавьте, из-за стирания типов, получение общей информации во время выполнения невозможно, если вы не используете что-то вроде TypeLiteral google-guice.googlecode.com/svn/trunk/javadoc/com/google/inject/…
Джереми Хейлер,
43
Это означает, чтоnew ArrayList<String>.getClass() == new ArrayList<Integer>.getClass()
Примечание для себя - придумайте имя
9
@ Джоб нет, это не так. И C ++ использует совершенно другой подход к метапрограммированию, называемый шаблонами. Он использует типизацию утки, и вы можете делать полезные вещи, например, template<T> T add(T v1, T v2) { return v1->add(v2); }и там у вас есть действительно общий способ создания функции, которая состоит из addдвух вещей, независимо от того, что это за вещи, им просто нужно иметь метод с именем add()с одним параметром.
Тринидад
7
@ Job это действительно сгенерированный код. Шаблоны предназначены для замены препроцессора C, и для этого они должны иметь возможность делать некоторые очень умные трюки и сами по себе являются языком, полным по Тьюрингу. Обобщения Java являются лишь сахаром для контейнеров, безопасных с точки зрения типов, а обобщения C # лучше, но все же шаблон C ++ для бедного человека.
Тринидад
3
@Job AFAIK, Обобщения Java не генерируют класс для каждого нового универсального типа, они просто добавляют типы типов в код, использующий обобщенные методы, поэтому на самом деле это не мета-программирование IMO. Шаблоны C # генерируют новый код для каждого универсального типа, то есть, если вы используете List <int> и List <double> в мире C #, генерируется код для каждого из них. По сравнению с шаблонами, C # дженерики по-прежнему требуют знать, какой тип вы можете передать им. Вы не можете реализовать простой Addпример, который я привел, невозможно знать заранее, к каким классам он может быть применен, что является недостатком.
Тринидад
26

Обратите внимание, что ответы, которые уже были предоставлены, сконцентрированы на сочетании языка Java, JVM и библиотеки классов Java.

Нет ничего плохого в обобщениях Java в том, что касается языка Java. Как описано в C # против обобщений Java, обобщение Java в значительной степени хорошо работает на уровне языка 1 .

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

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

Я полагаю, что различие, которое я делаю, является педантичным, поскольку язык Java повсеместно скомпилирован с JVM в качестве цели и библиотекой классов Java в качестве основной библиотеки.

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

Роман Старков
источник
14
JVM не нуждается в поддержке дженериков. Это было бы то же самое, что сказать, что в C ++ нет шаблонов, потому что реальная машина ix86 не поддерживает шаблоны, что неверно. Проблема заключается в подходе компилятора к реализации универсальных типов. И снова, шаблоны C ++ не предусматривают никаких штрафов во время выполнения, никакого отражения вообще не делается, я думаю, единственная причина, которая должна быть сделана в Java, - это просто плохой языковой дизайн.
Тринидад
2
@ Тринидад, но я не говорил, что у Java нет дженериков, поэтому я не понимаю, как это так. И да, JVM не нуждается в их поддержке, так же как C ++ не нужно оптимизировать код. Но не оптимизировать это, безусловно, будет считаться «что-то не так».
Роман Старков
3
Я утверждал, что JVM не нужно напрямую поддерживать дженерики, чтобы иметь возможность использовать их отражение. Другие сделали это так, что это может быть сделано, проблема в том, что Java не генерирует новые классы из обобщенных элементов, то есть не требует никакой реализации.
Тринидад
4
@Trinidad: В C ++, при условии, что компилятору предоставлено достаточно времени и памяти, шаблоны могут использоваться для выполнения чего угодно и что угодно, что можно сделать с информацией, доступной во время компиляции (поскольку компиляция шаблонов завершена по Тьюрингу), но есть нет способа создавать шаблоны с использованием информации, которая недоступна до запуска программы. В отличие от этого, в .net программа может создавать типы на основе ввода; было бы довольно легко написать программу, в которой число различных типов, которые могут быть созданы, превышает число электронов во вселенной.
суперкат
2
@Trinidad: Очевидно, что ни одно выполнение программы не могло бы создать все эти типы (или даже что-то большее, чем их бесконечно малая часть), но дело в том, что это не обязательно. Нужно только создавать типы, которые на самом деле используются. Можно иметь программу, которая может принимать любое произвольное число (например 8675309) и из нее создать тип, уникальный для этого числа (например Z8<Z6<Z7<Z5<Z3<Z0<Z9>>>>>>>), который будет иметь элементы, отличные от любого другого типа. В C ++ все типы, которые могут быть сгенерированы из любого ввода, должны создаваться во время компиляции.
суперкат
13

Обычная критика - отсутствие овеществления. То есть объекты во время выполнения не содержат информации об их общих аргументах (хотя информация все еще присутствует в полях, методах, конструкторах и расширенном классе и интерфейсах). Это означает, что вы могли бы сыграть, скажем, ArrayList<String>в List<File>. Компилятор выдаст вам предупреждения, но также предупредит вас, если вы возьмете ArrayList<String>его в качестве Objectссылки и затем приведете к нему List<String>. Плюсы в том, что с дженериками вы, вероятно, не должны выполнять кастинг, производительность лучше без лишних данных и, конечно, обратной совместимости.

Некоторые люди жалуются, что вы не можете перегружать на основе общих аргументов ( void fn(Set<String>)и void fn(Set<File>)). Вместо этого вам нужно использовать лучшие имена методов. Обратите внимание, что эта перегрузка не требует повторной реализации, поскольку перегрузка является статической проблемой во время компиляции.

Примитивные типы не работают с генериками.

Подстановочные знаки и границы довольно сложны. Они очень полезны. Если бы Java предпочитала интерфейсы неизменяемости и «говори-не-спрашивай», то более подходящими были бы обобщения на стороне объявления, а не на стороне использования.

Том Хотин - Tackline
источник
«Обратите внимание, что эта перегрузка не потребует реификации, поскольку перегрузка является статической проблемой во время компиляции». Не так ли? Как бы это работало без овеществления? Разве JVM не должна знать во время выполнения, какой тип набора у вас есть, чтобы знать, какой метод вызывать?
MatrixFrog
2
@ MatrixFrog Нет. Как я уже сказал, перегрузка - это проблема времени компиляции. Компилятор использует статический тип выражения для выбора определенной перегрузки, которая затем является методом, записанным в файл класса. Если у вас есть, Object cs = new char[] { 'H', 'i' }; System.out.println(cs);вы получите чепуху. Измените тип csна, char[]и вы получите Hi.
Том Хотин - tackline
10

Обобщения Java отстой, потому что вы не можете сделать следующее:

public class AsyncAdapter<Parser,Adapter> extends AsyncTask<String,Integer,Adapter> {
    proptected Adapter doInBackground(String... keywords) {
      Parser p = new Parser(keywords[0]); // this is an error
      /* some more stuff I was hoping for but couldn't do because
         the compiler wouldn't let me
      */
    }
}

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

davidk01
источник
Вам не нужно разделывать каждый класс. Вам просто нужно добавить подходящий фабричный класс и внедрить его в AsyncAdapter. (И по сути дело не в дженериках, а в том, что конструкторы не наследуются в Java).
Питер Тейлор
13
@PeterTaylor: Подходящий заводской класс просто заталкивает весь шаблон в какой-то другой невидимый беспорядок, и это все еще не решает проблему. Теперь каждый раз, когда я хочу создать новый экземпляр, Parserмне нужно сделать заводской код еще более запутанным. Принимая во внимание, что если бы «универсальный» действительно означал «универсальный», тогда не было бы необходимости в фабриках и прочей чепухе, которая называется «шаблонами проектирования». Это одна из многих причин, по которым я предпочитаю работать на динамических языках.
davidk01
2
Иногда аналогично в C ++, где вы не можете передать адрес конструктора, когда используете шаблоны + виртуалы - вы можете просто использовать фабричный функтор.
Марк К Коуэн
Java отстой, потому что вы не можете написать «защищен» перед именем метода? Бедный ты. Придерживайтесь динамических языков. И будь особенно осторожен с Хаскеллом.
fdreger
5
  • предупреждения компилятора об отсутствующих универсальных параметрах, которые не имеют смысла, делают язык бессмысленным, например: public String getName(Class<?> klazz){ return klazz.getName();}

  • Дженерики плохо работают с массивами

  • Потерянная информация о типе делает отражение беспорядком отливки и клейкой ленты.

Стив Б.
источник
Меня раздражает, когда я получаю предупреждение за использование HashMapвместо HashMap<String, String>.
Майкл К
1
@ Майкл, но это на самом деле следует использовать с дженериками ...
альтернатива
Проблема с массивом переходит в переоснащение. См. Книгу Java Generics (Аллигатор) для объяснения.
ncmathsadist
5

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

List<String> arr = new ArrayList<String>();
assertTrue( ArrayList.class, arr.getClass() );
TypeVarible[] types = arr.getClass().getTypedVariables();

К сожалению, возвращаемые типы не могут сказать вам, что универсальные типы arr - String. Это тонкая разница, но это важно. Поскольку arr создается во время выполнения, универсальные типы стираются во время выполнения, поэтому вы не можете понять это. Как некоторые утверждают, ArrayList<Integer>выглядит так же, как ArrayList<String>с точки зрения отражения.

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

Factory<MySpecialObject> factory = new Factory<MySpecialObject>();
MySpecialObject obj = factory.create();

Допустим, мы хотели, чтобы универсальная фабрика создала экземпляр, MySpecialObjectпотому что это конкретный универсальный тип, который мы объявили для этого экземпляра. Класс Factory не может опросить себя, чтобы узнать конкретный тип, объявленный для этого экземпляра, потому что Java стерла их.

В обобщениях .Net вы можете сделать это, потому что во время выполнения объект знает, что это обобщенные типы, потому что компилятор скомпилировал его в двоичный файл. С стиранием Java не может этого сделать.

chubbsondubs
источник
1

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

Большое психологическое раздражение: я иногда попадаю в ситуации, когда генерики не могут работать. (Массивы - самый простой пример.) И я не могу понять, не могут ли дженерики справиться с работой или я просто тупой. Я ненавижу это. Что-то вроде дженериков должно работать всегда. Каждый раз, когда я не могу делать то, что хочу, используя Java-язык, я знаю, что проблема во мне, и я знаю, что если я продолжу настаивать, я доберусь до конца. С дженериками, если я стану слишком настойчивым, я могу тратить много времени.

Но настоящая проблема заключается в том, что генерики добавляют слишком много сложности для слишком маленькой выгоды. В самых простых случаях это может помешать мне добавить яблоко в список автомобилей. Хорошо. Но без обобщений эта ошибка вызвала бы исключение ClassCastException очень быстро во время выполнения с небольшим потерянным временем. Если я добавлю автомобиль с детским креслом, в котором есть ребенок, нужно ли мне предупреждение во время компиляции, что этот список предназначен только для автомобилей с детскими сиденьями, в которых есть шимпанзе? Список простых экземпляров Object начинает выглядеть как хорошая идея.

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

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

RalphChapin
источник
Хорошая напыщенная речь Однако есть много библиотек / фреймворков, которые могут существовать без универсальных шаблонов, например, Guice, Gson, Hibernate. И дженерики не так сложны, как только вы к этому привыкнете. Печатание и чтение аргументов типа - это настоящая PITA; здесь val помогает, если вы можете использовать.
Maaartinus
1
@maaartinus: Написал это некоторое время назад. Также ОП спрашивал о «проблемах». Я нахожу дженерики чрезвычайно полезными и очень их люблю - гораздо больше сейчас, чем тогда. Однако, если вы пишете свою собственную коллекцию, их очень сложно выучить. И их полезность исчезает, когда тип вашей коллекции определяется во время выполнения - когда у коллекции есть метод, сообщающий вам класс ее записей. На данный момент генерики не работают, и ваша IDE выдаст сотни бессмысленных предупреждений. 99,99% Java-программирования не связаны с этим. Но у меня просто было много проблем с этим, когда я писал выше.
RalphChapin
Будучи разработчиком как на Java, так и на C #, мне интересно, почему вы предпочитаете коллекции Java?
Ивайло Славов
Fine. But without generics this error would throw a ClassCastException really quick at run time with little time wasted.Это действительно зависит от того, сколько времени выполнения происходит между запуском программы и попаданием в строку кода. Если пользователь сообщает об ошибке и для ее воспроизведения требуется несколько минут (или, в худшем случае, часы или даже дни), проверка во время компиляции начинает выглядеть все лучше и лучше ...
Мейсон Уилер,