jueves
mar292012
Java puzzle: la inexplicable ConcurrentModificationException
Al ejecutar este código, cambiar cualquier elemento de la segunda columna de la tabla (la que emplea un editor diferente del estándar) y volver a hacer clic en la primera columna se lanza una ConcurrentModificationException. Sin embargo, el código parece emplear sincronización de modo correcto para proteger la lista que lanza la excepción. ¿Cuál es el problema con este código?. Las respuestas en los comentarios, por favor :)
package examen; import java.awt.*; import java.util.*; import java.util.List; import javax.swing.*; import javax.swing.event.CellEditorListener; import javax.swing.table.*; public class ProblemaConcurrencia extends JFrame { public ProblemaConcurrencia() { this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Object[][] datos = { {"Mary", "Esquiar", }, {"Lucas", "Esquiar"}, {"Kathya", "Escalar"}, {"Marcus", "Andrews", "Correr"}, {"Angela", "Nadar"} }; String[] columnNames = {"Nombre", "Pasatiempo"}; DefaultTableModel tableModel = new DefaultTableModel(datos, columnNames); JTable tabla = new JTable(tableModel); tabla.setRowHeight(20); TableColumnModel tablaColumnModel = tabla.getColumnModel(); tablaColumnModel.getColumn(1).setCellEditor((new TableCellEditorJComboboxHobbbies())); JScrollPane scrollPane = new JScrollPane(tabla); tabla.setPreferredScrollableViewportSize(new Dimension(500, 70)); getContentPane().add(scrollPane, BorderLayout.CENTER); } public static void main(String[] args) { Runnable r = new Runnable() { public void run() { ProblemaConcurrencia frame = new ProblemaConcurrencia(); frame.pack(); frame.setVisible(true); } }; SwingUtilities.invokeLater(r); } } class TableCellEditorJComboboxHobbbies extends JComboBox implements TableCellEditor { private static String[] opciones = {"Esquiar", "Patinar", "Correr", "Nadar", "Escalar"}; private final List listeners = new ArrayList< CellEditorListener>(); public TableCellEditorJComboboxHobbbies() { super(opciones); } public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { this.setSelectedItem(value); return this; } public Object getCellEditorValue() { return this.getSelectedItem(); } public boolean isCellEditable(EventObject anEvent) { return true; } public boolean shouldSelectCell(EventObject anEvent) { return true; } public boolean stopCellEditing() { synchronized (listeners) { for (CellEditorListener l : listeners) { l.editingStopped(null); } } return true; } public void cancelCellEditing() { synchronized (listeners) { for (CellEditorListener l : listeners) { l.editingCanceled(null); } } } public void addCellEditorListener(CellEditorListener l) { synchronized (listeners) { listeners.add(l); } } public void removeCellEditorListener(CellEditorListener l) { synchronized (listeners) { listeners.remove(l); } } }
Reader Comments (3)
El problema esta en la implementacion del listener. Deberias utilizar CopyOnWriteArrayList en vez de ArrayList.
private final java.util.List<CellEditorListener> listeners = new CopyOnWriteArrayList<CellEditorListener>();
Al pasar a la otra celda se invoca stopCellEditing, que obtiene un candado sobre la lista y la recorre para invocar editingStopped en cada listener. Si desde ese método se invoca a removeCellEditorListener, el candado que está ahí dentro ya se tiene (porque se manejan a nivel hilo) de modo que se ejecuta de inmediato el listeners.remove y entonces en la siguiente iteración dentro de stopCellEditing se arroja la excepción.
Premio a los dos :) el problema está bien descrito por Enrique, y la solución de Mauro probablemente sea la ideal.