1

I'm new to programming, what i want to do is read/write data that are stored in a ObservableList, the data are from Student class which are Strings (firstName and lastName), and also i have a TableView to display the data.

Here is my code:

Student Class:

import java.io.Serializable;
import javafx.beans.property.SimpleStringProperty;

public class Student implements Serializable {
    private SimpleStringProperty fname;
    private SimpleStringProperty lname;

    Student() {
        this("","");
    }

    Student(String fn, String ln) {
       this.fname = new SimpleStringProperty(fn);
       this.lname = new SimpleStringProperty(ln);
    }


    public void setFirstName(String f) {
        fname.set(f);
    }
    public String getFirstName() {
        return fname.get();
    }

    public void setLastName(String l) {
        lname.set(l);
    }
    public String getLastName() {
        return lname.get();
    }


    @Override
    public String toString() {
        return String.format("%s %s", getFirstName(), getLastName());
    }
}

And here is my code for inputing data using TextFields:

    @FXML
    ObservableList<Student> data = FXCollections.observableArrayList();

    //Just to input the data
    @FXML
    private void handleButtonAction(ActionEvent event) {

        if(!"".equals(txtFirstName.getText()) && !"".equals(txtLastName.getText())){
            data.add(
                    new Student(txtFirstName.getText(),
                                txtLastName.getText()
            ));
        }

        txtFirstName.clear();
        txtLastName.clear();

        // System.out.println(data);
    }

And here is the problem...

Reading/Writing the ObservableList:

    @FXML
    private void HandleMenuSaveAction(ActionEvent event) {
         try {
            FileOutputStream f = new FileOutputStream(new File("saveStudentList.txt"));
            ObjectOutputStream o = new ObjectOutputStream(f);

            o.writeObject(data);
            o.close();
            f.close();

            System.out.println("File Saved Successfully.");

        } catch (FileNotFoundException ex) {
            System.err.println("Save: File not found.");
        } catch (IOException ex) {
            System.err.println("Save: Error initializing stream.");
            ex.printStackTrace();
        } 
    }

    @FXML
    private void HandleMenuLoadAction(ActionEvent event) {
         try {
            FileInputStream fi = new FileInputStream(new File("saveStudentList.txt"));
            ObjectInputStream oi = new ObjectInputStream(fi);

            data = (ObservableList) oi.readObject();

            System.out.println(data.toString());

            //Refresh the Table everytime we load data

            oi.close();
            fi.close();


        } catch (FileNotFoundException ex) {
            System.err.println("Load: File not found.");
        } catch (IOException ex) {
            System.err.println("Load: Error initializing stream.");
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
    }

This is causing me java.io.NotSerializableException, Does anyone have an idea how to change my code so that it works?

Khada Jhin
  • 13
  • 1
  • 7
  • `.txt` is a bad extension for storing serialisaton results. The result is not a text file. – fabian Sep 01 '18 at 15:01
  • Are you looking to serialize the list into a serialized file or into a plain text file? – Zephyr Sep 01 '18 at 15:23
  • @fabian @Zephyr Actually, i want to serialize the list into a serialized file. the only reason i used `.txt` extension is because i followed [this](https://www.mkyong.com/java/how-to-read-and-write-java-object-to-a-file/) tutorial. – Khada Jhin Sep 01 '18 at 16:08

3 Answers3

4

implement custom serialisation for the Student object (see https://stackoverflow.com/a/7290812/2991525) and copy the contents of the ObservableList to a ArrayList to create a serializable list:

public class Student implements Serializable {

    private void writeObject(ObjectOutputStream out)
            throws IOException {
        out.writeObject(getFirstName());
        out.writeObject(getLastName());
    }

    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        fname = new SimpleStringProperty((String) in.readObject());
        lname = new SimpleStringProperty((String) in.readObject());
    }

    ...
}

(De)serialisation example

ObservableList<Student> students = FXCollections.observableArrayList();

for(int i = 0; i < 100; i++) {
    students.add(new Student("Mark"+i, "Miller"+i));
}

ByteArrayOutputStream bos = new ByteArrayOutputStream();

// write list
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
    oos.writeObject(new ArrayList<>(students));
}

students = null; // make sure the old reference is no longer available

ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());

// read list
try (ObjectInputStream ois = new ObjectInputStream(bis)){
    students = FXCollections.observableList((List<Student>) ois.readObject());
}

System.out.println(students);
fabian
  • 80,457
  • 12
  • 86
  • 114
3

Your question is a little confusing as you are using object serialization but the filename you chose is a .txt file. Serialized objects are not human-readable like a plain text file would be.

Fabian's answer above is great if you do want to use serialization. However, if you were looking to generate a simple text file, take a look at the following sample program:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.io.BufferedReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {

        ObservableList<Student> students = FXCollections.observableArrayList();

        // Create some sample students
        students.addAll(
                new Student("Roger", "Rabbit"),
                new Student("Elmer", "Fudd"),
                new Student("Daffy", "Duck"),
                new Student("Foghorn", "Leghorn"),
                new Student("Betty", "Boop")
        );

        // Write the list to a text file
        try {
            writeToTextFile("students.txt", students);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Now, read the file into a new List<Student>
        List<Student> inputStudents = null;
        try {
             inputStudents = readStudents("students.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Print out the student names
        if (inputStudents != null) {
            for (Student student : inputStudents) {
                System.out.println(student.toString());
            }
        }

    }

    /**
     * Write the list of students to a simple text file with first and last names separated by a comma
     */
    private static void writeToTextFile(String filename, ObservableList<Student> students)
            throws IOException {

        FileWriter writer = new FileWriter(filename);
        for (Student student : students) {
            writer.write(student.getFirstName() + "," + student.getLastName() + "\n");
        }
        writer.close();
    }

    /**
     * Read the comma-separated list of student names from the text file
     */
    private static List<Student> readStudents(String filename)
            throws IOException {

        List<Student> students = new ArrayList<>();

        BufferedReader reader = Files.newBufferedReader(Paths.get(filename));
        String line;
        while ((line = reader.readLine()) != null) {
            String[] names = line.split(",");

            // Add the student to the list
            students.add(new Student(names[0], names[1]));

        }

        return students;
    }
}

This generates a very simple text file with each student's first and last name on its own line, separated by a comma. It is then read back into a new list, ready for use in your TableView.

If you do decide to go this route, I recommend finding a good CSV library to handle the reading/writing of CSV (comma-separated values) files. Apache Commons has a decent one available.

Zephyr
  • 9,885
  • 4
  • 28
  • 63
1

Here is an example that reads and writes data from an ObservableList of people (first names and last names) to a file in json format.

example screenshot

Example file content for saved json data from the list

[ {
  "firstName" : "Fred",
  "lastName" : "Flintstone"
}, {
  "firstName" : "Wilma",
  "lastName" : "Flintstone"
}, {
  "firstName" : "Barney",
  "lastName" : "Rubble"
} ]

Implementation Notes

Data items are stored as People records.

An ObservableList backs a TableView and holds records of the data items.

The 3rd party Jackson library is used to serialize and deserialize the list of data to JSON, which is stored and read from a file.

On startup, the application generates a temporary file name used to store the saved data file for the lifetime of the application.

On shutdown, the temporary save file is automatically deleted.

The module-info allows the Jackson databind module to perform reflection on the package containing the record definition of the items to be saved.

Before saving and restoring the data items, they are temporarily stored in an ArrayList rather than an ObservableList. This is done because you don't want to try to serialize an entire ObservableList. The ObservableList will also have entries for change listeners that may be attached to the list. You don't want to serialize those listeners.

The ListSerializer class which performs the serialization and deserialization using Jackson uses Java generics so it can save and load any type of data which can be serialized via Jackson (including the Person record in the example). The generics add some complications in the code for determining the correct types to be used in the serialization and deserialization process. The generics do allow for a more generic solution, so, in general, I think the addition of the generic solution is worth the tradeoff of additional complications in the implementation.

The ListSerializerController demonstrates the usage of the ListSerializer to save and load data to an ObservableList backing a TableView.

Maven is used as the build system.

JRE 18 and JavaFX 18 are used as the runtime.

Example Solution

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>ListSerialization</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>ListSerialization</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>5.8.1</junit.version>
        <javafx.version>18</javafx.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.13.2.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>18</source>
                    <target>18</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

src/main/java/module-info.java

module com.example.listserialization {
    requires javafx.controls;
    requires com.fasterxml.jackson.databind;

    opens com.example.listserialization to com.fasterxml.jackson.databind;
    exports com.example.listserialization;
}

src/main/java/com/example/listserialization/ListSerializerApp.java

package com.example.listserialization;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class ListSerializerApp extends Application {

    private Path peoplePath;

    @Override
    public void init() throws IOException {
        peoplePath = Files.createTempFile(
                "people",
                ".json"
        );
        peoplePath.toFile().deleteOnExit();

        System.out.println("Using save file name: " + peoplePath);
    }

    @Override
    public void start(Stage stage) throws IOException {
        ListSerializerController listSerializerController = new ListSerializerController(
                peoplePath
        );

        stage.setScene(
                new Scene(
                        listSerializerController.getLayout()
                )
        );
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

src/main/java/com/example/listserialization/Person.java

package com.example.listserialization;

record Person(String firstName, String lastName) {}

src/main/java/com/example/listserialization/ListSerializer.java

package com.example.listserialization;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.CollectionType;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;

public class ListSerializer<T> {
    private static final ObjectMapper mapper =
            new ObjectMapper()
                    .enable(SerializationFeature.INDENT_OUTPUT);

    private final CollectionType listType;

    public ListSerializer(Class<T> listItemClass) {
        listType =
               mapper.getTypeFactory()
                        .constructCollectionType(
                                ArrayList.class,
                                listItemClass
                        );
    }

    public void serializeList(Path path, ArrayList<T> list) throws IOException {
        Files.writeString(
                path,
                mapper.writeValueAsString(
                        list
                )
        );
    }

    public ArrayList<T> deserializeList(Path path) throws IOException {
        return mapper.<ArrayList<T>>readValue(
                Files.readString(path),
                listType
        );
    }
}

src/main/java/com/example/listserialization/ListSerializerController.java

package com.example.listserialization;

import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;

public class ListSerializerController {

    private final ListSerializer<Person> listSerializer = new ListSerializer<>(
            Person.class
    );

    private final Person[] TEST_PEOPLE = {
            new Person("Fred", "Flintstone"),
            new Person("Wilma", "Flintstone"),
            new Person("Barney", "Rubble")
    };

    private final TableView<Person> tableView = new TableView<>(
            FXCollections.observableArrayList(
                    TEST_PEOPLE
            )
    );

    private final Path peoplePath;
    private final Parent layout;

    public ListSerializerController(Path peoplePath) {
        this.peoplePath = peoplePath;
        layout = createLayout();
    }

    private Parent createLayout() {
        TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
        firstNameCol.setCellValueFactory(p -> 
                new ReadOnlyStringWrapper(
                        p.getValue().firstName()
                ).getReadOnlyProperty()
        );

        TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
        lastNameCol.setCellValueFactory(p ->
                new ReadOnlyStringWrapper(
                        p.getValue().lastName()
                ).getReadOnlyProperty()
        );

        //noinspection unchecked
        tableView.getColumns().setAll(firstNameCol, lastNameCol);
        tableView.setPrefHeight(150);

        Button save = new Button("Save");
        save.setOnAction(this::save);

        Button clear = new Button("Clear");
        clear.setOnAction(this::clear);

        Button load = new Button("Load");
        load.setOnAction(this::load);

        Button restoreDefault = new Button("Default Data");
        restoreDefault.setOnAction(this::restoreDefault);

        HBox controls = new HBox(10, save, clear, load, restoreDefault);

        VBox layout = new VBox(10, controls, tableView);
        layout.setPadding(new Insets(10));

        return layout;
    }

    public Parent getLayout() {
        return layout;
    }

    private void save(ActionEvent e) {
        try {
            listSerializer.serializeList(
                    peoplePath,
                    new ArrayList<>(
                            tableView.getItems()
                    )
            );

            System.out.println("Saved to: " + peoplePath);
            System.out.println(Files.readString(peoplePath));
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    private void clear(ActionEvent e) {
        tableView.getItems().clear();
        System.out.println("Cleared data in UI");
    }

    private void load(ActionEvent e) {
        try {
            if (!peoplePath.toFile().exists()) {
                tableView.getItems().clear();
                System.out.println("Saved data file does not exist, clearing data: " + peoplePath);

                return;
            }

            tableView.getItems().setAll(
                    listSerializer.deserializeList(peoplePath)
            );

            System.out.println("Loaded data from: " + peoplePath);
            System.out.println(Files.readString(peoplePath));
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    private void restoreDefault(ActionEvent e) {
        tableView.getItems().setAll(TEST_PEOPLE);
        System.out.println("Restored default data in UI");
    }

}
jewelsea
  • 150,031
  • 14
  • 366
  • 406