Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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, s.t. 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.

like image 525
markus Avatar asked Jun 08 '21 11:06

markus


1 Answers

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.

like image 90
Lino Avatar answered Sep 22 '22 14:09

Lino