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