Monday, 12 October 2015

Lambda expressions, events and weak references

What happens when you store a Java lambda expression in a weak reference? If this lambda expression is an instance method in an object that is still referenced then you might assume that it will be accessible until your object is out of scope. Your assumption may be wrong.

For some languages functions and methods are pointers in memory that can be passed around just like any other pointer. However, Java does not have pointers; so how does it allow us to pass a method to another method? In brief, a lambda expression is an anonymous class implementing a functional interface (with only one abstract method). Your lambda expression is no more than the instantiation of a compiler generated class.

Event driven example

Lets take an example of event driven architecture to illustrate this in action. The following uses an event emitter to notify observers of a change of state in a game of chess. The EventEmitter class has a set (implemented as a WeakHashMap) of callback functions that are executed with an event payload.

public class EventEmitter<E extends AbstractEvent> {
     private final WeakHashMap<EventObserver<E>, Object> observers=new WeakHashMap<>();

     public EventEmitter() {
          super();
     }

     public void add(EventObserver<E> observer) {
          observers.put(observer, null);
     }

     public void remove(EventObserver<E> observer) {
           observers.remove(observer);
     }

     public void fireEvent(E payload) {
          observers
          .keySet()
          .stream()
          .forEach(o -> o.callback(payload));
     }

     public int size() {
          return observers.size();
     }
}

The event payload

An event payload must extend AbstractEvent, which is no more than a marker class. A concrete example of this event is a SquareChangedEvent which signals to the graphical board that a square must be re-drawn. The logic for the chess game, including the computer player, are separated from the display logic. When the computer player makes a move it is signalled to the display for update. The event contains a payload of the square changed:

public class AbstractEvent {

}

public final class SquareChangedEvent extends AbstractEvent {
     private Square<ChessPiece> square;
 
     public SquareChangedEvent(Square<ChessPiece> square) {
          this.square = square;
     }

     public Square<ChessPiece> getSquare() {
          return square;
     }
}

The Observer

One or more objects may listen to emitted events. To receive the event, the class must implement a method with the correct signature. In this case, any method which takes as a single parameter a class extending AbstractEvent.

@FunctionalInterface
public interface EventObserver<E extends AbstractEvent> {
     void callback(E event);
}

// A MVC style controller that is a bridge between the chess engine and the display.
public class GameController {
     private final BoardPanel board;
     private final ChessGame game;

     public GameController(ChessGame game) {
          this.game=game;
          game.setOnSquareChanged(this::onSquareChanged);
     }

     // Have the board redraw the square 
     public void onSquareChanged(SquareChangedEvent event) {
          board.draw(event.getSquare());
     }
}

The Emitter

The event source keeps a track of observers that wish to receive the event using an EventEmitter for each event type. When the chess engine generates a move, the game signals the change by firing an event for the source and destination square affected.

public class ChessGame {
     private final EventEmitter<SquareChangedEvent> squareChangedObservers = new EventEmitter<>();

     public void setOnSquareChanged(EventObserver<SquareChangedEvent> observer) {
          squareChangedObservers.add(observer);
     } 
 
     public void move(String algebraic) {
               .
               .
               .
          squareChangedObservers.fireEvent(new SquareChangedEvent(from));
          squareChangedObservers.fireEvent(new SquareChangedEvent(to));
               .
               .
               .
     }
}

What is wrong?

On the surface, that all looks perfectly acceptable; it has a certain simplicity, even elegance. The trouble is that the events are never received by the Observer. The reason may not be obvious, but it is simple to understand. When game.setOnSquareChanged(this::onSquareChanged) is called passing the object method onSquareChanged the compiler is creating an anonymous class which delegates to this method. The object is only referenced in one place - the WeakReference held by a map. This makes it immediately available for garbage collection. The WeakHashMap has a ReferenceQueue that it polls and removes any dereferenced objects before returning a KeySet or other entries. It is as if it was never registered!

How to fix this?

The fix is easy. Either stop using Weak references or store at least one reference to the anonymous class before passing it to the event source. The first option is trivial, so let's look at the second option.

// A MVC style controller that is a bridge between the chess game and the display.
public class GameController {
     private final BoardPanel board;
     private final ChessGame game;
     // Keep a permanent reference to the event handler so it is not garbage collected
     private final EventObserver<SquareChangedEvent> onSquareChangedHandler = this::onSquareChanged;

     public GameController(ChessGame game) {
          this.game=game;
          game.setOnSquareChanged(onSquareChangedHandler);
     }

     // Have the board redraw the square 
     public void onSquareChanged(SquareChangedEvent event) {
          board.draw(event.getSquare());
     }
}

We now have a durable reference to this method which will last the lifetime of our object. When our object is dereferenced, so will the reference to the event handler in the WeakReferenceMap.

For many, using a weak reference in this manner is overkill. You could simply use a Set rather than a WeakHashMap if you do not have observers created and de-referenced on a regular basis.

1 comment: