Що класного в дженериках, навіщо їх використовувати?


87

Я думав, що запропоную цей софтбол тому, хто захоче вибити його з парку. Що таке дженерики, які переваги дженериків, чому, де, як я повинен їх використовувати? Будь ласка, зберігайте його досить базовим. Дякую.


1
Дублікат stackoverflow.com/questions/77632/… . Але проста відповідь, навіть якщо колекції не є частиною вашого API, я не люблю зайвого кастингу навіть у внутрішній реалізації.
Matthew Flaschen,

Питання досить схоже, але я не думаю, що прийнята відповідь відповідає на це.
Tom Hawtin - таклін


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

@jcollum, я думаю, що оригінальне запитання, яке я розмістив у цьому коментарі, було об’єднане з цим питанням.
Метью Флашен

Відповіді:


126
  • Дозволяє писати код / ​​використовувати бібліотечні методи, які захищають тип, тобто List <string> гарантовано буде списком рядків.
  • В результаті використання узагальнювачів компілятор може виконувати перевірку часу компіляції коду для безпеки типу, тобто чи намагаєтесь ви внести int до цього списку рядків? Використання ArrayList призведе до того, що це буде менш прозорою помилкою виконання.
  • Швидше, ніж використання об'єктів, оскільки це дозволяє уникнути боксу / розпакування (де .net повинен перетворити типи значень на посилальні типи або навпаки ) або перекидання з об'єктів на необхідний тип посилання.
  • Дозволяє писати код, який застосовується до багатьох типів з однаковою базовою поведінкою, тобто словник <string, int> використовує той самий базовий код, що і словник <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. Він не отримав би (автомобіль), він би мав такі методи, як buyFuel (100), який би купив 100 доларів палива та розподілив його між автомобілями, які найбільше потребують цього. Я говорю про справжні бізнес-класи, а не просто про "обгортки". Наприклад, ви майже ніколи не повинні їхати (автомобіль) або перебирати їх поза колекцією - ви не просите Об'єкт для його учасників, натомість ви просите його зробити операцію за вас.
Білл К

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

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

Білл - ви можете сказати про невикористання інкапсуляції, але решта аргументів - погане порівняння. Вартість вивчення нового синтаксису в цьому випадку мінімальна (порівняйте це з лямбдами в C #); порівнювати це із внесенням змін до Конституції немає сенсу. Конституція не має наміру визначати ПДР. Це більше схоже на перехід на друковану версію Serif-Font на дошці плакатів замість рукописної копії на пергаменті. Це те саме значення, але набагато зрозуміліше і легше для читання.
Джеймс Шек,

Приклад робить концепцію більш практичною. Дякую за це.
RayLoveless

22

Відсутність необхідності в typecast є однією з найбільших переваг Java дженериків , оскільки вона буде виконувати перевірку типу під час компіляції. Це зменшить можливість ClassCastExceptions, які можуть бути кинуті під час виконання, і може призвести до більш надійного коду.

Але я підозрюю, що ви це повністю знаєте.

Щоразу, коли я дивлюсь на Generics, це болить у мене. Я вважаю, що найкращою частиною Java є її простота, а мінімальний синтаксис та узагальнення не є простими і додають значну кількість нового синтаксису.

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

Сучасні середовища розробки спрощують написання коду із узагальненнями.

Більшість сучасних, пристойних середовищ розробки є достатньо розумними, щоб допомогти з написанням коду із загальними засобами, особливо з заповненням коду.

Ось приклад створення Map<String, Integer>a 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. Спроба вкинути a Stringпризведе до помилки компіляції.

Далі для кожного циклу береться всі три:

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

По-перше, Setмістять Integers використовуються в циклі для кожного циклу. Кожен елемент оголошений як 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);

У наведених вище рядках, Autoboxing знаходиться в грі - примітивне intзначення , 10і в 42даний час autoboxed в Integerоб'єкти, які додаються до Set. Однак пам’ятайте, Integerоб’єкти обробляються як Objects, оскільки немає інформації про тип, яка б допомогла компілятору зрозуміти, якого типу Setслід очікувати.

for (Object o : set) {

Це та частина, яка має вирішальне значення. Причина, по якій працює кожен цикл, полягає в тому, що Setреалізує Iterableінтерфейс, який повертає Iteratorінформацію з типом, якщо вона присутня. ( Iterator<T>тобто.)

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

Тепер, коли Objectфайл витягується з Set, його потрібно передати Integerвручну, щоб виконати додавання:

  total += (Integer)o;

Тут типова трансляція виконується від Objectдо до Integer. У цьому випадку ми знаємо, що це завжди буде працювати, але ручне наведення типів завжди змушує мене відчувати, що це крихкий код, який може бути пошкоджений, якщо незначні зміни внесені деінде. (Я відчуваю, що кожна верстка - це ClassCastExceptionочікування, але відбудуся ...)

IntegerТепер Unboxed в intі дозволено виконувати додавання в intзмінної total.

Сподіваюся, я міг би проілюструвати, що нові функції Java 5 можна використовувати з не-загальним кодом, але він просто не такий чистий і прямий, як написання коду із дженериками. І, на мій погляд, щоб повною мірою скористатися новими можливостями Java 5, слід розглянути загальні засоби, якщо, принаймні, дозволяють перевіряти час компіляції, щоб запобігти недійсним запитам типів, щоб викидати винятки під час виконання.


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

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

Це хороший момент, я цим не користувався (я вже згадував, що застряг на версії 1.4 і раніше вже багато років). Я згоден, що є переваги, але висновки, до яких я роблю, полягають у тому, що А) вони, як правило, є вигідними для людей, які неправильно використовують ОО і менш корисні для тих, хто робить, і Б) Переваги, які ви ' Перераховані перелічені переваги, але навряд чи вони варті того, щоб виправдати навіть незначну зміну синтаксису, не кажучи вже про великі зміни, необхідні для справжнього вивчення дженериків, реалізованих у Java. Але принаймні є переваги.
Білл К

15

Якби ви шукали в базі даних про помилки Java перед випуском 1.5, ви б виявили в сім разів більше помилок, NullPointerExceptionніж ClassCastException. Тому не здається, що це чудова функція - знайти помилки або принаймні помилки, які зберігаються після невеликого тестування на дим.

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

Тримати колекції об’єктів для себе - це не поганий стиль (але тоді загальним стилем є ефективне ігнорування інкапсуляції). Це скоріше залежить від того, що ви робите. Передача колекцій "алгоритмам" трохи легше перевірити (під час або перед компіляцією) за допомогою загальних засобів.


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

Хороший момент, але хіба це не досягається також шляхом включення колекції до класу, що визначає "колекцію об'єктів цього типу"?
Білл К

1
Джеймс: Так, справа в тому, що це документується в коді .
Том Хоутін - таклін

Не думаю, що я знаю хороших бібліотек, які використовують колекції для "Алгоритмів" (можливо, маючи на увазі, що ви говорите про "Функції", що змушує мене думати, що вони повинні бути методом в об'єкті колекції). Я думаю, що JDK майже завжди витягує приємний чистий масив, який є копією даних, так що з ним неможливо переплутатися - принаймні частіше, ніж ні.
Білл К

Крім того, чи не має об’єкт, що точно описує, як використовується колекція, і обмежує її використання, НАБАГАТО кращою формою документації в коді? Я вважаю, що інформація, представлена ​​в дженериках, надзвичайно зайва в хорошому коді.
Білл К

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і повертає 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? Хоча я можу помилитися з цього приводу .. Але це дійсно хороший момент, якщо я помиляюся.
Білл К

Навіщо вам потрібен динамічний загальний клас для обчислення хеш-значень? Чи не буде статична функція з відповідними параметрами до роботи ефективніше?
Ant_222

8

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


У цьому суть цього, чи не так? Приємно.
MrBoJangles

Ну не зовсім; вони корисні для безпеки типів + уникаючи закидів для посилальних типів, навіть не потрапляючи в бокс / розпакування
ljs

Тож я думаю, що наступне запитання: "Чому я коли-небудь хотів би використовувати ArrayList?" І, ризикуючи відповіддю, я б сказав, що ArrayLists чудово працюють, якщо ви не сподіваєтесь сильно розставляти та розпаковувати їх значення. Коментарі?
MrBoJangles

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

Якщо ви не зберігаєте типи значень (наприклад, ints чи структури), при використанні ArrayList немає боксу - класи вже є "об'єктом". Використовувати List <T> все-таки набагато краще через тип безпеки, і якщо ви дійсно хочете зберігати об'єкти, то краще використовувати List <object>.
Wilka

6

Найкраща перевага для Generics - повторне використання коду. Скажімо, у вас багато бізнес-об’єктів, і ви збираєтеся написати ДУЖЕ подібний код для кожної сутності, щоб виконувати однакові дії. (IE Linq для операцій 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<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: Мало кому потрібно використовувати всю потужність дженериків. Це потрібно лише тоді, коли ви пишете клас, який сам по собі є загальним.
Едді

5

Навести хороший приклад. Уявіть, у вас є клас під назвою Фу

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 () на кожному. Оскільки hte ArrayList заповнений об'єктами, вам потрібно їх привести, перш ніж ви зможете викликати панель. Але оскільки загальний список Foo може містити лише Foos, ви можете викликати Bar () безпосередньо на них.

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

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

Я припускаю, ви відповіли перед тим, як прочитати питання. Це два випадки, які насправді мені не надто допомагають (описується). Чи є інші випадки, коли це робить щось корисне?
Білл К

4

Ви ніколи не писали метод (або клас), коли ключова концепція методу / класу не була жорстко прив'язана до певного типу даних параметрів / змінних примірника (думаю, пов'язаний список, функції max / min, двійковий пошук тощо).

Вам ніколи не хотілося б повторно використовувати алгоритм / код, не вдаючись до повторного використання cut-n-paste або не компрометуючи інтенсивного набору тексту (наприклад, я хочу a ListStrings, а не Listте, що я сподіваюся, це strings!)?

Ось чому ви повинні захотіти використовувати дженерики (або щось краще).


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

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

Справа в тому, що ви не втрачаєте тип, передаючи його через метод. Ви можете викинути правильний тип винятку замість просто a 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 ваших дописів :) Якщо ні, ви можете врахувати, що проста доступність дженериків призвела до того, що ви ускладнили свій код без переваг.
Білл К

