2016-06-29 8 views
41

Ich habe auf ein Stück Code gestolpert, die mich fragen, hat, warum es erfolgreich kompiliert:Warum kompiliert dieser generische Code in Java 8?

public class Main { 
    public static void main(String[] args) { 
     String s = newList(); // why does this line compile? 
     System.out.println(s); 
    } 

    private static <T extends List<Integer>> T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 

Interessant ist, dass, wenn ich die Signatur der Methode newList mit <T extends ArrayList<Integer>> ändern es nicht mehr funktioniert.

Update nach Kommentare & Antworten: Wenn ich den generischen Typ aus dem Verfahren zur Klasse bewegen Sie den Code kompilieren nicht mehr:

public class SomeClass<T extends List<Integer>> { 
    public void main(String[] args) { 
     String s = newList(); // this doesn't compile anymore 
     System.out.println(s); 
    } 

    private T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 
+3

In Verbindung stehende http://stackoverflow.com/questions/36402646/generic-return-type-upper-bound-interface-vs-class-surfishly-valid-code – Tunaki

+1

Der Grund für das Update einen Unterschied machen ist das erste Version kann funktionieren, wenn es einen Typ gibt, der die Bedingungen erfüllt (T extends List & T extends String) [dh es ist eine existenzielle Quantifizierung], während die zweite Version nur funktionieren kann, wenn alle möglichen Typen T, die mit der Deklaration in der Klasse übereinstimmen, auch String [d. h. es ist eine universelle Quantifizierung]. –

Antwort

32

Wenn Sie einen Typparameter bei einer Methode deklarieren, können Sie dem Anrufer erlauben einen tatsächlichen Typ für sie zu holen, solange die tatsächliche Art der Einschränkungen erfüllen. Dieser Typ muss kein konkreter konkreter Typ sein, es könnte sich um einen abstrakten Typ, eine Typvariable oder einen Schnitttyp handeln, in anderen umgangssprachlichen Worten, einem hypothetischen Typ. Also, as said by Mureinik, könnte es einen Typ geben, der String erweitert und List implementiert. Wir können nicht manuell einen Kreuzungstyp für den Aufruf angeben, aber wir können eine Variable vom Typ verwenden, um die Logik zu demonstrieren:

public class Main { 
    public static <X extends String&List<Integer>> void main(String[] args) { 
     String s = Main.<X>newList(); 
     System.out.println(s); 
    } 

    private static <T extends List<Integer>> T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 

Natürlich newList() kann die Erwartung nicht erfüllen solcher eine Art Rückkehr, aber das ist das Problem der Definition (oder Implementierung) dieser Methode. Sie sollten eine "ungeprüfte" Warnung erhalten, wenn Sie ArrayList an T werfen. Die einzig mögliche korrekte Implementierung wäre die Rückgabe null hier, was die Methode ziemlich nutzlos macht.

Der Punkt, um die erste Aussage zu wiederholen, ist, dass der Aufrufer einer generischen Methode die tatsächlichen Typen für die Typparameter wählt. Wenn im Gegensatz dazu erklären Sie eine generische Klasse wie mit

public class SomeClass<T extends List<Integer>> { 
    public void main(String[] args) { 
     String s = newList(); // this doesn't compile anymore 
     System.out.println(s); 
    } 

    private T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 

der Typparameter Bestandteil des Vertrages der Klasse ist, wer auch immer so eine Instanz wird die tatsächlichen Typen für diese Instanz auswählen. Die Instanzmethode main ist Teil dieser Klasse und muss diesen Vertrag befolgen.Sie können nicht die gewünschte T auswählen; der tatsächliche Typ für T wurde festgelegt und in Java können Sie normalerweise nicht einmal herausfinden, was T ist.

Der Schlüsselpunkt der generischen Programmierung ist das Schreiben eines Codes, der unabhängig davon funktioniert, welche tatsächlichen Typen für die Typparameter ausgewählt wurden.

Aber beachten Sie, dass Sie eine andere, unabhängige Instanz mit dem gewünschten Typ erstellen können, und rufen Sie die Methode, z.

public class SomeClass<T extends List<Integer>> { 
    public <X extends String&List<Integer>> void main(String[] args) { 
     String s = new SomeClass<X>().newList(); 
     System.out.println(s); 
    } 

