JavaRush /Java-Blog /Random-DE /Generika für Katzen
Viacheslav
Level 3

Generika für Katzen

Veröffentlicht in der Gruppe Random-DE
Generika für Katzen – 1

Einführung

Heute ist ein großartiger Tag, um sich an das zu erinnern, was wir über Java wissen. Laut dem wichtigsten Dokument, d.h. Java-Sprachspezifikation (JLS – Java Language Specifiaction), Java ist eine stark typisierte Sprache, wie im Kapitel „ Kapitel 4. Typen, Werte und Variablen “ beschrieben. Was bedeutet das? Nehmen wir an, wir haben eine Hauptmethode:

public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
Durch starke Typisierung wird sichergestellt, dass der Compiler beim Kompilieren dieses Codes prüft, ob wir, wenn wir den Typ der Textvariablen als String angegeben haben, nicht versuchen, sie irgendwo als Variable eines anderen Typs (z. B. als Ganzzahl) zu verwenden. . Wenn wir beispielsweise versuchen, einen Wert anstelle von Text zu speichern 2L(d. h. long anstelle von String), erhalten wir beim Kompilieren eine Fehlermeldung:

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
Diese. Mit der starken Typisierung können Sie sicherstellen, dass Vorgänge an Objekten nur dann ausgeführt werden, wenn diese Vorgänge für diese Objekte zulässig sind. Dies wird auch Typensicherheit genannt. Wie im JLS angegeben, gibt es in Java zwei Kategorien von Typen: primitive Typen und Referenztypen. Sie können sich an primitive Typen aus dem Übersichtsartikel erinnern: „ Primitive Typen in Java: Sie sind nicht so primitiv “. Referenztypen können durch eine Klasse, eine Schnittstelle oder ein Array dargestellt werden. Und heute interessieren wir uns für Referenztypen. Und fangen wir mit Arrays an:

class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
Dieser Code läuft ohne Fehler. Wie wir wissen (zum Beispiel aus „ Oracle Java Tutorial: Arrays “) ist ein Array ein Container, der Daten nur eines Typs speichert. In diesem Fall nur Zeilen. Versuchen wir, long anstelle von String zum Array hinzuzufügen:
text[1] = 4L;
Lassen Sie uns diesen Code ausführen (zum Beispiel im Repl.it Online Java Compiler ) und eine Fehlermeldung erhalten:
error: incompatible types: long cannot be converted to String
Das Array und die Typsicherheit der Sprache erlaubten es uns nicht, in einem Array zu speichern, was nicht zum Typ passte. Dies ist eine Manifestation der Typensicherheit. Uns wurde gesagt: „Beheben Sie den Fehler, aber bis dahin werde ich den Code nicht kompilieren.“ Und das Wichtigste dabei ist, dass dies zum Zeitpunkt der Kompilierung geschieht und nicht beim Start des Programms. Das heißt, wir sehen Fehler sofort und nicht „irgendwann“. Und da wir uns an Arrays erinnern, erinnern wir uns auch an das Java Collections Framework . Wir hatten dort unterschiedliche Strukturen. Zum Beispiel Listen. Schreiben wir das Beispiel um:

import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
Beim Kompilieren erhalten wir testeine Fehlermeldung in der Variableninitialisierungszeile:
incompatible types: Object cannot be converted to String
In unserem Fall kann List jedes Objekt speichern (d. h. ein Objekt vom Typ Object). Daher sagt der Compiler, dass er eine solche Verantwortungslast nicht übernehmen kann. Daher müssen wir den Typ, den wir aus der Liste erhalten, explizit angeben:
String test = (String) text.get(0);
Diese Angabe wird Typkonvertierung oder Typumwandlung genannt. Und alles wird jetzt gut funktionieren, bis wir versuchen, das Element auf Index 1 zu bekommen, denn es ist vom Typ Long. Und wir werden einen fairen Fehler bekommen, aber schon während das Programm läuft (in Runtime):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
Wie wir sehen, gibt es hier mehrere wichtige Nachteile. Erstens sind wir gezwungen, den aus der Liste erhaltenen Wert in die String-Klasse „umzuwandeln“. Stimme zu, das ist hässlich. Zweitens sehen wir im Falle eines Fehlers diesen erst, wenn das Programm ausgeführt wird. Wenn unser Code komplexer wäre, könnten wir einen solchen Fehler möglicherweise nicht sofort erkennen. Und die Entwickler begannen darüber nachzudenken, wie sie die Arbeit in solchen Situationen einfacher und den Code klarer machen könnten. Und sie wurden geboren – Generics.
Generika für Katzen – 2

Generika

Also Generika. Was ist es? Ein Generic ist eine spezielle Art der Beschreibung der verwendeten Typen, die der Code-Compiler bei seiner Arbeit zur Gewährleistung der Typsicherheit verwenden kann. Es sieht ungefähr so ​​aus:
Generika für Katzen – 3
Hier ein kurzes Beispiel und eine Erklärung:

import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
In diesem Beispiel sagen wir, dass wir nicht nur List, sondern haben List, was NUR mit Objekten vom Typ String funktioniert. Und keine anderen. Was gerade in Klammern steht, können wir speichern. Solche „Klammern“ nennt man „Spitze Klammern“, d.h. spitze Klammern. Der Compiler wird freundlicherweise für uns prüfen, ob wir bei der Arbeit mit einer Liste von Zeichenfolgen (die Liste heißt Text) Fehler gemacht haben. Der Compiler wird erkennen, dass wir dreist versuchen, Long in die String-Liste aufzunehmen. Und beim Kompilieren wird ein Fehler angezeigt:
error: no suitable method found for add(long)
Sie haben sich vielleicht daran erinnert, dass String ein Nachkomme von CharSequence ist. Und entscheiden Sie sich für etwas wie:

public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
Dies ist jedoch nicht möglich und wir erhalten die Fehlermeldung: error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> Es scheint seltsam, weil. Die Zeile CharSequence sec = "test";enthält keine Fehler. Lass es uns herausfinden. Über dieses Verhalten sagen sie: „Generika sind invariant.“ Was ist eine „Invariante“? Mir gefällt, wie dazu auf Wikipedia im Artikel „ Kovarianz und Kontravarianz “ gesagt wird:
Generika für Katzen – 4
Somit ist Invarianz das Fehlen einer Vererbung zwischen abgeleiteten Typen. Wenn Cat ein Untertyp von Animals ist, dann ist Set<Cats> kein Untertyp von Set<Animals> und Set<Animals> ist kein Untertyp von Set<Cats>. Übrigens ist es erwähnenswert, dass ab Java SE 7 der sogenannte „ Diamond Operator “ erschien. Denn die beiden spitzen Klammern <> sind wie eine Raute. Dadurch können wir Generika wie diese verwenden:

public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
Basierend auf diesem Code versteht der Compiler, dass wir, wenn wir auf der linken Seite angeben, dass es ListObjekte vom Typ String enthalten wird, auf der rechten Seite meinen, dass wir lineseine neue ArrayList in einer Variablen speichern möchten, die auch ein Objekt speichert des links angegebenen Typs. Der Compiler der linken Seite versteht also den Typ für die rechte Seite oder leitet ihn ab. Aus diesem Grund wird dieses Verhalten im Englischen als Typinferenz oder „Type Inference“ bezeichnet. Eine weitere interessante Sache, die es zu erwähnen gilt, sind RAW-Typen oder „Rohtypen“. Weil Generika gab es nicht immer, und Java versucht, wann immer möglich, die Abwärtskompatibilität aufrechtzuerhalten. Dann sind Generika gezwungen, irgendwie mit Code zu arbeiten, in dem kein Generikum angegeben ist. Sehen wir uns ein Beispiel an:
List<CharSequence> lines = new ArrayList<String>();
Wie wir uns erinnern, lässt sich eine solche Zeile aufgrund der Invarianz von Generika nicht kompilieren.
List<Object> lines = new ArrayList<String>();
Und dieses lässt sich aus dem gleichen Grund auch nicht kompilieren.

