Generics Problem #4200

Hi,

kurze Frage, warum will das hier nicht gehen?

image

image

Die Implementierung der Klasse Listener fehlt.
Die Klasse java.net.http.WebSocket.Listener ist nicht generisch.

Bitte keine Screenshots posten.
Bitte möglichst immer ein vollständiges SSCCE angeben, welches man kopieren kann.
Bitte auch die Fehlermeldung(en) kenntlich machen…

1 „Gefällt mir“

Ich hoffe das genügt den Ansprüchen.

public class AdvValue<G> {
    private final WeakHashSet.WeakListenerCollection<? super G> listeners = new WeakHashSet.WeakListenerCollection<>();

    public G v(AdvParameter<?> listener) {
        listeners.add( new WeakHashSet.WeakListener<Object, Object>(listener.parameter, listener));
        return get();
    }
}
public class AdvParameter<T> implements WeakHashSet.Listener<Parameter<T>, Object> {
    protected Parameter<T> parameter;

    public AdvParameter(@NotNull Parameter<T> parameter) {
        this.parameter = parameter;
    }
}
public class WeakHashSet<K, E> {
    public static class WeakListenerCollection<V> extends HashMap<Object, WeakListener<?, V>> {
        public boolean add(WeakListener<?, V> oWeakListener) {
            Object owner = oWeakListener.owner.get();
            if( owner == null )
                return false;
            return this.add(owner, oWeakListener);
        }
    }

    public static class WeakListener<O, V> {
        private final WeakHashSet.Listener<? super O, ? super V> listener;
        private final WeakReference<? extends O> owner;

        public WeakListener(O owner, WeakHashSet.Listener<? super O, ? super V> listener) {
            this.listener = listener;
            this.owner = new WeakReference<>(owner);
        }

        public void listen(V v) {
            O ownerHardRef = owner.get();
            if( ownerHardRef != null )
                listener.listen(ownerHardRef, v);
        }
    }

    public interface Listener<O, V> {
        void listen(O o, V v);
    }
}

Ich kann meinen Beitrag leider nicht editieren, ich hab den Code mal refactored, sodass er sich besser zum Copy-Paste in eine IDE eignet:

package de.testsuit;

import java.lang.ref.WeakReference;
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
	// write your code here
    }

    public class AdvValue<G> {
        private final WeakHashSet.WeakListenerCollection<? super G> listeners = new WeakHashSet.WeakListenerCollection<>();

        public G v(AdvParameter<?> listener) {
            listeners.add(new WeakHashSet.WeakListener<Object, Object>(listener.parameter, listener));
            return null;
        }
    }

    public static class AdvParameter<T> implements WeakHashSet.Listener<T, Object> {
        protected T parameter;

        public AdvParameter(T parameter) {
            this.parameter = parameter;
        }

        @Override
        public void listen(T o, Object o2) {
        }
    }
    
    public static class WeakHashSet<K, E> {
        public static class WeakListenerCollection<V> extends HashMap<Object, WeakListener<?, V>> {
            public boolean add(WeakListener<?, V> oWeakListener) {
                Object owner = oWeakListener.owner.get();
                if( owner == null )
                    return false;
                this.put(owner, oWeakListener);
                return true;
            }
        }

        public static class WeakListener<O, V> {
            private final WeakHashSet.Listener<? super O, ? super V> listener;
            private final WeakReference<? extends O> owner;

            public WeakListener(O owner, WeakHashSet.Listener<? super O, ? super V> listener) {
                this.listener = listener;
                this.owner = new WeakReference<>(owner);
            }

            public void listen(V v) {
                O ownerHardRef = owner.get();
                if( ownerHardRef != null )
                    listener.listen(ownerHardRef, v);
            }
        }

        public interface Listener<O, V> {
            void listen(O o, V v);
        }
    }
}

Was genau hast du denn vor? Du willst, dass dir das System meldet, wenn ein Objekt abgeräumt wird, also aus deinem WeakHashSet entfernt wird, richtig? Das ist leider nicht so ganz einfach und die Methode, mit der du das hier versuchst, kann unheimlich tiefe Pitfalls beinhalten, denn wenn man eine WeakReference in einem Programm verwendet erfährt man ohnehin während des normalen Programmablaufs, ob die entsprechende StrongReference schon abgeräumt wurde, oder nicht - dafür braucht man keinen Listener.
Die WeakHashMap ist da um Einiges bequemer, denk’ ich mal.

Ich hab den Code etwas vereinfacht. Ich verwende eine WeakHashMap. Und der Sinn ist nicht herauszufinden ob ein Objekt abgeräumt wurde, dafür würde ich einfach eine ReferenceQueue nehmen.

Der Sinn ist es eine "Weak"Listener API zu bekommen. Also Listener die automatisch vom GC aufgeräumt werden, wenn sie nicht mehr benötigt werden. Mit dem Sinn dass man sich solche Aufräumaktionen sparen kann:

speaker.removeListener(target);
target = new ...;

Zum dem Zweck verwende ich eine WeakHashMap (im Code Beispiel oben, ist das nur eine HashMap) um die Listener als Key/Value pairs zu halten. Die Listener werden als Values gespeichert (hard reference), und als Key wird ein x-beliebiges Objekt als „Besitzer/Owner“ (soft reference) definiert. Sobald der Owner aufgeräumt wird, verfallen auch die Listener.

Jetzt kann es aber sein, dass ein Listener eine (Hard)Referenz auf seinen Besitzer/Owner hat und damit der Owner niemals aufgeräumt wird, obwohl sonst niemand eine Referenz darauf besitzt.

Und hier kommt die Wrapper-Klasse „WeakListener“ ins Spiel. Die hält auch eine WeakReference auf den Owner und übergibt sie dem eigentlichen Listener als Parameter.

Mit dem Konstrukt kann man am Ende schreiben:

listeners.add( new WeakListener( mutableString, (mutableString, value) -> mutableString.set(value) ) );
/* oder noch kürzer */
listeners.add( mutableString, (mutableString, value) -> mutableString.set(value) );

Das ist der Sinn. Aber wie du in meinem letzten Codebeispiel siehst, hab ich Probleme bei der Umsetzung mit den Generics.

  • Als jemand der Listener annimmt, ist mir völlig egal von welchem Typ der Owner ist, oder welchen Typ-Wert der Listener annimmt, solange Letzterer ein Supertyp des Wertes ist den ich produziere.
    Das versuche ich mit den Generics zu beschreiben, aber er meckert einfach nur die ganze Zeit mit nutzlosen Fehlermeldungen herum.

Die Generics sind wirklich gruselig. Ich bekomme den Fehler weg, wenn ich drei Sachen mache:

  • Listener interface raus aus WeakListener nehmen (keine Ahnung warum, aber anders komme ich gar nicht weiter)
  • WeakHashSet.WeakListenerCollection<G> schreiben, das <? super G> ist irgendwie Overkill
  • Und dann muss man das Argument casten. Eigentlich ist der zweite Parameter in AdvParameter ja Object, was ? super G erfüllen sollte, aber hey… Der Cast zu (Listener<? super Object, ? super G>) tut es

Ob das alles irgendwie Sinn ergibt, kann ich nicht sagen. Normalerweise gehe ich so vor, dass ich ohne Wildcards starte, probiere was nicht geht, und nur dann versuche welche hinzuzufügen.

1 „Gefällt mir“

Ja, mich verwirrt auch die Tatsache warum das hier geht

List<Wrapper<? super Number>> listeners = new ArrayList<>();
listeners.add( new Wrapper<Object>() );

aber hier will er nicht mehr

List<Wrapper<Li<? super Number>>> listeners = new ArrayList<>();
listeners.add( new Wrapper<Li<Object>>() );
Required: Li<? super Number>
Provided: Li<Object>

