今日、標準の Java シリアライゼーション API を使用した GUI デシリアライゼーション中に、swing リスナーを起動するという非常に厄介なバグに遭遇しました。この動作を再現する方法を簡潔に説明するのは少し難しいので、以下に小さなテスト ケースを投稿しました。このテスト ケースは、例外をスローせず、コンパイラの警告をトリガーしません。また、明確に定義された動作をしていないようです。 これはバグですか、Oracle のパッケージ相互運用ドキュメントの灰色の領域ですか、それとも以前に試みたものですか?
元のユースケースを明らかにするために、ディスクから構成コンポーネントをリロードするときにアセットエディターのいくつかのタブを自動的に更新しようとしていました。
package com.ihateswing.ssce;
import java.awt.BorderLayout;
public class SSCE implements Serializable {
private class Internal extends JPanel {
private final JComboBox<String> m_cb = new JComboBox<String>();
Internal(final JComboBox<String> cb) {
cb.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
DefaultComboBoxModel<String> dcbm = new DefaultComboBoxModel<String>();
for (int i = 0; i < cb.getModel().getSize(); ++i) {
dcbm.addElement(cb.getModel().getElementAt(i));
}
m_cb.setModel(dcbm);
}
});
super.add(m_cb);
}
}
private JFrame frame;
private JComboBox<String> jce = new JComboBox<String>();
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
SSCE window = new SSCE();
window.frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public SSCE() {
initialize();
}
private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
jce.setSelectedIndex(0); // <-- Seems to have undefined behavior?
}
private void initialize() {
frame = new JFrame();
frame.setBounds(100, 100, 450, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jce.setModel(new DefaultComboBoxModel<String>());
frame.getContentPane().add(new Internal(jce), BorderLayout.NORTH);
frame.getContentPane().add(jce, BorderLayout.CENTER);
JButton btnAddRandomItem = new JButton("Break");
btnAddRandomItem.addActionListener(new AbstractAction() {
private static final long serialVersionUID = 1L;
private int i;
public void actionPerformed(ActionEvent e) {
try {
((DefaultComboBoxModel<String>) jce.getModel())
.addElement(String.valueOf(i++));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(baos);
out.writeObject(SSCE.this);
ByteArrayInputStream in = new ByteArrayInputStream(baos
.toByteArray());
ObjectInputStream oin = new ObjectInputStream(in);
oin.readObject();
// The line below updates 'Internal' as expected, uncomment
// to see...
// however with the listener fired from the serialization
// method, it breaks?
// jce.setSelectedIndex(0);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
frame.getContentPane().add(btnAddRandomItem, BorderLayout.SOUTH);
}
}
出力付きサンプル (「Break」を 5 回クリック)
package com.ihateswing.ssce;
import java.awt.BorderLayout;
public class SSCE implements Serializable {
private class Internal extends JPanel {
private final JComboBox<String> m_cb = new JComboBox<String>();
Internal(final JComboBox<String> cb) {
cb.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
DefaultComboBoxModel<String> dcbm = new DefaultComboBoxModel<String>();
for (int i = 0; i < cb.getModel().getSize(); ++i) {
dcbm.addElement(cb.getModel().getElementAt(i));
}
m_cb.setModel(dcbm);
System.out.println("Set Internal.m_cb's model with " + m_cb.getModel().getSize() + " elements");
}
});
super.add(m_cb);
}
}
private JFrame frame;
private JComboBox<String> jce = new JComboBox<String>();
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
SSCE window = new SSCE();
window.frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public SSCE() {
initialize();
}
private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
jce.setSelectedIndex(0); // <-- Seems to have undefined behavior?
}
private void initialize() {
frame = new JFrame();
frame.setBounds(100, 100, 450, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final Internal internal = new Internal(jce);
jce.setModel(new DefaultComboBoxModel<String>());
frame.getContentPane().add(internal, BorderLayout.NORTH);
frame.getContentPane().add(jce, BorderLayout.CENTER);
JButton btnAddRandomItem = new JButton("Break");
btnAddRandomItem.addActionListener(new AbstractAction() {
private static final long serialVersionUID = 1L;
private int i;
public void actionPerformed(ActionEvent e) {
try {
((DefaultComboBoxModel<String>) jce.getModel())
.addElement(String.valueOf(i++));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(baos);
out.writeObject(SSCE.this);
ByteArrayInputStream in = new ByteArrayInputStream(baos
.toByteArray());
ObjectInputStream oin = new ObjectInputStream(in);
oin.readObject();
System.out.println("After returning, Internal.m_cb's model size is " + internal.m_cb.getModel().getSize() + " elements");
// The line below updates 'Internal' as expected, uncomment
// to see...
// however with the listener fired from the serialization
// method, it breaks?
// jce.setSelectedIndex(0);
} catch (Throwable t) {
t.printStackTrace();
}
}
});
frame.getContentPane().add(btnAddRandomItem, BorderLayout.SOUTH);
}
}
Set Internal.m_cb's model with 1 elements
Set Internal.m_cb's model with 1 elements
After returning, Internal.m_cb's model size is 1 elements
Set Internal.m_cb's model with 2 elements
After returning, Internal.m_cb's model size is 1 elements
Set Internal.m_cb's model with 3 elements
After returning, Internal.m_cb's model size is 1 elements
Set Internal.m_cb's model with 4 elements
After returning, Internal.m_cb's model size is 1 elements
Set Internal.m_cb's model with 5 elements
After returning, Internal.m_cb's model size is 1 elements