JavaFX:如何将 ObservableList 读写到文件中?

JavaFX: How to read and write an ObservableList to a file?

我是编程新手,我想做的是读取/写入存储在[=13中的数据=],数据来自 Student class,它们是字符串(firstName 和 lastName),我还有一个 TableView 来显示数据。

这是我的代码:

学生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());
    }
}

这是我使用 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);
    }

问题来了...

Reading/Writing 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();
        }
    }

这导致我 java.io.NotSerializableException, 有谁知道如何更改我的代码以使其正常工作?

Student 对象实施自定义序列化(参见 )并将 ObservableList 的内容复制到 ArrayList 以创建可序列化列表:

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

    ...
}

(反)序列化示例

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

您的问题有点令人困惑,因为您正在使用对象序列化,但您选择的文件名是 .txt 文件。序列化对象不像纯文本文件那样是人类可读的。

如果您想使用序列化,Fabian 上面的回答很好。但是,如果您希望生成一个简单的文本文件,请查看以下示例程序:

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

这会生成一个非常简单的文本文件,每个学生的名字和姓氏各占一行,以逗号分隔。然后将其读回到新列表中,准备好在您的 TableView.

中使用

如果您决定走这条路,我建议您找一个好的 CSV 库来处理 reading/writing 的 CSV(逗号分隔值)文件。 Apache Commons 有一个不错的可用。

这里是一个示例,它以 json 格式从一个 ObservableList 人员(名字和姓氏)读取数据并将其写入文件。

列表中已保存 json 数据的示例文件内容

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

实施说明

数据项存储为人员记录。

ObservableList 支持 TableView 并保存数据项的记录。

第 3 方 Jackson 库用于将数据列表序列化和反序列化为 JSON,存储并从文件中读取。

启动时,应用程序会生成一个临时文件名,用于在应用程序的生命周期内存储已保存的数据文件。

关机时,临时存档会自动删除。

module-info 允许 Jackson 数据绑定模块对包含要保存的项目的记录定义的包执行反射。

在保存和恢复数据项之前,它们被临时存储在一个ArrayList中,而不是一个ObservableList中。这样做是因为您不想尝试序列化整个 ObservableList。 ObservableList 还将具有可能附加到列表的更改侦听器的条目。您不想序列化那些侦听器。

使用 Jackson 执行序列化和反序列化的 ListSerializer class 使用 Java 泛型,因此它可以保存和加载可以通过 Jackson 序列化的任何类型的数据(包括 Person 记录在例子)。泛型在代码中增加了一些复杂性,以确定要在序列化和反序列化过程中使用的正确类型。泛型确实允许更通用的解决方案,因此,总的来说,我认为添加通用解决方案值得权衡实现中的额外复杂性。

ListSerializerController 演示了如何使用 ListSerializer 将数据保存和加载到支持 TableView 的 ObservableList。

Maven 用作构建系统。

JRE 18 和 JavaFX 18 用作运行时。

示例解决方案

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

}