Це правда, що це додає додаткової складності. Однак я думаю, що це має певне значення. Проблема полягає в тому, що ви не можете викинути звичайний старий Throwableіз тіла методу, який має конкретні заявлені винятки. Альтернатива - це написання окремих методів для повернення кожного типу винятку, або самостійне виконання приведення за допомогою не загального методу, який повертає a Throwable. Перший занадто багатослівний і цілком марний, а другий не отримає допомоги від компілятора. Використовуючи дженерики, компілятор автоматично вставить правильний для вас склад. Отже, питання: чи варто це складності?
jigawot

О, я зрозумів. Так це уникає виходу акторського складу ... хороший момент. Я винен тобі 5.
Білл К

2

Основною перевагою, як зазначає Мітчел, є сильний набір тексту без необхідності визначати кілька класів.

Таким чином, ви можете робити такі речі, як:

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

Без загальних засобів вам потрібно було б призначати бла [0] до правильного типу, щоб отримати доступ до його функцій.


Враховуючи вибір, я волів би не акторський склад, аніж актори.
MrBoJangles

Сильне введення тексту - це найкращий аспект загальних засобів імхо, особливо з огляду на перевірку типу компіляції, яка дозволяє.
ljs

2

jvm в будь-якому випадку призводить ... він неявно створює код, який розглядає загальний тип як "Об'єкт" і створює приведення до бажаного екземпляра. Загальні засоби для 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, ви можете вказати, з якими типами об'єктів ви хочете мати справу, і будь-які проблеми, пов'язані з типом, відображатимуться під час компіляції, а не під час виконання.

Дженерики також можуть мати свої межі, визначені ключовими словами "супер" і "розширює". Наприклад, якщо ви хочете мати справу із загальним типом, але хочете переконатися, що він розширює клас під назвою 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: якщо код жорстко контролюється (тобто використовується тільки вами), можливо, ви ніколи не зіткнетеся з цим як з проблемою. Це чудово! Найбільші ризики - у великих проектах, над якими працюють багато людей, IMO. Це захисна мережа та "найкраща практика".
Демі

@Bill K: Ось приклад. Скажімо, у вас є метод, який повертає ArrayList. Скажімо, що ArrayList не є загальною колекцією. Чийсь код бере цей ArrayList і намагається виконати якусь операцію над його елементами. Єдина проблема полягає в тому, що ви заповнили ArrayList BillTypes, а споживач намагається оперувати ConsumerTypes. Це компілюється, але підривається під час виконання. Якщо ви використовуєте загальні колекції, натомість у вас будуть помилки компілятора, з якими набагато приємніше мати справу.
Демі

@Bill K: Я працював із людьми з різним рівнем кваліфікації. У мене немає розкоші вибирати, з ким я ділюся проектом. Таким чином, безпека типу дуже важлива для мене. Так, я знайшов (і виправив) помилки, перетворивши код для використання Generics.
Едді

