Чем хороши дженерики, зачем их использовать?


87

Я подумал, что предлагаю этот софтбол любому, кто захочет попасть в него из парка. Что такое дженерики, каковы преимущества дженериков, почему, где и как их использовать? Пожалуйста, оставьте это довольно простым. Спасибо.


1
Дубликат stackoverflow.com/questions/77632/… . Но простой ответ: даже если коллекции не являются частью вашего API, мне не нравится ненужное приведение даже во внутренней реализации.
Мэтью Флашен,

Вопрос очень похож, но я не думаю, что принятый ответ отвечает на него.
Tom Hawtin - tackline


4
@MatthewFlaschen странно, ваш дубликат наводит на этот вопрос ... глюк в матрице!
jcollum

@jcollum, я думаю, что исходный вопрос, который я опубликовал в этом комментарии, был объединен с этим вопросом.
Мэтью Флашен,

Ответы:


126
  • Позволяет вам писать код / ​​использовать библиотечные методы, которые являются типобезопасными, т.е. List <string> гарантированно будет списком строк.
  • В результате использования дженериков компилятор может выполнять проверки кода во время компиляции на предмет безопасности типов, т.е. пытаетесь ли вы поместить int в этот список строк? Использование ArrayList приведет к менее прозрачной ошибке времени выполнения.
  • Быстрее, чем использование объектов, так как он либо избегает упаковки / распаковки (где .net должен преобразовывать типы значений в ссылочные типы, или наоборот ), либо преобразования объектов в требуемый ссылочный тип.
  • Позволяет писать код, который применим ко многим типам с одинаковым базовым поведением, т.е. Dictionary <string, int> использует тот же базовый код, что и Dictionary <DateTime, double>; Используя дженерики, команде фреймворка нужно было написать только один фрагмент кода, чтобы добиться обоих результатов с вышеупомянутыми преимуществами.

9
Ах, очень мило, и мне нравятся списки. Я думаю, что это наиболее полный, но краткий ответ на данный момент.
MrBoJangles

все объяснение находится в контексте «коллекций». Я ищу более широкий взгляд. есть идеи?
jungle_mole

хороший ответ, я просто хочу добавить 2 других преимущества, общее ограничение имеет 2 преимущества помимо 1 - вы можете использовать свойства ограниченного типа в универсальном типе (например, где T: IComparable обеспечивает использование CompareTo) 2 - человек, который напишет код после того, как вы узнаете, что делать
Хамит ЙИЛДИРИМ

52

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

Вместо создания:

class MyObjectList  {
   MyObject get(int index) {...}
}
class MyOtherObjectList  {
   MyOtherObject get(int index) {...}
}
class AnotherObjectList  {
   AnotherObject get(int index) {...}
}

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

class MyList<T> {
   T get(int index) { ... }
}

Теперь я в 3 раза эффективнее, и мне нужно поддерживать только одну копию. Почему вы НЕ ХОТИТЕ поддерживать меньше кода?

Это также верно для классов, не являющихся коллекциями, таких как a Callable<T>или a, Reference<T>которые должны взаимодействовать с другими классами. Вы действительно хотите расширить Callable<T>и Future<T>каждый другой связанный класс для создания типобезопасных версий?

Я не.


1
Я не уверен, что понимаю. Я не предлагаю вам делать оболочки MyObject, это было бы ужасно. Я предлагаю вам написать объект Garage, если ваша коллекция объектов представляет собой коллекцию автомобилей. У него не было бы get (car), у него были бы такие методы, как buyFuel (100), которые покупали бы топливо на 100 долларов и распределяли его среди автомобилей, которые в нем больше всего нуждаются. Я говорю о настоящих бизнес-классах, а не просто о «фантиках». Например, вы почти никогда не должны получать (автомобиль) или перебирать их за пределами коллекции - вы не запрашиваете у объекта его членов, вместо этого вы просите его выполнить операцию за вас.
Bill K

Даже в вашем гараже у вас должна быть безопасность типов. Так что либо у вас есть List <Cars>, ListOfCars, либо вы забрасываете везде. Я не чувствую необходимости указывать тип чаще, чем необходимо.
Джеймс Шек

Я согласен, что безопасность типов была бы хороша, но она гораздо менее полезна, поскольку ограничивается гаражным классом. Тогда он инкапсулирован и легко контролируется, поэтому я хочу сказать, что он дает вам это небольшое преимущество за счет нового синтаксиса. Это все равно что изменить конституцию США, сделав проезд на красный свет незаконным - да, это всегда должно быть незаконным, но оправдывает ли это поправку? Я также думаю, что это побуждает людей НЕ использовать инкапсуляцию в этом случае, что на самом деле сравнительно вредно.
Bill K

Билл - у вас может быть точка зрения об отказе от инкапсуляции, но остальная часть ваших аргументов - плохое сравнение. Стоимость изучения нового синтаксиса в этом случае минимальна (сравните это с лямбдами в C #); сравнивать это с поправкой к Конституции бессмысленно. Конституция не имеет намерения определять код трафика. Это больше похоже на переход на печатную версию шрифта с засечками на плакатной доске, а не на рукописную копию на пергаменте. Это то же значение, но оно намного яснее и легче для чтения.
Джеймс Шек

Пример делает концепцию более практичной. Спасибо за это.
RayLoveless

22

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

Но я подозреваю, что вы это прекрасно знаете.

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

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

Современные IDE упрощают написание кода с использованием универсальных шаблонов.

Большинство современных достойных IDE достаточно умны, чтобы помочь с написанием кода с помощью обобщений, особенно с автозавершением кода.

Вот пример создания файла Map<String, Integer>с расширением HashMap. Мне нужно было ввести следующий код:

Map<String, Integer> m = new HashMap<String, Integer>();

И действительно, это много для того, чтобы просто создать новый HashMap. Однако на самом деле мне нужно было набрать столько, прежде чем Eclipse узнал, что мне нужно:

Map<String, Integer> m = new Ha Ctrl+Space

Да, мне действительно нужно было выбрать HashMapиз списка кандидатов, но в основном среда IDE знала, что добавить, включая общие типы. С правильными инструментами использование дженериков не так уж и плохо.

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

Ключевое преимущество дженериков заключается в том, как они хорошо сочетаются с новыми функциями Java 5. Вот пример подбрасывания целых чисел в a Setи вычисления его суммы:

Set<Integer> set = new HashSet<Integer>();
set.add(10);
set.add(42);

int total = 0;
for (int i : set) {
  total += i;
}

В этом фрагменте кода присутствуют три новые функции Java 5:

Во-первых, дженерики и автобокс примитивов позволяют использовать следующие строки:

set.add(10);
set.add(42);

Целое число 10автоматически упаковывается в объект Integerсо значением 10. (И то же самое для 42). Затем это Integerбросается в то, Setчто, как известно, содержит Integers. Попытка вставить Stringвызовет ошибку компиляции.

Далее, for-each цикл принимает все три из них:

for (int i : set) {
  total += i;
}

Во-первых, Setсодержащие Integers используются в цикле for-each. Каждый элемент объявлен как объект, intи это разрешено, поскольку Integerон распаковывается обратно в примитив int. И тот факт, что происходит распаковка, известен, потому что для указания того, что они были Integerсохранены в Set.

Обобщения могут быть связующим звеном, которое объединяет новые функции, представленные в Java 5, и просто делает кодирование более простым и безопасным. И в большинстве случаев IDE достаточно умны, чтобы помочь вам с хорошими предложениями, так что, как правило, вам не нужно больше печатать.

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

Изменить - пример без дженериков

Ниже приводится иллюстрация приведенного выше Setпримера без использования дженериков. Возможно, но не совсем приятно:

Set set = new HashSet();
set.add(10);
set.add(42);

int total = 0;
for (Object o : set) {
  total += (Integer)o;
}

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

При использовании неуниверсальных коллекций типы, которые вводятся в коллекцию, являются объектами типа Object. Следовательно, в этом примере a Object- это то, что addдобавляется в набор.

set.add(10);
set.add(42);

В приведенных выше строках задействован автобоксинг - примитивное intзначение 10и 42автоматически упаковываются в Integerобъекты, которые добавляются в Set. Однако имейте в виду, что Integerобъекты обрабатываются как Objects, так как нет информации о типе, которая помогала бы компилятору узнать, какой тип Setследует ожидать.

for (Object o : set) {

Это самая важная часть. Причина, по которой цикл for-each работает, заключается в том, что он Setреализует Iterableинтерфейс, который возвращает информацию о Iteratorтипе, если таковая имеется. ( Iterator<T>, то есть.)

Однако, поскольку информации о типе нет, Setбудет возвращен, Iteratorкоторый вернет значения в Setas Objects, и именно поэтому элемент, извлекаемый в цикле for-each, должен иметь тип Object.

Теперь, когда объект получен Objectиз Set, его необходимо Integerвручную преобразовать в объект, чтобы выполнить добавление:

  total += (Integer)o;

Здесь выполняется приведение типа от an Objectк Integer. В этом случае мы знаем, что это всегда будет работать, но ручное приведение типов всегда заставляет меня думать, что это хрупкий код, который может быть поврежден, если незначительное изменение будет сделано в другом месте. (Я чувствую, что каждое приведение типов - это ClassCastExceptionожидание, но я отвлекаюсь ...)

IntegerТеперь Unboxed в intи разрешено выполнять добавление в intпеременной total.

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


Интеграция с расширенным циклом for действительно хороша (хотя я думаю, что этот чертов цикл сломан, потому что я не могу использовать тот же синтаксис с необщей коллекцией - он должен просто использовать cast / autobox для int, у него есть вся необходимая информация!), но вы правы, помощь в IDE, наверное, хороша. Я до сих пор не знаю, достаточно ли этого, чтобы оправдать дополнительный синтаксис, но, по крайней мере, я могу извлечь выгоду (что отвечает на мой вопрос).
Bill K

@Bill K: Я добавил пример использования функций Java 5 без использования дженериков.
coobird 05

Это хороший момент, я не использовал это (я упоминал, что застрял на 1.4 и более ранних версиях уже много лет). Я согласен с тем, что есть преимущества, но выводы, к которым я пришел, таковы: A) они имеют тенденцию быть довольно выгодными для людей, которые неправильно используют объектно-ориентированный объект, и менее полезны для тех, кто использует, и B) преимущества, которые вы ' Мы перечислили преимущества, но их вряд ли достаточно, чтобы оправдать даже небольшое изменение синтаксиса, не говоря уже о крупных изменениях, необходимых для того, чтобы действительно разобраться в дженериках, реализованных в Java. Но, по крайней мере, преимущества есть.
Bill K

