How to add listeners to bidirectionally bound objects

后端 未结 2 730
醉话见心
醉话见心 2021-01-20 20:01

I am trying to bind a TextArea\'s textProperty to a StringProperty in controller\'s initialize() method.

Both of them are listened by listeners to perform some behav

2条回答
  •  一生所求
    2021-01-20 20:28

    Your binding, and the toBind property, are getting garbage collected.

    A succinct description of the "premature garbage collection" problem is provided by Tomas Mikula on his blog.


    First, a quick aside for anyone trying to reproduce this issue. Since the behavior described depends on garbage collection occurring, it may not always occur (it depends on memory allocation, the GC implementation being used, and other factors). If you add the line

    root.setOnMouseClicked(e -> System.gc());
    

    to the start() method, then clicking on the blank area in the scene will request garbage collection, and the issue will (at least be more likely to) manifest itself after that (if it hasn't already).


    The problem is that bindings use WeakListeners to listen to changes in properties and propagate those changes to the bound properties. A weak listener is designed not to prevent the property to which it is attached from being garbage collected if there are no other live references to that property. (The rationale is to avoid having to force programmers to manually clean up bindings when properties are no longer in scope.)

    In your example code, the controller and its property toBind are eligible for garbage collection.

    After the start() method completes, all that you are guaranteed to have references to are the Application instance created when you call launch(), the Stage that is shown, and anything referenced from those. This of course includes the Scene (referenced by the Stage), its root, the children of the root, their children, etc, properties of those, and (non-weak) listeners on any of those properties.

    So the stage has a reference to the scene, which has a reference to the GridPane which is its root, and that has a reference to the TextArea.

    The TextArea has a reference to the listener that is attached to it, but that listener keeps no additional references.

    (In the second version of your code, the non-weak ChangeListener attached to the textArea.textProperty() has a reference to toBind. So in that version, the ChangeListener prevents toBind from being GC'd, and you see the output from the listener on it.)

    When you load the FXML, the FXMLLoader creates the controller instance. While that controller instance has references to the string property and the text area, the reverse is not true. So once loading is complete, there are no live references to the controller, and it is eligible for garbage collection, along with the StringProperty it defines. The text area's textProperty() has only a weak reference to a listener on toBind, so the text area cannot prevent toBind being garbage collected.

    In most real scenarios, this won't be a problem. You are unlikely to create this additional StringProperty unless you are going to use it somewhere. So if you add in any code that uses this in a "natural" way, you are likely to see the issue disappear.

    So, e.g., suppose you add a label:

    and bind its text to the property:

      public void initialize() {
        textArea.textProperty().bindBidirectional(toBind);
    
        textArea.textProperty().addListener((observable, oldValue, newValue) -> {
          System.out.print("textArea: ");
          System.out.println(newValue);
        });
    
        toBind.addListener((observable, oldValue, newValue) -> {
          System.out.print("toBind: ");
          System.out.println(newValue);
        });
    
        label.textProperty().bind(toBind);
      }
    

    Then the scene has a reference to the label, etc, so it is not GC'd, and the label's textProperty has a weak reference via its binding to toBind. Since the label is not GC'd, the weak reference survives garbage collection, and toBind cannot be GC'd, so you see the output you expect.

    Alternatively, if you reference the toBind property elsewhere, e.g. in the Application instance:

    public class Controller {
    
      @FXML
      TextArea textArea;
    
      private StringProperty toBind = new SimpleStringProperty();
    
      public void initialize() {
        textArea.textProperty().bindBidirectional(toBind);
    
        textArea.textProperty().addListener((observable, oldValue, newValue) -> {
          System.out.print("textArea: ");
          System.out.println(newValue);
        });
    
        toBind.addListener((observable, oldValue, newValue) -> {
          System.out.print("toBind: ");
          System.out.println(newValue);
        });
    
      }
    
      public StringProperty boundProperty() {
          return toBind ;
      }
    }
    

    and then

    package sample;
    
    import javafx.application.Application;
    import javafx.beans.property.StringProperty;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class Main extends Application {
    
        private StringProperty boundProperty ;
    
        @Override
        public void start(Stage primaryStage) throws Exception{
            FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
            Parent root = loader.load();
            Controller controller = loader.getController();
            boundProperty = controller.boundProperty();
            root.setOnMouseClicked(e -> System.gc());
            primaryStage.setScene(new Scene(root, 400, 300));
            primaryStage.show();
        }
    
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    you again see the expected behavior (even after garbage collection).

    Finally (and this last point gets very subtle), if you replace the listener on textArea.textProperty() with an anonymous inner class:

    textArea.textProperty().addListener(new ChangeListener() {
    
      @Override
      public void changed(ObservableValue observable, String oldValue, String newValue) {
        System.out.print("textArea: ");
        System.out.println(newValue);
      }
    });
    

    then this also prevents GC of toBind. The reason here is that instances of anonymous inner classes contain implicit references to the enclosing instance (i.e. the instance of the controller in this case): and here the controller keeps a reference to toBind. Lambda expressions, by contrast, don't do this.

提交回复
热议问题