Co jest fajnego w produktach generycznych, po co ich używać?

87

Pomyślałem, że zaoferuję tę softball każdemu, kto chciałby ją uderzyć z parku. Co to są leki generyczne, jakie są zalety leków generycznych, dlaczego, gdzie, jak ich używać? Proszę, niech to będzie dość podstawowe. Dzięki.

MrBoJangles
źródło
1
Duplikat stackoverflow.com/questions/77632/… . Ale prosta odpowiedź, nawet jeśli kolekcje nie są częścią twojego API, nie lubię niepotrzebnego rzutowania nawet w wewnętrznej implementacji.
Matthew Flaschen
Pytanie jest dość podobne, ale nie sądzę, aby przyjęta odpowiedź odpowiadała na to.
Tom Hawtin - tackline
1
Sprawdź także stackoverflow.com/questions/520527
Clint Miller
4
@MatthewFlaschen dziwne, twój duplikat kieruje do tego pytania ... usterka w matrixie!
jcollum
@jcollum, myślę, że pierwotne pytanie, które zamieściłem w komentarzu, zostało połączone z tym pytaniem.
Matthew Flaschen

Odpowiedzi:

126
  • Umożliwia pisanie kodu / korzystanie z metod bibliotecznych, które są bezpieczne dla typów, tj. List <string> gwarantuje, że jest to lista ciągów.
  • W wyniku użycia typów generycznych kompilator może przeprowadzić kontrolę kodu w czasie kompilacji pod kątem bezpieczeństwa typów, tj. Czy próbujesz umieścić int na tej liście ciągów? Użycie ArrayList spowodowałoby, że byłby to mniej przejrzysty błąd w czasie wykonywania.
  • Szybsze niż używanie obiektów, ponieważ pozwala uniknąć pakowania / rozpakowywania (gdzie .net musi konwertować typy wartości na typy referencyjne lub odwrotnie ) lub rzutowania z obiektów na wymagany typ odniesienia.
  • Umożliwia pisanie kodu, który ma zastosowanie do wielu typów o tym samym zachowaniu, tj. Dictionary <string, int> używa tego samego kodu źródłowego co Dictionary <DateTime, double>; używając typów generycznych, zespół frameworka musiał napisać tylko jeden fragment kodu, aby osiągnąć oba wyniki z wyżej wymienionymi zaletami.
ljs
źródło
9
Ach, bardzo ładnie i lubię listy punktowane. Myślę, że jak dotąd jest to najbardziej kompletna, ale zwięzła odpowiedź.
MrBoJangles
całe wyjaśnienie jest w kontekście „kolekcji”. Szukam szerszego spojrzenia. jakieś pomysły?
jungle_mole
dobra odpowiedź Chcę tylko dodać 2 inne zalety, ogólne ograniczenie ma 2 korzyści oprócz 1 - Możesz użyć właściwości typu ograniczonego w typie ogólnym (na przykład gdzie T: IComparable zapewnia użycie CompareTo) 2 - Osoba, która będzie pisać kod po tym, jak będziesz wiedział, co zrobi
Hamit YILDIRIM
52

Naprawdę nienawidzę się powtarzać. Nienawidzę pisać tego samego częściej niż muszę. Nie lubię powtarzać rzeczy wielokrotnie z niewielkimi różnicami.

Zamiast tworzyć:

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

Mogę zbudować jedną klasę wielokrotnego użytku ... (w przypadku, gdy z jakiegoś powodu nie chcesz używać kolekcji surowej)

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

Jestem teraz 3 razy wydajniejszy i muszę zachować tylko jedną kopię. Dlaczego NIE chcesz utrzymywać mniej kodu?

Dotyczy to również klas niebędących kolekcjami, takich jak a Callable<T>lub a, Reference<T>które muszą współdziałać z innymi klasami. Czy naprawdę chcesz rozszerzyć Callable<T>i Future<T>każdą inną skojarzoną klasę, aby utworzyć wersje bezpieczne dla typów?

Ja nie.