15

Если бы вы провели поиск в базе данных ошибок Java незадолго до выпуска версии 1.5, вы бы обнаружили в семь раз больше ошибок с NullPointerExceptionчем ClassCastException. Поэтому не кажется, что это отличная функция - находить ошибки или, по крайней мере, ошибки, которые сохраняются после небольшого дымового тестирования.

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

Хранение коллекций объекта в себе - неплохой стиль (но тогда общий стиль - эффективно игнорировать инкапсуляцию). Это скорее зависит от того, что вы делаете. Передачу коллекций в «алгоритмы» немного легче проверить (во время компиляции или до нее) с помощью универсальных шаблонов.


6
Он не только задокументирован (что само по себе является огромной победой), но и обеспечивается компилятором.
Джеймс Шек,

Хороший момент, но разве это не достигается путем включения коллекции в класс, который определяет «коллекцию этого типа объекта»?
Bill K

1
Джеймс: Да, дело в том, что это задокументировано в коде .
Tom Hawtin - tackline

Я не думаю, что знаю хорошие библиотеки, которые используют коллекции для «алгоритмов» (возможно, подразумевая, что вы говорите о «функциях», что заставляет меня думать, что они должны быть методом в объекте коллекции). Я думаю, что JDK почти всегда извлекает хороший, чистый массив, который является копией данных, так что с ними нельзя связываться - по крайней мере, чаще всего.
Bill K

Кроме того, нет объекта, точно описывающего, как используется коллекция, и ограничивающего его использовать НАМНОГО лучшую форму документации в коде? Я считаю, что информация, содержащаяся в дженериках, чрезвычайно избыточна в хорошем коде.
Bill K

11

Обобщения в Java упрощают параметрический полиморфизм . С помощью параметров типа вы можете передавать аргументы типам. Подобно тому, как такой метод String foo(String s)моделирует какое-то поведение не только для конкретной строки, но и для любой строки s, такой тип, как List<T>моделирование некоторого поведения, не только для определенного типа, но и для любого типа . List<T>говорит, что для любого типа Tсуществует тип List, элементами которого являются Ts . Так Listчто на самом деле это конструктор типа . Он принимает тип в качестве аргумента и в результате создает другой тип.

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