1

Узагальнюючі засоби дозволяють створювати об'єкти, що мають строгий тип, проте вам не потрібно визначати конкретний тип. Я думаю, що найкращим корисним прикладом є Список та подібні класи.

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


1

Загальні засоби дозволяють використовувати сильний набір тексту для об’єктів та структур даних, які повинні вмістити будь-який об’єкт. Це також усуває нудні та дорогі типові запити при отриманні об'єктів із загальних структур (бокс / розпакування).

Одним із прикладів, що використовує обидва, є зв’язаний список. Якої б користі зв'язаний класний список, якщо він міг використовувати лише об'єкт Foo? Для реалізації зв’язаного списку, який може обробляти будь-який об’єкт, зв’язаний список і вузли у внутрішньому класі гіпотетичного вузла повинні бути загальними, якщо ви хочете, щоб список містив лише один тип об’єкта.


1

Якщо ваша колекція містить типи значень, їм не потрібно вставляти / розпаковувати об'єкти при вставленні в колекцію, тому ваша продуктивність різко зростає. Класні доповнення, такі як resharper, можуть генерувати для вас більше коду, наприклад цикли foreach.


1

Ще однією перевагою використання дженериків (особливо з колекціями / списками) є отримання перевірки типу компіляції часу. Це дійсно корисно при використанні загального списку замість списку об’єктів.


1

Найбільша причина полягає в тому, що вони забезпечують безпеку типу

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

на відміну від,

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

як простий приклад.


Безпека типу, обговорення саме по собі. Можливо, хтось повинен поставити запитання щодо цього.
MrBoJangles

Так, але на що ти натякаєш? Вся суть Типової безпеки?
Він

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

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

Перш за все, дженерики - це мовно-незалежне поняття, і, ІМО, це може мати більше сенсу, якщо ви одночасно думаєте про регулярний (час виконання) поліморфізм.

Наприклад, поліморфізм, як ми знаємо з об’єктно-орієнтованого проектування, має поняття часу виконання, коли об’єкт, що викликає, з’ясовується під час виконання програми під час виконання програми, і відповідний метод викликається відповідно, залежно від типу виконання. У дженериках ідея дещо схожа, але все відбувається під час компіляції. Що це означає і як ти цим користуєшся?

(Давайте дотримуватимемось загальних методів, щоб зберегти його компактністю) Це означає, що ви все одно можете мати той самий метод для окремих класів (як це було раніше в поліморфних класах), але цього разу вони автоматично генеруються компілятором залежно від набору типів під час компіляції. Ви параметризуєте свої методи за типом, який ви надаєте під час компіляції. Отже, замість того, щоб писати методи з нуля для кожного окремого типу у вас, як у поліморфізмі середовища виконання (перевизначення методу), ви дозволяєте компіляторам виконувати роботу під час компіляції. Це має очевидну перевагу, оскільки вам не потрібно робити висновки про всі можливі типи, які можуть використовуватися у вашій системі, що робить її набагато більш масштабованою без зміни коду.

Класи працюють майже однаково. Ви параметризуєте тип і код генерується компілятором.

Отримавши уявлення про "час компіляції", ви можете використовувати "обмежені" типи та обмежити те, що можна передавати як параметризований тип через класи / методи. Отже, ви можете контролювати, через що проходити, що є сильною річчю, особливо у вас є структура, яку споживають інші люди.

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
Без загальних препаратів ви можете зробити: Список stuffList = getStuff (); for (Object stuff: stuffList) {((Stuff) stuff) .doStuff (); }, що мало чим відрізняється.
Том Хоутін - таклін

Без дженериків я роблю stuffList.doAll (); Я сказав, що завжди маю клас обгортки, який є саме місцем для такого коду. Інакше, як уникнути копіювання цього циклу в іншому місці? Де ви ставите його для повторного використання? Клас обгортки матиме певний цикл, напевно, але в цьому класі, оскільки цей клас стосується всього "матеріалу", використання циклу casted for, як запропоновано Томом, досить чітке / самодокументування.
Білл К

@Jherico, Це справді цікаво. Цікаво, чи є причина, по якій ти так почуваєшся, і чи це те, чого я не розумію, - що в природі людей є щось, що вважає будь-який кастинг з будь-якої причини якимось непристойним і готовим піти на неприродну довжину (як додаючи набагато складніший синтаксис), щоб уникнути його. Чому "Якщо вам потрібно щось кинути, ви вже програли"?
Білл К

@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.