简体   繁体   中英

How do I implement removeable listeners when they're passed as lambdas or method references?

I am wondering what may be a good way of implementing some kind of observable in Java without much interface-stuff.

I thought it would be nice to use the predefined functional interfaces. For this example, I use a String Consumer to represent a listener that takes a String for notification.

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

I found the explanation:

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

So I can't use lambdas/function pointers as they are.

There is always the alternative to have the good old interfaces back, st we register object instances but not lambdas. But i hoped there is something more lightweight.

EDIT:

From current responses, I see the following approaches:

a) Return a Handle that holds the original reference
b) store the original reference yourself
c) Return some ID (integer) that can be used in subject.remove() instead of the original reference

I tend to like a). You still have to keep track of the Handle .

I'm using rjxs quite often lately, and there they've used a custom return value called Subscription which can be called to remove the registered listener again. The same could be done in your case:

public interface Subscription {
    void unsubscribe();
}

Then change your addListener method to this:

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

The removeListener method can be removed entirely. And this can now be called like this:

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

This works, because the returned lambda in addListener() still uses the same reference of listener and thus can again be removed from the List . Side note: it would probably make more sense to use a Set unless you really care about the iteration order of your listeners

A nice read would also be Is there a way to compare lambdas? , which goes into more detail, why printListener::print:= printListener::print .

Your assumption is that each call to printListener::print returns the same instance of print .

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

The above code adds one listener, and tries to remove another listener, since 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);

The above works, requiring you to keep a reference to the listener if you need to remove it. Although if you wanted to keep it very lightweight, you could stick to only Consumer s without the concrete PrintListener class, if the implementations are very simple.

Consumer<String> listener = System.out::println;
subject.add(listener);
subject.remove(listener);

You're right that relying on equality of functions becomes a bit janky.

The way I've sometimes solved this in the past is have the add method generate a random ID. The caller can use that same ID if it wants to remove the listener in future, or it can simply discard the ID if it never plans on removing the listener.

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); }
}

The generateRandomId() could be anything. It doesn't have to be complex. You could use consecutive integers if you want.

For this to work, PrintListener needs to implement Consumer<String> since this is the required signature for a listener (as per your definition):

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

To test it, you can implement a simple notify -methdod in the Subject :

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

Now your code compiles and adding/removing the listener works as it should. To address the second part of your question, I added a listener using a lambda to demonstrate that this works just as well:

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");

The above outputs abc twice as expected.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM