Search code examples
javajavafxtreeviewnotserializableexception

How do I make JavaFX TreeView and TreeItem serializable?


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);
  }

}

Solution

  • 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 StackOverflowErrors.)

    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