Also soweit ich weiß, müsstest du dazu die komplette (AWT)EventQueue umbauen, wenn diese nicht schon WeakReferences für Listener verwendet (was sie vermutlich jedoch nicht tut, weil Listener gemulticastet werden). Solange nämlich in der Queue auch nur eine StrongReference eines Listeners verbleibt (was natürlich erst dann nicht mehr der Fall ist, wenn er removed wurde, springt deine WeakReference erst gar nicht an. Die Listener, die du den Eventquellen (z.B. speaker) hinzufügst, müssten selber WeakReferences sein, aber dazu müsste ich mir selber noch etwas ausdenken. War aber trotzdem eine nette Idee.

Andererseits… Wenn man sich die addSomethingListener-Methoden ansieht, dann stellt man fest, dass die Eventquellen immer nur ein Element der ganzen gemulticasteten Listener speichern und das bedeutet, dass wenn dieses Element flöten geht, weil die Eventquelle abgeräumt wurde, dann wird auch die ganze Kette hinter diesem Element automatisch abgeräumt, und WeakReferences sind gar nicht mehr nötig.

Die eigentliche Intention is nachvollziehbar und das Problem bekannt. Als spezielle Form davon: Man ist leicht versucht, sowas zu schreiben wie

interface Listener { 
    void changed();
}
class Model {
    void addListener(Listener listener) { ... }
    void removeListener(Listener listener) { ... }
}

class View implements Listener {
    public View(Model model) {
        model.addListener(this);
    }
    @Override
    public void changed() {
        System.out.println("Something changed");
    }
}

Das ist eine Art „Anti-Pattern“ (für das ich inzwischen glaube, ein geschärftes Auge zu haben, aber … manchmal stolpert man doch noch drüber, weil’s vermeintlich erstmal einfacher ist…).

Das Problem ist, dass wenn man dann schreibt:

Model model = new Model();
View view = new View(model);

// Later:
view = new View(model);

// Even later:
view = new View(model);

man bei jeder Änderung des Models die drei Views benachrichtigt. Die Views „leben“ noch (in dem Sinne, dass sie nicht vom GC aufgesammelt werden können) weil sie in der Listener-Liste des Modells rumlungern. Gleichzeitig hat man keine „eigene“ Referenz mehr darauf. Und selbst wenn man eine hätte: Die Verbindung wird im Konstruktor hergestellt, und kann nicht mehr rückgängig gemacht werden.

Die Lösung ist eigentlich einfach: Das Setzen des Modells muss eine eigene Methode sein!

interface Listener { 
    void changed();
}
class Model {
    void addListener(Listener listener) { ... }
    void removeListener(Listener listener) { ... }
}

class View implements Listener {
    private Model model = null;
    public View(Model model) {
        setModel(model);
    }

    // Das ist das generische Pattern zum Setzen des Modells, und zum
    // passenden Verwalten der Listener:
    public void setModel(Model newModel) {
        if (this.model != null) {
            model.removeListener(this);
        }
        this.model = newModel;
        if (this.model != null) {
            model.addListener(this);
        }
    }

    @Override
    public void changed() {
        System.out.println("Something changed");
    }
}

Dann kann man das ganze so machen:

Model model = new Model();
View view = new View(model);

// Later:
view.setModel(null); // Listener-Verbindung kappen
view = new View(model);

// Even later:
view.setModel(null); // Listener-Verbindung kappen
view = new View(model);

Und ja, es ist verlockend, zu versuchen, das mit Weak/Soft-References zu lösen. Aber wie schon gesagt wurde: Da gibt’s viele Caveats. Die Frage, wann genau welche Referenz in eine ReferenceQueue gelegt wird, ist diffizil. Es kann funktionieren, aber man sollte sich sehr genau überlegen, ob und wie man das machen will.


Die eigentliche Frage war ja aber anscheinend eine andere. Also nicht „Ist das vernünftig, was ich hier mache?“, sondern „Warum funktioniert das mit den shyce Generics nicht!?!?!“ :wink:

Leider ist der „echte“ Code etwas zu kompliziert, um ihn jetzt auf die Schnelle im Detail nachzuvollziehen (z.B. die Rolle von AdvValue, AdvParameter, und welchen Zweck welche Typparameter dort erfüllen sollen). Allgemein hat man bei Listenern „oft“ nicht so viel Flexibilität, weil man schlicht nicht weiß, was sie machen. Zum Beispiel:

interface Listener<T> {
    void changed(T t);
}
class Model<T> {
    void addListener(Listener<T> listener) {
        ...
    }
}

Dort würde man intuitiv und wenn man Generics ein paar Jahre lang geatmet hat eigentlich addListener(Listener<? extends T> listener) schreiben, aber … nope, es funktioniert leider nicht. (Kann man begründen, ist aber kompliziert, und vermutlich gerade nicht relevant).


Das vereinfachte Beispiel war ja dann das mit dem Wrapper und dem Li.

Die Antwort auf alle Fragen in dieser Hinsicht (auch die, die man selbst nie gestellt hätte, weil man, um die Frage überhaupt formulieren zu können, so viel wissen müßte, dass man die Frage gar nicht mehr stellen würde), steckt in Angelika Langer, Generics FAQ: Which super-subtype relationships exist among instantiations of generic types?

Hier ist das Beispiel nochmal (editiert - war bei einem Vesuch, das auf List zu verallgemeinern, selbst ins Stolpern gekommen…)

package bytewelt;

import java.util.ArrayList;
import java.util.List;

class Wrapper<T>
{

}

interface Li<T>
{

}

public class YetAnotherGenericsQuestion
{
    public static void main(String[] args)
    {
        List<Wrapper<? super Number>> listenersA = new ArrayList<>();
        listenersA.add(new Wrapper<Object>()); // Geht

        List<Wrapper<Li<? super Number>>> listenersB = new ArrayList<>();
        //listenersB.add(new Wrapper<Li<Object>>()); // Geht nicht
        
        List<Wrapper<? extends Li<? super Number>>> listenersC = new ArrayList<>();
        listenersC.add(new Wrapper<Li<Object>>()); // Geht
    }

}

Das erste geht, weil Wrapper<Object> ein subtype von Wrapper<? super Number> ist.

Das zweite geht nicht. Der Grund ist: Li<Object> ist zwar ein subtype von Li<? super Number> aber Wrapper<Li<Object>> ist kein subtype von Wrapper<Li<? super Number>>. Man kann da gedanklich wirklich eine String-Ersetzung duchführen. Hier mal mit List als Beispiel:

List<? extends X> = List<Y> funktioniert, wenn Y ein subtyp von X ist.

Wenn man für X nun Number einsetzt, und für Y einen subtyp davon, nämlich Integer, dann ist es sichtbarer:
List<Number> = List<Integer> : Geht nicht
List<? extends Number> = List<Integer> : Geht

Wenn man (wie in deinem Fall) für X nun List<? super Number> einsetzt, und für Y einen subtyp davon, nämlich List<Integer>, dann ist das Muster das gleiche:
List<List<? super> Number> = List<List<Integer>> : Geht nicht
List<? extends List<? super Number>> = List<List<Integer>> : Geht

1 „Gefällt mir“

Fehlt da nicht irgendwo das C - also der Controller?

class Contol {
  private Model m;
  public Control(Model m) {
    setModel(m);
  }
  public void setModel(Model m) {
    if(m == null) {
      return;
    }
    if(this.m != null) {
      //cleanUp();
    }
    this.m = m;
  }
  public void registerView(View v) {
    // class check
    // init view for model with repeated
    // model.addListener(listenerInstanceInControl)
    // depending on instanceOfView
    // ...
  }
}
class Model {
  public void addListener(Listener l) {}
  public void removeListener(Listener l) {}
}
class View {
  public View(Control c) {
    c.registerView(this);
  }
}
interface Listener {
  void somethingChangedSomething(Event e);
}

Ich habs hinbekommen. Am liebsten würde ich jetzt erklären was jetzt der entscheidende Punkt war damit wir alle was daraus lernen können, aber ich hab ehrlich gesagt keine eine Ahnung.

Ich hab einfach nochmal alles komplett neu programmiert mit den folgenden Erkenntnissen:

Sowie etwas was ich auf Google gelesen habe, Hilfsgenerics bilden um captures (?) aufzulösen bzw. zu verbinden.

Obwohl im folgenden Codeschnipsel der Generic Typ von AdvParameter eigentlich völlig uninteressant ist und keinen Einfluss ausübt, weshalb auch nur „?“ angefragt wird, funktioniert es nicht („Cannot infer arguments“ bei new Entry<>(p))

public G v(AdvParameter<?> p) {
        final Entry<?> entry = new Entry<>(p);
        listeners.add(entry);
        return get();
    }

aber mit einem Hilfsgeneric <T> geht das schon

public <T> G v(AdvParameter<T> p) {
        final Entry<?> entry = new Entry<>(p);
        listeners.add(entry);
        return get();
    }

Klasse. Jetzt hab ich eine Collection für Listener, mit folgenden Funktionen:

  • Weakreference-Listener: Listener wird aufgeräumt, wenn keine Referenz mehr existiert
  • Weak-Ownership reference-Listener: Listener wird aufgeräumt wenn einer der Besitzer aufgeräumt wird
  • Und der klassischen Hardreferenz-Listener:: Listener muss manuell wieder entfernt werden

Einziges Problem: Es gibt keine Garantie darauf, dass ein Listener tatsächlich sofort vom GC aufgeräumt wird, sobald keine Hardreference mehr existiert. Logisch.
Aber das bringt halt das praktische Problem - über das ich vorher nicht nachgedacht habe -, dass ein Listener noch auslösen kann, auch wenn man mit seiner Existenz nicht mehr rechnet.

switch1.addWeakListener((listener = (b) -> this::turnOn));
switch2.addWeakListener((listener = (b) -> this::turnOn));

In dem Beispiel, kann this.turnOn(Boolean) also noch eine Zeit lang von switch1 weiterhin ausgelöst. Da hilft nur manuelles entfernen.


Wie dem auch sei, vielen Dank an alle was die Generics betrifft! :smiley:

public class Multiplexing {
	public static void main(String[] args) {
		// Add your test code here
	}
}
interface Listener {
	// Help interface without any generics
}
class MoreConcreteEvent {
	// dummy class
}
class WhatEver {
	// another dummy class
}
interface MoreConcreteListener<T> extends Listener {
	void SomethigsChanged(MoreConcreteEvent e);
}
class EventMulticaster implements Listener {
	public final Listener a, b;

	private EventMulticaster(Listener a, Listener b) {
		this.a = a;
		this.b = b;
	}

	private Listener remove(Listener oldl) {
        if (oldl == a)  return b;
        if (oldl == b)  return a;
        Listener a2 = removeInternal(a, oldl);
        Listener b2 = removeInternal(b, oldl);
        if (a2 == a && b2 == b) {
            return this;        // it's not here
        }
        return addInternal(a2, b2);
    }

	public static Listener addInternal(Listener a, Listener b) {
        if (a == null)  return b;
        if (b == null)  return a;
        return new EventMulticaster(a, b);
    }

    public static Listener removeInternal(Listener l, Listener oldl) {
        if (l == oldl || l == null) {
            return null;
        } else if (l instanceof EventMulticaster) {
            return ((EventMulticaster)l).remove(oldl);
        } else {
            return l;           // it's not here
        }
    }
}
class SomeClassTriggeringEvents {
	private Listener l = null;

	public void add(MoreConcreteListener<? super WhatEver> l) {
		this.l = EventMulticaster.addInternal(l, this.l);
	}

	public void removeListener(MoreConcreteListener<? super WhatEver> l) {
        if (l == null) {
            return;
        }
        this.l = EventMulticaster.removeInternal(l, this.l);
	}

	public void add(Listener l) {
		this.l = EventMulticaster.addInternal(l, this.l);
	}

	public void removeListener(Listener l) {
        if (l == null) {
            return;
        }
        this.l = EventMulticaster.removeInternal(l, this.l);
	}
}

Schaut es euch genau an… Was denkt ihr wohl, was mit 100 geaddeten Listenern passiert, wenn eine Instanz von SomeClassTriggeringEvents abgeräumt wird?
Es wird eine Instanz der Klasse EventMulticaster mit abgeräumt und in Folge dessen die ganze Kette Listener, die in dieser und weiteren Instanzen referenziert wurden. Das Einzige, was nicht abgeräumt wird, sind Listener, die noch irgendwo anders referenziert sind, wie z.B. in meiner Control Klasse aus dem vorhergehenden Beispiel… Aber was solls… die kann man ja auch als WeakReference halten, nur hätte man nichts davon, wenn man möglichst wenig (wiederverwertbare) Listener Instanzen haben möchte.

Ich verstehe nicht ganz was du zu argumentieren versuchst. Was ist die Aussage und in welchen Zusammenhang steht das zum restlichen Thema. :slight_smile:

Dein Code ist praktisch äquivalent zu einer LinkedList<Listener> (?)

Nun, ich versuche zu Argumentieren, dass du dir deine WeakListener vollkommen sparen kannst, weil Listener normalerweise bereits „weak“ verkettet werden und ja, es ist eine LinkedList.

Mein letzter Code ist die Kurzform des AWTEventMulticasters, welcher auch für Swing verwendet wird.

Ehrlich gesagt: So ganz hatte ich das auch nicht verstanden. Die Ähnlichkeit zum AWTEventMulticaster ist erkennbar, aber ob man die listener in einer verketteten Liste speichert, oder in einer normalen Liste, macht ja keinen Unterschied. Das Problem ist ja, dass in beiden Fällen in dieser Liste die letzte und einzige Referenz auf einen Listener liegen kann. Dass also der Listener nicht GC’t werden kann, nur weil er noch in dieser Liste liegt.

Oder anders: Wenn man sowas macht wie

class View implements Listener {
    public View(Model model) {
        model.addListener(this);
    }
}

und dann sagt

void doit(Model model) {
    View view = null;
    view = new View(model);
    view = new View(model);
    view = new View(model);
}

dann hängen an dem Model drei Listener, obwohl die view-Objekte eigentlich aufgeräumt werden könnten und sollten.

(Wenn man die Listener eben nicht direkt, sondern nur über WeakReference speichern würde, dann würden die view-Objekte (zumindest theoretisch!) irgendwann aufgeräumt werden…)

Nun eben nicht. Zumindest dann nicht, wenn Model diese Listener entsprechend des Multicasters speichert. Dann hängt am Model nämlich nach dem zweiten addListener bereits eine Instanz zweier gecasteter Listener und wenn die verschwindet, verschwinden auch alle Anderen. Der Fehler, den du hier machst, ist ganz einfach der, dass du View selbst als Listener verwendest und bei der Initialisierung addest. Wenn du nun diese Instanz von View einfach wieder verwirfst, ohne den Listener wieder zu removen, bleibt der Listener natürlich in Model noch erhalten. So etwas würde ich zumindest niemals tun (NB: Ich hatte es mal so getan und kam damit in Teufels Küche… wobei mir btw. auch wieder die Methode .finalize() einfällt, in welcher View ja mal eben model.removeListener(this) aufrufen könnte, was aber auch keinen Sinn macht, weil View nicht abgeräumt wird, solange es in der Listener-Kette hängt.).

Und wenn man das Ganze nun mit WeakReferences macht, bleibt immer noch genau jene Instanz des Referenz-Wrappers in der Liste der Listener. Deswegen empfiehlt es sich immer View und Listener zu trennen, beides im Controller beim adden zu referenzieren. Die View kann dann nur noch dann verloren gehen, wenn sie aus dem Controller removed wird und dann kann man auch dazu referenzierte Listener aus dem Model entsprechend removen… ganz ohne WeakReference

Dass das Ganze auch mit einer normalen List funktioniert, ist durchaus möglich, aber die Java-Entwickler haben sich Sicher schon etwas dabei gedacht, warum sie das so und nicht anders implementiert haben.

geil wie sich wieder alles darum dreht das man aufräumen muss/will - weil das Destruktor-freie-System da so seine Probleme hat. Wer auch immer auf die f****** Idee gekommen ist aus einem Deterministischen System ein Nicht-Deterministisches zu machen.

Die Idee hinter Java ist doch eben genau um sich um solche Dinge nicht mehr kümmern zu müssen, aber Stroustrup hatte schon recht als er über Java sagte, Destruktoren braucht man immer.

Bei einem View sehe ich keine Probleme. Wenn das noch eine Zeit geupdated wird, aber niemand hat eine Referenz darauf, wird das in den meisten Fällen niemanden interessieren. Es sei denn natürlich da wird krass etwas herumgerechnet. Problematisch ist eher, wenn der Listener noch in irgendeiner Weise das restliche System beeinflussen kann obwohl er das nicht sollte.

Es war ein Experiment, ich finds cool das es funktioniert hat, aber je mehr ich darüber nachdenke desto weniger denke ich ist es wirklich sinnvoll :smiley:

Btw. sollte sich jemand dafür interessieren:


Anwendung ganz leicht.
/* Setup */
WeakListenerCollection<String> collection = new WeakListenerCollection<>();

/* Prepare */
Object owner = new Object();
collection.add( Entry.owner( owner, (o, v) -> System.out.println( v ) ) );

/* Use */
collection.callListeners( "HelloWorld" );
owner = null;
System.gc();
collection.callListeners( "Hello World #2" );