Generics - No suitable constructor found

So, nachdem mir IntelliJ jetzt alle Raw Types anzeigt und ich versuche die eins nach dem anderen auszumerzen, platzen mir die Generics links und rechts.

Im Moment bekomme ich einen merkwürdigen Compilerfehler, der wird von IntelliJ auch nicht erkannt, sondern tritt erst bei einem Rebuild auf.

error: no suitable constructor found for SceneGraphBuilderConnection(P#1)
constructor SceneGraphBuilderConnection.<P#1>SceneGraphBuilderConnection(P#1) is not applicable
(explicit type argument P#1 does not conform to declared bound(s) SceneGraphParent<? super BoardSystemLogic<?>>)
constructor SceneGraphBuilderConnection.<P#2>SceneGraphBuilderConnection(BoardSystemLogic<?>,P#2) is not applicable (explicit type argument P#1 does not conform to declared bound(s) SceneGraphParent>)
where P#1,C,P#2 are type-variables:
P#1 extends SceneGraphParent<? super C> declared in constructor SceneGraphBuilderConnection(P#1) C extends SceneGraphCluster<?,?> declared in class SceneGraphBuilderConnection
P#2 extends SceneGraphParent<BoardSystemLogic<?>> declared in constructor <P#2>SceneGraphBuilderConnection(C,P#2)

Soweit so nichts besonderes. Aber der aufrufende Code den er anmeckert sieht so aus (extra ausgeschrieben)

SceneGraphParent<? super BoardSystemLogic<?>> sgp1 = spaceobject.getLogic().getBoardSystemManager();
SceneGraphBuilderConnection<BoardSystemLogic<?>> sgbc1 = new SceneGraphBuilderConnection<BoardSystemLogic<?>>(sgp1) {
            @Override
            public BoardSystemLogic<?> init() {
                return logic;
            }
        };

Wenn ich ein kleines kompilierbares Beispiel schreiben möchte, compiliert er natürlich auch alles einwandfrei. Ich sehe den Fehler einfach nicht. Er möchte ein SceneGraphParent<? super BoardSystemLogic<?>> und das bekommt er doch auch? Und wie gesagt, IntelliJ meckert an der Stelle auch nichts an.

Bin mir nicht sicher ob ich blind bin, oder der Compiler durch die verschachtelten Generics verwirrt wird.

Ich schreibe hier nur mal ein paar Gedanken heraus

P#1 extends SceneGraphParent<? super C> declared in constructor <P#1>SceneGraphBuilderConnection(P#1)

C extends SceneGraphCluster<?,?>

= P#1 extends SceneGraphParent<? super BoardSystemLogic<?>>

explicit type argument P#1 does not conform to declared bound(s) SceneGraphParent<? super BoardSystemLogic<?>>

:dizzy_face:

Wenn ich einen zweiten Konstruktor einführe, der explizit

public SceneGraphBuilderConnection( SceneGraphParent<? super BoardSystemLogic<?>> parent ) {
}

entgegen nimmt, dann funktioniert der Aufruf zwar, aber dann meckert er stattdessen

‚SceneGraphBuilderConnection(SceneGraphParent<? super BoardSystemLogic<?>>)‘ clashes with ‚SceneGraphBuilderConnection(P)‘; both methods have same erasure

geil :+1:

Mit super wird nur der nächsthöhere Typ in der Vererbungshierarchie überprüft, mit extends hingegen alles bis zur Urklasse. Das es genügt nicht, dass <? super BoardSystemLogic<?>> BoardSystemLogic<?> erweitert, es muss eine direkte Unterklasse davon sein und nicht etwa die Erweiterung einer Klasse, die ihrerseits BoardSystemLogic<?> erweitert. Dieses super ist also ein wenig spezieller als extends. Ich würde also definitiv extends verwenden, so lässt man sich Möglichkeiten für ungeahnte Erweiterungen offen.

Ne, super ist schon richtig.
Mit ? extends T frage ich nach einem Objekt das mindestens T als Rückgabeparameter zurück gibt.
Mit ? super T frage ich nach einem Objekt das mindestens T als Übergabeparameter akzeptiert.

Vielleicht ist das hier aber auch verwirrend, weil nicht ganz klar ist das ich hier absichtlich nach einer ParentNode Frage, die eine ganz bestimmten ChildNode Vererbung als Parameter akzeptiert, aber eventuell auch andere ChildNode Vererbungen enthalten KANN. Es ist also nicht sicher ob die ParentNode nicht auch andere Klassen enthält.

I beg to differ. Mit extends werden die Klasse selbst und alle Unterklassen erfasst, mit super die Klasse selbst und alle Oberklassen, bis hin zu Object. Dabei wird extends für Kovarianz benötigt (z.B. kann ein Supplier<Integer> dort verwendet werden, wo ein Supplier<Number> benötigt wird, und um das zu erlauben, kann man stattdessen Supplier<? extends Number> schreiben). Dagegen wird super für Kontravarianz benötigt (z.B. kann ein Comparator<Number> dort verwendet werden, wo ein Comparator<Integer> benötigt wird, und um das zu erlauben, kann man stattdessen Comparator<? super Integer> schreiben. Und das geht nicht nur eine Stufe weit, auch ein Comparator<Object> wäre dann an dieser Stelle erlaubt).

Ich hatte noch auf Landeis Antwort gewartet, aber da er auch nur noch mal die Bedeutung von extend und super erläutert hat (und ganz ehrlich, super ist verwirrend je mehr man darüber nachdenkt und der Nutzen auch nicht gleich ersichtlich)(außerdem finde ich meine Erklärung simpler :stuck_out_tongue:) aber ich habe die „Lösung gefunden“, naja zumindest ist der Compiler zufrieden

public class Foo<A> {
    Bar<? super A> bar;
    public <P extends Bar<? super A>> Foo(P p) {
    }
}

public class Foo<A> {
    Bar<? super A> bar;
    public Foo(Bar<? super A> p) {
    }
}

Ersteres war komplett unnötig und auch nur ein Überbleibsel, ist aber eigentlich legitim. Den Compiler hat es im kleinen kompilierbaren Beispiel auch nicht gestört. Dabei war es nur eine Kopie der Klassen und Funktionen ohne Logik. Im Produktionscode aber schien er nicht assoziieren können das ? = ?. Die Lösung kam mir als er beim Aufruf von do( BoardSystemLogic) geantwortet hatte „BoardSystemLogic capture ?“ nicht zu kennen - völlig daneben.

Ähm… ja… genau… das sollte der springende Punkt meiner Aussage sein. Ich jedenfalls würde bei Parameterübergaben (Konstruktoren und Methoden) jedenfalls super nicht verwenden wollen. man will ja ein Minimum übergeben bekommen und wie weit das noch erweitert ist (Maximum), soll dabei meist egal sein, oder nicht? (Naja, bei Interfaces mag das etwas Anderes sein.)

Wie genau super und extends arbeiten, war mir bisher selbst nicht ganz klar, aber mit Landeis Erklärung hat sich das nun geändert. Danke dafür.

Naja scheinbar hat Landeis Erklärung trotzdem noch nicht ganz eingeschlagen - wie gesagt, es ist verwirrend. Wie du sagst „Warum sollte man ein Generic denn deckeln wollen?“, und dann kommen wir auf das zurück was Landei und ich erklärt haben:

List<? super Number> l = ...;
l.add( new Object() ); // geht nicht
l.add( new Integer() ); // geht

Also nichts mit Deckelung, im Gegenteil: Mit extends wäre Integer nicht erlaubt! Denn die eigentliche Klasse „?“ bleibt ja weiterhin unbekannt und wir wissen nur das die generische Klasse mindestens (extends) Number sein muss. Einer Liste mit extends kann man deshalb keine Elemente hinzufügen! Man kann aber Elemente per get lesen und sie wie Number oder wie Object behandeln.

Mit super aber wissen wir die Liste akzeptiert mindestens Number und alle vererbenden Klassen. Deshalb geht add(Integer), aber nicht add(String). List.get() gibt dafür maximal nur Object zurück, weil ? ist natürlich auch hier unbekannt.

Deshalb hab ich geschrieben:

Mit ? extends T frage ich nach einem Objekt das mindestens T als Rückgabeparameter zurück gibt.
Mit ? super T frage ich nach einem Objekt das mindestens T als Übergabeparameter akzeptiert.

Das ist die praktische Erklärung eines Ingenieurs zu Landeis ausführlicher theoretischer Informatik. :smiley:

Die „praktische Erklärung“ (ohne die Verwendung von Worten wie „Kovarianz“ :wink: ) wird manchmal auch als „PECS“ bezeichnet: What is PECS (Producer Extends Consumer Super)?

Ganz allgemein:

  • Wenn das Ding, das einen Generic-Parameter hat, etwas produziert (also „liefert“), dann kann man es mit <? extends T> auflockern: Wenn es eine Number liefern soll, dann kann es auch etwas sein, was etwas spezifischeres liefert - z.B. einen Integer, weil der ja eine Number ist
  • Wenn das Ding, das einen Generic-Parameter hat, etwas konsumiert (also „empfängt“), dann kann man es mit <? super T> auflockern: Wenn es eine Number bekommen soll, dann kann es auch etwas sein, was etwas allgemeineres bekommen kann - z.B. ein Object, weil eine Number ja ein Object ist

Ich hatte irgendwann mal ein paar Beispiele dazu erstellt - die genauen Beispiele waren spezifischer, aber ich hab’ den „Kern“ nochmal hier zusammengefasst:

package bytewelt;

import java.util.function.Consumer;
import java.util.function.Supplier;

public class PECS
{
    public static void main(String[] args)
    {
        Supplier<Object> objectSupplier = null;
        Supplier<Number> numberSupplier = null;
        Supplier<Double> doubleSupplier = null;
        
        Consumer<Object> objectConsumer = null;
        Consumer<Number> numberConsumer = null;
        Consumer<Double> doubleConsumer = null;
        
        // Without PECS:
        
        passItOnWithoutPecs(objectSupplier, objectConsumer); // OK :-)
        //passItOnWithoutPecs(objectSupplier, numberConsumer); // Not OK :-(
        //passItOnWithoutPecs(objectSupplier, doubleConsumer); // Not OK :-(

        //passItOnWithoutPecs(numberSupplier, objectConsumer); // Not OK :-(
        passItOnWithoutPecs(numberSupplier, numberConsumer); // OK :-)
        //passItOnWithoutPecs(numberSupplier, doubleConsumer); // Not OK :-(
        
        //passItOnWithoutPecs(doubleSupplier, objectConsumer); // Not OK :-(
        //passItOnWithoutPecs(doubleSupplier, numberConsumer); // Not OK :-(
        passItOnWithoutPecs(doubleSupplier, doubleConsumer); // OK :-)
        
        // With PECS:
        
        passItOnWithPecs(objectSupplier, objectConsumer); // OK :-)
        //passItOnWithPecs(objectSupplier, numberConsumer); // Not OK :-(
        //passItOnWithPecs(objectSupplier, doubleConsumer); // Not OK :-(

        passItOnWithPecs(numberSupplier, objectConsumer); // OK :-)
        passItOnWithPecs(numberSupplier, numberConsumer); // OK :-)
        //passItOnWithPecs(numberSupplier, doubleConsumer); // Not OK :-(
        
        passItOnWithPecs(doubleSupplier, objectConsumer); // OK :-)
        passItOnWithPecs(doubleSupplier, numberConsumer); // OK :-)
        passItOnWithPecs(doubleSupplier, doubleConsumer); // OK :-)
        
    }
    
    private static <T> void passItOnWithoutPecs(
        Supplier<T> supplier, Consumer<T> consumer)
    {
        consumer.accept(supplier.get());
    }
    
    private static <T> void passItOnWithPecs(
        Supplier<? extends T> supplier, Consumer<? super T> consumer)
    {
        consumer.accept(supplier.get());
    }
}

(Dass hier der Typ als <T> bei der Methode steht, „verzerrt“ das ganze ein bißchen. In der Realität geht es oft um eine komplette Klasse, die ein <T> hat, und wo man PECS dann bei einzelnen Methoden anwendet, die ggf. nur einen Producer oder nur einen Consumer bekommen. Aber wenn man es so kombiniert, ist es IMHO recht übersichtlich, und geeignet, um den Punkt zu verdeutlichen: )

Ohne PECS muss der Typ von Producer und Consumer genau zueinander passen. Mit PECS wird das erreicht, was man intuitiv erwarten würde: Jemand, der eine Number liefert, kann sie auch an jemanden weitergeben, der ein Object erwartet.

(Da das Ding in Java nicht „Producer“ heißt, sondern „Supplier“, wäre „Supplier extends, Consumer super“ eigentlich passender. Das Akronym wäre dann aber „SECS“. *albern kichert*)


Das spezifische Beispiel, um das es damals ging, war etwas, was mit einer „Distanzmatrix“ zu tun hatte. Das grobe Muster wa da sowas:

class Computer<T> {

    DistanceFunction<T> distanceFunction; // + setter

    DistanceMatrix compute(List<T> elements) { 
        ....
        distances[i][j] = distanceFunction.apply(ei, ej);
        ...
    }
}

Was man dann leicht verallgemeinern konnte: Mit einem

    DistanceFunction<? super T> distanceFunction;  // super hier
    DistanceMatrix compute(List<? extends T> elements) {  // extends hier

konnten damit dann viel mehr Kombinationen abgefrühstückt werden


Aber nebenbei: Ich habe den spezifischen Fehler, um den es hier ursprünglich ging, nicht im Detail nachvollzogen. Was ich aber auch (schmerzhaft) erfahren habe, ist das irgendwelche „Selbstreferentiellen Typen“ (also Baumstukturen, wie etwa ein Szenegraph) nur schwer schön zu generifizieren sind. Irgendwo fährt man sich immer fest, und in einem Fall habe ich dann schlicht aufgegeben, und mich mit einigen unchecked casts abgefunden, um den Code zumindest ansatzweise les- und nachvollziehbar zu halten…