Почему меня должно волновать, что в Java нет обобщенных дженериков?

102

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

Очевидно, что из-за того, что в Java допустимы необработанные типы (и небезопасные проверки), можно разрушить обобщенные типы и получить в результате a, List<Integer>который (например) фактически содержит Strings. Очевидно, что это можно было бы сделать невозможным, если бы информация о типе была переработана; но должно быть нечто большее !

Могут ли люди публиковать примеры того, что они действительно хотели бы делать , если бы были доступны обобщенные дженерики? Я имею в виду, очевидно, что вы могли получить тип a Listво время выполнения - но что бы вы с ним сделали?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

РЕДАКТИРОВАТЬ : быстрое обновление этого, поскольку ответы, похоже, в основном связаны с необходимостью передать a Classв качестве параметра (например EnumSet.noneOf(TimeUnit.class)). Я искал что-то вроде того, где это просто невозможно . Например:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?
Oxbow_lakes
источник
Означает ли это, что вы можете получить класс универсального типа во время выполнения? (если так, у меня есть пример!)
Джеймс Б.
Я думаю, что большинство людей, которые используют обобщенные шаблоны в основном с коллекциями, хотят, чтобы эти коллекции вели себя как массивы.
kdgregory
2
Более интересный вопрос (для меня): что нужно для реализации дженериков стиля C ++ в Java? Это, безусловно, кажется выполнимым во время выполнения, но нарушит все существующие загрузчики классов (потому findClass()что пришлось бы игнорировать параметризацию, но defineClass()не смог). И, как мы знаем, для The Powers That Be обратная совместимость имеет первостепенное значение.
kdgregory
1
На самом деле Java действительно предоставляет обобщенные дженерики очень ограниченным образом . Я предоставляю более подробную информацию в этой теме SO: stackoverflow.com/questions/879855/…
Ричард Гомес
В JavaOne Keynote указано, что Java 9 будет поддерживать реификацию.
Rangi Keen

Ответы:

81

Из тех нескольких раз, когда я сталкивался с этой «необходимостью», в конечном итоге она сводится к следующей конструкции:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Это работает в C # при условии, что у Tнего есть конструктор по умолчанию . Вы даже можете получить тип среды выполнения typeof(T)и получить конструкторы Type.GetConstructor().

Распространенным решением Java было бы передать Class<T>аргумент as.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(его необязательно передавать в качестве аргумента конструктора, поскольку аргумент метода также подходит, приведенное выше является лишь примером, также try-catchопущено для краткости)

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

BalusC
источник
5
Это работает (например) в C #? Как узнать, что у T есть конструктор по умолчанию?
Thomas Jung
4
Да, это так. И это только базовый пример (допустим, мы работаем с javabeans). Все дело в том, что с Java вы не можете получить класс во время выполнения с помощью T.classили T.getClass(), чтобы вы могли получить доступ ко всем его полям, конструкторам и методам. Это также делает невозможным строительство.
BalusC
3
Мне это кажется очень слабым как «большая проблема», особенно потому, что это может быть полезно только в сочетании с очень хрупким отражением вокруг конструкторов / параметров и т. Д.
oxbow_lakes
33
Он компилируется на C # при условии, что вы объявляете тип как: public class Foo <T>, где T: new (). Это ограничит допустимые типы T теми, которые содержат конструктор без параметров.
Мартин Харрис,
3
@Colin Фактически вы можете сообщить java, что данный универсальный тип требуется для расширения более чем одного класса или интерфейса. См. Раздел «Множественные границы» на docs.oracle.com/javase/tutorial/java/generics/bounded.html . (Конечно, вы не можете сказать, что объект будет одним из определенного набора, не имеющего общих методов, которые могут быть абстрагированы в интерфейс, который может быть в некоторой степени реализован на C ++ с использованием специализации шаблонов.)
JAB
99

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

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }
RHSeeger
источник
5
Для этого совершенно не требуется овеществление. Выбор метода выполняется во время компиляции, когда доступна информация о типе времени компиляции.
Tom Hawtin - tackline
26
@Tom: это даже не компилируется из-за стирания типа. Оба компилируются как public void my_method(List input) {}. Однако я никогда не сталкивался с этой потребностью просто потому, что у них не было того же имени. Если у них то же имя, я бы сомневался, public <T extends Object> void my_method(List<T> input) {}не лучшая ли идея.
BalusC
1
Хм, я бы вообще старался избегать перегрузки с одинаковым количеством параметров и предпочел бы что-то подобное, myStringsMethod(List<String> input)и myIntegersMethod(List<Integer> input)даже если бы перегрузка для такого случая была возможна в Java.
Fabian Steeg
4
@Fabian: Это означает, что у вас должен быть отдельный код и предотвращать те преимущества, которые вы получаете от <algorithm>C ++.
Дэвид Торнли
Я сбит с толку, это практически то же самое, что и мой второй пункт, но я получил 2 отрицательных голоса? Кто-нибудь хочет меня просветить?
rsp
34

