16 septiembre 2005

Un pequeño problema con Swing

Conozco a varias personas, incluida yo, que han tenido un pequeño problema con las tablas de Swing.

Supongamos que tenemos nuestro JTable y nuestro TableModel, bien el DefaultTableModel o bien uno hecho a medida. Como es natural, cuando modificamos datos en nuestro TableModel, se actualiza automáticamente el JTable. Esto se debe a que los TableModel implementan un mecanismo de suscripción a cambios. El JTable se suscribe a cambios en el TableModel, se entera cuando éste cambia y se refresca en pantalla.

Los repintados en pantalla se hacen todos en un hilo (thread) aparte, llamado hilo de awt. Cuando el JTable necesita ser repintado, por algún cambio en el TableModel, el hilo de awt comienza a preguntar al TableModel por sus datos para pintarlos en el JTable.

Ahora supongamos que tenemos un hilo independiente, lo voy a llamar hilo A, que se dedica de forma más o menos rápida a añadir y borrar datos del TableModel. Puede pasarnos y de hecho a veces lo hace, que ocurre la siguiente secuencia:
  • El JTable necesita ser repintado. El hilo de awt le pide al TableModel cuántos elementos tiene. Este, por ejemplo, dice que tiene 10.
  • El hilo de awt se mete en un bucle de i=0; i<10;>
  • El hilo independiente A, cuando el hilo de awt está en medio del bucle, le da por borrar el elemento número 9. El TableModel avisa por medio del mecanismo de suscripcion y el evento de cambio queda encolado hasta que el hilo de awt pueda atenderlo
  • El hilo de awt sigue con su bucle y cuando llega al tableModel.getValueAt (9, j), le salta una excepción de indice fuera de rango, el elemento 9 ya no existe. En la ventana el JTable puede quedarse en estado catatónico
Este problema pude comprobar que pasaba también con JList y DefaultListModel y supongo que es general. De hecho, se advierte que las clases de Swing no son seguras con multihilo.

Los synchronize no sirve de nada, puesto que el hilo de awt está en un bucle y hace varias llamadas al modelo, bloqueándolo y liberándolo consecutivamente. El hilo A puede colarse entre medias y borrar.

¿Cual es la solución?.

Solución 1.

La primera solución que se le ocurre a todo el mundo es que el hilo A añada y borre elementos usando SwingUtilities.invokeLater(). Esto hace que el añadido y borrado de elementos en el TableModel se encole para que lo haga el hilo de awt. Si el hilo de awt está en su bucle refrescando el JTable, no va a añadir ni borrar elementos hasta que termine.

Esta solución no me gusta demasiado. Hay que llenar el código de SwingUtilities.invokeLater() por todos lados y acordarse de hacerlo siempre. Además, el parámetro de éste método es un Runnable, por lo que todavía es más incómodo llamarlo.

Solución 2.

La solución que más me gusta es duplicar el modelo. Los datos no se duplican, así que no es un gasto excesivo de memoria. A uno lo llamo modelo_real y al otro modelo_awt. Una clase hecha una sola vez, se suscribe a cambios en modelo_real y los replica, usando SwingUtilities.invokeLater() en modelo_awt. El modelo_awt es el que se pasa al JTable y queda más o menos oculto para todo el mundo. El hilo A y cualquier otro sitio del código, deben trabajar con el modelo_real.

La ventaja de esta solución es que haciéndo una clase una sóla vez y quizás un TableModel para que nos haga de modelo_awt, ya no tenemos que volver a tirar código. Luego nadie debe acordarse de llamar al SwingUtilities.invokeLater().

La pega es que si alguien le pide el modelo al JTable, va a obtener un modelo que no es el de trabajo del resto del código y puede haber algun problema.

De hecho, me he hecho mi propia versión de JTable que redefine los métodos que tienen que ver con el TableModel (incluidos constructores). Yo uso mi JTable y le paso mi modelo_real. Mi JTable se encarga de instanciar la clase de réplica y el modelo_awt. El método getModel() devuelve el modelo_real. Ahora casi nunca uso un JTable directamente, sino MiJTable.

2 comentarios:

Anónimo dijo...

Mirate el uso de SwingWorker creo que te servira para lo que comentas aqui y sin necesidad de liar tanto!!

hector dijo...

maestro...
lo que pasa es que estoy realizando un programas que carga los datos de una BD a un JTable este a su vez tiene un DefaultableModel (modelo). Lo que pasa que al refrescar yo llamo denuevo a mostrar(modelo); que es la clase que me desvuelve el modelo y efectivamente la escribe pero 2 veces pone otra tabla al lado y escribe a continuacion de los datos que ya estaban..

es algo asi

nombre apellido
mario guty

y al agregar por ejemplo queda asi.

nombre apellido nombre apellido
mario guty
alan brito.


no se que hacer yo se que tu lo puedes solucionar porque segun lo que he leido eres seco!! porfa cumpa.. espero respuesta.


pd: e utilizado repaint(); y otras funciones que ni me acuerdo ya...


espero respuesta...