Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8290040

Provide simplified deterministic way to manage listeners

XMLWordPrintable

    • Icon: Enhancement Enhancement
    • Resolution: Fixed
    • Icon: P4 P4
    • jfx20
    • jfx19
    • javafx
    • None
    • b12

      Relying on the garbage collector to clean up weak listeners is perhaps not ideal. How GC operates is subject to the chosen implementation, the version of that implementation, its parameters and possibly even the chosen architecture.

      When listeners are not cleaned up, memory leaks can easily be created. A simple example is a dialog box that is created and shown on demand which binds one of its properties to a long lived property of the application (for example, to display the name and version of the application in the Dialog's title).

      The long lived property would have a listener that refers to the title property of the dialog. The dialog itself is a container with parent and child references. This single reference therefore prevents the entire dialog, including all its children and parent containers from being eligible for GC.

      To prevent this, JavaFX has implemented all its bindings using weak listeners. However, this can result in subtle bugs and surprises:

      1) Even though weak bindings prevent memory leaks in the long run, there is no guarantee when the GC will clean them up. Any side effects resulting from the binding will keep being triggered even after the developer of the dialog considers the dialog to be "finished" -- in reality however this may take up until the next GC to be truly the case.

      This can lead to odd behavior, for example, a side effect may seem to be called twice seemingly for the same dialog. In reality, the side effect runs for the currently open dialog and for one that was closed recently (two different dialog instances).

      2) Weak bindings can also cause side effects which are intended by the developer to stop working when a GC is triggered. Adding a listener to an ObservableValue which itself is not strongly referenced (if such an ObservableValue was passed as a parameter or returned as a result, this fact may be impossible to determine) can result in the listener to initially perform as intended but to stop working when a GC occurs.

      ---

      To encourage users more towards predictable behavior which does not rely on the GC, JavaFX should offer easier ways to dispose of listeners and groups of listeners at the right moment.

      There are many possibilities here, some ideas:

      1) subscriptions, which encapsulate the listener

           Subscription s = x.addListener( ... );
           s.unsubscribe();

      This is less cumbersome as the listener Lambda need not be tracked for a future call to removeListener. Use of subscription would also allow chaining of subscriptions:

           Subscription all = x.addListener( ... )
                .and(y.addListener( ... ));
           all.unsubscribe();

      A big issue with Subscriptions is that it is hard to help the user to automatically unsubscribe these at the right time. When a Node is removed for example, it could be re-added later. Should its listeners be removed when a Node has no parent so it could be GC'd? What if it wasn't GC'd (because the user has a reference) and its re-added? The listeners would be removed already. Using a Cleaner to trigger clean-up would again lead us to the path where this behavior can't really be relied upon.

      Clearing subscriptions is therefore something more suitable for `dispose` or `onClose` type methods which must be overriden by the user to clear subscriptions deterministically.

      2) extend Fluent bindings with a "while" type condition, which maintains the listeners involved only when its condition evaluates to true, and removes the listeners when the condition evaluates to false (and vice versa!): suggested name for this method is `conditionOn`. Sample usage:

            BooleanProperty maintainListeners = new SimpleBooleanProperty(true);

            x.conditionOn(maintainListeners).map( ... ).addListener( ... );

      Set maintainListeners to false to temporarily disable all such mappings. This can also be useful to disable updates of properties when multiple properties must be changed together and the intermediate values would not make much sense (like changing the x and y values of a coordinate).

      Nodes could offer a standard property "isShowing" (already used internally by ProgressBar for example) which users could use to make their bindings conditional on whether the relevant UI is actually visible or not:

            label.textProperty().bind(
                   longLivedCaption.conditionOn(label::isShowing)
            );

      This is deterministic because the user knows exactly when such a binding (or listener) would operate (ie. only when the UI is showing, and never when it is not showing).

      When a piece of UI is removed from its parent, the listeners are disabled (as the UI is no longer considered to be showing). With the listeners gone, the only strong reference there still might be is one that the user holds to later re-add the UI. If that one also disappears, there is nothing stopping it from being GC'd. As the listeners are already disabled, no indeterministic behavior results when properties are modified that formerly were monitored by these listeners.

      Note that `conditionOn` can already be simulated now (assuming Node has an isShowing method):

            label.textProperty().bind(
                  label.isShowing().flatMap(showing -> showing ? longLivedCaption : null)
            );

      However, this is quite a bit more cumbersome and less intuitive.

            jhendrikx John Hendrikx
            jhendrikx John Hendrikx
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated:
              Resolved: