CodeGym /Java-Blog /Random-DE /Platzhalter in Generika
Autor
Artem Divertitto
Senior Android Developer at United Tech

Platzhalter in Generika

Veröffentlicht in der Gruppe Random-DE
Hallo! Lassen Sie uns unser Studium der Generika fortsetzen. Sie haben in früheren Lektionen bereits umfangreiches Wissen darüber erworben (über die Verwendung von Varargs bei der Arbeit mit Generika und über das Löschen von Typen ), aber es gibt ein wichtiges Thema, das wir noch nicht berücksichtigt haben – Platzhalter . Dies ist ein sehr wichtiges Merkmal von Generika. So sehr, dass wir ihm eine eigene Lektion gewidmet haben! Allerdings sind Wildcards nicht besonders kompliziert. Das werden Sie sofort sehen :) Platzhalter in Generika – 1Schauen wir uns ein Beispiel an:

public class Main {

   public static void main(String[] args) {
      
       String str = new String("Test!");
       // No problem
       Object obj = str;
      
       List<String> strings = new ArrayList<String>();
       // Compilation error!
       List<Object> objects = strings;
   }
}
Was ist denn hier los? Wir sehen zwei sehr ähnliche Situationen. In diesem Fall wandeln wir ein StringObjekt in ein ObjectObjekt um. Hier gibt es keine Probleme – alles funktioniert wie erwartet. Aber in der zweiten Situation generiert der Compiler einen Fehler. Aber wir machen das Gleiche, nicht wahr? Dieses Mal verwenden wir einfach eine Sammlung mehrerer Objekte. Aber warum tritt der Fehler auf? Was ist der Unterschied? Wandeln wir ein StringObjekt in ein Objectoder 20 Objekte um? Es gibt einen wichtigen Unterschied zwischen einem Objekt und einer Sammlung von Objekten . Wenn die BKlasse ein untergeordnetes Element der AKlasse ist, Collection<B>ist sie kein untergeordnetes Element von Collection<A>. Aus diesem Grund konnten wir unser List<String>Ziel nicht erreichenList<Object>. Stringist ein Kind von Object, aber List<String>kein Kind von List<Object>. Dies scheint möglicherweise nicht besonders intuitiv zu sein. Warum haben die Schöpfer der Sprache es so gemacht? Stellen wir uns vor, der Compiler gibt uns keinen Fehler:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
In diesem Fall könnten wir beispielsweise Folgendes tun:

objects.add(new Object());
String s = strings.get(0);
Da uns der Compiler keinen Fehler gemeldet hat und uns erlaubt hat, eine List<Object>Referenz zu erstellen, die auf zeigt strings, können wir Objectder stringsSammlung jedes alte Objekt hinzufügen! Somit haben wir die Garantie verloren, dass unsere Sammlung nur die StringObjekte enthält, die durch das Typargument im generischen Typaufruf angegeben werden . Mit anderen Worten: Wir haben den Hauptvorteil von Generika verloren – die Typensicherheit. Und weil der Compiler uns nicht daran gehindert hat, erhalten wir nur zur Laufzeit einen Fehler, der immer viel schlimmer ist als ein Kompilierungsfehler. Um Situationen wie diese zu verhindern, gibt uns der Compiler eine Fehlermeldung:

// Compilation error
List<Object> objects = strings;
...und erinnert uns daran, dass es List<String>kein Nachkomme von ist List<Object>. Dies ist eine eiserne Regel für Generika und muss bei der Arbeit mit ihnen beachtet werden. Lass uns weitermachen. Angenommen, wir haben eine kleine Klassenhierarchie:

public class Animal {

   public void feed() {

       System.out.println("Animal.feed()");
   }
}

public class Pet extends Animal {

   public void call() {

       System.out.println("Pet.call()");
   }
}

public class Cat extends Pet {

   public void meow() {

       System.out.println("Cat.meow()");
   }
}
An der Spitze der Hierarchie steht eine einfache Animal-Klasse, die von Pet geerbt wird. Haustier hat zwei Unterklassen: Hund und Katze. Nehmen wir nun an, dass wir eine einfache Methode erstellen müssen iterateAnimals(). Die Methode sollte eine Sammlung beliebiger Tiere ( Animal, Pet, Cat, Dog) annehmen, alle Elemente durchlaufen und bei jeder Iteration eine Meldung auf der Konsole anzeigen. Versuchen wir, eine solche Methode zu schreiben:

public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
Es scheint, dass das Problem gelöst ist! Wie wir jedoch kürzlich erfahren haben, sind List<Cat>, List<Dog>und List<Pet>keine Nachkommen von List<Animal>! iterateAnimals()Das bedeutet, dass wir einen Kompilierungsfehler erhalten, wenn wir versuchen, die Methode mit einer Liste von Katzen aufzurufen :

import java.util.*;

public class Main3 {


   public static void iterateAnimals(Collection<Animal> animals) {

       for(Animal animal: animals) {

           System.out.println("Another iteration in the loop!");
       }
   }

   public static void main(String[] args) {


       List<Cat> cats = new ArrayList<>();
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());

       // Compilation error!
       iterateAnimals(cats);
   }
}
Die Situation sieht für uns nicht sehr gut aus! Müssen wir separate Methoden zur Zählung jeder Tierart schreiben? Nein, das tun wir eigentlich nicht :) Und wie sich herausstellt, helfen uns Wildcards dabei! Wir können das Problem mit einer einfachen Methode mithilfe des folgenden Konstrukts lösen:

public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
Dies ist ein Platzhalter. Genauer gesagt ist dies die erste von mehreren Arten von Wildcards. Es ist als Platzhalter mit Obergrenze bekannt und wird durch ? ausgedrückt. erstreckt sich . Was sagt uns dieses Konstrukt? Dies bedeutet, dass die Methode eine Sammlung von AnimalObjekten oder eine Sammlung von Objekten einer beliebigen Klasse akzeptiert, die von Animal(? erweitert Animal) abstammt. Mit anderen Worten: Die Methode kann eine Sammlung von Animal, Pet, Dogoder CatObjekten akzeptieren – es macht keinen Unterschied. Überzeugen wir uns selbst davon, dass es funktioniert:

public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);
   iterateAnimals(dogs);
}
Konsolenausgabe:

Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Wir haben insgesamt 4 Sammlungen und 8 Objekte erstellt und es gibt genau 8 Einträge auf der Konsole. Alles funktioniert super! :) Der Platzhalter ermöglichte es uns, die notwendige Logik, die an bestimmte Typen gebunden ist, einfach in einer einzigen Methode unterzubringen. Wir haben die Notwendigkeit beseitigt, für jede Tierart eine separate Methode zu schreiben. Stellen Sie sich vor, wie viele Methoden wir benötigt hätten, wenn unsere Anwendung von einem Zoo oder einem Veterinäramt genutzt worden wäre :) Aber schauen wir uns nun eine andere Situation an. Unsere Vererbungshierarchie bleibt unverändert: Die oberste Klasse ist Animal, mit der PetKlasse direkt darunter und den Klassen Catund Dogauf der nächsten Ebene. Jetzt müssen Sie die Methode umschreiben iterateAnimals(), damit sie mit jeder Tierart außer Hunden funktioniert . Das heißt, es sollte akzeptieren Collection<Animal>,Collection<Pet>oder Collection<Car>, aber mit sollte es nicht funktionieren Collection<Dog>. Wie können wir das erreichen? Es scheint, dass wir erneut vor der Herausforderung stehen, für jeden Typ eine eigene Methode zu schreiben :/ Wie sonst erklären wir dem Compiler, was passieren soll? Eigentlich ist es ganz einfach! Auch hier kommen uns Wildcards zu Hilfe. Aber dieses Mal werden wir einen anderen Platzhaltertyp verwenden – einen Platzhalter mit niedrigerer Grenze , der durch super ausgedrückt wird .

public static void iterateAnimals(Collection<? super Cat> animals) {

   for(int i = 0; i < animals.size(); i++) {

       System.out.println("Another iteration in the loop!");
   }
}
Hier ist das Prinzip ähnlich. Das <? super Cat>Konstrukt teilt dem Compiler mit, dass die Methode eine Sammlung von Objekten oder einen beliebigen Vorfahren der Klasse als Eingabe iterateAnimals()akzeptieren kann . In diesem Fall stimmen die Klasse, ihr übergeordnetes Element und das übergeordnete Element ihres übergeordneten Elements alle mit dieser Beschreibung überein. Die Klasse entspricht nicht unserer Einschränkung, daher führt der Versuch, die Methode mit einem Argument zu verwenden, zu einem Kompilierungsfehler: CatCatCatPetAnimalDogList<Dog>

public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);
  
   // Compilation error!
   iterateAnimals(dogs);
}
Wir haben unser Problem gelöst und wieder einmal haben sich Wildcards als äußerst nützlich erwiesen :) Damit ist die Lektion zu Ende. Jetzt sehen Sie, wie wichtig Generika für Ihr Java-Studium sind – wir hatten ganze vier Lektionen darüber! Aber jetzt bist du bestens mit der Thematik vertraut und kannst dein Können in Vorstellungsgesprächen unter Beweis stellen :) Und jetzt geht es wieder an die Aufgaben! Viel Erfolg im Studium! :) :)
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION