我试图在控制器的initialize()方法中将TextArea的textProperty绑定到StringProperty。
值更改时,侦听器会监听这两个对象,以执行某些行为。
但是有些奇怪的事情发生了。
我建立了一个简单的模型来重现这种情况。
Main.java
package sample;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
sample.fxml
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.TextArea?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10"
prefHeight="300" prefWidth="400">
<TextArea fx:id="textArea"/>
</GridPane>
我认为以上代码与该问题无关。但以防万一,我把它放在这里。
这是控制器。
Controller.java
package sample;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;
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);
});
}
}
使用此控制器,当我将序列“abcd”输入到文本区域时,我得到:
textArea: a
textArea: ab
textArea: abc
textArea: abcd
似乎未触发toBind对象的change事件。
因此,我尝试在textArea的Listener中打印toBind的值。
新的代码是:
package sample;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;
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);
// ----- New statements. -----
System.out.print("toBind value in textArea: ");
System.out.println(toBind.get());
// ----- New statements. -----
});
toBind.addListener((observable, oldValue, newValue) -> {
System.out.print("toBind: ");
System.out.println(newValue);
});
}
}
然后我得到:
toBind: a
textArea: a
toBind value in textArea: a
toBind: ab
textArea: ab
toBind value in textArea: ab
toBind: abc
textArea: abc
toBind value in textArea: abc
toBind: abcd
textArea: abcd
toBind value in textArea: abcd
为什么会这样?该事件已正确触发。
最佳答案
您的绑定和toBind
属性正在被垃圾回收。
托马斯·米库拉(Tomas Mikula)在他的blog上对“过早的垃圾收集”问题进行了简要描述。
首先,对于任何试图重现此问题的人来说,请速记一点。由于所描述的行为取决于发生的垃圾回收,因此它可能并不总是发生(取决于内存分配,所使用的GC实现以及其他因素)。如果添加行
root.setOnMouseClicked(e -> System.gc());
到
start()
方法中,然后单击场景中的空白区域将请求垃圾回收,问题将(至少更有可能)在那之后(如果尚未出现)显现出来。问题是绑定使用
WeakListener
侦听属性更改并将这些更改传播到绑定的属性。如果没有其他 Activity 引用,弱侦听器的设计目的是不要阻止对其附加的属性进行垃圾回收。 (其基本原理是避免在属性不再在范围内时强迫程序员强制清理绑定。)在您的示例代码中,控制器及其属性
toBind
可以进行垃圾回收。start()
方法完成后,可以保证拥有的所有引用都是调用Application
时显示的launch()
实例,显示的Stage
以及从其中引用的所有内容。当然,这包括Scene
(由Stage
引用),其root
,root
的子代,它们的子代等,它们的属性以及这些属性中任何一个的(非弱)侦听器。因此,
stage
引用了scene
,后者引用了作为其根的GridPane
,并且引用了TextArea
。TextArea
附加了对该侦听器的引用,但该侦听器未保留其他引用。(在第二版代码中,附加到
ChangeListener
的非弱textArea.textProperty()
引用了toBind
。因此,在该版本中,ChangeListener
防止toBind
进行GC,然后您可以在其上看到来自侦听器的输出。 )加载FXML时,
FXMLLoader
创建控制器实例。虽然该控制器实例具有对string属性和文本区域的引用,但事实并非如此。因此,一旦加载完成,就没有对控制器的实时引用,并且可以对其进行定义的StringProperty
进行垃圾回收。文本区域的textProperty()
仅对toBind
上的侦听器进行弱引用,因此,文本区域无法防止toBind
被垃圾回收。在大多数实际情况下,这将不是问题。除非要在某个地方使用它,否则您不太可能创建此附加的
StringProperty
。因此,如果您添加以“自然”方式使用此代码的任何代码,则很可能会看到问题消失。因此,例如,假设您添加了一个标签:
<Label fx:id="label" GridPane.rowIndex="1"/>
并将其文本绑定到属性:
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);
}
然后,场景具有对标签的引用,等等,因此它不是GC标记,并且标签的
textProperty
通过绑定到toBind
而具有弱引用。由于label
未进行GC处理,因此弱引用可幸免于垃圾回收,而toBind
无法进行GC处理,因此您将看到期望的输出。另外,如果您在其他地方引用了
toBind
属性,例如在Application
实例中: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 ;
}
}
然后
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);
}
}
您会再次看到预期的行为(即使在垃圾回收之后)。
最终(最后一点很微妙),如果您用匿名内部类替换
textArea.textProperty()
上的侦听器:textArea.textProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
System.out.print("textArea: ");
System.out.println(newValue);
}
});
那么这也可以防止
toBind
的GC。原因是匿名内部类的实例包含对封闭实例的隐式引用(在这种情况下,即控制器的实例):此处,控制器保留对toBind
的引用。相比之下,Lambda表达式则不这样做。