Ich dachte, ich würde diesen Softball jedem anbieten, der ihn aus dem Park schlagen möchte. Was sind Generika, was sind die Vorteile von Generika, warum, wo, wie soll ich sie verwenden? Bitte halten Sie es ziemlich einfach. Vielen Dank.
Ich dachte, ich würde diesen Softball jedem anbieten, der ihn aus dem Park schlagen möchte. Was sind Generika, was sind die Vorteile von Generika, warum, wo, wie soll ich sie verwenden? Bitte halten Sie es ziemlich einfach. Vielen Dank.
Antworten:
Ich hasse es wirklich, mich zu wiederholen. Ich hasse es, öfter dasselbe zu tippen, als ich muss. Ich mag es nicht, Dinge mehrmals mit kleinen Unterschieden zu wiederholen.
Anstatt zu erstellen:
class MyObjectList {
MyObject get(int index) {...}
}
class MyOtherObjectList {
MyOtherObject get(int index) {...}
}
class AnotherObjectList {
AnotherObject get(int index) {...}
}
Ich kann eine wiederverwendbare Klasse erstellen ... (für den Fall, dass Sie die Raw-Sammlung aus irgendeinem Grund nicht verwenden möchten)
class MyList<T> {
T get(int index) { ... }
}
Ich bin jetzt 3x effizienter und muss nur noch eine Kopie pflegen. Warum möchten Sie nicht weniger Code pflegen?
Dies gilt auch für Nicht-Sammlungsklassen wie a Callable<T>
oder a Reference<T>
, die mit anderen Klassen interagieren müssen. Wollen Sie wirklich erweitern Callable<T>
und Future<T>
und alle anderen zugehörigen Klasse typsichere Versionen zu erstellen?
Ich nicht.
Die Notwendigkeit einer Typumwandlung ist einer der größten Vorteile von Java-Generika , da sie zur Kompilierungszeit eine Typprüfung durchführen. Dies verringert die Möglichkeit von ClassCastException
s, die zur Laufzeit ausgelöst werden können, und kann zu robusterem Code führen.
Aber ich vermute, dass Sie sich dessen voll bewusst sind.
Jedes Mal, wenn ich Generika anschaue, bekomme ich Kopfschmerzen. Ich finde, das Beste an Java ist seine Einfachheit und minimale Syntax, und Generika sind nicht einfach und fügen eine erhebliche Menge neuer Syntax hinzu.
Anfangs habe ich auch den Nutzen von Generika nicht gesehen. Ich fing an, Java aus der 1.4-Syntax zu lernen (obwohl Java 5 zu diesem Zeitpunkt nicht verfügbar war), und als ich auf Generika stieß, hatte ich das Gefühl, dass es mehr Code zum Schreiben war, und ich verstand die Vorteile wirklich nicht.
Moderne IDEs erleichtern das Schreiben von Code mit Generika.
Die meisten modernen, anständigen IDEs sind intelligent genug, um das Schreiben von Code mit Generika zu unterstützen, insbesondere bei der Code-Vervollständigung.
Hier ist ein Beispiel für das Erstellen eines Map<String, Integer>
mit a HashMap
. Der Code, den ich eingeben müsste, lautet:
Map<String, Integer> m = new HashMap<String, Integer>();
Und in der Tat ist das eine Menge zu tippen, nur um eine neue zu machen HashMap
. In Wirklichkeit musste ich jedoch nur so viel tippen, bevor Eclipse wusste, was ich brauchte:
Map<String, Integer> m = new Ha
Ctrl+Space
Zwar musste ich HashMap
aus einer Liste von Kandidaten auswählen , aber im Grunde wusste die IDE, was sie hinzufügen sollte, einschließlich der generischen Typen. Mit den richtigen Tools ist die Verwendung von Generika nicht schlecht.
Da die Typen bekannt sind, verhält sich die IDE beim Abrufen von Elementen aus der generischen Auflistung so, als ob dieses Objekt bereits ein Objekt des deklarierten Typs ist. Es ist kein Casting erforderlich, damit die IDE den Typ des Objekts kennt ist.
Ein wesentlicher Vorteil von Generika besteht darin, dass sie mit neuen Java 5-Funktionen gut funktionieren. Hier ist ein Beispiel für das Werfen von ganzen Zahlen in a Set
und das Berechnen der Summe:
Set<Integer> set = new HashSet<Integer>();
set.add(10);
set.add(42);
int total = 0;
for (int i : set) {
total += i;
}
In diesem Code sind drei neue Java 5-Funktionen enthalten:
Erstens erlauben Generika und Autoboxing von Grundelementen die folgenden Zeilen:
set.add(10);
set.add(42);
Die Ganzzahl 10
wird automatisch in eine Integer
mit dem Wert von geschrieben 10
. (Und das gleiche für 42
). Dann Integer
wird das in das geworfen, von dem Set
bekannt ist, dass es Integer
s hält. Der Versuch, ein zu werfen, String
würde einen Kompilierungsfehler verursachen.
Als nächstes werden für jede Schleife alle drei verwendet:
for (int i : set) {
total += i;
}
Zunächst werden die Set
enthaltenden Integer
s in einer for-each-Schleife verwendet. Jedes Element wird als ein deklariert int
und dies ist zulässig, da das Integer
Element nicht in das Grundelement zurückgeführt wird int
. Und die Tatsache, dass dieses Unboxing auftritt, ist bekannt, da Generika verwendet wurden, um anzugeben, dass Integer
s in der gehalten wurden Set
.
Generika können der Klebstoff sein, der die in Java 5 eingeführten neuen Funktionen zusammenbringt, und sie machen das Codieren einfach und sicherer. Und die meiste Zeit sind IDEs klug genug, um Ihnen mit guten Vorschlägen zu helfen. Im Allgemeinen wird es also nicht viel mehr tippen.
Und ehrlich gesagt, wie aus dem Set
Beispiel hervorgeht, kann die Verwendung von Java 5-Funktionen den Code präziser und robuster machen.
Bearbeiten - Ein Beispiel ohne Generika
Das Folgende ist eine Illustration des obigen Set
Beispiels ohne die Verwendung von Generika. Es ist möglich, aber nicht gerade angenehm:
Set set = new HashSet();
set.add(10);
set.add(42);
int total = 0;
for (Object o : set) {
total += (Integer)o;
}
(Hinweis: Der obige Code generiert beim Kompilieren eine ungeprüfte Konvertierungswarnung.)
Wenn Sie nicht generische Sammlungen verwenden, sind die Typen, die in die Sammlung eingegeben werden, Objekte vom Typ Object
. Daher wird in diesem Beispiel eine Object
ist , was wird add
in die Menge ed.
set.add(10);
set.add(42);
In den obigen Zeilen ist Autoboxing im Spiel - der primitive int
Wert 10
und 42
wird in Integer
Objekte autoboxed , die dem hinzugefügt werden Set
. Beachten Sie jedoch, dass die Integer
Objekte als Object
s behandelt werden, da es keine Typinformationen gibt, die dem Compiler helfen, zu wissen, welchen Typ der Set
erwarten sollte.
for (Object o : set) {
Dies ist der entscheidende Teil. Der Grund, warum die for-each-Schleife funktioniert, liegt darin, dass Set
sie die Iterable
Schnittstelle implementiert , die Iterator
, falls vorhanden , eine mit Typinformationen zurückgibt . ( Iterator<T>
das heißt.)
Da es jedoch keine Typinformationen, die Set
kehren ein , Iterator
die die Werte in der Rückkehr wird Set
als Object
s, und deshalb ist das Element in der for-Schleife jeden abgerufen werden muß vom Typ sein Object
.
Nachdem das aus dem Object
abgerufen wurde Set
, muss es Integer
manuell in ein umgewandelt werden, um das Hinzufügen durchzuführen:
total += (Integer)o;
Hier wird eine Typumwandlung von a Object
nach a durchgeführt Integer
. In diesem Fall wissen wir, dass dies immer funktionieren wird, aber bei der manuellen Typisierung habe ich immer das Gefühl, dass es sich um fragilen Code handelt, der beschädigt werden könnte, wenn an anderer Stelle eine geringfügige Änderung vorgenommen wird. (Ich habe das Gefühl, dass jede Typografie darauf ClassCastException
wartet, passiert zu werden, aber ich schweife ab ...)
Das Integer
ist jetzt in eine entpackt int
und darf die Addition in die int
Variable durchführen total
.
Ich hoffe, ich konnte veranschaulichen, dass die neuen Funktionen von Java 5 mit nicht generischem Code verwendet werden können, aber es ist nicht so sauber und unkompliziert wie das Schreiben von Code mit generischem Code. Um die neuen Funktionen in Java 5 optimal nutzen zu können, sollte man sich meiner Meinung nach mit Generika befassen, wenn zumindest Überprüfungen zur Kompilierungszeit möglich sind, um zu verhindern, dass ungültige Typecasts zur Laufzeit Ausnahmen auslösen.
Wenn Sie die Java-Fehlerdatenbank kurz vor der Veröffentlichung von 1.5 durchsuchen würden, würden Sie siebenmal mehr Fehler mit NullPointerException
als finden ClassCastException
. Es scheint also keine großartige Funktion zu sein, Fehler zu finden, oder zumindest Fehler, die nach einem kleinen Rauchtest bestehen bleiben.
Für mich ist der große Vorteil von Generika, dass sie wichtige Typinformationen im Code dokumentieren . Wenn ich nicht wollte, dass diese Typinformationen im Code dokumentiert werden, würde ich eine dynamisch typisierte Sprache oder zumindest eine Sprache mit impliziterer Typinferenz verwenden.
Die Sammlungen eines Objekts für sich zu behalten, ist kein schlechter Stil (aber der übliche Stil besteht darin, die Kapselung effektiv zu ignorieren). Es kommt eher darauf an, was Sie tun. Das Übergeben von Sammlungen an "Algorithmen" ist mit Generika etwas einfacher (zur oder vor der Kompilierungszeit) zu überprüfen.
Generika in Java erleichtern den parametrischen Polymorphismus . Mithilfe von Typparametern können Sie Argumente an Typen übergeben. So wie eine Methode wie ein String foo(String s)
bestimmtes Verhalten modelliert, nicht nur für eine bestimmte Zeichenfolge, sondern für eine beliebige Zeichenfolge s
, so List<T>
modelliert ein Typ wie ein bestimmtes Verhalten, nicht nur für einen bestimmten Typ, sondern für einen beliebigen Typ . List<T>
sagt, dass es für jeden Typ T
einen Typ gibt, List
dessen Elemente T
s sind . Ist List
also eigentlich ein Typkonstruktor . Es nimmt einen Typ als Argument und erstellt als Ergebnis einen anderen Typ.
Hier sind einige Beispiele für generische Typen, die ich jeden Tag verwende. Erstens eine sehr nützliche generische Schnittstelle:
public interface F<A, B> {
public B f(A a);
}
Diese Schnittstelle besagt, dass es für zwei Typen A
und B
eine Funktion (aufgerufen f
) gibt, die a nimmt A
und a zurückgibt B
. Wenn Sie diese Schnittstelle implementieren, A
und B
kann jede Art sein , das Sie wollen, solange Sie eine Funktion zur Verfügung stellen f
, die die ehemalige nimmt und die letztere. Hier ist eine Beispielimplementierung der Schnittstelle:
F<Integer, String> intToString = new F<Integer, String>() {
public String f(int i) {
return String.valueOf(i);
}
}
Vor Generika wurde Polymorphismus durch Unterklassen mit dem extends
Schlüsselwort erreicht. Mit Generika können wir die Unterklasse tatsächlich beseitigen und stattdessen parametrischen Polymorphismus verwenden. Betrachten Sie beispielsweise eine parametrisierte (generische) Klasse, die zum Berechnen von Hash-Codes für einen beliebigen Typ verwendet wird. Anstatt Object.hashCode () zu überschreiben, würden wir eine generische Klasse wie diese verwenden:
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);
}
}
Dies ist viel flexibler als die Verwendung von Vererbung, da wir beim Thema der Verwendung von Komposition und parametrischem Polymorphismus bleiben können, ohne spröde Hierarchien zu blockieren.
Javas Generika sind jedoch nicht perfekt. Sie können über Typen abstrahieren, aber Sie können beispielsweise nicht über Typkonstruktoren abstrahieren. Das heißt, Sie können "für jeden Typ T" sagen, aber Sie können nicht "für jeden Typ T sagen, der einen Typparameter A annimmt".
Ich habe hier einen Artikel über diese Grenzen von Java-Generika geschrieben.
Ein großer Gewinn bei Generika ist, dass Sie damit Unterklassen vermeiden können. Unterklassen führen in der Regel zu spröden Klassenhierarchien, deren Erweiterung schwierig ist, und zu Klassen, die einzeln schwer zu verstehen sind, ohne die gesamte Hierarchie zu betrachten.
Wereas vor Generika könnten Sie Klassen wie Widget
durch erweitert FooWidget
, BarWidget
und BazWidget
mit Generika können Sie eine einzelne generische Klasse , Widget<A>
die eine nimmt Foo
, Bar
oder Baz
in seinem Konstruktor Sie zu geben Widget<Foo>
, Widget<Bar>
und Widget<Baz>
.
Generika vermeiden den Leistungseinbruch beim Boxen und Unboxen. Schauen Sie sich grundsätzlich ArrayList vs List <T> an. Beide erledigen die gleichen Kernaufgaben, aber List <T> ist viel schneller, da Sie nicht zum / vom Objekt boxen müssen.
Der beste Vorteil von Generics ist die Wiederverwendung von Code. Nehmen wir an, Sie haben viele Geschäftsobjekte und schreiben SEHR ähnlichen Code für jede Entität, um dieselben Aktionen auszuführen. (IE Linq to SQL-Operationen).
Mit Generics können Sie eine Klasse erstellen, die mit einem der Typen arbeiten kann, die von einer bestimmten Basisklasse erben, oder eine bestimmte Schnittstelle wie folgt implementieren:
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" });
}
}
Beachten Sie die Wiederverwendung desselben Dienstes angesichts der verschiedenen Typen in der obigen DoSomething-Methode. Wirklich elegant!
Es gibt viele andere gute Gründe, Generika für Ihre Arbeit zu verwenden. Dies ist mein Favorit.
Ich mag sie nur, weil sie Ihnen eine schnelle Möglichkeit bieten, einen benutzerdefinierten Typ zu definieren (da ich sie sowieso verwende).
Anstatt beispielsweise eine Struktur zu definieren, die aus einer Zeichenfolge und einer Ganzzahl besteht, und dann eine ganze Reihe von Objekten und Methoden für den Zugriff auf ein Array dieser Strukturen usw. implementieren zu müssen, können Sie einfach ein Wörterbuch erstellen
Dictionary<int, string> dictionary = new Dictionary<int, string>();
Und der Compiler / die IDE erledigt den Rest des schweren Hebens. Insbesondere in einem Wörterbuch können Sie den ersten Typ als Schlüssel verwenden (keine wiederholten Werte).
Typisierte Sammlungen - auch wenn Sie sie nicht verwenden möchten, müssen Sie sie wahrscheinlich aus anderen Bibliotheken und Quellen verarbeiten.
Generische Eingabe bei der Klassenerstellung:
öffentliche Klasse Foo <T> {public T get () ...
Vermeidung von Casting - Ich habe Dinge wie immer nicht gemocht
neuer Komparator {public int compareTo (Objekt o) {if (o Instanz von classIcareAbout) ...
Wo Sie im Wesentlichen nach einer Bedingung suchen, die nur vorhanden sein sollte, weil die Schnittstelle in Objekten ausgedrückt wird.
Meine anfängliche Reaktion auf Generika war ähnlich wie Ihre - "zu chaotisch, zu kompliziert". Ich habe die Erfahrung gemacht, dass man sich nach einer kurzen Verwendung an sie gewöhnt und sich Code ohne sie weniger klar spezifiziert und einfach weniger komfortabel anfühlt. Abgesehen davon verwendet der Rest der Java-Welt sie, so dass Sie irgendwann mit dem Programm fertig werden müssen, oder?
Um ein gutes Beispiel zu geben. Stellen Sie sich vor, Sie haben eine Klasse namens Foo
public class Foo
{
public string Bar() { return "Bar"; }
}
Beispiel 1 Nun möchten Sie eine Sammlung von Foo-Objekten haben. Sie haben zwei Optionen, LIst oder ArrayList, die beide auf ähnliche Weise funktionieren.
Arraylist al = new ArrayList();
List<Foo> fl = new List<Foo>();
//code to add Foos
al.Add(new Foo());
f1.Add(new Foo());
Wenn ich im obigen Code versuche, eine Klasse von FireTruck anstelle von Foo hinzuzufügen, fügt die ArrayList diese hinzu, aber die generische Liste von Foo führt dazu, dass eine Ausnahme ausgelöst wird.
Beispiel zwei.
Jetzt haben Sie Ihre zwei Array-Listen und möchten jeweils die Bar () -Funktion aufrufen. Da die ArrayList mit Objekten gefüllt ist, müssen Sie diese umwandeln, bevor Sie bar aufrufen können. Da die generische Liste von Foo jedoch nur Foos enthalten kann, können Sie Bar () direkt auf diesen aufrufen.
foreach(object o in al)
{
Foo f = (Foo)o;
f.Bar();
}
foreach(Foo f in fl)
{
f.Bar();
}
Haben Sie noch nie eine Methode (oder eine Klasse) geschrieben, bei der das Schlüsselkonzept der Methode / Klasse nicht eng an einen bestimmten Datentyp der Parameter / Instanzvariablen gebunden war (denken Sie an verknüpfte Liste, Max / Min-Funktionen, binäre Suche)? , etc.).
Wünschten Sie sich nicht jemals, Sie könnten den Algorithmus / Code wiederverwenden, ohne auf die Wiederverwendung durch Ausschneiden und Einfügen zurückzugreifen oder die starke Typisierung zu beeinträchtigen (z. B. möchte ich einen List
von Strings, keinen List
von Dingen, von denen ich hoffe, dass sie Strings sind!)?
Deshalb sollten Sie wollen Generika (oder etwas besser) zu verwenden.
Vergessen Sie nicht, dass Generika nicht nur von Klassen verwendet werden, sondern auch von Methoden. Nehmen Sie zum Beispiel das folgende Snippet:
private <T extends Throwable> T logAndReturn(T t) {
logThrowable(t); // some logging method that takes a Throwable
return t;
}
Es ist einfach, kann aber sehr elegant verwendet werden. Das Schöne ist, dass die Methode alles zurückgibt, was sie gegeben hat. Dies ist hilfreich, wenn Sie Ausnahmen behandeln, die erneut an den Anrufer zurückgegeben werden müssen:
...
} catch (MyException e) {
throw logAndReturn(e);
}
Der Punkt ist, dass Sie den Typ nicht verlieren, indem Sie ihn durch eine Methode übergeben. Sie können den richtigen Ausnahmetyp anstelle von nur a auslösen Throwable
, was alles wäre, was Sie ohne Generika tun könnten.
Dies ist nur ein einfaches Beispiel für eine Verwendung für generische Methoden. Es gibt noch einige andere nette Dinge, die Sie mit generischen Methoden tun können. Das coolste ist meiner Meinung nach der Typ, der auf Generika schließen lässt. Nehmen Sie das folgende Beispiel (entnommen aus Josh Blochs Effective Java 2nd Edition):
...
Map<String, Integer> myMap = createHashMap();
...
public <K, V> Map<K, V> createHashMap() {
return new HashMap<K, V>();
}
Dies macht nicht viel, verringert jedoch die Unordnung, wenn die generischen Typen lang (oder verschachtelt, dh Map<String, List<String>>
) sind.
Throwable
aus einem Methodenkörper mit bestimmten deklarierten Ausnahmen werfen können . Die Alternative besteht darin, entweder separate Methoden zu schreiben, um jeden Ausnahmetyp zurückzugeben, oder die Umwandlung selbst mit einer nicht generischen Methode durchzuführen, die a zurückgibt Throwable
. Ersteres ist zu ausführlich und ziemlich nutzlos, und letzteres wird vom Compiler keine Hilfe erhalten. Durch die Verwendung von Generika fügt der Compiler automatisch die richtige Besetzung für Sie ein. Die Frage ist also: Ist das die Komplexität wert?
Der Hauptvorteil ist, wie Mitchel betont, die starke Typisierung, ohne dass mehrere Klassen definiert werden müssen.
Auf diese Weise können Sie Dinge tun wie:
List<SomeCustomClass> blah = new List<SomeCustomClass>();
blah[0].SomeCustomFunction();
Ohne Generika müssten Sie blah [0] auf den richtigen Typ umstellen, um auf seine Funktionen zugreifen zu können.
Das JVM-Casting wird trotzdem ausgeführt. Es wird implizit Code erstellt, der den generischen Typ als "Objekt" behandelt und Casts für die gewünschte Instanziierung erstellt. Java-Generika sind nur syntaktischer Zucker.
Ich weiß, dass dies eine C # -Frage ist, aber Generika werden auch in anderen Sprachen verwendet und ihre Verwendung / Ziele sind ziemlich ähnlich.
Java-Sammlungen verwenden Generika seit Java 1.5. Ein guter Ort, um sie zu verwenden, ist, wenn Sie Ihr eigenes sammlungsähnliches Objekt erstellen.
Ein Beispiel, das ich fast überall sehe, ist eine Pair-Klasse, die zwei Objekte enthält, aber generisch mit diesen Objekten umgehen muss.
class Pair<F, S> {
public final F first;
public final S second;
public Pair(F f, S s)
{
first = f;
second = s;
}
}
Wann immer Sie diese Pair-Klasse verwenden, können Sie angeben, mit welcher Art von Objekten sie behandelt werden soll, und alle Arten von Cast-Problemen werden zur Kompilierungszeit und nicht zur Laufzeit angezeigt.
Bei Generika können die Grenzen auch mit den Schlüsselwörtern "Super" und "Erweitert" definiert werden. Wenn Sie beispielsweise mit einem generischen Typ arbeiten möchten, aber sicherstellen möchten, dass er eine Klasse namens Foo (mit einer setTitle-Methode) erweitert:
public class FooManager <F extends Foo>{
public void setTitle(F foo, String title) {
foo.setTitle(title);
}
}
Obwohl es für sich genommen nicht sehr interessant ist, ist es nützlich zu wissen, dass Sie bei jedem Umgang mit einem FooManager wissen, dass er MyClass-Typen verarbeitet und dass MyClass Foo erweitert.
In der Sun Java-Dokumentation als Antwort auf "Warum sollte ich Generika verwenden?":
"Generics bietet Ihnen die Möglichkeit, dem Compiler den Typ einer Sammlung mitzuteilen, damit dieser überprüft werden kann. Sobald der Compiler den Elementtyp der Sammlung kennt, kann der Compiler überprüfen, ob Sie die Sammlung konsistent verwendet haben, und einfügen Die korrekte Umwandlung von Werten, die aus der Sammlung entfernt werden ... Der Code, der Generika verwendet, ist klarer und sicherer. Der Compiler kann zur Kompilierungszeit überprüfen, ob die Typeinschränkungen zur Laufzeit nicht verletzt werden [Hervorhebung von mir] Wenn das Programm ohne Warnungen kompiliert wird, können wir mit Sicherheit feststellen, dass es zur Laufzeit keine ClassCastException auslöst. Der Nettoeffekt der Verwendung von Generika, insbesondere in großen Programmen, ist eine verbesserte Lesbarkeit und Robustheit . [Hervorhebung meiner] "
Mit Generika können Sie Objekte erstellen, die stark typisiert sind, ohne jedoch den spezifischen Typ definieren zu müssen. Ich denke, das beste nützliche Beispiel ist die Liste und ähnliche Klassen.
Mit der generischen Liste können Sie eine Listenlistenliste haben, was immer Sie wollen, und Sie können immer auf die starke Typisierung verweisen, Sie müssen nicht konvertieren oder etwas, wie Sie es mit einem Array oder einer Standardliste tun würden.
Mit Generics können Sie eine starke Typisierung für Objekte und Datenstrukturen verwenden, die in der Lage sein sollten, jedes Objekt aufzunehmen. Außerdem werden mühsame und teure Typecasts beim Abrufen von Objekten aus generischen Strukturen (Boxen / Unboxen) vermieden.
Ein Beispiel, das beide verwendet, ist eine verknüpfte Liste. Was nützt eine verknüpfte Listenklasse, wenn sie nur das Objekt Foo verwenden könnte? Um eine verknüpfte Liste zu implementieren, die jede Art von Objekt verarbeiten kann, müssen die verknüpfte Liste und die Knoten in einer hypothetischen inneren Knotenklasse generisch sein, wenn die Liste nur einen Objekttyp enthalten soll.
Ein weiterer Vorteil der Verwendung von Generika (insbesondere bei Sammlungen / Listen) besteht darin, dass Sie die Typprüfung für die Kompilierungszeit erhalten. Dies ist sehr nützlich, wenn Sie eine generische Liste anstelle einer Liste von Objekten verwenden.
Single meisten Grund ist , sie bieten Typsicherheit
List<Customer> custCollection = new List<Customer>;
im Gegensatz zu,
object[] custCollection = new object[] { cust1, cust2 };
als einfaches Beispiel.
Zusammenfassend lässt sich sagen, dass Sie mit Generika genauer angeben können, was Sie tun möchten (stärkere Eingabe).
Dies hat mehrere Vorteile für Sie:
Da der Compiler mehr darüber weiß, was Sie tun möchten, können Sie viel Typ-Casting weglassen, da er bereits weiß, dass der Typ kompatibel ist.
Dadurch erhalten Sie auch früheres Feedback zur Richtigkeit Ihres Programms. Dinge, die zuvor zur Laufzeit fehlgeschlagen wären (z. B. weil ein Objekt nicht in den gewünschten Typ umgewandelt werden konnte), schlagen jetzt zur Kompilierungszeit fehl und Sie können den Fehler beheben, bevor Ihre Testabteilung einen kryptischen Fehlerbericht einreicht.
Der Compiler kann weitere Optimierungen vornehmen, z. B. das Vermeiden von Boxen usw.
Ein paar Dinge, die hinzugefügt / erweitert werden müssen (aus der Sicht von .NET):
Mit generischen Typen können Sie rollenbasierte Klassen und Schnittstellen erstellen. Dies wurde bereits in grundlegenderen Begriffen gesagt, aber ich finde, Sie beginnen, Ihren Code mit Klassen zu entwerfen, die typunabhängig implementiert sind - was zu hoch wiederverwendbarem Code führt.
Generische Argumente zu Methoden können dasselbe bewirken, aber sie helfen auch dabei, das Prinzip "Tell Don't Ask" auf das Casting anzuwenden, dh "gib mir, was ich will, und wenn du nicht kannst, sagst du mir warum".
Ich verwende sie zum Beispiel in einem mit SpringORM und Hibernate implementierten GenericDao, das so aussieht
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
}
}));
}
.
.
.
}
Durch die Verwendung von Generika zwingen meine Implementierungen dieser DAOs den Entwickler, ihnen nur die Entitäten zu übergeben, für die sie entwickelt wurden, indem sie nur das GenericDao unterordnen
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
}
Mein kleines Framework ist robuster (habe Dinge wie Filtern, Lazy-Loading, Suchen). Ich habe es hier nur vereinfacht, um Ihnen ein Beispiel zu geben
Ich sagte, wie Steve und Sie, am Anfang "Zu chaotisch und kompliziert", aber jetzt sehe ich seine Vorteile
Offensichtliche Vorteile wie "Typensicherheit" und "kein Gießen" werden bereits erwähnt, sodass ich vielleicht über einige andere "Vorteile" sprechen kann, von denen ich hoffe, dass sie helfen.
Erstens sind Generika ein sprachunabhängiges Konzept, und IMO ist es möglicherweise sinnvoller, wenn Sie gleichzeitig über regulären (Laufzeit-) Polymorphismus nachdenken.
Zum Beispiel hat der Polymorphismus, wie wir ihn aus dem objektorientierten Design kennen, einen Laufzeitbegriff, bei dem das Aufruferobjekt zur Laufzeit während der Programmausführung ermittelt wird und die entsprechende Methode je nach Laufzeittyp entsprechend aufgerufen wird. Bei Generika ist die Idee etwas ähnlich, aber alles geschieht zur Kompilierungszeit. Was bedeutet das und wie nutzen Sie es?
(Bleiben wir bei generischen Methoden, um sie kompakt zu halten.) Dies bedeutet, dass Sie dieselbe Methode immer noch für separate Klassen verwenden können (wie zuvor in polymorphen Klassen), diesmal jedoch vom Compiler automatisch generiert werden. Dies hängt von den festgelegten Typen ab zur Kompilierungszeit. Sie parametrisieren Ihre Methoden anhand des Typs, den Sie zur Kompilierungszeit angeben. Anstatt die Methoden für jeden einzelnen Typ von Grund auf neu zu schreiben, wie Sie es beim Laufzeitpolymorphismus tun (Methodenüberschreibung), lassen Sie Compiler die Arbeit während der Kompilierung erledigen. Dies hat einen offensichtlichen Vorteil, da Sie nicht alle möglichen Typen ableiten müssen, die in Ihrem System verwendet werden könnten, wodurch es ohne Codeänderung weitaus skalierbarer wird.
Der Unterricht funktioniert ziemlich ähnlich. Sie parametrisieren den Typ und der Code wird vom Compiler generiert.
Sobald Sie die Idee der "Kompilierungszeit" haben, können Sie "begrenzte" Typen verwenden und einschränken, was über Klassen / Methoden als parametrisierter Typ übergeben werden kann. Sie können also steuern, was durchlaufen werden soll. Dies ist eine leistungsstarke Sache, insbesondere wenn Sie ein Framework haben, das von anderen Personen verwendet wird.
public interface Foo<T extends MyObject> extends Hoo<T>{
...
}
Niemand außer MyObject kann jetzt etwas anderes einstellen.
Außerdem können Sie Typbeschränkungen für Ihre Methodenargumente "erzwingen", was bedeutet, dass Sie sicherstellen können, dass beide Methodenargumente vom gleichen Typ abhängen.
public <T extends MyObject> foo(T t1, T t2){
...
}
Hoffe, das alles macht Sinn.
Ich habe einmal einen Vortrag zu diesem Thema gehalten. Sie finden meine Folien, meinen Code und meine Audioaufzeichnung unter http://www.adventuresinsoftware.com/generics/ .
Die Verwendung von Generika für Sammlungen ist einfach und sauber. Selbst wenn Sie überall darauf herumstochern, ist der Gewinn aus den Sammlungen für mich ein Gewinn.
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();
}
oder
List stuffList = getStuff();
for(int i = 0; i < stuffList.size(); i++) {
Stuff stuff = (Stuff)stuffList.get(i);
stuff.do();
}
Das allein ist die marginalen "Kosten" von Generika wert, und Sie müssen kein generischer Guru sein, um dies zu nutzen und Wert zu erhalten.
Generika bieten Ihnen auch die Möglichkeit, wiederverwendbarere Objekte / Methoden zu erstellen und gleichzeitig typspezifische Unterstützung zu bieten. In einigen Fällen erhalten Sie auch viel Leistung. Ich kenne nicht die vollständige Spezifikation für die Java-Generika, aber in .NET kann ich Einschränkungen für den Type-Parameter angeben, z. B. Implementiert eine Schnittstelle, einen Konstruktor und eine Ableitung.