public interface F<A, B> {
  public B f(A a);
}

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

F<Integer, String> intToString = new F<Integer, String>() {
  public String f(int i) {
    return String.valueOf(i);
  }
}

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

public final class Hash<A> {
  private final F<A, Integer> hashFunction;

  public Hash(final F<A, Integer> f) {
    this.hashFunction = f;
  }

  public int hash(A a) {
    return hashFunction.f(a);
  }
}

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

Однако дженерики Java не идеальны. Вы можете абстрагироваться от типов, но, например, вы не можете абстрагироваться от конструкторов типов. То есть вы можете сказать «для любого типа T», но вы не можете сказать «для любого типа T, который принимает параметр типа A».

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

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

Wereas до дженериков вы можете иметь классы , как Widgetпродлен FooWidget, BarWidgetи BazWidget, с обобщениями вы можете иметь один общий класс , Widget<A>который принимает Foo, Barили Bazв его конструктор , чтобы дать вам Widget<Foo>, Widget<Bar>и Widget<Baz>.


Если бы у Java не было интерфейсов, было бы хорошо сказать о подклассе. Разве не было бы правильно, если бы FooWidget, BarWidtet и BazWidget реализуют Widget? Хотя я могу ошибаться в этом ... Но это действительно хороший момент, если я ошибаюсь.
Bill K

Зачем вам нужен динамический универсальный класс для вычисления хеш-значений? Разве статическая функция с соответствующими параметрами не могла бы работать более эффективно?
Ant_222

8

Дженерики избегают снижения производительности, связанного с упаковкой и распаковкой. По сути, посмотрите на ArrayList vs List <T>. Оба делают одни и те же основные вещи, но List <T> будет намного быстрее, потому что вам не нужно вставлять в / из объекта.


В этом суть, не так ли? Ницца.
MrBoJangles

Ну не совсем; они полезны для безопасности типов + избегая приведения типов для ссылок, вообще без упаковки / распаковки.
ljs

Поэтому я предполагаю, что следующий вопрос: «Зачем мне вообще использовать ArrayList?» И, рискуя ответить, я бы сказал, что списки ArrayLists хороши, если вы не ожидаете, что их значения будут часто упаковывать и распаковывать. Комментарии?
MrBoJangles

Нет, они не работают с .net 2; полезно только для обратной совместимости. ArrayLists медленнее, небезопасен по типу и требует либо упаковки / распаковки, либо преобразования.
ljs

Если вы не сохраняете типы значений (например, целые числа или структуры), при использовании ArrayList упаковки нет - классы уже являются «объектами». Использование List <T> по-прежнему намного лучше из-за типа безопасности, и если вы действительно хотите хранить объекты, то лучше использовать List <object>.
Wilka

6

Лучшее преимущество Generics - повторное использование кода. Допустим, у вас много бизнес-объектов, и вы собираетесь написать ОЧЕНЬ похожий код для каждой сущности, чтобы выполнять одни и те же действия. (IE Linq to SQL-операции).

С помощью дженериков вы можете создать класс, который сможет работать с любым из типов, которые наследуются от данного базового класса, или реализовать данный интерфейс следующим образом:

public interface IEntity
{

}

public class Employee : IEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int EmployeeID { get; set; }
}

public class Company : IEntity
{
    public string Name { get; set; }
    public string TaxID { get; set }
}

public class DataService<ENTITY, DATACONTEXT>
    where ENTITY : class, IEntity, new()
    where DATACONTEXT : DataContext, new()
{

    public void Create(List<ENTITY> entities)
    {
        using (DATACONTEXT db = new DATACONTEXT())
        {
            Table<ENTITY> table = db.GetTable<ENTITY>();

            foreach (ENTITY entity in entities)
                table.InsertOnSubmit (entity);

            db.SubmitChanges();
        }
    }
}

public class MyTest
{
    public void DoSomething()
    {
        var dataService = new DataService<Employee, MyDataContext>();
        dataService.Create(new Employee { FirstName = "Bob", LastName = "Smith", EmployeeID = 5 });
        var otherDataService = new DataService<Company, MyDataContext>();
            otherDataService.Create(new Company { Name = "ACME", TaxID = "123-111-2233" });

    }
}

Обратите внимание на повторное использование одного и того же сервиса с учетом разных типов в методе DoSomething выше. Действительно элегантно!

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


