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
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 WeakListener
s 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 extends String> 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.