    private T newList() { 
     return (T) new ArrayList<Integer>(); 
    } 
} 

Hier wählt der Ersteller der neuen Instanz die tatsächlichen Typen für diese Instanz aus. Wie gesagt, dieser tatsächliche Typ muss kein konkreter Typ sein.

17

Ich vermute, dies liegt daran, dass List eine ist Schnittstelle. Wenn wir die Tatsache ignorieren, dass String für eine Sekunde final ist, könnten Sie theoretisch eine Klasse haben, die extends String (was bedeutet, dass Sie es s zuweisen könnten) aber implements List<Integer> (was bedeutet, dass es von newList() zurückgegeben werden konnte). Wenn Sie den Rückgabetyp von einer Schnittstelle (T extends List) in eine konkrete Klasse (T extends ArrayList) geändert haben, kann der Compiler daraus schließen, dass sie nicht voneinander zuweisbar sind und einen Fehler erzeugen.

Das bricht natürlich ab, da String in Wirklichkeit final ist, und wir könnten erwarten, dass der Compiler dies berücksichtigt. IMHO, es ist ein Bug, obwohl ich zugeben muss, ich bin kein Compiler-Experte und es könnte einen guten Grund geben, den Modifikator final zu diesem Zeitpunkt zu ignorieren.

+1

Hmm, das ist eigentlich ein wirklich guter Punkt. Ich habe nicht über die Möglichkeit nachgedacht, eine Klasse zu haben, die theoretisch sowohl String als auch List implementieren könnte. Wie Sie gesagt haben, trifft dies nicht wirklich zu, da String endgültig ist, aber es ist eine interessante Idee. –

+7

Die Tatsache, dass eine Klasse zur Kompilierungszeit "final" ist, wird in solchen Situationen niemals berücksichtigt, da es keine Garantie gibt, dass die Klasse zur Laufzeit noch "final" ist. – Holger

+0

@errantlinguist Ich bin mir nicht sicher, ob ich dem folge, was du sagst. Das Problem ist, dass der vorgestellte Code kompiliert, wenn ich einen Wert vom Typ T extends List einem Wert vom Typ String zuweist. –

6

Ich weiß nicht, warum das kompilieren. Auf der anderen Seite kann ich erklären, wie Sie die Compile-Time-Checks voll ausnutzen können.

Also, newList() ist eine generische Methode, es hat einen Typ Parameter. Wenn Sie diesen Parameter angeben, dann wird der Compiler, dass Sie überprüfen:

Fails zu kompilieren:

String s = Main.<String>newList(); // this doesn't compile anymore 
System.out.println(s); 

Pässe bei der Kompilierung:

List<Integer> l = Main.<ArrayList<Integer>>newList(); // this compiles and works well 
System.out.println(l); 

Angeben thetype Parameter

T Die Typparameter bieten nur die Überprüfung der Kompilierzeit. Dies ist von Entwurf, Java verwendet type erasure für generische Typen. Um den Compiler für Sie arbeiten zu lassen, müssen Sie diese Typen im Code angeben.

Type-Parameter auf Instanz-creation

Der häufigste Fall sind das Muster für eine Objektinstanz angeben. I.e. für Listen:

List<String> list = new ArrayList<>(); 

Hier können wir sehen, dass List<String> den Typ für die Listeneinträge angibt. Auf der anderen Seite, neue ArrayList<>() nicht. Es verwendet stattdessen die diamond operator. I.e. der Java-Compiler infers der Typ basierend auf der Deklaration.

Implizite Typparameter bei Methodenaufruf

Wenn Sie eine statische Methode aufrufen, dann müssen Sie den Typ in einer anderen Art und Weise angeben. Manchmal kann man es als Parameter angeben:

public static <T extends Number> T max(T n1, T n2) { 
    if (n1.doubleValue() < n2.doubleValue()) { 
     return n2; 
    } 
    return n1; 
} 

Die Sie es wie folgt verwenden können:

int max = max(3, 4); // implicit param type: Integer 

Oder wie folgt aus:

double max2 = max(3.0, 4.0); // implicit param type: Double 

Explizite Typparameter bei Methodenaufruf:

Sagen Sie zum Beispiel, das ist, wie Sie kann eine typsichere leere Liste erstellen:

List<Integer> noIntegers = Collections.<Integer>emptyList(); 

Der Typ-Parameter <Integer>emptyList() an die Methode übergeben wird. Die einzige Einschränkung besteht darin, dass Sie auch die Klasse angeben müssen. I.e. Sie können dies nicht tun:

import static java.util.Collections.emptyList; 
... 
List<Integer> noIntegers = <Integer>emptyList(); // this won't compile 

Runtime Typ Token

Wenn keine dieser Tricks, die Sie helfen können, dann können Sie eine runtime type token angeben. I.e. Sie stellen eine Klasse als Parameter bereit. Ein gängiges Beispiel ist die EnumMap:

private static enum Letters {A, B, C}; // dummy enum 
... 
public static void main(String[] args) { 
    Map<Letters, Integer> map = new EnumMap<>(Letters.class); 
} 
+0

Können Sie die Erklärung vereinfachen? –

+0

Die einfache Antwort ist, dass Sie die Typparameter innerhalb der <> Klammern angeben können.Ansonsten habe ich die Erklärung länger gemacht, damit Sie alle Alternativen sehen können. –

Verwandte Themen