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

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

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

Matthew Flaschen 05.06.2009 03:37

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

Tom Hawtin - tackline 05.06.2009 03:53

Также оформить заказ stackoverflow.com/questions/520527

Clint Miller 05.06.2009 04:10

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

jcollum 17.03.2012 01:49

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

Matthew Flaschen 17.03.2012 03:09

Как упоминалось в @Ijs - write code which is applicable to many types with the same underlying behavior

LCJ 18.12.2013 19:47

Ссылка на дженерики: blogs.msdn.com/kirillosenkov/archive/2008/08/19/….

MrBoJangles 10.03.2014 19:52
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
87
7
64 747
28
Перейти к ответу Данный вопрос помечен как решенный

Ответы 28

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

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

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

В этом вся суть, не так ли? Отлично.

MrBoJangles 17.09.2008 02:02

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

ljs 17.09.2008 02:06

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

MrBoJangles 17.09.2008 02:08

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

ljs 17.09.2008 02:09

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

Wilka 17.09.2008 03:41

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

ljs 17.09.2008 13:35

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

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

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

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

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

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

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

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

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

MrBoJangles 17.09.2008 02:10

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

jungle_mole 13.11.2015 18:51

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

Hamit YILDIRIM 17.05.2019 17:12

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

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

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

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

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

MrBoJangles 17.09.2008 02:15

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

ljs 17.09.2008 02:23

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

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

Единственная причина в том, что они предоставляют Безопасность типов

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

в отличие от,

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

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

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

MrBoJangles 17.09.2008 04:13

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

Vin 01.10.2008 20:01

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

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

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

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

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

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

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

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

Я знаю, что это вопрос 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.

Я как-то выступал на эту тему. Вы можете найти мои слайды, код и аудиозапись на http://www.adventuresinsoftware.com/generics/.

Лучшее преимущество 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 выше. Поистине элегантно!

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

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

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

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

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

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

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

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

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

Demi 05.06.2009 04:11

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

Bill K 05.06.2009 04:20

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

Eddie 05.06.2009 04:53

Я использую их, например, в 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
}

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

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

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

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();
}

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

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

Tom Hawtin - tackline 05.06.2009 03:51

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

Bill K 05.06.2009 04:04

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

Bill K 05.06.2009 04:07

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

Demi 05.06.2009 04:13

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

Eddie 05.06.2009 04:57

@Eddie: согласен - это тоже проще писать и проще понять, IMO. @Bill K: вы действительно считаете общий синтаксис Java "намного более сложным", чем неуниверсальный синтаксис?

harto 05.06.2009 05:07

Я считаю, что это самая сложная часть Java для изучения / освоения. Есть несколько уникальных шаблонов синтаксиса Java, которые бросают новых программистов - внутренние классы, исключения, запоминание «public static void main (String [])» - хм - тернарный оператор ... подробнее? Включите этот список, разве дженерики от FAR не являются самой сложной частью «уникального» синтаксиса в java? Я просто не вижу выгоды от этой сложности, когда редкое удачно размещенное приведение представляет собой очень простой и понятный синтаксис, необходимость которого в любом случае даже полностью не устраняется Generics (или это так? Если так, я мог бы дать дженерикам немного больше свобода действий)

Bill K 05.06.2009 22:22

Я думаю, что общий синтаксис СОВЕРШЕННО сложен, если взять все пространство. Но для ЭТОГО СЛУЧАЯ, который я считаю наиболее распространенным, синтаксис (для меня) довольно прост. По крайней мере, в Java >> не является "ключевым словом", как в C++, поэтому List <Map <Integer>> является допустимым, тогда как в C++ (в прошлом) этого не было, вам нужно было сделать List <Map <Integer> >, возможно, они уже исправили это.

Will Hartung 06.06.2009 01:01

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

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

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

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

James Schek 05.06.2009 03:50

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

Bill K 05.06.2009 03:51

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

Tom Hawtin - tackline 05.06.2009 03:57

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

Bill K 05.06.2009 03:58

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

Bill K 05.06.2009 04:00

Том - извините, я просто пытаюсь отличиться от тех, кто считает «в коде» венгерским наименованием ...

James Schek 05.06.2009 04:02

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

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

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

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

Bill K 05.06.2009 03:49

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

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

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

Bill K 05.06.2009 04:11

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

Demi 05.06.2009 04:18

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

Demi 05.06.2009 04:26

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

Eddie 05.06.2009 04:59

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

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

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> и любой другой связанный класс для создания типобезопасных версий?

Я не.

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

Bill K 05.06.2009 11:12

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

James Schek 05.06.2009 17:51

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

Bill K 05.06.2009 22:13

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

James Schek 15.06.2009 18:38

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

RayLoveless 23.05.2018 21:53

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

joel 26.05.2020 23:17

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

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

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, что было бы всем, что вы могли бы сделать без дженериков.

Это всего лишь простой пример использования универсальных методов. Есть еще немало других интересных вещей, которые вы можете сделать с помощью универсальных методов. Самым крутым, на мой взгляд, является определение типов с помощью дженериков. Возьмем следующий пример (взятый из Josh Bloch's Effective Java 2nd Edition):

...
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 05.06.2009 11:25

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

jigawot 05.06.2009 20:41

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

Bill K 05.06.2009 22:14

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

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

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

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

Bill K 05.06.2009 10:59

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

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

Bill K 05.06.2009 11:18

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

Ant_222 24.01.2018 00:33

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по теме