На ум приходит безопасность типов . Преобразование в параметризованный тип всегда будет небезопасным без обобщенных дженериков:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

Кроме того, абстракции будут меньше просачиваться - по крайней мере, те, которые могут быть заинтересованы в информации времени выполнения об их параметрах типа. Сегодня, если вам нужна какая-либо информация времени выполнения о типе одного из общих параметров, вы также должны передать его Class. Таким образом, ваш внешний интерфейс зависит от вашей реализации (независимо от того, используете ли вы RTTI для своих параметров или нет).

Gustafc
источник
1
Да, у меня есть способ обойти это, я создаю объект, ParametrizedListкоторый копирует данные в типах проверки исходной коллекции. Это немного похоже, Collections.checkedListно для начала можно засеять коллекцией.
oxbow_lakes
@tackline - ну несколько абстракций просочились бы меньше. Если вам нужен доступ к метаданным типа в вашей реализации, внешний интерфейс сообщит вам, потому что клиенты должны отправить вам объект класса.
gustafc
... это означает, что с обобщенными универсальными шаблонами вы можете добавлять такие вещи, как T.class.getAnnotation(MyAnnotation.class)(где T- универсальный тип), не изменяя внешний интерфейс.
gustafc
@gustafc: если вы думаете, что шаблоны C ++ обеспечивают полную безопасность типов, прочтите это: kdgregory.com/index.php?page=java.generics.cpp
kdgregory
@kdgregory: Я никогда не говорил, что C ++ на 100% безопасен по типу - просто стирание вредит безопасности типов. Как вы сами говорите: «Оказывается, в C ++ есть своя собственная форма стирания типов, известная как приведение указателя в стиле C». Но Java выполняет только динамическое приведение (не переинтерпретирует), поэтому реификация включит все это в систему типов.
gustafc
26

Вы сможете создавать в своем коде универсальные массивы.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}
Turnor
источник
что не так с Object? В любом случае массив объектов - это массив ссылок. Это не похоже на то, что данные объекта сидят в стеке - это все в куче.
Ран Бирон
19
Тип безопасности. Я могу поместить в Object [] все, что захочу, но только строки в String [].
Turnor
3
Ран: Без сарказма: вы можете использовать язык сценариев вместо Java, тогда у вас будет гибкость нетипизированных переменных где угодно!
летающая овца
2
Массивы ковариантны (и, следовательно, не являются типизированными) на обоих языках. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Прекрасно компилируется на обоих языках. Работает notsofine.
Martijn
16

Это старый вопрос, на него есть масса ответов, но я думаю, что существующие ответы не соответствуют действительности.

«овеществленный» просто означает настоящий и обычно означает противоположность стирания шрифта.

Большая проблема, связанная с Java Generics:

  • Это ужасное требование бокса и разрыв между примитивами и ссылочными типами. Это напрямую не связано с овеществлением или стиранием шрифта. C # / Scala исправит это.
  • Никаких самотипов. По этой причине в JavaFX 8 пришлось удалить «строителей». Абсолютно никакого отношения к стиранию шрифта. Scala исправляет это, но не уверен в C #.
  • Нет отклонения типа стороны объявления. В C # 4.0 / Scala это есть. Абсолютно никакого отношения к стиранию шрифта.
  • Не может перегрузить void method(List<A> l)и method(List<B> l). Это происходит из-за стирания шрифта, но это очень мелочь.
  • Нет поддержки для отражения типа во время выполнения. Это суть стирания шрифтов. Если вам нравятся суперсовременные компиляторы, которые проверяют и подтверждают как можно больше логики вашей программы во время компиляции, вам следует как можно меньше использовать отражение, и этот тип стирания типа не должен вас беспокоить. Если вам нравится более неоднородное, скриптовое, динамическое программирование типов и вы не очень заботитесь о том, чтобы компилятор проверял как можно большую часть вашей логики, тогда вам нужно лучшее отражение, и важно исправить стирание типов.
user2684301
источник
1
В основном мне сложно работать с сериализацией. Вы часто хотели бы иметь возможность унюхать типы классов для сериализуемых универсальных вещей, но вас останавливают из-за стирания типа. Сложно сделать что-то подобноеdeserialize(thingy, List<Integer>.class)
Cogman
3
Думаю, это лучший ответ. Особенно части, описывающие, какие проблемы действительно возникают из-за стирания типов, а какие просто проблемы проектирования языка Java. Первое, что я говорю людям, начинающим ныть по поводу стирания, это то, что Scala и Haskell также работают с стиранием типа.
aemxdp 05
13

С овеществлением сериализация была бы более простой. Мы бы хотели

deserialize(thingy, List<Integer>.class);

Что нам нужно сделать

deserialize(thing, new TypeReference<List<Integer>>(){});

выглядит некрасиво и прикольно работает.

Бывают также случаи, когда было бы действительно полезно сказать что-то вроде

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Эти штуки кусаются не часто, но кусаются, когда случаются.

Cogman
источник
Это. В тысячу раз больше. Каждый раз, когда я пытаюсь написать код десериализации на Java, я провожу день, сетуя на то, что я не работаю над чем-то другим
Basic
11

Мое знакомство с Java Geneircs довольно ограничено, и помимо пунктов, которые уже упоминались в других ответах, есть сценарий, объясненный в книге Java Generics and Collections Морисом Нафталином и Филипом Уолдером, где овеществленные дженерики полезны.

Поскольку типы не реифицируемы, невозможно иметь параметризованные исключения.

Например, декларация формы ниже недействительна.

class ParametericException<T> extends Exception // compile error

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

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

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

В книге также упоминается, что если общие шаблоны Java определены аналогично тому, как определены шаблоны C ++ (расширение), это может привести к более эффективной реализации, поскольку это дает больше возможностей для оптимизации. Но не предлагает никаких объяснений, кроме этого, поэтому любые объяснения (указатели) от знающих людей были бы полезны.

сатиш
источник
1
Это верный аргумент, но я не совсем уверен, почему такой параметризованный класс исключений может быть полезен. Не могли бы вы изменить свой ответ, чтобы он содержал краткий пример того, когда это может быть полезно?
oxbow_lakes
@oxbow_lakes: Извините, мои познания в Java Generics довольно ограничены, и я пытаюсь улучшить их. Итак, теперь я не могу придумать ни одного примера, где могло бы быть полезно параметризованное исключение. Постараюсь подумать об этом. Спасибо.
sateesh
Он может выступать в качестве замены множественного наследования типов исключений.
meriton
Повышение производительности заключается в том, что в настоящее время параметр типа должен наследовать от Object, что требует упаковки примитивных типов, что приводит к накладным расходам на выполнение и память.
meriton
8

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

Хэнк Гей
источник
2
Конечно, но у них все равно будут проблемы. List<String>не является List<Object>.
Том Хотин - tackline
Согласитесь - но только для примитивов (Integer, Long и т. Д.). Для «обычного» Объекта то же самое. Поскольку примитивы не могут быть параметризованным типом (гораздо более серьезная проблема, по крайней мере, IMHO), я не вижу в этом настоящей боли.
Ран Бирон
3
Проблема с массивами заключается в их ковариантности, а не в материализации.
Рекурсия
5

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

API выглядит как Iterator<T> T - это некоторый тип, который может быть построен с использованием только строк в конструкторе. Затем Iterator просматривает строки, возвращаемые из запроса sql, и затем пытается сопоставить их с конструктором типа T.

В текущем способе реализации дженериков я также должен передать класс объектов, которые я буду создавать из своего набора результатов. Если я правильно понимаю, если бы дженерики были реифицируются, я мог бы просто вызвать T.getClass (), чтобы получить его конструкторы, и тогда не пришлось бы приводить результат Class.newInstance (), что было бы намного аккуратнее.

В принципе, я думаю, что это упрощает написание API (в отличие от простого написания приложения), потому что вы можете сделать гораздо больше из объектов, и, следовательно, потребуется меньшая конфигурация ... Я не оценил значение аннотаций, пока я не видел, как они используются в таких вещах, как spring или xstream, а не в кучах config.

Джеймс Б.
источник
Но переходить в класс мне кажется более безопасным. В любом случае, рефлексивное создание экземпляров из запросов к базе данных крайне уязвимо для изменений, таких как рефакторинг в любом случае (как в вашем коде, так и в базе данных). Думаю, я искал вещи, в которых просто невозможно предоставить класс
oxbow_lakes
5

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

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

kvb
источник
2
По сути, это сводится к возможности делать new T[]там, где T имеет примитивный тип!
oxbow_lakes
2

Это не значит, что вы добьетесь чего-то экстраординарного. Просто будет проще понять. Стирание типов кажется трудным для новичков, и в конечном итоге требует понимания того, как работает компилятор.

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

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

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

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!
ExCodeCowboy
источник
0

Вот один, который меня поймал сегодня: без реификации, если вы напишете метод, который принимает список универсальных элементов varargs ... вызывающие могут ДУМАТЬ, что они безопасны по типу, но случайно передают любую старую грязь и взорвут ваш метод.

Кажется маловероятным, что это произойдет? ... Конечно, пока ... вы не используете Class в качестве типа данных. На этом этапе ваш вызывающий с радостью отправит вам множество объектов Class, но простая опечатка отправит вам объекты Class, которые не соответствуют T, и произойдет катастрофа.

(NB: я, возможно, ошибся здесь, но поискав в Google "generics varargs", вышеприведенное, похоже, именно то, что вы ожидаете. То, что делает это практической проблемой, - это использование Class, я думаю - вызывающие абоненты кажутся быть менее осторожным :()


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

например, это отлично работает в Java Generics (тривиальный пример):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

например, без повторения в Java Generics, этот принимает ЛЮБОЙ объект «Класс». И это лишь крошечное расширение предыдущего кода:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

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

Адам
источник