5

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

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

Dictionary<int, string> dictionary = new Dictionary<int, string>();

А компилятор / IDE делает остальную тяжелую работу. В частности, словарь позволяет использовать первый тип в качестве ключа (без повторяющихся значений).


5
  • Типизированные коллекции - даже если вы не хотите их использовать, вам, вероятно, придется иметь дело с ними из других библиотек, других источников.

  • Общая типизация при создании класса:

    открытый класс Foo <T> {public T get () ...

  • Избегание кастинга - мне всегда не нравились такие вещи, как

    новый компаратор {public int compareTo (Object o) {if (o instanceof classIcareAbout) ...

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

Моя первоначальная реакция на дженерики была похожа на вашу - «слишком грязно, слишком сложно». Мой опыт показывает, что после некоторого использования их вы привыкаете к ним, и код без них кажется менее четко определенным и просто менее удобным. Кроме того, весь остальной мир Java использует их, так что в конечном итоге вам придется разобраться с программой, верно?


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

Возможно, мне кажется, что я застрял в длинной череде компаний, не желающих подниматься выше 1.4, так что кто знает, я могу уйти на пенсию, прежде чем дойду до 5. В последнее время я также думал, что разветвление "SimpleJava" может быть интересной концепцией. --Java будет продолжать добавлять функции, и для большей части кода, который я видел, это не будет полезно. Я уже имею дело с людьми, которые не умеют кодировать цикл - мне не хотелось бы видеть, как они пытаются внедрить дженерики в свои бессмысленно развернутые циклы.
Bill K

@Bill K: Мало кому нужно использовать всю мощь дженериков. Это необходимо только тогда, когда вы пишете универсальный класс.
Эдди

5

Чтобы дать хороший пример. Представьте, что у вас есть класс под названием Foo

public class Foo
{
   public string Bar() { return "Bar"; }
}

Пример 1 Теперь вы хотите иметь коллекцию объектов Foo. У вас есть два варианта: LIst или ArrayList, оба работают одинаково.

Arraylist al = new ArrayList();
List<Foo> fl = new List<Foo>();

//code to add Foos
al.Add(new Foo());
f1.Add(new Foo());

В приведенном выше коде, если я попытаюсь добавить класс FireTruck вместо Foo, ArrayList добавит его, но Generic List of Foo вызовет исключение.

Пример второй.

Теперь у вас есть два списка массивов, и вы хотите вызвать функцию Bar () для каждого из них. Поскольку список ArrayList заполнен объектами, вы должны преобразовать их, прежде чем вы сможете вызвать bar. Но поскольку общий список Foo может содержать только Foo, вы можете вызывать Bar () непосредственно для них.

foreach(object o in al)
{
    Foo f = (Foo)o;
    f.Bar();
}

foreach(Foo f in fl)
{
   f.Bar();
}

Я предполагаю, что вы ответили, прежде чем читать вопрос. Это два случая, которые мне особо не помогают (описанные в вопросе). Есть ли другие случаи, когда он делает что-то полезное?
Bill K

4

Разве вы никогда не писали метод (или класс), в котором ключевая концепция метода / класса не была жестко привязана к конкретному типу данных переменных / параметров экземпляра (вспомните связанный список, функции max / min, двоичный поиск , так далее.).

Разве вы никогда не хотели, чтобы вы могли повторно использовать алгоритм / код, не прибегая к повторному использованию вырезания и вставки или компрометации строгой типизации (например, мне нужно несколько Listстрок, а не то, Listчто, я надеюсь, является строками!)?

Вот почему вы должны хотите использовать дженерики (или что - то лучше).


Мне никогда не приходилось копировать / вставлять повторное использование для такого рода вещей (не уверен, как это вообще могло произойти?) Вы реализуете интерфейс, который соответствует вашим потребностям, и используете этот интерфейс. Редкий случай, когда вы не можете этого сделать, - это когда вы пишете чистую утилиту (например, коллекции), и для них (как я описал в вопросе) это действительно никогда не было проблемой. Я иду на компромисс со строгой типизацией - как и вам, если вы используете отражение. Вы категорически отказываетесь использовать отражение, потому что у вас нет строгой типизации? (Если мне нужно использовать отражение, я просто инкапсулирую его как коллекцию).
Bill K

3

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

private <T extends Throwable> T logAndReturn(T t) {
    logThrowable(t); // some logging method that takes a Throwable
    return t;
}

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

    ...
} catch (MyException e) {
    throw logAndReturn(e);
}

Дело в том, что вы не теряете тип, передавая его через метод. Вы можете создать исключение правильного типа, а не просто исключение Throwable, что было бы всем, что вы могли бы сделать без дженериков.

Это всего лишь простой пример использования универсальных методов. Есть еще немало других интересных вещей, которые вы можете сделать с помощью универсальных методов. Самым крутым, на мой взгляд, является определение типов с помощью дженериков. Возьмем следующий пример (взятый из книги Джоша Блоха «Эффективное Java 2-е издание»):

...
Map<String, Integer> myMap = createHashMap();
...
public <K, V> Map<K, V> createHashMap() {
    return new HashMap<K, V>();
}

Это мало что дает, но снижает беспорядок, когда общие типы длинные (или вложенные; т.е. Map<String, List<String>>).


Я не думаю, что это правда в отношении исключений. Это заставляет меня задуматься, но я на 95% уверен, что вы получите точно такие же результаты без использования дженериков (и более четкого кода). Информация о типе - это часть объекта, а не то, к чему вы его приводите. У меня есть соблазн попробовать - может быть, я завтра на работе вернусь к вам ... Вот что я вам скажу, я собираюсь попробовать, и если вы правы, я собираюсь пройти через ваш список сообщений и проголосуйте за 5 ваших сообщений :) Если нет, вы можете подумать, что простая доступность дженериков заставила вас усложнить ваш код без каких-либо преимуществ.
Bill K

Это правда, что это добавляет дополнительную сложность. Однако я думаю, что от этого есть польза. Проблема в том, что вы не можете выбросить простой старый Throwableизнутри тела метода, который имеет определенные объявленные исключения. Альтернативный вариант - написать отдельные методы для возврата каждого типа исключения или выполнить приведение самостоятельно с помощью неуниверсального метода, который возвращает Throwable. Первое слишком многословно и совершенно бесполезно, а второму компилятор не поможет. Используя дженерики, компилятор автоматически вставит для вас правильное приведение. Итак, вопрос: стоит ли это сложности?
jigawot 05

О, я понял. Так что это позволяет избежать выхода гипса ... хороший момент. Я должен тебе 5.
Bill K

2

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

Таким образом вы можете делать что-то вроде:

List<SomeCustomClass> blah = new List<SomeCustomClass>();
blah[0].SomeCustomFunction();

Без дженериков вам пришлось бы привести blah [0] к правильному типу, чтобы получить доступ к его функциям.


Имея выбор, я бы предпочел не бросать, чем бросать.
MrBoJangles

Сильная типизация - лучший аспект дженериков imho, особенно с учетом проверки типов во время компиляции, которая позволяет.
ljs

2

jvm все равно приводит ... он неявно создает код, который обрабатывает общий тип как "Object" и создает приведение к желаемому экземпляру. Дженерики Java - это просто синтаксический сахар.


2

Я знаю, что это вопрос C #, но дженерики используются и на других языках, и их использование / цели очень похожи.

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

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

class Pair<F, S> {
    public final F first;
    public final S second;

    public Pair(F f, S s)
    { 
        first = f;
        second = s;   
    }
}  

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

Для обобщений также могут быть определены их границы с помощью ключевых слов super и extends. Например, если вы хотите иметь дело с универсальным типом, но хотите, чтобы он расширял класс с именем Foo (у которого есть метод setTitle):

public class FooManager <F extends Foo>{
    public void setTitle(F foo, String title) {
        foo.setTitle(title);
    }
}

Хотя это само по себе не очень интересно, полезно знать, что всякий раз, когда вы имеете дело с FooManager, вы знаете, что он будет обрабатывать типы MyClass и что MyClass расширяет Foo.


2

Из документации Sun Java в ответ на вопрос «почему я должен использовать дженерики?»:

"Generics предоставляет вам способ сообщить тип коллекции компилятору, чтобы его можно было проверить. Как только компилятор узнает тип элемента коллекции, компилятор может проверить, что вы использовали эту коллекцию последовательно и можете вставить правильные преобразования значений, извлекаемых из коллекции ... Код, использующий универсальные шаблоны, более ясен и безопасен ... компилятор может проверить во время компиляции, что ограничения типа не нарушаются во время выполнения [курсив мой]. программа компилируется без предупреждений, мы можем с уверенностью сказать, что она не вызовет исключение ClassCastException во время выполнения. Чистый эффект от использования дженериков, особенно в больших программах, - улучшенная читаемость и надежность . [курсив мой] "


Я никогда не встречал случая, когда универсальная типизация коллекций помогла бы моему коду. Мои коллекции сильно ограничены. Вот почему я сказал: «Это может мне помочь?». В качестве побочного вопроса, вы говорите, что до того, как вы начали использовать дженерики, такое случалось с вами время от времени? (добавление неправильного типа объекта в вашу коллекцию). Мне кажется, что это решение, в котором нет настоящих проблем, но, может быть, мне просто повезло.
Bill K

@Bill K: если код жестко контролируется (то есть используется только вами), возможно, вы никогда не столкнетесь с этим как с проблемой. Замечательно! Самые большие риски связаны с крупными проектами, над которыми работают несколько человек, ИМО. Это подстраховка и «передовой опыт».
Деми

@Bill K: Вот пример. Скажем, у вас есть метод, возвращающий ArrayList. Предположим, что ArrayList не является общей коллекцией. Чей-то код берет этот ArrayList и пытается выполнить некоторую операцию с его элементами. Единственная проблема в том, что вы заполнили ArrayList BillTypes, а потребитель пытается работать с ConsumerTypes. Это компилируется, но срывается во время выполнения. Если вы используете общие коллекции, вместо этого у вас будут ошибки компилятора, с которыми гораздо легче справиться.
Деми

@Bill K: Я работал с людьми с самыми разными уровнями навыков. Я не могу позволить себе роскошь выбирать, с кем делиться проектом. Таким образом, безопасность типов очень важна для меня. Да, я нашел (и исправил) ошибки, преобразовав код для использования Generics.
Эдди

1

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

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


1

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

Одним из примеров использования обоих является связанный список. Какая польза была бы от класса связанного списка, если бы он мог использовать только объект Foo? Чтобы реализовать связанный список, который может обрабатывать любой тип объекта, связанный список и узлы в гипотетическом внутреннем классе узла должны быть универсальными, если вы хотите, чтобы список содержал только один тип объекта.


1

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


1

Еще одно преимущество использования Generics (особенно с Collections / Lists) - это проверка типа во время компиляции. Это действительно полезно при использовании универсального списка вместо списка объектов.


1

Единственная причина - они обеспечивают безопасность типов.

List<Customer> custCollection = new List<Customer>;

в отличие от,

object[] custCollection = new object[] { cust1, cust2 };

в качестве простого примера.


Типовая безопасность, обсуждение само по себе. Может быть, кому-то стоит задать вопрос по этому поводу.
MrBoJangles

Да, но на что ты намекаешь? В чем весь смысл безопасности типов?
Vin

1

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

Это дает вам несколько преимуществ:

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

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

  • Компилятор может делать больше оптимизаций, например, избегать боксов и т. Д.


1

Пара вещей, которые нужно добавить / расширить (говоря с точки зрения .NET):

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

Общие аргументы методов могут делать то же самое, но они также помогают применить принцип «Говори, не спрашивай», то есть «дай мне то, что я хочу, а если не можешь, скажи мне, почему».


1

Я использую их, например, в GenericDao, реализованном с помощью SpringORM и Hibernate, которые выглядят так

public abstract class GenericDaoHibernateImpl<T> 
    extends HibernateDaoSupport {

    private Class<T> type;

    public GenericDaoHibernateImpl(Class<T> clazz) {
        type = clazz;
    }

    public void update(T object) {
        getHibernateTemplate().update(object);
    }

    @SuppressWarnings("unchecked")
    public Integer count() {
    return ((Integer) getHibernateTemplate().execute(
        new HibernateCallback() {
            public Object doInHibernate(Session session) {
                    // Code in Hibernate for getting the count
                }
        }));
    }
  .
  .
  .
}

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

public class UserDaoHibernateImpl extends GenericDaoHibernateImpl<User> {
    public UserDaoHibernateImpl() {
        super(User.class);     // This is for giving Hibernate a .class
                               // work with, as generics disappear at runtime
    }

    // Entity specific methods here
}

Моя маленькая структура более надежна (есть такие вещи, как фильтрация, отложенная загрузка, поиск). Я просто упростил здесь, чтобы дать вам пример

Я, как и Стив, и вы, вначале сказал: «Слишком грязно и сложно», но теперь я вижу его преимущества.


1

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

Прежде всего, дженерики - это концепция, не зависящая от языка, и, IMO, это может иметь больше смысла, если вы одновременно думаете о регулярном (во время выполнения) полиморфизме.

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

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

Классы работают примерно так же. Вы параметризуете тип, и код генерируется компилятором.

Как только вы получите представление о «времени компиляции», вы можете использовать «ограниченные» типы и ограничить то, что может быть передано как параметризованный тип через классы / методы. Таким образом, вы можете контролировать, что нужно передать, что является мощной вещью, особенно если у вас есть структура, потребляемая другими людьми.

public interface Foo<T extends MyObject> extends Hoo<T>{
    ...
}

Теперь никто не может устанавливать что-либо, кроме MyObject.

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

public <T extends MyObject> foo(T t1, T t2){
    ...
}   

Надеюсь, все это имеет смысл.



0

Использовать дженерики для коллекций просто и понятно. Даже если вы попробуете повсюду, прибыль от коллекций будет для меня победой.

List<Stuff> stuffList = getStuff();
for(Stuff stuff : stuffList) {
    stuff.do();
}

против

List stuffList = getStuff();
Iterator i = stuffList.iterator();
while(i.hasNext()) {
    Stuff stuff = (Stuff)i.next();
    stuff.do();
}

или

List stuffList = getStuff();
for(int i = 0; i < stuffList.size(); i++) {
    Stuff stuff = (Stuff)stuffList.get(i);
    stuff.do();
}

Одно только это стоит предельной «стоимости» дженериков, и вам не нужно быть универсальным гуру, чтобы использовать это и получить ценность.


1
Без дженериков вы можете: List stuffList = getStuff (); for (Материал объекта: stuffList) {((Материал) материал) .doStuff (); }, что не сильно отличается.
Tom Hawtin - tackline

Без дженериков я делаю stuffList.doAll (); Я сказал, что у меня всегда есть класс-оболочка, и это как раз место для такого кода. В противном случае, как избежать копирования этого цикла в другом месте? Куда положить для повторного использования? Класс-оболочка, вероятно, будет иметь какой-то цикл, но в этом классе, поскольку этот класс полностью посвящен «материалу», использование цикла for, подобного предложенному выше Томом, довольно ясно / самодокументируется.
Bill K

@Jherico, это действительно интересно. Интересно, есть ли причина, по которой вы так думаете, и если это то, что я не понимаю, - что в природе людей есть что-то такое, что любой кастинг по любой причине кажется непристойным и готов пойти на неестественные меры (например, добавив гораздо более сложный синтаксис), чтобы этого избежать. Почему «Если тебе нужно что-то разыграть, ты уже проиграл»?
Bill K

@Jherico: согласно документации Sun, дженерики предоставляют компилятору информацию о том, к чему приводить объекты. Поскольку преобразование для дженериков выполняется компилятором, а не пользователем, меняет ли это ваш оператор?
Деми

Я преобразовал код из более ранней версии Java 1.5 в Generics и в процессе обнаружил ошибки, при которых в коллекцию был помещен объект неправильного типа. Я также попадаю в лагерь «если вам нужно разыграть, вы проиграли», потому что, если вам нужно разыграть вручную, вы рискуете получить ClassCastException. С Дженерики компилятор делает это незаметно для вас, но шанс на ClassCastException является способом снижается из - за безопасность типа. Да, без Generics вы можете использовать Object, но для меня это ужасный запах кода, такой же, как «выдает исключение».
Эдди

0

Обобщения также дают вам возможность создавать больше повторно используемых объектов / методов, при этом обеспечивая поддержку конкретного типа. В некоторых случаях вы также получаете большую производительность. Я не знаю полной спецификации Java Generics, но в .NET я могу указать ограничения для параметра Type, например, реализует интерфейс, конструктор и деривацию.


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

  2. Более строгие проверки типов во время компиляции - компилятор Java применяет строгую проверку типов к универсальному коду и выдает ошибки, если код нарушает безопасность типов. Исправлять ошибки времени компиляции проще, чем исправлять ошибки времени выполнения, которые бывает сложно найти.

  3. Устранение слепков.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.