英文:
How do I make JavaFX TreeView and TreeItem serializable?
问题
我在尝试使用ObjectOutputStream保存我的TreeView时遇到了这个错误(java.io.NotSerializableException: javafx.scene.control.TreeView)。
我有两个类实现了Serializable接口,还有一个主类没有实现Serializable接口。
这两个类是Vendor和Address。Vendor类包含4个变量(name、age、gender、属于Address类类型的address),一个使用set方法设置所有变量的构造函数,一个仅使用set方法设置name变量的构造函数,以及变量的get/set方法。
Address类包含2个变量(街道名称和邮政编码),使用set方法设置变量的默认构造函数,以及变量的get/set方法。
这是我的主类:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class SimpleTreeView extends Application {
private TreeView<Vendor> treeView;
public void start(Stage stage) {
stage.setTitle("Simple TreeView");
treeView = new TreeView<>();
TreeItem<Vendor> root = new TreeItem<>(new Vendor("Root"));
root.setExpanded(true);
treeView.setRoot(root);
treeView.setShowRoot(false);
TreeItem<Vendor> start = new TreeItem<>(new Vendor("Start"));
root.getChildren().add(start);
Button saveButton = new Button("Save");
saveButton.setOnMouseClicked(event -> saveTreeView(stage));
VBox vBox = new VBox(20);
vBox.getChildren().addAll(treeView, saveButton);
stage.setScene(new Scene(vBox));
stage.show();
}
private void saveTreeView(Stage stage) {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save");
File file = fileChooser.showSaveDialog(stage);
if (file != null) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
os.writeObject(treeView);
os.close();
}
catch (Exception e) {
System.out.println(e);
}
}
}
public static void main(String[] args) {
launch(args);
}
}
英文:
I'm got this error (java.io.NotSerializableException: javafx.scene.control.TreeView) when trying to save my TreeView using ObjectOutputStream.
I have 2 classes which implements Serializable and 1 main class which doesn't implements Serializable.
The 2 classes are Vendor and Address. Vendor class contain 4 variables (name, age, gender, address of Address class type), constructor which uses the set method to set all the variables, constructor which uses the set method to set the name variable only, and get/set method for the variables.
Address class contain 2 variables (street name and postal code), default constructor which uses the set method to set the variables, and get/set method for the variables.
This is my main class
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class SimpleTreeView extends Application {
private TreeView<Vendor> treeView;
public void start(Stage stage) {
stage.setTitle("Simple TreeView");
treeView = new TreeView<>();
TreeItem<Vendor> root = new TreeItem<>(new Vendor("Root"));
root.setExpanded(true);
treeView.setRoot(root);
treeView.setShowRoot(false);
TreeItem<Vendor> start = new TreeItem<>(new Vendor("Start"));
root.getChildren().add(start);
Button saveButton = new Button("Save");
saveButton.setOnMouseClicked(event -> saveTreeView(stage));
VBox vBox = new VBox(20);
vBox.getChildren().addAll(treeView, saveButton);
stage.setScene(new Scene(vBox));
stage.show();
}
private void saveTreeView(Stage stage) {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save");
File file = fileChooser.showSaveDialog(stage);
if (file != null) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
os.writeObject(treeView);
os.close();
}
catch (Exception e) {
System.out.println(e);
}
}
}
public static void main(String[] args) {
launch(args);
}
}
答案1
得分: 4
Nodes不能被序列化(除非是你自己实现的自定义节点),因为有太多内部状态需要恢复,这会变得非常复杂。在不能向节点添加方法/接口的情况下(不通过继承),无法实现Serializable
接口,并添加保存恢复状态所需数据的方法以及正确读取这些数据的方法。
最好创建一个可序列化的包装类,允许你恢复你实际感兴趣的属性。我认为最好不要尝试序列化节点;在加载数据时创建一个新节点,并用加载的数据填充它。
以下示例展示了如何使用TreeItem<? extends Serializable>
来实现这一点;这里缺少一些数据,比如展开的属性,但你应该能够恢复value
属性和子节点。(这个实现比对于深度较小的TreeItem
结构来说稍微复杂了一些,但在某些深度上,你需要意识到更简单的递归方法可能会导致StackOverflowError
。)
在这个案例中,每个项目都通过写入子节点的数量、自身的值属性,然后对每个子节点执行相同的操作来进行序列化。这将生成一系列的整数和值对,可以用来恢复数据:
public class TreeItemSerialisation {
public static void main(String[] args) throws IOException, ClassNotFoundException {
TreeItem<String> root = new TreeItem<>("root");
TreeItem<String> c1 = new TreeItem<>("root.1");
TreeItem<String> c3 = new TreeItem<>("root.3");
root.getChildren().addAll(c1, new TreeItem<>("root.2"), c3);
TreeItem<String> c3_1 = new TreeItem<>("root.3.1");
c3_1.getChildren().add(new TreeItem<>("root.3.1.1"));
c3.getChildren().add(c3_1);
c1.getChildren().addAll(new TreeItem<>("root.1.1"), new TreeItem<>("root.1.2"));
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(new TreeItemSerialisationWrapper(root));
}
// 反序列化
TreeItem<String> root2;
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
root2 = (TreeItem<String>) ois.readObject();
}
// TODO 使用 root2 进行操作
}
}
public class TreeItemSerialisationWrapper<T extends Serializable> implements Serializable {
private static final long serialVersionUID = 1L;
private transient TreeItem<T> item;
public TreeItemSerialisationWrapper(TreeItem<T> item) {
if (item == null) {
throw new IllegalArgumentException();
}
this.item = item;
}
/**
* 自定义的写入 TreeItem 结构的方法
*/
private void writeObject(ObjectOutputStream out) throws IOException {
Stack<TreeItem<T>> stack = new Stack<>();
stack.push(item);
out.defaultWriteObject();
do {
TreeItem<T> current = stack.pop();
int size = current.getChildren().size();
out.writeInt(size);
// 在这里写入需要恢复的所有数据
out.writeObject(current.getValue());
// “安排”子节点的序列化。第一个子节点最后插入,因为栈顶的节点最先被检索出来
for (int i = size - 1; i >= 0; --i) {
stack.push(current.getChildren().get(i));
}
} while (!stack.isEmpty());
}
/**
* 在 readResolve 之前调用;重新创建 TreeItem 结构
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
class Container {
int count;
final TreeItem<T> item;
Container(ObjectInputStream in) throws ClassNotFoundException, IOException {
// 在这里读取单个 TreeItem 的数据
this.count = in.readInt();
this.item = new TreeItem<>((T) in.readObject());
}
}
in.defaultReadObject();
Container root = new Container(in);
this.item = root.item;
if (root.count > 0) {
Stack<Container> stack = new Stack<>();
stack.push(root);
do {
Container current = stack.peek();
--current.count;
if (current.count <= 0) {
// 我们完成了这个节点
stack.pop();
}
Container newContainer = new Container(in);
current.item.getChildren().add(newContainer.item);
if (newContainer.count > 0) {
// 安排读取非叶子节点的子节点
stack.push(newContainer);
}
} while (!stack.isEmpty());
}
}
/**
* 实际上我们对这个对象不感兴趣,只对 TreeItem 感兴趣
* @return TreeItem
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
return item;
}
}
关于readObject
、readResolve
和writeObject
的工作原理,请参阅Serializable的Javadoc。
英文:
Nodes cannot be serialized (except possibly for custom ones you implemented youself) there are too many internal states that would be too complex to restore. Lacking the possiblity to add methods/interfaces to nodes (without extending them) makes it impossible to add the Serializable
interface and add the methods to save those parts of the data that are needed to restore the state and read this data properly.
You're best of creating a serializable wrapper class that allows you to restore the properties you're actually interested in. Imho it's best not to try to serialize nodes; Create a new node when loading the data and fill it with the data loaded.
The following example shows how you could do this with TreeItem<? extends Serializable>
; There is data missing such as the expanded properties, but you should be able to restore the value
property and the children. (The implementation is a bit more complex than needed for TreeItem
structures with small depth, but certain depths you need to be aware that a simpler recursive approach could lead to StackOverflowError
s.)
In this case every item is serialized by writing the number of children, it's own value property and then doing the same with every child. This results in a sequence of int and value pairs that can be used to restore the data:
public class TreeItemSerialisation {
public static void main(String[] args) throws IOException, ClassNotFoundException {
TreeItem<String> root = new TreeItem<>("root");
TreeItem<String> c1 = new TreeItem<>("root.1");
TreeItem<String> c3 = new TreeItem<>("root.3");
root.getChildren().addAll(c1, new TreeItem<>("root.2"), c3);
TreeItem<String> c3_1 = new TreeItem<>("root.3.1");
c3_1.getChildren().add(new TreeItem<>("root.3.1.1"));
c3.getChildren().add(c3_1);
c1.getChildren().addAll(new TreeItem<>("root.1.1"), new TreeItem<>("root.1.2"));
// serialize
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(new TreeItemSerialisationWrapper(root));
}
// unserialize
TreeItem<String> root2;
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
root2 = (TreeItem<String>) ois.readObject();
}
// TODO do something with root2
}
}
public class TreeItemSerialisationWrapper<T extends Serializable> implements Serializable {
private static final long serialVersionUID = 1L;
private transient TreeItem<T> item;
public TreeItemSerialisationWrapper(TreeItem<T> item) {
if (item == null) {
throw new IllegalArgumentException();
}
this.item = item;
}
/**
* Custom way of writing the TreeItem structure
*/
private void writeObject(ObjectOutputStream out)
throws IOException {
Stack<TreeItem<T>> stack = new Stack<>();
stack.push(item);
out.defaultWriteObject();
do {
TreeItem<T> current = stack.pop();
int size = current.getChildren().size();
out.writeInt(size);
// write all the data that needs to be restored here
out.writeObject(current.getValue());
// "schedule" serialisation of children.
// the first one is inserted last, since the top one from the stack is
// retrieved first
for (int i = size - 1; i >= 0; --i) {
stack.push(current.getChildren().get(i));
}
} while (!stack.isEmpty());
}
/**
* happens before readResolve; recreates the TreeItem structure
*/
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
class Container {
int count;
final TreeItem<T> item;
Container(ObjectInputStream in) throws ClassNotFoundException, IOException {
// read the data for a single TreeItem here
this.count = in.readInt();
this.item = new TreeItem<>((T) in.readObject());
}
}
in.defaultReadObject();
Container root = new Container(in);
this.item = root.item;
if (root.count > 0) {
Stack<Container> stack = new Stack<>();
stack.push(root);
do {
Container current = stack.peek();
--current.count;
if (current.count <= 0) {
// we're done with this item
stack.pop();
}
Container newContainer = new Container(in);
current.item.getChildren().add(newContainer.item);
if (newContainer.count > 0) {
//schedule reading children of non-leaf
stack.push(newContainer);
}
} while(!stack.isEmpty());
}
}
/**
* We're not actually interested in this object but the treeitem
* @return the treeitem
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
return item;
}
}
For an description of how readObject
, readResolve
and writeObject
work, refer to the javadoc of Serializable
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论