List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
Solche Zeilen werden kompiliert und funktionieren. In ihnen werden Rohtypen verwendet, d. h. nicht spezifizierte Typen. Es lohnt sich noch einmal darauf hinzuweisen, dass Rohtypen in modernem Code NICHT verwendet werden sollten.
Generika für Katzen – 5

Typisierte Klassen

Also getippte Klassen. Mal sehen, wie wir unsere eigene typisierte Klasse schreiben können. Wir haben zum Beispiel eine Klassenhierarchie:

public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
Wir möchten eine Klasse erstellen, die einen Tiercontainer implementiert. Es wäre möglich, eine Klasse zu schreiben, die beliebige enthält Animal. Das ist einfach und verständlich, ABER... Hunde und Katzen zu vermischen ist schlecht, sie sind keine Freunde miteinander. Wenn jemand einen solchen Behälter erhält, kann es außerdem passieren, dass er versehentlich Katzen aus dem Behälter in ein Rudel Hunde wirft... und das hat nichts Gutes. Und hier helfen uns Generika. Schreiben wir die Implementierung beispielsweise so:

public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
Unsere Klasse arbeitet mit Objekten des Typs, der durch einen generischen Namen namens T angegeben wird. Dies ist eine Art Alias. Weil Das Generic wird im Klassennamen angegeben, dann erhalten wir es bei der Deklaration der Klasse:

public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
Wie wir sehen können, haben wir angegeben, dass wir haben Box, was nur mit funktioniert Cat. Der Compiler hat erkannt, dass Sie catBoxanstelle eines Generikums Tden Typ überall dort ersetzen müssen, Catwo der Name des Generikums angegeben ist T:
Generika für Katzen – 6
Diese. Es ist Box<Cat>dem Compiler zu verdanken, dass er versteht, was slotses eigentlich sein soll List<Cat>. Denn Box<Dog>im Inneren wird es sein slots, enthaltend List<Dog>. Eine Typdeklaration kann mehrere Generika enthalten, zum Beispiel:
public static class Box<T, V> {
Der generische Name kann beliebig sein, es wird jedoch empfohlen, einige unausgesprochene Regeln einzuhalten – „Konventionen zur Benennung von Typparametern“: Elementtyp – E, Schlüsseltyp – K, Zahlentyp – N, T – für Typ, V – für Werttyp . Denken Sie übrigens daran, dass wir gesagt haben, dass Generika invariant sind, d. h. Behalten Sie die Vererbungshierarchie nicht bei. Tatsächlich können wir darauf Einfluss nehmen. Das heißt, wir haben die Möglichkeit, Generika CO-varianten zu machen, d. h. Erbschaften in der gleichen Reihenfolge halten. Dieses Verhalten wird als „Bounded Type“ bezeichnet, d. h. begrenzte Typen. Unsere Klasse Boxkönnte zum Beispiel alle Tiere enthalten, dann würden wir ein Generikum wie dieses deklarieren:
public static class Box<T extends Animal> {
Das heißt, wir legen die Obergrenze für die Klasse fest Animal. Wir können auch mehrere Typen nach dem Schlüsselwort angeben extends. Das bedeutet, dass der Typ, mit dem wir arbeiten, von einer Klasse abstammen und gleichzeitig eine Schnittstelle implementieren muss. Zum Beispiel:
public static class Box<T extends Animal & Comparable> {
Wenn wir in diesem Fall versuchen, Boxetwas einzufügen, das kein Vererber ist Animalund nicht implementiert wird Comparable, erhalten wir beim Kompilieren eine Fehlermeldung:
error: type argument Cat is not within bounds of type-variable T
Generika für Katzen – 7

Methodentypisierung

Generika werden nicht nur in Typen, sondern auch in einzelnen Methoden verwendet. Die Anwendung der Methoden ist im offiziellen Tutorial zu sehen: „ Generics Methods “.

Hintergrund:

Generika für Katzen – 8
Schauen wir uns dieses Bild an. Wie Sie sehen können, schaut sich der Compiler die Methodensignatur an und stellt fest, dass wir eine undefinierte Klasse als Eingabe verwenden. Durch die Signatur wird nicht festgestellt, dass wir eine Art Objekt zurückgeben, d. h. Objekt. Wenn wir also beispielsweise eine ArrayList erstellen möchten, müssen wir Folgendes tun:
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
Sie müssen explizit schreiben, dass die Ausgabe eine ArrayList sein wird, was hässlich ist und die Möglichkeit erhöht, einen Fehler zu machen. Wir können zum Beispiel solchen Unsinn schreiben und er wird kompiliert:
ArrayList object = (ArrayList) createObject(LinkedList.class);
Können wir dem Compiler helfen? Ja, Generika ermöglichen uns dies. Schauen wir uns das gleiche Beispiel an:
Generika für Katzen – 9
Dann können wir ein Objekt einfach so erstellen:
ArrayList<String> object = createObject(ArrayList.class);
Generika für Katzen – 10

WildCard

Laut Oracles Tutorial zu Generics, insbesondere im Abschnitt „ Wildcards “, können wir einen „unbekannten Typ“ mit einem Fragezeichen beschreiben. Wildcard ist ein praktisches Tool, um einige der Einschränkungen von Generika zu mildern. Wie wir beispielsweise bereits besprochen haben, sind Generika invariant. Dies bedeutet, dass alle Klassen zwar Nachkommen (Subtypen) des Objekttyps sind, es List<любой тип>sich jedoch nicht um einen Subtyp handelt List<Object>. ABER List<любой тип>es ist ein Untertyp List<?>. Wir können also den folgenden Code schreiben:

public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
Wie reguläre Generika (d. h. ohne Verwendung von Wildcards) können Generika mit Wildcards eingeschränkt werden. Der obere begrenzte Platzhalter kommt mir bekannt vor:

public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
Sie können es aber auch durch den Platzhalter „Untere Grenze“ einschränken:
public static void printCatList(List<? super Cat> list) {
Somit beginnt die Methode, alle Katzen sowie höhere Hierarchieebenen (bis zum Objekt) zu akzeptieren.
Generika für Katzen – 11

Geben Sie „Löschen“ ein

Apropos Generika: Es lohnt sich, etwas über „Type Erasing“ zu wissen. Tatsächlich geht es bei der Typlöschung um die Tatsache, dass Generika Informationen für den Compiler sind. Während der Programmausführung erfolgt keine weitere Information über Generics, dies wird als „Löschen“ bezeichnet. Diese Löschung bewirkt, dass der generische Typ durch den spezifischen Typ ersetzt wird. Wenn das Generikum keine Grenze hatte, wird der Objekttyp ersetzt. Wenn der Rahmen angegeben wurde (z. B. <T extends Comparable>), wird er ersetzt. Hier ist ein Beispiel aus Oracles Tutorial: „ Erasure of Generic Types “:
Generika für Katzen – 12
Wie oben erwähnt, Twird in diesem Beispiel das Generikum bis zum Rand gelöscht, d. h. Vor Comparable.
Generika für Katzen – 13

Abschluss

Generika sind ein sehr interessantes Thema. Ich hoffe, dass dieses Thema für Sie von Interesse ist. Zusammenfassend können wir sagen, dass Generics ein hervorragendes Werkzeug sind, das Entwickler erhalten haben, um den Compiler mit zusätzlichen Informationen zu versorgen, um einerseits Typsicherheit und andererseits Flexibilität zu gewährleisten. Und wenn Sie interessiert sind, empfehle ich Ihnen, einen Blick auf die Ressourcen zu werfen, die mir gefallen haben: #Wjatscheslaw
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION