Fragen zum Java Generics-Interview (+ Antworten)

Dieser Artikel ist Teil einer Reihe: • Fragen zum Interview mit Java Collections

• Fragen zum Java Type System-Interview

• Fragen zum Java Concurrency-Interview (+ Antworten)

• Fragen zum Java-Klassenstruktur- und Initialisierungsinterview

• Fragen zum Java 8-Interview (+ Antworten)

• Speicherverwaltung in Java Interview Fragen (+ Antworten)

• Fragen zum Java Generics-Interview (+ Antworten) (aktueller Artikel) • Fragen zum Java Flow Control-Interview (+ Antworten)

• Fragen zum Interview mit Java-Ausnahmen (+ Antworten)

• Fragen zum Interview mit Java Annotations (+ Antworten)

• Fragen zum Top Spring Framework-Interview

1. Einleitung

In diesem Artikel werden einige beispielhafte Fragen und Antworten zu Java-Generika-Interviews behandelt.

Generika sind ein Kernkonzept in Java, das erstmals in Java 5 eingeführt wurde. Aus diesem Grund werden sie von fast allen Java-Codebasen verwendet, was fast garantiert, dass ein Entwickler sie irgendwann antreffen wird. Aus diesem Grund ist es wichtig, sie richtig zu verstehen, und deshalb werden sie höchstwahrscheinlich während eines Interviewprozesses gefragt.

2. Fragen

Q1. Was ist ein generischer Typparameter?

Typ ist der Name einer Klasse oder Schnittstelle . Wie der Name andeutet, ist ein generischer Typparameter, wenn ein Typ als Parameter in einer Klassen-, Methoden- oder Schnittstellendeklaration verwendet werden kann.

Beginnen wir mit einem einfachen Beispiel, eines ohne Generika, um dies zu demonstrieren:

public interface Consumer { public void consume(String parameter) }

In diesem Fall ist der Methodenparametertyp der Consume () -Methode String. Es ist nicht parametriert und nicht konfigurierbar.

Ersetzen wir nun unseren String- Typ durch einen generischen Typ, den wir T nennen . Er wird gemäß Konvention wie folgt benannt:

public interface Consumer { public void consume(T parameter) }

Wenn wir unseren Verbraucher implementieren, können wir den Typ angeben, den er als Argument verwenden soll. Dies ist ein generischer Typparameter:

public class IntegerConsumer implements Consumer { public void consume(Integer parameter) }

In diesem Fall können wir jetzt ganze Zahlen verbrauchen. Wir können diesen Typ gegen alles austauschen, was wir brauchen.

Q2. Was sind einige Vorteile der Verwendung generischer Typen?

Ein Vorteil der Verwendung von Generika besteht darin, Abgüsse zu vermeiden und die Typensicherheit zu gewährleisten. Dies ist besonders nützlich, wenn Sie mit Sammlungen arbeiten. Lassen Sie uns dies demonstrieren:

List list = new ArrayList(); list.add("foo"); Object o = list.get(0); String foo = (String) o;

In unserem Beispiel ist der Elementtyp in unserer Liste dem Compiler unbekannt. Dies bedeutet, dass nur garantiert werden kann, dass es sich um ein Objekt handelt. Wenn wir also unser Element abrufen, erhalten wir ein Objekt zurück. Als Autoren des Codes wissen wir, dass es sich um einen String handelt, aber wir müssen unser Objekt in einen umwandeln, um das Problem explizit zu beheben. Dies erzeugt viel Lärm und Kesselplatte.

Wenn wir als nächstes über den Raum für manuelle Fehler nachdenken, wird das Casting-Problem schlimmer. Was wäre, wenn wir versehentlich eine Ganzzahl in unserer Liste hätten?

list.add(1) Object o = list.get(0); String foo = (String) o;

In diesem Fall erhalten wir zur Laufzeit eine ClassCastException , da eine Ganzzahl nicht in String umgewandelt werden kann.

Versuchen wir nun, uns zu wiederholen, diesmal mit Generika:

List list = new ArrayList(); list.add("foo"); String o = list.get(0); // No cast Integer foo = list.get(0); // Compilation error

Wie wir sehen können, haben wir mithilfe von Generika eine Überprüfung des Kompilierungstyps, die ClassCastExceptions verhindert und das Casting überflüssig macht .

Der andere Vorteil besteht darin, Codeduplizierungen zu vermeiden . Ohne Generika müssen wir denselben Code kopieren und einfügen, jedoch für verschiedene Typen. Bei Generika müssen wir das nicht tun. Wir können sogar Algorithmen implementieren, die für generische Typen gelten.

Q3. Was ist Typlöschung?

Es ist wichtig zu wissen, dass generische Typinformationen nur dem Compiler zur Verfügung stehen, nicht der JVM. Mit anderen Worten bedeutet Typlöschung, dass der JVM zur Laufzeit keine generischen Typinformationen zur Verfügung stehen, sondern nur zur Kompilierungszeit .

Die Gründe für die Wahl der Hauptimplementierung sind einfach: Die Abwärtskompatibilität mit älteren Java-Versionen bleibt erhalten. Wenn ein generischer Code in Bytecode kompiliert wird, ist es so, als ob der generische Typ nie existiert hätte. Dies bedeutet, dass die Zusammenstellung:

  1. Ersetzen Sie generische Typen durch Objekte
  2. Ersetzen Sie begrenzte Typen (mehr dazu in einer späteren Frage) durch die erste gebundene Klasse
  3. Fügen Sie beim Abrufen generischer Objekte das Äquivalent von Casts ein.

Es ist wichtig, die Typlöschung zu verstehen. Andernfalls könnte ein Entwickler verwirrt sein und glauben, dass er den Typ zur Laufzeit abrufen kann:

public foo(Consumer consumer) { Type type = consumer.getGenericTypeParameter() }

Das obige Beispiel ist ein Pseudocode-Äquivalent dazu, wie Dinge ohne Löschen des Typs aussehen könnten, aber leider ist dies unmöglich. Auch hier sind die allgemeinen Typinformationen zur Laufzeit nicht verfügbar.

Q4. Wenn beim Instanziieren eines Objekts ein generischer Typ weggelassen wird, wird der Code dann weiterhin kompiliert?

Da Generika vor Java 5 nicht existierten, ist es möglich, sie überhaupt nicht zu verwenden. Beispielsweise wurden Generika für die meisten Standard-Java-Klassen wie Sammlungen nachgerüstet. Wenn wir uns unsere Liste aus Frage 1 ansehen, werden wir sehen, dass wir bereits ein Beispiel für das Weglassen des generischen Typs haben:

List list = new ArrayList();

Obwohl kompiliert werden kann, ist es dennoch wahrscheinlich, dass der Compiler eine Warnung ausgibt. Dies liegt daran, dass wir die zusätzliche Überprüfung der Kompilierungszeit verlieren, die wir durch die Verwendung von Generika erhalten.

Der zu beachtende Punkt ist, dass Abwärtskompatibilität und Typlöschung zwar das Weglassen generischer Typen ermöglichen, es jedoch eine schlechte Praxis ist.

Q5. Wie unterscheidet sich eine generische Methode von einem generischen Typ?

Bei einer generischen Methode wird ein Typparameter in eine Methode eingeführt, die im Rahmen dieser Methode liegt. Versuchen wir dies anhand eines Beispiels:

public static  T returnType(T argument) { return argument; }

Wir haben eine statische Methode verwendet, hätten aber auch eine nicht statische verwenden können, wenn wir dies gewünscht hätten. Durch die Nutzung der Typinferenz (in der nächsten Frage behandelt) können wir diese wie jede gewöhnliche Methode aufrufen, ohne dabei Typargumente angeben zu müssen.

Q6. Was ist Typinferenz?

Typinferenz ist, wenn der Compiler den Typ eines Methodenarguments untersuchen kann, um auf einen generischen Typ zu schließen. Wenn wir beispielsweise T an eine Methode übergeben haben, die T zurückgibt , kann der Compiler den Rückgabetyp ermitteln. Probieren wir dies aus, indem wir unsere generische Methode aus der vorherigen Frage aufrufen:

Integer inferredInteger = returnType(1); String inferredString = returnType("String");

Wie wir sehen können, ist keine Besetzung erforderlich und es muss kein generisches Typargument übergeben werden. Der Argumenttyp leitet nur den Rückgabetyp ab.

Q7. Was ist ein gebundener Typparameter?

Bisher haben alle unsere Fragen generische Argumente behandelt, die unbegrenzt sind. Dies bedeutet, dass unsere generischen Typargumente jeder gewünschte Typ sein können.

Wenn wir begrenzte Parameter verwenden, beschränken wir die Typen, die als generische Typargumente verwendet werden können.

Nehmen wir als Beispiel an, wir möchten unseren generischen Typ zwingen, immer eine Unterklasse von Tieren zu sein:

public abstract class Cage { abstract void addAnimal(T animal) }

Durch den Einsatz erstreckt , zwingen wir T eine Unterklasse von Tier zu sein . Wir könnten dann einen Käfig mit Katzen haben:

Cage catCage;

Aber wir könnten keinen Käfig mit Objekten haben, da ein Objekt keine Unterklasse eines Tieres ist:

Cage objectCage; // Compilation error

Ein Vorteil davon ist, dass dem Compiler alle Tiermethoden zur Verfügung stehen. Wir wissen, dass unser Typ ihn erweitert, sodass wir einen generischen Algorithmus schreiben können, der auf jedes Tier angewendet wird. Dies bedeutet, dass wir unsere Methode nicht für verschiedene Tierunterklassen reproduzieren müssen:

public void firstAnimalJump() { T animal = animals.get(0); animal.jump(); }

Q8. Ist es möglich, einen mehrfach begrenzten Parameter zu deklarieren?

Das Deklarieren mehrerer Grenzen für unsere generischen Typen ist möglich. In unserem vorherigen Beispiel haben wir eine einzelne Grenze angegeben, aber wir können auch mehr angeben, wenn wir dies wünschen:

public abstract class Cage

In unserem Beispiel ist das Tier eine Klasse und vergleichbar eine Schnittstelle. Nun muss unser Typ diese beiden Obergrenzen respektieren. Wenn unser Typ eine Unterklasse von Tieren wäre, aber keine vergleichbaren implementieren würde, würde der Code nicht kompiliert. Es ist auch erwähnenswert, dass wenn eine der oberen Grenzen eine Klasse ist, dies das erste Argument sein muss.

Q9. Was ist ein Wildcard-Typ?

Ein Platzhaltertyp repräsentiert einen unbekannten Typ . Es wird mit einem Fragezeichen wie folgt gezündet:

public static void consumeListOfWildcardType(List list)

Hier geben wir eine Liste an, die von jedem Typ sein kann . Wir könnten eine Liste von allem an diese Methode übergeben.

Q10. Was ist eine Upper Bounded Wildcard?

Ein Platzhalter mit oberen Grenzen ist, wenn ein Platzhaltertyp von einem konkreten Typ erbt . Dies ist besonders nützlich, wenn Sie mit Sammlungen und Vererbung arbeiten.

Versuchen wir dies mit einer Farmklasse zu demonstrieren, in der Tiere gespeichert werden, zunächst ohne den Platzhaltertyp:

public class Farm { private List animals; public void addAnimals(Collection newAnimals) { animals.addAll(newAnimals); } }

Wenn wir mehrere Subklassen von Tiere haben , wie Katze und Hund , könnten wir die falsche Annahme, dass wir sie alle zu unserem Hof hinzufügen:

farm.addAnimals(cats); // Compilation error farm.addAnimals(dogs); // Compilation error

Dies liegt daran, dass der Compiler eine Sammlung des konkreten Tieres erwartet , nicht eine, die er in Unterklassen unterteilt.

Lassen Sie uns nun einen oberen Platzhalter in unsere Methode zum Hinzufügen von Tieren einführen:

public void addAnimals(Collection newAnimals)

Wenn wir es jetzt erneut versuchen, wird unser Code kompiliert. Dies liegt daran, dass wir den Compiler jetzt anweisen, eine Sammlung eines beliebigen Subtyps eines Tieres zu akzeptieren.

Q11. Was ist ein unbegrenzter Platzhalter?

Ein unbegrenzter Platzhalter ist ein Platzhalter ohne Ober- oder Untergrenze, der einen beliebigen Typ darstellen kann.

Es ist auch wichtig zu wissen, dass der Platzhaltertyp nicht gleichbedeutend mit Objekt ist. Dies liegt daran, dass ein Platzhalter ein beliebiger Typ sein kann, während ein Objekttyp speziell ein Objekt ist (und keine Unterklasse eines Objekts sein kann). Lassen Sie uns dies anhand eines Beispiels demonstrieren:

List wildcardList = new ArrayList(); List objectList = new ArrayList(); // Compilation error

Der Grund, warum die zweite Zeile nicht kompiliert wird, ist, dass eine Liste von Objekten erforderlich ist, keine Liste von Zeichenfolgen. Die erste Zeile wird kompiliert, da eine Liste eines unbekannten Typs akzeptabel ist.

Q12. Was ist eine Wildcard mit niedrigerer Grenze?

Ein Platzhalter mit niedrigerer Grenze ist, wenn anstelle einer oberen Grenze eine untere Grenze mit dem Schlüsselwort super angegeben wird . Mit anderen Worten, ein Platzhalter mit niedrigerer Grenze bedeutet, dass wir den Typ zwingen, eine Oberklasse unseres begrenzten Typs zu sein . Versuchen wir dies anhand eines Beispiels:

public static void addDogs(List list) { list.add(new Dog("tom")) }

Mit super könnten wir addDogs für eine Liste von Objekten aufrufen:

ArrayList objects = new ArrayList(); addDogs(objects);

Dies ist sinnvoll, da ein Objekt eine Oberklasse von Tieren ist. Wenn wir den Platzhalter mit der unteren Grenze nicht verwenden würden, würde der Code nicht kompiliert, da eine Liste von Objekten keine Liste von Tieren ist.

Wenn wir darüber nachdenken, können wir keinen Hund zu einer Liste von Tierunterklassen wie Katzen oder sogar Hunden hinzufügen. Nur eine Superklasse von Tieren. Dies würde beispielsweise nicht kompilieren:

ArrayList objects = new ArrayList(); addDogs(objects);

Q13. Wann würden Sie einen Typ mit niedrigerer Grenze im Vergleich zu einem Typ mit oberer Grenze verwenden?

Beim Umgang mit Sammlungen ist PECS eine gängige Regel für die Auswahl zwischen Platzhaltern mit oberen oder unteren Grenzen. PECS steht für Produzent erweitert, Verbraucher super.

Dies kann leicht durch die Verwendung einiger Standard-Java-Schnittstellen und -Klassen demonstriert werden.

Produzent erstreckt nur bedeutet , dass , wenn Sie einen Hersteller einer generischen Art zu schaffen, dann die sich Schlüsselwort. Versuchen wir, dieses Prinzip auf eine Sammlung anzuwenden, um herauszufinden, warum es sinnvoll ist:

public static void makeLotsOfNoise(List animals) { animals.forEach(Animal::makeNoise); }

Hier möchten wir makeNoise () für jedes Tier in unserer Sammlung aufrufen . Dies bedeutet, dass unsere Sammlung ein Produzent ist , da wir damit nur Tiere zurückgeben, damit wir unsere Operation durchführen können. Wenn wir losgeworden erstreckt , würden wir nicht in der Lage sein , in den Listen der Katzen passieren , Hunde oder andere Subklassen von Tieren. Durch die Anwendung des Produzentenerweiterungsprinzips haben wir die größtmögliche Flexibilität.

Consumer Super bedeutet, dass sich das Gegenteil zum Produzenten erstreckt. Alles was es bedeutet ist, dass wir das Super- Schlüsselwort verwenden sollten, wenn wir es mit etwas zu tun haben, das Elemente verbraucht . Wir können dies demonstrieren, indem wir unser vorheriges Beispiel wiederholen:

public static void addCats(List animals) { animals.add(new Cat()); }

Wir ergänzen nur unsere Tierliste, daher ist unsere Tierliste ein Verbraucher. Aus diesem Grund verwenden wir das Schlüsselwort super . Dies bedeutet, dass wir eine Liste aller Tierklassen abgeben können, jedoch keine Unterklasse. Wenn wir beispielsweise versuchen würden, eine Liste von Hunden oder Katzen weiterzugeben, wird der Code nicht kompiliert.

Als letztes ist zu prüfen, was zu tun ist, wenn eine Sammlung sowohl Verbraucher als auch Produzent ist. Ein Beispiel hierfür könnte eine Sammlung sein, in der Elemente hinzugefügt und entfernt werden. In diesem Fall sollte ein unbegrenzter Platzhalter verwendet werden.

Q14. Gibt es Situationen, in denen generische Typinformationen zur Laufzeit verfügbar sind?

Es gibt eine Situation, in der zur Laufzeit ein generischer Typ verfügbar ist. In diesem Fall ist ein generischer Typ wie folgt Teil der Klassensignatur:

public class CatCage implements Cage

Durch die Verwendung von Reflektion erhalten wir diesen Typparameter:

(Class) ((ParameterizedType) getClass() .getGenericSuperclass()).getActualTypeArguments()[0];

Dieser Code ist etwas spröde. Dies hängt beispielsweise davon ab, welcher Typparameter in der unmittelbaren Oberklasse definiert wird. Es zeigt jedoch, dass die JVM über diese Typinformationen verfügt.

Weiter » Java Flow Control-Interviewfragen (+ Antworten) « Vorherige Speicherverwaltung in Java-Interviewfragen (+ Antworten)