Me pregunto qué puede ser una buena manera de implementar algún tipo de observable en Java sin muchas cosas de interfaz. Pensé que sería bueno usar las interfaces funcionales predefinidas. Para este ejemplo, utilizo un consumidor de cadena para representar a un oyente que toma una cadena para la notificación.

class Subject {

  List<Consumer<String>> listeners = new ArrayList<>();
  
  void addListener(Consumer<String> listener) {listeners.add(listener);}

  void removeListener(Consumer<String> listener {listeners.remove(listener);}

  ...
}

class PrintListener{
  public void print(String s) { System.out.println(s); }
}

Subject subject = new ...
PrintListener printListener= new ...
subject.add(printListener); // works, i find it in the listener list
subject.remove(printListener); // does NOT work. I still find it in the list

Encontré la explicación:

Consumer<String> a = printListener::print;
Consumer<String> b = printListener::print;

// it holds:
// a==b       : false
// a==a       : true
// a.equals(b): false
// a.equals(a): true

Así que no puedo usar los punteros de lambdas / función como lo son.

Siempre hay una alternativa para tener las buenas interfaces antiguas, S.T. Registramos instancias de objetos pero no lambdas. Pero esperaba que haya algo más ligero.

Editar: De las respuestas actuales, veo los siguientes enfoques:

A) devuelve un asa que sostiene la referencia original
b) almacenar la referencia original usted mismo
c) devolver un poco de identificación (entero) que se puede usar en sujeto.reemove () en lugar de la referencia original

Tiendo a gustar a un). Aún tienes que hacer un seguimiento del asa.

9
markus 8 jun. 2021 a las 14:15

4 respuestas

La mejor respuesta

Estoy usando RJXS con bastante frecuencia últimamente, y allí han usado un valor de retorno personalizado llamado Subscription que se puede llamar para retirar el oyente registrado nuevamente. Lo mismo se podría hacer en su caso:

public interface Subscription {
    void unsubscribe();
}

Luego cambie su método addListener a esto:

public Subscription addListener(Consumer<String> listener) {
    listeners.add(listener);
    return () -> listeners.remove(listener);
}

El método removeListener se puede eliminar por completo. Y esto ahora puede ser llamado así:

Subscription s = subject.addListener(printListener::print);
// later on when you want to remove the listener
s.unsubscribe();

Esto funciona, porque la lambda devuelta en addListener() todavía utiliza la misma referencia de listener y, por lo tanto, se puede quitar de nuevo de la List. Nota lateral: Probablemente tendría más sentido usar un Set a menos que realmente se preocupe por la orden de iteración de su listeners

Una lectura agradable también sería ¿Hay alguna manera de comparar lambdas?, que entra en más detalle, por qué printListener::print != printListener::print.

6
Lino 8 jun. 2021 a las 12:10

Su suposición es que cada llamada a printListener::print devuelve la misma instancia de print.

Subject subject = new Subject();
PrintListener printListener= new PrintListener();
subject.add(printListener::print);
subject.remove(printListener::print);

El código anterior agrega un oyente e intenta eliminar a otro oyente, ya que printListener::print.equals(printListener::print) == false.

Subject subject = new Subject();
PrintListener printListener= new PrintListener();
Consumer<String> listener = printListener::print;
subject.add(listener);
subject.remove(listener);

Los trabajos anteriores, lo que le requieren mantener una referencia al oyente si necesita eliminarlo. Aunque si quisiera mantenerlo Muy liviano, podría adherirse a solo Consumer s sin la clase de concreto PrintListener, si las implementaciones son muy simples.

Consumer<String> listener = System.out::println;
subject.add(listener);
subject.remove(listener);
6
Kayaman 8 jun. 2021 a las 16:54

Tienes razón que confiar en la igualdad de funciones se convierte en un poco JANKY.

La forma en que a veces he resuelto esto en el pasado es que el método add genere una identificación aleatoria. La persona que llama puede usar esa misma ID si quiere eliminar al oyente en el futuro, o simplemente puede descartar la identificación si nunca planea eliminar al oyente.

class Subject {
    Map<String, Consumer<String>> idToListener = new HashMap<>();
  
    // Returns the ID of the listener, use it to remove 
    String addListener(Consumer<String> listener) {
       String id = generateRandomId();
       idToListeners.put(id, listener);
       return id;
    }

    void removeListener(String id) { idToListener.remove(id); }
}

El generateRandomId() podría ser cualquier cosa. No tiene que ser complejo. Puede usar enteros consecutivos si lo desea.

3
Michael 8 jun. 2021 a las 11:59

Para que esto funcione, PrintListener necesita implementar Consumer<String>, ya que esta es la firma requerida para un oyente (según su definición):

class PrintListener implements Consumer<String> {
    public void accept(String s) {
        System.out.println(s);
    }
}

Para probarlo, puede implementar una notificación simple -method en el Subject:

void notify(String s) {
    listeners.forEach(listener -> listener.accept(s));
}

Ahora su código compila y agrega / eliminando al oyente funciona como debería. Para abordar la segunda parte de su pregunta, agregué a un oyente usando una lambda para demostrar que esto funciona igual:

Subject subject = new Subject();

PrintListener printListener = new PrintListener();
Consumer<String> lambdaListener = System.out::println;

subject.addListener(printListener);
subject.addListener(lambdaListener);

subject.notify("abc");
subject.removeListener(printListener);
subject.removeListener(lambdaListener);
subject.notify("xyz");

Las salidas anteriores abc dos veces se esperan.

2
Matt 8 jun. 2021 a las 11:57