Summary
Provides API for managing listeners (and bindings) life cycles in a deterministic way, where a listener gets automatically added or removed when a certain condition is met. This makes clean up of listeners simple enough that it can be done pro-actively instead of relying on weak listeners.
Problem
When a listener is added to a property, the property will keep a reference to the listeners target class to be able to notify it of invalidations or changes. When such listeners are registered on properties which have a much longer life cycle then the target, the target class cannot be garbage collected until the property itself becomes eligible for GC.
The current solution is to either manually remove listeners at the appropriate time, or to use weak listeners. Both of these options are sub-optimal:
Manual management and removal of listeners:
1) Manual removal requires tracking of the exact listener that was registered. This can lead to surprises when not the exact same instance was removed (removal of a listener which was never registered is a no-op without warning). This can happen easily when using a method reference for example, as there is no guarantee the method reference that was registered is the same as the one that is being removed; the method reference would have to be tracked specifically for this to work!
2) The timing of when to do manual removal is hard to get right. There is no "disposal" event or "dispose" method that can be used when a Node is no longer needed, nor can there be as Nodes are reusable. The user would have to manually arrange for a trigger to start clean up of listeners.
Using weak listeners:
Weak listeners allow you to register a listener without having to do manual clean up. However, they have their own problems:
1) Failing to reference the wrapped listener will result in it being cleaned up earlier than expected. This can lead to problems where a listener appears to work, but breaks after a GC occurs. This also impacts bindings which use weak listeners; if the final binding or listener added to such a binding is not referenced, the binding may stop working at any time.
2) Weak reference clean-up is unpredictable and not specified to occur at all or in a timely fashion. This means that in effect listeners may not be cleaned up at all or far later than one would expect.
3) Weak listeners leave behind a stub on the property they were registered after they are cleaned up. Additional action is taken by JavaFX to clean up dead listener stubs which is sub optimal.
Solution
Provide a new default method called when
on ObservableValue
(a parent interface for all properties), which creates a new ObservableValue
that can be disconnected and reconnected at any time from its source based on a condition.
This breaks the chain of references that may prevent garbage collection deterministically and immediately when the condition becomes false.
The condition itself is another ObservableValue
, like a BooleanProperty
, that can be toggled to unregister listeners involved in all bindings that use this property as their condition. The condition itself can be a complex binding which determines when exactly the listeners should be removed.
Applications for this construct include temporarily disabling updates of bindings (to batch multiple updates for related properties), doing coordinated clean up of listeners in a dispose
method by setting the condition to false
and to disable listeners when for example controls are invisible or currently not part of an active Scene
at all.
For this last scenario, a convenience method can be used to create a property that is only true
when a node is part of a scene, the scene is part of a window, and the window is currently shown:
public static ObservableValue<Boolean> shown(Node node) {
return node.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(Window::showingProperty)
.orElse(false);
}
The above method in combination with the when
construct allows UI programmers to tie models to their controls in a way that they only listen to their models when the controls are actively in use. As soon as the controls are no longer used, the listeners are removed. If there are no other references to the UI components, they will be eligible for GC.
This pro-active approach doesn't use weak listeners, doesn't require manual tracking of registered listeners and doesn't need additional work to trigger a timely disposal. It only requires a suitable condition which will trigger clean up automatically once it becomes false.
See the PR for more discussion, scenario's and (illustrated) explanations: https://github.com/openjdk/jfx/pull/830
Specification
The changes in ObservableValue:
diff --git a/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java b/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
index 664a27a383..fb54e86588 100644
--- a/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
+++ b/modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java
@@ -251,4 +252,55 @@ public interface ObservableValue<T> extends Observable {
default <U> ObservableValue<U> flatMap(Function<? super T, ? extends ObservableValue<? extends U>> mapper) {
return new FlatMappedBinding<>(this, mapper);
}
+
+ /**
+ * Returns an {@code ObservableValue} that holds this value and is updated only
+ * when {@code condition} holds {@code true}.
+ * <p>
+ * The returned {@code ObservableValue} only observes this value when
+ * {@code condition} holds {@code true}. This allows this {@code ObservableValue}
+ * and the conditional {@code ObservableValue} to be garbage collected if neither is
+ * otherwise strongly referenced when {@code condition} holds {@code false}.
+ * This is in contrast to the general behavior of bindings, where the binding is
+ * only eligible for garbage collection when not observed itself.
+ * <p>
+ * A {@code condition} holding {@code null} is treated as holding {@code false}.
+ * <p>
+ * For example:
+ * <pre>{@code
+ * ObservableValue<Boolean> condition = new SimpleBooleanProperty(true);
+ * ObservableValue<String> longLivedProperty = new SimpleStringProperty("A");
+ * ObservableValue<String> whenProperty = longLivedProperty.when(condition);
+ *
+ * // observe whenProperty, which will in turn observe longLivedProperty
+ * whenProperty.addListener((ov, old, current) -> System.out.println(current));
+ *
+ * longLivedProperty.setValue("B"); // "B" is printed
+ *
+ * condition.setValue(false);
+ *
+ * // After condition becomes false, whenProperty stops observing longLivedProperty; condition
+ * // and whenProperty may now be eligible for GC despite being observed by the ChangeListener
+ *
+ * longLivedProperty.setValue("C"); // nothing is printed
+ * longLivedProperty.setValue("D"); // nothing is printed
+ *
+ * condition.setValue(true); // longLivedProperty is observed again, and "D" is printed
+ * }</pre>
+ *
+ * @param condition a boolean {@code ObservableValue}, cannot be {@code null}
+ * @return an {@code ObservableValue} that holds this value whenever the given
+ * condition evaluates to {@code true}, otherwise holds the last seen value;
+ * never returns {@code null}
+ * @since 20
+ */
+ default ObservableValue<T> when(ObservableValue<Boolean> condition) {
+ return new ConditionalBinding<>(this, condition);
+ }
}
- csr of
-
JDK-8290040 Provide simplified deterministic way to manage listeners
-
- Resolved
-