Java Generics Interviewfragen (+ Antworten)

Fragen im Vorstellungsgespräch bei Java Generics (+ Antworten)

1. Einführung

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 sicherstellt, dass ein Entwickler irgendwann auf sie stößt. 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?

Type ist der Name einesclass oderinterface. Wie der Name andeutet,a generic type parameter is when a type can be used as a parameter in a class, method or interface declaration.

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 Methodeconsume()String.. Er ist nicht parametrisiert und nicht konfigurierbar.

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

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

Wenn wir unseren Verbraucher implementieren, können wir dietype, die er verbrauchen soll, als Argument angeben. Dies ist ein generischer Typparameter:

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

In diesem Fall können wir jetzt ganze Zahlen konsumieren. Wir können diesetype gegen alles austauschen, was wir benötigen.

Q2. Was sind einige Vorteile der Verwendung generischer Typen?

Ein Vorteil der Verwendung von Generika ist die Vermeidung von Kosten und die Gewährleistung von Typensicherheit. 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(1);
String foo = (String) o;

In unserem Beispiel ist der Elementtyp in unserer Liste dem Compiler unbekannt. Das heißt, es kann nur garantiert werden, 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 einString,handelt, aber wir müssen unser Objekt in eins umwandeln, um das Problem explizit zu beheben. Dies erzeugt viel Lärm und Kesselblech.

Als nächstes wird das Casting-Problem noch schlimmer, wenn wir uns Gedanken über den Raum für manuelle Fehler machen. Was wäre, wenn wir versehentlich eine ganze Zahl in unserer Liste hätten?

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

In diesem Fall würden wir zur Laufzeit einClassCastException erhalten, da einInteger nicht inString. umgewandelt werden kann

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

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

Wie wir sehen können,by using generics we have a compile type check which prevents ClassCastExceptions and removes the need for casting.

The other advantage is to avoid code duplication. Ohne Generika müssen wir den gleichen Code für verschiedene Typen kopieren und einfügen. Bei Generika müssen wir dies 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, type erasure means that generic type information is not available to the JVM at runtime, only compile time.

Die Hauptgründe für die Auswahl der Implementierung 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 Kompilierung:

  1. Ersetzen Sie generische Typen durch Objekte

  2. Ersetzen Sie gebundene Typen (mehr dazu in einer späteren Frage) durch die erste gebundene Klasse

  3. Fügen Sie das Äquivalent von Casts ein, wenn Sie generische Objekte abrufen.

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 dessen, wie Dinge ohne Typlöschung aussehen könnten, aber leider ist es unmöglich. Noch einmalthe generic type information is not available at runtime.

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 ab 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, dasswhile backward compatibility and type erasure make it possible to omit generic types, it is bad practice.

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

A generic method is where a type parameter is introduced to a method,living within the scope of that method. 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. Indem wir die (in der nächsten Frage behandelte) Typinferenz nutzen, können wir diese wie jede gewöhnliche Methode aufrufen, ohne dabei irgendwelche Typargumente angeben zu müssen.

Q6. Was ist Typinferenz?

Typinferenz ist, wenn der Compiler den Typ eines Methodenarguments betrachten kann, um einen generischen Typ abzuleiten. Wenn wir beispielsweiseT an eine Methode übergeben, dieT, 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 schließt nur an den Rückgabetyp an.

Q7. Was ist ein gebundener Typparameter?

Bisher haben alle unsere Fragen generische Argumente behandelt, die unbegrenzt sind. Dies bedeutet, dass unsere generischen Typargumente jeder Typ sein können, den wir möchten.

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 die Verwendung von Extend, zwingen wirT, eine Unterklasse von Tier. zu sein. Wir könnten dann einen Käfig mit Katzen haben:

Cage catCage;

Wir könnten aber 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 tierischen Methoden zur Verfügung stehen. Wir wissen, dass unser Typ es erweitert, sodass wir einen generischen Algorithmus schreiben können, der auf jedes Tier angewendet werden kann. 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?

Die Angabe 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 möchten:

public abstract class Cage

In unserem Beispiel ist das Tier eine Klasse und vergleichbar ist eine Schnittstelle. Jetzt muss unser Typ diese beiden oberen Schranken respektieren. Wenn unser Typ eine Unterklasse von animal wäre, aber keine vergleichbaren implementiert, dann würde der Code nicht kompiliert. It’s also worth remembering that if one of the upper bounds is a class, it must be the first argument.

Q9. Was ist ein Wildcard-Typ?

A wildcard type represents an unknown type. Es wird wie folgt mit einem Fragezeichen gezündet:

public static consumeListOfWildcardType(List list)

Hier geben wir eine Liste an, die austype bestehen kann. Wir könnten eine Liste von allem in diese Methode übergeben.

Q10. Was ist eine Upper Bounded Wildcard?

An upper bounded wildcard is when a wildcard type inherits from a concrete type. 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 Unterklassen von Tier,wie Katze und Hund,hätten, könnten wir die falsche Annahme treffen, dass wir sie alle zu unserer Farm hinzufügen können:

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

Dies liegt daran, dass der Compiler erwartet, dass eine Sammlung des konkreten Tieres,nicht untergeordnet wird.

Lassen Sie uns nun unserer Methode zum Hinzufügen von Tieren einen Platzhalter mit oberen Grenzen hinzufügen:

public void addAnimals(Collection newAnimals)

Wenn wir es jetzt noch einmal versuchen, wird unser Code kompiliert. Dies liegt daran, dass wir den Compiler jetzt auffordern, eine Sammlung von Subtypen von Tieren 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 mit einem unbekannten Typ zulässig 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üsselwortsuper angegeben wird. Mit anderen Worten,a lower bounded wildcard means we are forcing the type to be a superclass of our bounded type. Versuchen wir dies anhand eines Beispiels:

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

Mitsuper, können 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 nicht den Platzhalter mit der unteren Grenze verwenden, wird 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 Unterklassen von Tieren wie Katzen oder sogar Hunden hinzufügen. Nur eine Superklasse von Tieren. Dies würde zum Beispiel nicht kompilieren:

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

Q13. Wann würden Sie sich für einen Typ mit niedrigerer Schranke entscheiden als für einen Typ mit niedrigerer Schranke? ein Typ mit oberen Grenzen?

Beim Umgang mit Sammlungen ist PECS eine häufige Regel für die Auswahl zwischen Platzhaltern mit oberer oder unterer Grenze. PECS steht fürproducer extends, consumer super.

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

Producer extends bedeutet nur, dass Sie das Schlüsselwortextendsverwenden, wenn Sie einen Produzenten eines generischen Typs erstellen. 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 wirmakeNoise() 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 wirextends loswerden würden, könnten wir keine Listen mit Katzenhunden von,oder anderen Unterklassen von Tieren weitergeben. Durch die Anwendung des Producer-Extends-Prinzips haben wir die größtmögliche Flexibilität.

Consumer super bedeutet das Gegenteil vonproducer extends.. Alles was es bedeutet ist, dass wir das Schlüsselwortsuperverwenden sollten, wenn wir uns mit etwas befassen, das Verbraucherelemente enthält. 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üsselwortsuper. Dies bedeutet, dass wir eine Liste mit allen Oberklassen von Tieren übergeben können, jedoch keine Unterklasse. Wenn wir zum Beispiel versuchen, eine Liste von Hunden oder Katzen einzureichen, wird der Code nicht kompiliert.

Als letztes ist zu überlegen, was zu tun ist, wenn eine Kollektion sowohl Verbraucher als auch Produzent ist. Ein Beispiel hierfür ist eine Sammlung, 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 ein generischer Typ zur Laufzeit verfügbar ist. Dies ist der Fall, wenn ein generischer Typ wie folgt Teil der Klassensignatur ist:

public class CatCage implements Cage

Durch die Verwendung von Reflection 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.