设置外观时,序列化AbstractTableModel失败

时间:2017-08-07 12:43:43

标签: java exception serialization look-and-feel

在设置了某种外观后,序列化一个扩展AbstractTableModel的类会导致java.io.NotSerializableException: com.sun.java.swing.plaf.windows.XPStyle$Skin异常,然后是不同的NullPointerException

我找到了一个解决方案并正在回答我自己的问题,以帮助其他人节省时间,并从一开始就正确实现序列化。附件是一个在我的机器上重现错误的最小例子(Win 10,Netbeans IDE 8.2,Java JDK 1.8)。下面的答案是主要代码段以及更多细节。

1 个答案:

答案 0 :(得分:2)

主要类扩展JFrame,设置外观并显示JTable。它有一个成员变量Tip tip,其中Tip扩展AbstractTableModel(见下文)。

public class MainFrame extends javax.swing.JFrame {

    // a single tip
    // in my final app, there was a list of tips
    Tip tip = new Tip();

    public MainFrame() {
        initComponents();

        // fill new instance with some dummy data for answers
        tip.addAnswer("first answer", 1, "first reply");
        tip.addAnswer("second answer", 2, "second reply");

        // assign the table model
        jTable.setModel(tip);
    }

    // ... more code (see attachment)

    public static void main(String args[]) {

        /* Set the look and feel */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Windows".equals(info.getName())) {
                    /**
                     * when setting LaF to 'Nimbus' de-/serialization fails on the first save/load:
                     * Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
                     * at javax.swing.plaf.synth.SynthLookAndFeel.paintRegion(SynthLookAndFeel.java:371)
                     * 
                     * when setting LaF to 'Windows' the second save/load fails with:
                     * java.io.NotSerializableException: com.sun.java.swing.plaf.windows.XPStyle$Skin
                     * at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
                     * 
                     * when setting LaF to 'Metal' save/load works just fine
                     * 
                     * not setting LaF at all (uncomment the line below) also works fine
                     */
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (Exception ex) {
            Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
        }


        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new MainFrame().setVisible(true);
            }
        });
    }

Tip包含应该在表中显示的一些成员变量(自定义类型TipAnswer),以及业务逻辑所需的其他成员变量,但不会显示在表:

public class Tip extends AbstractTableModel {

    // ... more member variables that don't show up in the table

    // a list of answers to the tip which show up in the table
    private ArrayList<TipAnswer> answers = new ArrayList<>();

    // adds an answer to the tip
    public void addAnswer(String answer, int cost, String reply) {    
        answers.add(new TipAnswer(answer, cost, reply));
    }

    // ... more methods that override the methods 
    //     required by AbstractTableModel (see attachment)
}

主类在按下按钮时作出反应,成员变量tip被序列化并立即反序列化。反序列化的结果显示在表中。

private void jSaveAndLoadActionPerformed(java.awt.event.ActionEvent evt) {

    // make temp file
    Path path;
    try {
        path = Files.createTempFile("TestTableModel", ".txt");
    } catch (IOException ex) {
        Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
        return;
    }

    // write to file (serialize)
    try ( FileChannel channel = FileChannel.open(path, StandardOpenOption.CREATE,
                                                       StandardOpenOption.WRITE,
                                                       StandardOpenOption.TRUNCATE_EXISTING);
          ObjectOutputStream oos = new ObjectOutputStream(Channels.newOutputStream(channel))
        ) {

        // write object to file
        oos.writeObject(tip);
        System.out.println("Tip table saved as " + path);

    } catch (Exception ex) {
        Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
        return;
    }

    // set an empty table model for demonstration purposes
    jTable.setModel(new Tip());

    // read from file (deserialize)
    Tip tipFromFile;
    try ( FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
          ObjectInputStream ois = new ObjectInputStream(Channels.newInputStream (channel)) ) {

        // the instance to return
        tipFromFile = (Tip)ois.readObject();

    } catch (Exception ex) {
        Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
        return;
    }

    // store the tip which has just been read in this instance
    tip = tipFromFile;

    // show the tip in the table
    jTable.setModel(tip);
}

如主要类的注释中所示,在未设置“外观”或选择“金属”时,保存和加载tip可正常工作。设置'Nimbus'LOF,序列化失败,NullPointerException。设置'Windows'Laf,序列化失败,java.io.NotSerializableException: com.sun.java.swing.plaf.windows.XPStyle$Skin例外,至少在我的机器上。

javax.swing.UIManager.LookAndFeelInfo的Javadoc声称它实现了Serializable,所以我不会预料到这个例外。

使用文本编辑器检查保存的文件显示,除了tip的成员变量之外,还存储了许多其他字段,例如javax.swing.event.TableModelListenersautoCreateColumnsFromModel等等。起初我怀疑这会导致异常,我只需要覆盖类writeObject(java.io.ObjectOutputStream out)中的writeObject(java.io.ObjectOutputStream out)Tip,但其他字段仍会保存到磁盘。再想一想,原因很清楚。有趣的是,我不会猜到额外的字段会被保存,因为我找不到AbstractTableModel的javadoc和TableModel中的那些字段的提示,但是,不是那么关键。

所以有助于实现一个在实例化时收到TipTableModel extends AbstractTableMode的单独的类tip

public TipTableModel(Tip tip) {
    this.tip = tip;
}

因此,不是序列化实现AbstractTableModel的类型的变量,而是建议将数据与表模型分开并仅对数据进行序列化。

在调试时,我还意识到扩展AbstractListModel(List,而不是Table)的对象除了在类中声明的成员变量之外还保存了许多字段,但序列化这些成员不会导致任何异常虽然将数据和列表模型分开也是可取的。

恢复:1)始终在两个单独的类中实现数据和表模型。 2)可能需要增强某些外观(尽管我仍然认为我犯了一个错误而不是Java的开发人员),并且3)向AbstractTableModel的javadoc添加注释以澄清许多非明显的字段也被序列化。

Netbeans project of this example