James Schek
źródło
1
Nie jestem pewien, czy rozumiem. Nie sugeruję tworzenia opakowań „MyObject”, to byłoby okropne. Sugeruję, że jeśli twoja kolekcja obiektów jest zbiorem samochodów, to napiszesz obiekt Garaż. Nie miałby (samochodu), miałby metody takie jak buyFuel (100), które kupiłoby paliwo za 100 dolarów i rozdzieliłoby je między samochody, które najbardziej go potrzebują. Mówię o prawdziwych zajęciach biznesowych, a nie tylko o „opakowaniach”. Na przykład, prawie nigdy nie powinieneś pobierać (samochodu) ani zapętlać ich poza kolekcją - nie prosisz obiektu o jego członków, zamiast tego poprosisz go o wykonanie operacji za Ciebie.
Bill K
Nawet w garażu powinieneś mieć bezpieczeństwo typów. Więc albo masz List <Cars>, ListOfCars, albo przesyłasz wszędzie. Nie czuję potrzeby podawania typu więcej niż minimalne wymagane czasy.
James Schek
Zgadzam się, że bezpieczeństwo typów byłoby fajne, ale jest znacznie mniej pomocne, ponieważ ogranicza się do klasy garażowej. Jest wtedy hermetyzowany i łatwo kontrolowany, więc chodzi mi o to, że daje ci tę niewielką przewagę kosztem nowej składni. To tak, jakby zmienić konstytucję Stanów Zjednoczonych, aby jazda na czerwonym świetle była nielegalna - tak, zawsze powinno być nielegalne, ale czy uzasadnia zmianę? Myślę też, że zachęca to ludzi do NIE używania kapsułkowania w tym przypadku, co jest w rzeczywistości porównywalnie szkodliwe.
Bill K
Bill - możesz mieć rację co do nieużywania hermetyzacji, ale reszta twojego argumentu to kiepskie porównanie. Koszt nauki nowej składni jest w tym przypadku minimalny (porównaj to z lambdami w C #); porównywanie tego do zmiany konstytucji nie ma sensu. Konstytucja nie ma na celu określenia kodu ruchu. To bardziej przypomina przejście na wersję drukowaną czcionką szeryfową na tablicy kartonowej zamiast odręcznej kopii na pergaminie. Ma to samo znaczenie, ale jest o wiele jaśniejsze i łatwiejsze do odczytania.
James Schek
Przykład sprawia, że ​​koncepcja jest bardziej praktyczna. Dziękuję za to.
RayLoveless
22

Brak konieczności rzutowania jest jedną z największych zalet generycznych języków Java , ponieważ sprawdza typ w czasie kompilacji. Zmniejszy to prawdopodobieństwo, że ClassCastExceptions, które mogą zostać wyrzucone w czasie wykonywania, i może prowadzić do bardziej niezawodnego kodu.

Ale podejrzewam, że jesteś tego w pełni świadomy.

Za każdym razem, gdy patrzę na leki generyczne, boli mnie głowa. Uważam, że najlepszą częścią Javy jest prostota i minimalna składnia, a typy generyczne nie są proste i dodają znaczną ilość nowej składni.

Na początku też nie widziałem korzyści ze stosowania leków generycznych. Zacząłem uczyć się Javy od składni 1.4 (mimo że Java 5 była wtedy niedostępna) i kiedy napotkałem generics, poczułem, że pisanie wymaga więcej kodu i naprawdę nie rozumiałem korzyści.

Nowoczesne IDE ułatwiają pisanie kodu za pomocą typów ogólnych.

Większość nowoczesnych, przyzwoitych IDE jest wystarczająco inteligentnych, aby pomagać w pisaniu kodu za pomocą typów ogólnych, zwłaszcza w uzupełnianiu kodu.

Oto przykład tworzenia pliku Map<String, Integer>z rozszerzeniem HashMap. Kod, który musiałbym wpisać, to:

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

I rzeczywiście, to dużo pisać tylko po to, by stworzyć nowy HashMap. Jednak w rzeczywistości musiałem wpisać tylko tyle, zanim Eclipse wiedział, czego potrzebuję:

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

To prawda, musiałem dokonać wyboru HashMapz listy kandydatów, ale w zasadzie IDE wiedziało, co dodać, w tym typy ogólne. Dzięki odpowiednim narzędziom używanie leków generycznych nie jest takie złe.

Ponadto, ponieważ typy są znane, podczas pobierania elementów z kolekcji generycznej IDE będzie działało tak, jakby ten obiekt był już obiektem zadeklarowanego typu - nie ma potrzeby rzutowania, aby IDE wiedział, jaki jest typ obiektu jest.

Główną zaletą leków generycznych jest ich dobre współdziałanie z nowymi funkcjami Java 5. Oto przykład podrzucania liczb całkowitych do a Seti obliczania jego sumy:

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

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

W tym fragmencie kodu są obecne trzy nowe funkcje Java 5:

Po pierwsze, typy generyczne i autoboxing prymitywów pozwalają na następujące wiersze:

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

Liczba całkowita 10jest automatycznie umieszczana w ankiecie o Integerwartości 10. (I to samo dla 42). Następnie Integerjest wrzucany do tego, o Setktórym wiadomo, że ma Integers. Próba dodania a Stringspowodowałaby błąd kompilacji.

Następnie pętla for-each przyjmuje wszystkie trzy z nich:

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

Po pierwsze, Setzawierające Integers są używane w pętli for-each. Każdy element jest zadeklarowany jako inti jest dozwolony, ponieważ Integerjest rozpakowywany z powrotem do prymitywu int. Fakt, że następuje rozpakowanie, jest znany, ponieważ używano leków generycznych do określenia, że ​​były one Integerprzechowywane w Set.

Generics mogą być spoiwem łączącym nowe funkcje wprowadzone w Javie 5 i po prostu sprawiają, że kodowanie jest prostsze i bezpieczniejsze. W większości przypadków IDE są wystarczająco sprytne, aby pomóc Ci z dobrymi sugestiami, więc ogólnie nie będzie to o wiele więcej pisanie.

I szczerze mówiąc, jak widać na Setprzykładzie, uważam, że wykorzystanie funkcji Java 5 może uczynić kod bardziej zwięzłym i solidnym.

Edycja - przykład bez typów ogólnych

Poniżej znajduje się ilustracja powyższego Setprzykładu bez użycia typów ogólnych. Jest to możliwe, ale niezbyt przyjemne:

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

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

(Uwaga: powyższy kod wygeneruje ostrzeżenie o niezaznaczonej konwersji w czasie kompilacji).

W przypadku korzystania z kolekcji innych niż ogólne typy, które są wprowadzane do kolekcji, są obiektami typu Object. Dlatego w tym przykładzie a Objectjest tym, co jest addwstawiane do zestawu.

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

W powyższych liniach w grę wchodzi autoboxing - prymitywna intwartość 10i 42są automatycznie dodawane do Integerobiektów, które są dodawane do Set. Należy jednak pamiętać, że Integerobiekty są obsługiwane jako Objects, ponieważ nie ma informacji o typie, które pomogłyby kompilatorowi dowiedzieć się, jakiego typu Setpowinien się spodziewać.

for (Object o : set) {

To jest ta część, która jest kluczowa. Powodem, dla którego pętla for-each działa, jest to, że Setimplementuje Iterableinterfejs, który zwraca Iteratorinformację o typie, jeśli występuje. ( Iterator<T>to znaczy.)

Ponieważ jednak nie ma informacji o typie, Setzwróci an, Iteratorktóry zwróci wartości w Setas Objects, dlatego element pobierany w pętli for-each musi być typu Object.

Teraz, gdy plik Objectjest pobierany z Setpliku, należy go Integerręcznie rzutować na plik, aby wykonać dodanie:

  total += (Integer)o;

Tutaj, typecast jest wykonywany od an Objectdo an Integer. W tym przypadku wiemy, że to zawsze zadziała, ale ręczne rzutowanie zawsze sprawia, że ​​czuję, że jest to delikatny kod, który może zostać uszkodzony, jeśli wprowadzona zostanie niewielka zmiana w innym miejscu. (Czuję, że każda seria ClassCastExceptionczeka na to, aby się wydarzyć, ale dygresję ...)

IntegerObecnie opakowania, w produkt inti pozostawiono do przeprowadzenia dodanie do tej intzmiennej total.

Mam nadzieję, że mógłbym zilustrować, że nowe funkcje Java 5 są możliwe do użycia z kodem nieogólnym, ale po prostu nie jest to tak przejrzyste i proste, jak pisanie kodu za pomocą typów generycznych. Moim zdaniem, aby w pełni wykorzystać nowe funkcje w Javie 5, należy przyjrzeć się rodzajom generycznym, jeśli przynajmniej zezwala na sprawdzanie czasu kompilacji, aby zapobiec rzucaniu wyjątków w czasie wykonywania przez nieprawidłowe typy typów.

coobird
źródło
Integracja z ulepszoną pętlą for jest naprawdę fajna (chociaż myślę, że ta cholerna pętla jest zepsuta, ponieważ nie mogę użyć dokładnie tej samej składni z nieogólną kolekcją - powinien po prostu użyć cast / autobox do int, ma wszystkie potrzebne informacje!), ale masz rację, pomoc w IDE jest prawdopodobnie dobra. Nadal nie wiem, czy wystarczy uzasadnić dodatkową składnię, ale przynajmniej na tym mogę skorzystać (co odpowiada na moje pytanie).
Bill K
@Bill K: Dodałem przykład użycia funkcji Java 5 bez używania ogólnych.
coobird
To dobra uwaga, nie używałem tego (wspomniałem, że utknąłem na 1.4 i wcześnie od lat). Zgadzam się, że są zalety, ale wnioski, do których dochodzę, są takie, że A) są one raczej korzystne dla osób, które nie używają OO poprawnie i są mniej przydatne dla tych, którzy to robią, oraz B) Zalety, Wymieniliśmy zalety, ale nie są one wystarczająco warte uzasadnienia nawet niewielkiej zmiany składni, nie mówiąc już o dużych zmianach wymaganych do stworzenia generyków w Javie. Ale przynajmniej są zalety.
Bill K
15

Gdybyś przeszukał bazę błędów Javy tuż przed wydaniem wersji 1.5, znalazłbyś siedem razy więcej błędów z NullPointerExceptionniż ClassCastException. Nie wydaje się więc, aby znajdowanie błędów lub przynajmniej błędów, które utrzymują się po krótkich testach z dymem, jest świetną funkcją.

Dla mnie ogromną zaletą typów generycznych jest to, że dokumentują w kodzie ważne informacje o typach. Gdybym nie chciał, aby te informacje o typie były udokumentowane w kodzie, użyłbym języka dynamicznie wpisywanego lub przynajmniej języka z bardziej niejawnym wnioskiem o typie.

Utrzymywanie kolekcji obiektu dla siebie nie jest złym stylem (ale typowym stylem jest skuteczne ignorowanie hermetyzacji). Raczej zależy to od tego, co robisz. Przekazywanie kolekcji do „algorytmów” jest nieco łatwiejsze do sprawdzenia (w czasie kompilacji lub przed nią) za pomocą typów ogólnych.

Tom Hawtin - haczyk
źródło
6
Nie tylko jest to udokumentowane (co samo w sobie jest ogromną wygraną), ale jest wymuszane przez kompilator.
James Schek
Słuszna uwaga, ale czy nie można tego również osiągnąć poprzez zamknięcie kolekcji w klasie, która definiuje „zbiór tego typu obiektów”?
Bill K
1
James: Tak, o to chodzi w dokumentowaniu tego w kodzie .
Tom Hawtin - tackline
Chyba nie znam dobrych bibliotek, które używają kolekcji dla „Algorytmów” (prawdopodobnie sugerując, że mówisz o „Funkcjach”, co prowadzi mnie do przekonania, że ​​powinny one być metodą w obiekcie kolekcji). Myślę, że JDK prawie zawsze wyodrębnia ładną, czystą tablicę, która jest kopią danych, aby nie można było z nią bawić - przynajmniej częściej niż nie.
Bill K
Ponadto, czy nie ma obiektu opisującego dokładnie, w jaki sposób kolekcja jest używana i ograniczającego, używa DUŻO lepszej formy dokumentacji w kodzie? Uważam, że informacje podane w rodzajach generycznych są wyjątkowo zbędne w dobrym kodzie.
Bill K
11

Generics w Javie ułatwiają parametryczny polimorfizm . Za pomocą parametrów typu można przekazywać argumenty do typów. Podobnie jak metoda String foo(String s)modeluje pewne zachowanie, nie tylko dla określonego ciągu, ale dla dowolnego ciągu s, tak typ taki jak List<T>modeluje pewne zachowanie, nie tylko dla określonego typu, ale dla dowolnego typu . List<T>mówi, że dla każdego typu Tistnieje typ, Listktórego elementy to Ts . Więc Listjest to właściwie konstruktor typu . Przyjmuje typ jako argument i konstruuje inny typ jako wynik.

Oto kilka przykładów typów ogólnych, których używam na co dzień. Po pierwsze, bardzo przydatny interfejs ogólny:

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

Ten interfejs mówi, że dla niektórych dwóch typów Ai Bistnieje funkcja (nazywana f), która przyjmuje Ai zwraca B. Po zaimplementowaniu tego interfejsu Ai Bmogą być dowolne typy, o ile zapewnisz funkcję, fktóra przyjmuje pierwszy i zwraca drugi. Oto przykładowa implementacja interfejsu:

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

Przed odmianami generycznymi polimorfizm był uzyskiwany przez tworzenie podklas za pomocą extendssłowa kluczowego. Dzięki rodzajom generycznym możemy w rzeczywistości pozbyć się podklas i zamiast tego użyć parametrycznego polimorfizmu. Na przykład rozważmy sparametryzowaną (ogólną) klasę używaną do obliczania kodów skrótów dla dowolnego typu. Zamiast przesłonić Object.hashCode (), użylibyśmy klasy ogólnej takiej jak ta:

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

Jest to znacznie bardziej elastyczne niż stosowanie dziedziczenia, ponieważ możemy pozostać przy motywie stosowania kompozycji i polimorfizmu parametrycznego bez blokowania kruchych hierarchii.

Jednak typy generyczne Javy nie są doskonałe. Możesz abstrahować od typów, ale nie możesz na przykład abstrahować od konstruktorów typów. Oznacza to, że możesz powiedzieć „dla dowolnego typu T”, ale nie możesz powiedzieć „dla dowolnego typu T, który przyjmuje parametr typu A”.

Napisałem tutaj artykuł o tych ograniczeniach generycznych Java.

Ogromną zaletą w przypadku leków generycznych jest to, że pozwalają uniknąć podklas. Tworzenie podklas prowadzi zwykle do kruchych hierarchii klas, których rozszerzenie jest niewygodne, oraz klas, które są trudne do zrozumienia indywidualnie bez patrzenia na całą hierarchię.

Wereas przed generycznych może mieć zajęcia jak Widgetprzedłużony o FooWidget, BarWidgeti BazWidget, z rodzajowych można mieć jedną klasę ogólną Widget<A>, która trwa Foo, Barlub Bazw jego konstruktora, aby dać Ci Widget<Foo>, Widget<Bar>i Widget<Baz>.

Apocalisp
źródło
Byłoby dobrym pomysłem na tworzenie podklas, gdyby Java nie miała interfejsów. Czy nie byłoby poprawne, gdyby FooWidget, BarWidtet i BazWidget zaimplementowały Widget? Chociaż mogę się co do tego mylić ... Ale to jest naprawdę dobra uwaga, jeśli się mylę.
Bill K
Dlaczego potrzebujesz dynamicznej klasy ogólnej do obliczania wartości skrótu? Czy statyczna funkcja z odpowiednimi parametrami do zadania nie byłaby wydajniejsza?
Ant_222
8

Leki generyczne unikają uderzenia w wydajność boksu i rozpakowywania. Zasadniczo spójrz na ArrayList vs List <T>. Oba robią te same podstawowe rzeczy, ale List <T> będzie znacznie szybszy, ponieważ nie musisz boksować do / z obiektu.

Darren Kopp
źródło
To jest istota tego, prawda? Miły.
MrBoJangles,
Cóż, nie do końca; są przydatne dla bezpieczeństwa typów + unikania rzutowania również dla typów referencyjnych bez wchodzenia w to w ogóle w boxing / unboxing.
ljs
Więc wydaje mi się, że następujące pytanie brzmi: „Dlaczego miałbym kiedykolwiek chcieć używać ArrayList?” I ryzykując odpowiedzią, powiedziałbym, że ArrayLists są w porządku, o ile nie spodziewasz się, że będziesz dużo pudełkować i rozpakowywać ich wartości. Komentarze?
MrBoJangles
Nie, są one nieistniejące od .net 2; przydatne tylko w celu zapewnienia kompatybilności wstecznej. ArrayLists są wolniejsze, nie są bezpieczne dla typów i wymagają pakowania / rozpakowywania lub rzutowania.
ljs
Jeśli nie przechowujesz typów wartości (np. Ints lub structs), nie ma boksu podczas korzystania z ArrayList - klasy są już „obiektami”. Korzystanie z List <T> jest nadal znacznie lepsze ze względu na typ bezpieczeństwa, a jeśli naprawdę chcesz przechowywać obiekty, lepiej jest używać List <object>.
Wilka
6

Największą korzyścią ze stosowania Generics jest ponowne wykorzystanie kodu. Powiedzmy, że masz wiele obiektów biznesowych i zamierzasz napisać BARDZO podobny kod dla każdej jednostki, aby wykonywać te same czynności. (Operacje IE z Linq do SQL).

Dzięki rodzajom możesz stworzyć klasę, która będzie w stanie działać na dowolnym typie dziedziczącym z danej klasy bazowej lub zaimplementować dany interfejs w następujący sposób:

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

    }
}

Zwróć uwagę na ponowne użycie tej samej usługi z różnymi typami w metodzie DoSomething powyżej. Naprawdę eleganckie!

Jest wiele innych wspaniałych powodów, dla których warto używać leków generycznych w swojej pracy, to mój ulubiony.

Dean Poulin
źródło
5

Po prostu je lubię, ponieważ pozwalają szybko zdefiniować typ niestandardowy (ponieważ i tak ich używam).

Na przykład zamiast definiować strukturę składającą się z ciągu znaków i liczby całkowitej, a następnie zaimplementować cały zestaw obiektów i metod w celu uzyskania dostępu do tablicy tych struktur i tak dalej, możesz po prostu utworzyć słownik

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

A kompilator / IDE wykonuje resztę ciężkiej pracy. W szczególności Słownik pozwala na użycie pierwszego typu jako klucza (bez powtarzających się wartości).

Tom Kidd
źródło
5
  • Kolekcje typowane - nawet jeśli nie chcesz ich używać, prawdopodobnie będziesz musiał się nimi zajmować z innych bibliotek lub z innych źródeł.

  • Ogólne wpisywanie w tworzeniu klas:

    public class Foo <T> {public T get () ...

  • Unikanie castingu - zawsze nie lubiłem takich rzeczy

    nowy komparator {public int compareTo (Object o) {if (o instanceof classIcareAbout) ...

Gdzie zasadniczo sprawdzasz warunek, który powinien istnieć tylko dlatego, że interfejs jest wyrażony w postaci obiektów.

Moja początkowa reakcja na leki generyczne była podobna do Twojej - „zbyt niechlujna, zbyt skomplikowana”. Z mojego doświadczenia wynika, że ​​po pewnym czasie przyzwyczajasz się do nich, a kod bez nich wydaje się mniej jasno określony i po prostu mniej wygodny. Poza tym reszta świata java używa ich, więc w końcu będziesz musiał zabrać się za program, prawda?


1
aby być dokładnym, jeśli chodzi o unikanie rzutowania: zgodnie z dokumentacją Sun'a rzutowanie odbywa się, ale jest wykonywane przez kompilator. Możesz po prostu uniknąć pisania rzutów w swoim kodzie.
Demi

Być może wydaje mi się, że utknąłem w długim szeregu firm, które nie chcą przekroczyć 1,4, więc kto wie, mogę przejść na emeryturę, zanim dojdę do 5. Ostatnio też myślałem, że rozwidlenie „SimpleJava” może być interesującą koncepcją - Java będzie nadal dodawać funkcje i dla większości kodu, który widziałem, nie będzie to zdrowa rzecz. Mam już do czynienia z ludźmi, którzy nie potrafią zakodować pętli - nie chciałbym patrzeć, jak próbują wstrzykiwać leki generyczne do swoich bezcelowo rozwijanych pętli.
Bill K

@Bill K: Niewiele osób musi wykorzystywać pełną moc leków generycznych. Jest to konieczne tylko wtedy, gdy piszesz klasę, która sama w sobie jest ogólna.
Eddie,

5

Aby dać dobry przykład. Wyobraź sobie, że masz klasę o nazwie Foo

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

Przykład 1 Teraz chcesz mieć kolekcję obiektów Foo. Masz dwie opcje, LIst lub ArrayList, które działają w podobny sposób.

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

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

W powyższym kodzie, jeśli spróbuję dodać klasę FireTruck zamiast Foo, ArrayList doda ją, ale Generic List of Foo spowoduje wyrzucenie wyjątku.

Przykład drugi.

Teraz masz dwie listy tablic i chcesz wywołać funkcję Bar () na każdej z nich. Ponieważ hte ArrayList jest wypełniona obiektami, musisz je rzutować, zanim będziesz mógł wywołać bar. Ale ponieważ Ogólna lista Foo może zawierać tylko Foos, możesz wywołać Bar () bezpośrednio na nich.

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

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

Zgaduję, że odpowiedziałeś przed przeczytaniem pytania. To są dwa przypadki, które nie bardzo mi pomagają (opisane w pytaniu). Czy są inne przypadki, w których robi coś pożytecznego?
Bill K

4

Czy nigdy nie napisałeś metody (lub klasy), w której kluczowa koncepcja metody / klasy nie była ściśle powiązana z określonym typem danych parametrów / zmiennych instancji (myśl, że lista połączona, funkcje max / min, wyszukiwanie binarne itp.).

Czy nigdy nie żałowałeś, że nie możesz ponownie użyć algorytmu / kodu bez uciekania się do ponownego użycia typu wytnij i wklej lub narażania na szwank silnego pisania (np. Chcę mieć ciąg znaków List, a nie Listrzeczy, które mam nadzieję są łańcuchami!)?

Dlatego powinieneś chcieć używać leków generycznych (lub czegoś lepszego).


Nigdy nie musiałem kopiować / wklejać ponownego wykorzystania do tego rodzaju rzeczy (nie wiesz, jak to się w ogóle stanie?) Implementujesz interfejs, który pasuje do twoich potrzeb i używasz tego interfejsu. Rzadkim przypadkiem, w którym nie możesz tego zrobić, jest pisanie czystego narzędzia (takiego jak kolekcje), a dla nich (jak opisałem w pytaniu) nigdy nie było to problemem. Idę na kompromis z silnym pisaniem - tak jak musisz, jeśli używasz refleksji. Czy absolutnie odmawiasz używania refleksji, ponieważ nie możesz silnie pisać? (Jeśli muszę użyć odbicia, po prostu hermetyzuję to jako zbiór).
Bill K

3

Nie zapominaj, że typy generyczne są używane nie tylko przez klasy, ale mogą być również używane przez metody. Na przykład weź następujący fragment:

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

Jest prosty, ale można go używać bardzo elegancko. Fajne jest to, że metoda zwraca to, co została podana. To pomaga, gdy obsługujesz wyjątki, które muszą zostać ponownie przesłane z powrotem do dzwoniącego:

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

Chodzi o to, że nie tracisz typu, przepuszczając go metodą. Możesz zgłosić poprawny typ wyjątku zamiast tylkoThrowable zgłosić , co byłoby wszystkim, co możesz zrobić bez ogólnych.

To tylko prosty przykład zastosowania metod ogólnych. Istnieje kilka innych fajnych rzeczy, które można zrobić za pomocą metod ogólnych. Moim zdaniem najfajniejszy jest typ wywodzący się z generyków. Weźmy następujący przykład (zaczerpnięty z Effective Java 2nd Edition Josha Blocha):

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

Nie robi to wiele, ale pozwala zaoszczędzić trochę bałaganu, gdy typy ogólne są długie (lub zagnieżdżone; tj Map<String, List<String>>.).


Nie sądzę, żeby to prawda w przypadku wyjątków. To sprawia, że ​​myślę, ale jestem w 95% pewien, że uzyskasz dokładnie takie same wyniki bez używania typów ogólnych (i jaśniejszego kodu). Informacje o typie są częścią obiektu, a nie tym, na co je przesyłasz. Kusi mnie jednak, żeby spróbować - może jutro będę w pracy i wrócę do Ciebie .. Co powiem, spróbuję i jeśli masz rację, przejdę swoją listę postów i zagłosuj na 5 swoich postów :) Jeśli jednak nie, możesz wziąć pod uwagę, że prosta dostępność typów generycznych nie spowodowała, że ​​skomplikowałeś swój kod bez żadnych korzyści.
Bill K

Prawdą jest, że powoduje to dodatkową złożoność. Myślę jednak, że ma to jakiś pożytek. Problem polega na tym, że nie można wyrzucić zwykłego starego Throwablez treści metody, która ma określone zadeklarowane wyjątki. Alternatywą jest napisanie oddzielnych metod zwracających każdy typ wyjątku lub samodzielne wykonanie rzutowania przy użyciu metody innej niż ogólna, która zwraca plik Throwable. Pierwsza jest zbyt rozwlekła i zupełnie bezużyteczna, a druga nie otrzyma żadnej pomocy od kompilatora. Używając typów generycznych, kompilator automatycznie wstawi dla Ciebie poprawne rzutowanie. Tak więc pytanie brzmi: czy to jest warte złożoności?
jigawot

Oh, już rozumiem. Dzięki temu unika się wychodzenia obsady ... słuszna uwaga. Jestem ci winien 5.
Bill K

2

Podstawową zaletą, jak wskazuje Mitchel, jest mocne pisanie bez konieczności definiowania wielu klas.

W ten sposób możesz robić takie rzeczy jak:

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

Bez typów ogólnych musiałbyś rzutować blah [0] na właściwy typ, aby uzyskać dostęp do jego funkcji.


Mając wybór, wolałbym raczej nie rzucać niż rzucać.
MrBoJangles,

Silne pisanie jest z pewnością najlepszym aspektem imho typów ogólnych, szczególnie biorąc pod uwagę sprawdzanie typów w czasie kompilacji, które na to pozwala.
ljs

2

jvm i tak rzuca ... niejawnie tworzy kod, który traktuje typ ogólny jako „Object” i tworzy rzuty do żądanej instancji. Generics Java to tylko cukier syntaktyczny.


2

Wiem, że to jest pytanie C #, ale ogólne w języku są również używane w innych językach, a ich użycie / cele są dość podobne.

Kolekcje Java używają typów ogólnych od wersji Java 1.5. Dlatego dobrym miejscem do ich wykorzystania jest tworzenie własnego obiektu przypominającego kolekcję.

Przykładem, który widzę prawie wszędzie, jest klasa Pair, która zawiera dwa obiekty, ale musi radzić sobie z tymi obiektami w ogólny sposób.

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

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

Za każdym razem, gdy używasz tej klasy Pair, możesz określić, z jakim rodzajem obiektów ma się ona obchodzić, a wszelkie problemy z rzutowaniem typu pojawią się w czasie kompilacji, a nie w czasie wykonywania.

Rodzaje mogą również mieć swoje granice zdefiniowane za pomocą słów kluczowych „super” i „extends”. Na przykład, jeśli chcesz poradzić sobie z typem ogólnym, ale chcesz się upewnić, że rozszerza on klasę o nazwie Foo (która ma metodę setTitle):

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

Chociaż samo w sobie nie jest to zbyt interesujące, warto wiedzieć, że za każdym razem, gdy masz do czynienia z FooManager, wiesz, że będzie on obsługiwał typy MyClass i że MyClass rozszerza Foo.


2

Z dokumentacji Sun Java, w odpowiedzi na pytanie „dlaczego powinienem używać typów ogólnych?”:

„Typy generyczne umożliwiają przekazanie kompilatorowi typu kolekcji, aby można było go sprawdzić. Gdy kompilator zna typ elementu kolekcji, może sprawdzić, czy kolekcja została użyta konsekwentnie i może wstawić poprawne rzutowanie na wartości pobierane z kolekcji ... Kod używający typów ogólnych jest bardziej przejrzysty i bezpieczniejszy .... kompilator może zweryfikować w czasie kompilacji, czy ograniczenia typu nie są naruszane w czasie wykonywania [podkreślenie moje]. program kompiluje się bez ostrzeżeń, możemy z całą pewnością stwierdzić, że nie zgłosi on wyjątku ClassCastException w czasie wykonywania. Efektem netto używania typów ogólnych, szczególnie w dużych programach, jest poprawiona czytelność i niezawodność . [podkreślenie moje] "


Nigdy nie znalazłem przypadku, w którym ogólne wpisywanie kolekcji pomogłoby mojemu kodowi. Moje kolekcje są ściśle określone. Dlatego sformułowałem to „Czy to może mi pomóc?”. Na marginesie, czy mówisz, że zdarzało ci się to czasami, zanim zacząłeś używać leków generycznych? (umieszczenie niewłaściwego typu obiektu w swojej kolekcji). Wydaje mi się, że jest to rozwiązanie bez prawdziwego problemu, ale może po prostu mam szczęście.
Bill K

@Bill K: jeśli kod jest ściśle kontrolowany (to znaczy używany tylko przez Ciebie), być może nigdy nie napotkasz tego jako problemu. To wspaniale! Największe ryzyko występuje w dużych projektach, nad którymi pracuje wiele osób, IMO. Jest to siatka bezpieczeństwa i „najlepsza praktyka”.
Demi

@Bill K: Oto przykład. Załóżmy, że masz metodę, która zwraca ArrayList. Powiedzmy, że ArrayList nie jest kolekcją ogólną. Czyjś kod pobiera tę ArrayList i próbuje wykonać jakąś operację na jej elementach. Jedynym problemem jest to, że wypełniłeś ArrayList BillTypes, a konsument próbuje operować na ConsumerTypes. To kompiluje się, ale wybucha w czasie wykonywania. Jeśli używasz ogólnych kolekcji, zamiast tego będziesz mieć błędy kompilatora, z którymi łatwiej sobie poradzić.
Demi

@Bill K: Pracowałem z ludźmi o bardzo różnych poziomach umiejętności. Nie mam luksusu wyboru, z kim dzielę projekt. Dlatego bezpieczeństwo typu jest dla mnie bardzo ważne. Tak, znalazłem (i poprawiłem) błędy, konwertując kod na generyczne.
Eddie

1

Generics umożliwiają tworzenie obiektów, które są silnie wpisane, ale nie musisz definiować określonego typu. Myślę, że najlepszym użytecznym przykładem jest Lista i podobne klasy.

Korzystając z ogólnej listy, możesz mieć List List List, cokolwiek chcesz i zawsze możesz odwołać się do silnego pisania, nie musisz konwertować ani niczego takiego, jak w przypadku tablicy lub standardowej listy.


1

Generics pozwalają na użycie silnego pisania dla obiektów i struktur danych, które powinny być w stanie pomieścić dowolny obiekt. Eliminuje również żmudne i kosztowne typowanie podczas pobierania obiektów ze struktur ogólnych (boxing / unboxing).

Jednym z przykładów, w którym są używane oba, jest lista połączona. Jaki pożytek przyniosłaby połączona klasa listy, gdyby mogła używać tylko obiektu Foo? Aby zaimplementować listę połączoną, która może obsługiwać dowolny rodzaj obiektu, lista połączona i węzły w klasie wewnętrznej hipotetycznego węzła muszą być ogólne, jeśli lista ma zawierać tylko jeden typ obiektu.


1

Jeśli Twoja kolekcja zawiera typy wartości, nie muszą one umieszczać / rozpakowywać w obiektach po wstawieniu do kolekcji, więc wydajność znacznie wzrasta. Fajne dodatki, takie jak resharper, mogą wygenerować dla Ciebie więcej kodu, na przykład pętle foreach.


1

Kolejną zaletą używania typów ogólnych (szczególnie w przypadku kolekcji / list) jest sprawdzanie typu czasu kompilacji. Jest to naprawdę przydatne, gdy używasz listy ogólnej zamiast listy obiektów.


1

Jednym z głównych powodów jest to, że zapewniają bezpieczeństwo typu

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

w przeciwieństwie do

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

jako prosty przykład.


Bezpieczeństwo typu, dyskusja sama w sobie. Może ktoś powinien o to zapytać.
MrBoJangles

Tak, ale do czego sugerujesz? Cała kwestia bezpieczeństwa typów?
Vin

1

Podsumowując, typy ogólne umożliwiają dokładniejsze określenie tego, co zamierzasz zrobić (silniejsze pisanie).

Ma to kilka zalet:

  • Ponieważ kompilator wie więcej o tym, co chcesz zrobić, pozwala pominąć wiele rzutowania typów, ponieważ już wie, że typ będzie zgodny.

  • Dzięki temu uzyskasz również wcześniejsze opinie na temat poprawności Twojego programu. Rzeczy, które poprzednio zawiodłyby w czasie wykonywania (np. Ponieważ obiekt nie mógł zostać rzucony w żądanym typie), teraz zawodzą w czasie kompilacji i możesz naprawić błąd, zanim Twój dział testów prześle tajemniczy raport o błędzie.

  • Kompilator może wykonać więcej optymalizacji, takich jak unikanie pudełek itp.


1

Kilka rzeczy do dodania / rozszerzenia (mówiąc z punktu widzenia .NET):

Typy ogólne umożliwiają tworzenie klas i interfejsów opartych na rolach. Zostało to już powiedziane w bardziej podstawowych terminach, ale uważam, że zaczynasz projektować swój kod za pomocą klas, które są zaimplementowane w sposób niezależny od typu - co powoduje, że kod jest wysoce wielokrotnego użytku.

Ogólne argumenty dotyczące metod mogą zrobić to samo, ale pomagają też zastosować zasadę „Nie pytaj” do rzutowania, tj. „Daj mi to, czego chcę, a jeśli nie możesz, powiedz mi dlaczego”.


1

Używam ich np. W pliku Genericxpress zaimplementowanym w SpringORM i Hibernate, które wyglądają tak

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

Używając typów generycznych, moje implementacje tego DAO zmuszają programistę do przekazania im tylko encji, dla których są przeznaczone, przez podklasę Generic replace

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
}

Mój mały framework jest bardziej niezawodny (ma takie rzeczy jak filtrowanie, leniwe ładowanie, wyszukiwanie). Po prostu uprościłem to, aby dać przykład

Ja, podobnie jak Ty i Steve, powiedziałem na początku: „Zbyt bałaganiarskie i skomplikowane”, ale teraz widzę jego zalety


1

Oczywiste korzyści, takie jak „bezpieczeństwo typu” i „brak rzucania”, są już wspomniane, więc może mógłbym porozmawiać o innych „korzyściach”, które, mam nadzieję, pomogą.

Po pierwsze, generyczne są koncepcją niezależną od języka i IMO może mieć więcej sensu, jeśli w tym samym czasie pomyślisz o zwykłym polimorfizmie (w czasie wykonywania).

Na przykład polimorfizm, jaki znamy z projektowania obiektowego, ma pojęcie czasu wykonywania, w którym obiekt wywołujący jest obliczany w czasie wykonywania programu, a odpowiednia metoda jest wywoływana odpowiednio w zależności od typu środowiska wykonawczego. W generyce pomysł jest nieco podobny, ale wszystko dzieje się w czasie kompilacji. Co to oznacza i jak z tego korzystasz?

(Trzymajmy się metod ogólnych, aby zachować zwartość) Oznacza to, że nadal możesz mieć tę samą metodę na oddzielnych klasach (tak jak wcześniej w klasach polimorficznych), ale tym razem są one automatycznie generowane przez kompilator w zależności od ustawionych typów w czasie kompilacji. Parametryzujesz swoje metody na typie, który podajesz w czasie kompilacji. Więc zamiast pisać metody od podstaw dla każdego typu , jak to robisz w przypadku polimorfizmu w czasie wykonywania (nadpisywanie metody), pozwalasz kompilatorom wykonywać pracę podczas kompilacji. Ma to oczywistą zaletę, ponieważ nie musisz wywnioskować wszystkich możliwych typów, które mogą być używane w twoim systemie, co czyni go znacznie bardziej skalowalnym bez zmiany kodu.

Zajęcia działają w podobny sposób. Parametryzujesz typ, a kod jest generowany przez kompilator.

Kiedy już zrozumiesz pojęcie "czasu kompilacji", możesz użyć "ograniczonych" typów i ograniczyć to, co może być przekazane jako sparametryzowany typ przez klasy / metody. Możesz więc kontrolować, przez co ma zostać przepuszczone, co jest potężną rzeczą, szczególnie jeśli struktura jest konsumowana przez innych ludzi.

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

Nikt nie może teraz ustawić czegoś innego niż MyObject.

Możesz także „wymusić” ograniczenia typu w argumentach metody, co oznacza, że ​​możesz się upewnić, że oba argumenty metody będą zależeć od tego samego typu.

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

Mam nadzieję, że to wszystko ma sens.



0

Używanie typów ogólnych do kolekcji jest proste i przejrzyste. Nawet jeśli będziesz go łowić wszędzie indziej, zysk z kolekcji jest dla mnie wygraną.

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

vs

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

lub

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

Już samo to jest warte krańcowego „kosztu” leków generycznych i nie musisz być generycznym Guru, aby używać tego i uzyskać wartość.


1
Bez typów generycznych możesz to zrobić: List stuffList = getStuff (); for (Object stuff: stuffList) {((Stuff) stuff) .doStuff (); }, co niewiele się różni.
Tom Hawtin - tackline

źródło
Bez typów generycznych robię stuffList.doAll (); Powiedziałem, że zawsze mam klasę opakowującą, która jest dokładnie miejscem na taki kod. W przeciwnym razie, jak uniknąć kopiowania tej pętli w innym miejscu? Gdzie chcesz go ponownie wykorzystać? Klasa opakowująca prawdopodobnie będzie miała jakąś pętlę, ale w tej klasie, ponieważ ta klasa dotyczy „rzeczy”, użycie pętli rzutowanej for, jak zasugerował Tom, jest całkiem jasne / samodokumentujące.
Bill K
@Jherico, To jest naprawdę interesujące. Zastanawiam się, czy istnieje powód, dla którego tak myślisz, a jeśli tego nie rozumiem - że jest coś w ludzkiej naturze, co uważa, że ​​jakikolwiek casting z jakiegokolwiek powodu jest w jakiś sposób nieprzyzwoity i jest skłonny posunąć się do nienaturalnych długości (np. dodanie znacznie bardziej złożonej składni), aby tego uniknąć. Dlaczego „Jeśli musisz coś rzucić, już przegrałeś”?
Bill K
@Jherico: zgodnie z dokumentacją Sun, typy generyczne zapewniają kompilatorowi wiedzę o tym, na co rzucać obiekty. Skoro kompilator wykonuje rzutowanie dla typów generycznych, a nie dla użytkownika, czy to zmienia instrukcję?
Demi
Przekonwertowałem kod z wersji pre Java 1.5 na Generics i podczas procesu znalazłem błędy, w których niewłaściwy typ obiektu został umieszczony w kolekcji. Wchodzę również w obóz „jeśli musisz rzucać, straciłeś”, ponieważ jeśli musisz rzucać ręcznie, ryzykujesz wyjątek ClassCastException. W przypadku Generics kompilator robi to niewidocznie za Ciebie, ale szansa na wyjątek ClassCastException jest znacznie zmniejszona ze względu na bezpieczeństwo typów. Tak, bez Generics możesz używać Object, ale dla mnie to okropny zapach kodu, tak samo jak „rzuca Wyjątek”.
Eddie,
0

Generics daje również możliwość tworzenia obiektów / metod wielokrotnego użytku, jednocześnie zapewniając obsługę specyficzną dla typu. W niektórych przypadkach zyskujesz również dużą wydajność. Nie znam pełnej specyfikacji Java Generics, ale w .NET mogę określić ograniczenia dla parametru Type, takie jak Implements a Interface, Constructor i Derivation.

Eric Schneider
źródło
0
  1. Umożliwienie programistom implementowania algorytmów ogólnych - używając typów ogólnych programiści mogą implementować algorytmy ogólne, które działają na kolekcjach różnych typów, można je dostosowywać, są bezpieczne dla typów i łatwiejsze do odczytania.

  2. Silniejsze sprawdzanie typów w czasie kompilacji - kompilator Java stosuje silne sprawdzanie typów w kodzie ogólnym i generuje błędy, jeśli kod narusza bezpieczeństwo typów. Naprawianie błędów w czasie kompilacji jest łatwiejsze niż naprawianie błędów czasu wykonania, które mogą być trudne do znalezienia.

  3. Eliminacja odlewów.

Popiół
źródło