Что хорошего в дженериках, зачем их использовать? - PullRequest
79 голосов
/ 17 сентября 2008

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

Ответы [ 28 ]

120 голосов
/ 17 сентября 2008
  • Позволяет вам писать код / ​​использовать библиотечные методы, которые являются типобезопасными, то есть список гарантированно будет списком строк.
  • В результате использования обобщений компилятор может выполнять проверки кода во время компиляции для обеспечения безопасности типов, т. Е. Вы пытаетесь поместить int в этот список строк? Использование ArrayList приведет к менее прозрачной ошибке времени выполнения.
  • Быстрее, чем использование объектов, поскольку это позволяет избежать упаковки / распаковки (где .net необходимо преобразовать типы значений в ссылочные типы или наоборот ) или преобразования объектов в требуемый ссылочный тип.
  • Позволяет вам писать код, который применим ко многим типам с одинаковым базовым поведением, то есть словарь использует тот же базовый код, что и словарь ; используя дженерики, команда разработчиков фреймворка должна была написать только один фрагмент кода, чтобы достичь обоих результатов с вышеупомянутыми преимуществами.
45 голосов
/ 05 июня 2009

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

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

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 раза эффективнее, и мне нужно сохранить только одну копию. Почему вы не хотите поддерживать меньше кода?

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

Не знаю.

20 голосов
/ 05 июня 2009

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

Но я подозреваю, что вы полностью осведомлены об этом.

Каждый раз, когда я смотрю на дженерики, это дает у меня болит голова. Я нахожу лучшую часть 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 + Пробел

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

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

Ключевое преимущество дженериков заключается в том, что они хорошо работают с новыми функциями Java 5. Вот пример добавления целых чисел в 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, который, как известно, содержит Integer с. Попытка добавить String приведет к ошибке компиляции.

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

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

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

было проведено Integer s.

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

И, честно говоря, как видно из примера 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. Следовательно, в этом примере Object - это то, что add вводится в набор.

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

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

for (Object o : set) {

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

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

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

  total += (Integer)o;

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

Integer теперь распакован в int и может выполнять добавление в переменную int total.

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

15 голосов
/ 05 июня 2009

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

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

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

10 голосов
/ 05 июня 2009

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

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

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

8 голосов
/ 17 сентября 2008

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

5 голосов
/ 17 сентября 2008

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

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

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

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

5 голосов
/ 17 сентября 2008

Лучшим преимуществом для Generics является повторное использование кода. Допустим, у вас много бизнес-объектов, и вы собираетесь написать ОЧЕНЬ похожий код для каждой сущности для выполнения одних и тех же действий. (Операции I.E 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 голосов
/ 05 июня 2009

Чтобы привести хороший пример. Представьте, что у вас есть класс с именем 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 добавит его, но общий список Foo вызовет исключение.

Пример два.

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

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

foreach(Foo f in fl)
{
   f.Bar();
}
5 голосов
/ 05 июня 2009
  • Типизированные коллекции - даже если вы не хотите их использовать, вам, скорее всего, придется иметь дело с ними из других библиотек, других источников.

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

    публичный класс Foo { public T get () ...

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...