jun

25

Death to DefaultTableModel! AbstractTableModel Rulez!

By Felipe Saab

Antes que alguém venha dizer: ‘Olha o título, esse cara num sabe nada!’ ou ‘DefaultTableModel extends AbstractTableModel!! Asshole!!!’ eu já vou dizendo: Eu sei que DefaultTableModel herda de AbstractTableModel, a ideia do título é abandonar a DefaultTableModel e criar suas próprias classes utilizando os métodos da AbstractTableModel do jeito que nos servir melhor.

Dito isso, vamos  ao assunto de hoje: ‘DefaultTableModel e o porque de não utilizá-la’.

Quem já programou utilizando a API Swing e nunca utilizou a JTable que atire a primeira pedra.

Agora, quem já usou JTable e nunca utilizou a DefaultTableModel pode atirar pedras a vontade!

Aqui no blog mesmo… Eu já utilizei DefaultTabelModel em alguns posts. Mas pois bem, vamos abolir essa má prática. Seguem alguns motivos do porque não devemos utilizar a DefaultTableModel:

1. É mais difícil que escrever seu próprio TableModel;
2. É mais lento (usa classes sincronizadas);
3. Ocupa mais espaço em memória (duplica seus dados);
4. Deixa o código mais confuso e difícil de manter;
5. Usa casts inseguros;
6. Força que você tenha que exibir informações desnecessárias (como o ID) na tabela, ou controlar o ID numa lista separada;
7. Faz a sua mulher te deixar, o leite da sua geladeira azedar, e pessoas apontarem o dedo para você na rua.

Motivos muito bem explicados pelo ViniGodoy neste post do GUJ.

Mas se a DefaultTableModel é tão ruim assim, o que devemos fazer? Tudo se resume a uma classe: AbstractTableModel.

Tudo o que temos que fazer é criar uma nova classe que estenda AbstractTableModel e customizá-la para exibir os dados que queremos. Concordo que em um primeiro momento vai parecer meio desnecessário escrever uma classe para cada tabela do seu sistema (ou uma genérica), mas isso compensa muito na hora de utilizá-la, e compensa mais ainda na hora de reutilizá-la (o famoso reuso da orientação a objetos).

Mas chega de conversa, vamos a prática pois é o melhor jeito de fixar as coisas…

Nosso escopo será o seguinte: uma loja qualquer quer controlar seu estoque. Em algum lugar dentro do software que realizará esta tarefa vai ter uma tela de consulta dos produtos e suas respectivas quantidades disponíveis em estoque.

É nessa tela que nosso tutorial irá decorrer. Uma tela de consulta de produtos com alguns botões só para demonstrar o funcionamento do nosso modelo.

Abra o NetBeans, crie um novo projeto para área de trabalho, apague tudo que ele cria, crie um novo pacote, crie um novo JFrame dentro dele e deixe parecido com este:

O JFrame contém os seguintes componentes:

  • 1 JTable (que já vem dentro do seu JScrollPane)
  • 5 JButtons

Os textos dos botões já explicam qual será a função de cada um deles certo?

Agora vamos a nossa classe base (da camada de modelo) que vai representar os produtos do nosso controle de estoque. Segue a classe Produto.java:

public class Produto {
 
    private int cod;
    private String nome;
    private int quant;
 
    public Produto() {
    }
 
    public Produto(int cod, String nome, int quant) {
        this.cod = cod;
        this.nome = nome;
        this.quant = quant;
    }
 
    public int getCod() {
        return cod;
    }
 
    public void setCod(int cod) {
        this.cod = cod;
    }
 
    public String getNome() {
        return nome;
    }
 
    public void setNome(String nome) {
        this.nome = nome;
    }
 
    public int getQuant() {
        return quant;
    }
 
    public void setQuant(int quant) {
        this.quant = quant;
    }
}

Uma simples classe para representar um produto. Coloquei o código também porque geralmente sempre precisamos controlar o código dos registros que estão sendo apresentados na tabela. Utilizando DefaultTableModel nós deveríamos ter uma lista separada para poder controlar esses códigos. Mais a frente veremos como seria com nosso novo modelo para a tabela.

Vamos agora para a parte que importa: criar uma classe que herde de AbstractTableModel. A classe AbstractTableModel define alguns métodos que nós podemos reescrever para criar um modelo bem mais fácil de trabalhar. Seguem os métodos, que iremos utilizar neste exemplo, e suas respectivas funções:

  • int getRowCount() -> retorna o número de linhas que a tabela tem;
  • int getColumnCount() -> retorna o número de colunas que a tabela tem;
  • String getColumnName(int column)-> retorna o nome que será exibido na coluna (o índice da primeira coluna é 0);
  • Class<?> getColumnClass(int columnIndex) -> retorna a classe que representa cada coluna. String irá mostrar o texto alinhado à esquerda, Integer irá mostrar o número alinhado à direita, Boolean irá mostrar um checkbox… ;
  • Object getValueAt(int rowIndex, int columnIndex) -> método utilizado pela JTable para escrever os valores nas células. Internamente a JTable passa em todas as celulas chamando este método para poder setar os valores;
  • ??void setValueAt(Object aValue, int rowIndex, int columnIndex) -> método que a JTable chama quando uma célula é editada;
  • boolean isCellEditable(int rowIndex, int columnIndex) -> método para saber se a célula é ou não editável;
  • void fireTableDataChanged() -> método que avisa todos os listeners da tabela que houve uma mudança nos dados, um deles é o responsável por desenhar a tabela e ao ser notificado irá redesenhá-la.

Ok, agora que já conhecemos alguns dos métodos que a AbstractTableModel fornece, vamos reescrevê-los. Segue o código do arquivo ProdutoTableModel.java:

public class ProdutoTableModel extends AbstractTableModel {
 
    //constantes que vão representar as colunas
    //(só para facilitar o entendimento do código)
    private final int COL_NOME = 0;
    private final int COL_QUANT = 1;
 
    //lista dos produtos que serão exibidos
    private List<produto> produtos;
 
    public ProdutoTableModel() {
        produtos = new ArrayList();
    }
 
    public ProdutoTableModel(List<produto> lista) {
        this();
        produtos.addAll(lista);
    }
 
    public int getRowCount() {
        //cada produto na lista será uma linha
        return produtos.size();
    }
 
    public int getColumnCount() {
        //vamos exibir só Nome e Quantidade, então são 2 colunas
        return 2;
    }
 
    @Override
    public String getColumnName(int column) {
        //qual o nome da coluna
        if (column == COL_NOME) {
            return "Nome";
        } else if (column == COL_QUANT) {
            return "Quant. Disp";
        }
        return "";
    }
 
    @Override
    public Class getColumnClass(int columnIndex) {
        //retorna a classe que representa a coluna
        if (columnIndex == COL_NOME) {
            return String.class;
        } else if (columnIndex == COL_QUANT) {
            return Integer.class;
        }
        return String.class;
    }
 
    public Object getValueAt(int rowIndex, int columnIndex) {
        //pega o produto da linha
        Produto p = produtos.get(rowIndex);
 
        //verifica qual valor deve ser retornado
        if (columnIndex == COL_NOME) {
            return p.getNome();
        } else if (columnIndex == COL_QUANT) {
            return p.getQuant();
        }
        return "";
    }
 
    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        //pega o produto da linha
        Produto p = produtos.get(rowIndex);
 
        //verifica qual valor vai ser alterado
        if (columnIndex == COL_NOME) {
            p.setNome(aValue.toString());
        } else if (columnIndex == COL_QUANT) {
            p.setQuant(Integer.parseInt(aValue.toString()));
        }
 
        //avisa que os dados mudaram
        fireTableDataChanged();
    }
 
    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        //no nosso caso todas vão ser editáveis, entao retorna true pra todas
        return true;
    }

Até agora eu só reescrevi os métodos que foram citados anteriormente. Agora nós vamos à parte produtiva da coisa: vamos escrever os métodos para inserir um novo produto, para remover produtos, para ordená-los e para embaralhá-los, ou seja, vamos dar vida aos nossos botões e o melhor: ver como isso é muito fácil!

Mais alguns métodos para a nossa classe ProdutoTableModel.java:

    public void inserir(Produto p) {
        produtos.add(p);
 
        fireTableDataChanged();
    }
 
    public void excluir(int pos) {
        produtos.remove(pos);
 
        fireTableDataChanged();
    }
 
    public void excluir(Produto p) {
        produtos.remove(p);
 
        fireTableDataChanged();
    }
 
    public void ordenarPorNome() {
        //ordena pelo nome
        Collections.sort(produtos, new Comparator<produto>() {
 
            public int compare(Produto o1, Produto o2) {
                return o1.getNome().compareTo(o2.getNome());
            }
        });
 
        //avisa que a tabela foi alterada
        fireTableDataChanged();
    }
 
    public void ordenarPorQuantidade() {
        //ordena pelo nome
        Collections.sort(produtos, new Comparator<produto>() {
 
            public int compare(Produto o1, Produto o2) {
                return o1.getQuant() - o2.getQuant();
            }
        });
 
        //avisa que a tabela foi alterada
        fireTableDataChanged();
    }
 
    public void misturar() {
        //mistura a lista
        Collections.shuffle(produtos);
 
        //avisa que a tabela foi alterada
        fireTableDataChanged();
    }
 
    public Produto getProduto(int pos) {
        if (pos < 0 || pos >= produtos.size()) {
            return null;
        }
 
        return produtos.get(pos);
    }

Pronto! Veja como é fácil manipular os dados da tabela agora. Basta manipular uma lista!

Acho que os únicos métodos que merecem alguma explicação são os de ordenação, onde, em cada um deles é criado um objeto que implementa a interface Comparator que ordena de acordo com a necessidade: se é por ordem alfabética dos nomes, compara os nomes, se é pela quantidade em estoque, compara as quantidades.

OBS: Lembre sempre de chamar o método fireTableDataChanged() para avisar que os dados mudaram e a tabela ser redesenahda.

Voltando para aquela ideia de que esta tela seria uma tela de busca… possívelmente ela poderia ser chamada de uma outra tela (a de entrada de produtos por exemplo) para facilitar a busca de um produto. Como seria a recuperação do código do produto selecionado na tabela? Mais ou menos assim:

public class BuscaProdutos extends JDialog {
 
    //....
 
    public Produto retornaProdutoSelecionado() {
        ProdutoTableModel model = (ProdutoTableModel) jT_TabelaProdutos.getModel();
        return model.getProduto(jT_TabelaProdutos.getSelectedRow());
    }
 
    //...
}
 
public class EntradaProdutos extends JDialog {
 
    //...
 
    public void buscaProduto() {
        //cria a tela de busca como modal
        BuscaProdutos tela_busca = new BuscaProdutos(null, true);
 
        //exibe
        tela_busca.setVisible(true);
 
        //recupera os dados
        Produto p = tela_busca.retornaProdutoSelecionado();
 
        if (p != null) {
            jTF_Codigo.setText(String.valueOf(p.getCod()));
            jTF_Nome.setText(p.getNome());
            jTF_Quantidade.setText(String.valueOf(p.getQuant()));
        }
    }
    //...
}

A tabela ficaria na tela de busca. Tal tela seria chamada como modal (só ela poderia ter o foco enquanto estivesse visível) e quando se tornasse invisível, o produto selecionado seria recuperado e usado como necessário.

Quanto trabalho não seria necessário para fazer isso utilizando a DefaultTableModel?

Voltando à nossa aplicação inicial, ainda temos que linkar os botões com as funções do modelo. Vamos ao código da classe Principal.java (JFrame da foto lá em cima):

public class Principal extends JFrame {
 
    private ProdutoTableModel model;
 
    //variável só para controlar os códigos dos produtos
    private int ultimoCod;
 
    public Principal() {
        initComponents();
        ultimoCod = 1;
 
        //cria a lista com os produtos
        List<produto> lista = new ArrayList<produto>();
        lista.add(new Produto(ultimoCod++,"Lapiseira 0.7",30));
        lista.add(new Produto(ultimoCod++,"Caneta preta",100));
        lista.add(new Produto(ultimoCod++,"Caneta azul",70));
        lista.add(new Produto(ultimoCod++,"Caneta vermelha",100));
 
        //cria o modelo de Produto
        model = new ProdutoTableModel(lista);
 
        //atribui o modelo à tabela
        jTable1.setModel(model);
    }
 
    //Ação do botão Inserir
    private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
        Random r = new Random();
        Produto p = new Produto(ultimoCod++, "Produto "+String.valueOf(r.nextInt(100)), r.nextInt(1000));
        model.inserir(p);
    }
 
    //Ação do botão Excluir
    private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {
        int selecionados[] = jTable1.getSelectedRows();
        if (selecionados.length > 0){
            List<produto> seraoExcluidos = new ArrayList<produto>();
 
            for (int i=0; i<selecionados.length; i++)
                seraoExcluidos.add(model.getCliente(selecionados[i]));
 
            for (Produto p : seraoExcluidos)
                model.excluir(p);
        }else{
            JOptionPane.showMessageDialog(this, "Selecione alguém neh...");
        }
    }
 
    //Ação do botão Ordenar por Quantidade
    private void jButton5ActionPerformed(java.awt.event.ActionEvent evt) {
        model.ordenarPorQuantidade();
    }
 
    //Ação do botão Ordenar por Nome
    private void jButton3ActionPerformed(java.awt.event.ActionEvent evt) {
        model.ordenarPorNome();
    }
 
    //Ação do botão Misturar
    private void jButton4ActionPerformed(java.awt.event.ActionEvent evt) {
        model.misturar();
    }
 
    /...
}

No construtor é criada uma lista com alguns produtos fictícios e também é instanciado o nosso TableModel.

O botão inserir insere um produto com o nome “Produto i” e quantidade Q, onde ‘i’ e ‘Q’ são números randômicos.

O botão excluir trata a exclusão de vários ítens selecionados ao mesmo tempo. Primeiro todos são guardados em uma lista auxiliar e depois eles vão sendo excluídos um a um (Você sabe por que eu fiz assim? Pense um pouco… qualquer coisa a área de comentários está logo abaixo).

Os botões Ordenar por Nome, Ordenar por Quantidade e Misturar são auto explicativos e fazem exatamente o que o seu nome quer dizer.

Bom, chegamos ao fim de mais um artigo aqui do Java Simples. E fica meu conselho: pare de usar DefaultTableModel e começe agora mesmo a escrever suas próprias TableModels.

O código fonte do projeto está disponível aqui! E se achou interessante compartilhe este artigo utilizando a barra social logo abaixo. =]

[]s e até a próxima,
Saab.

24 Responses so far

Interessante seu tutorial amigo. Parabens!

Sou iniciante ainda, mas pude compreender o que voce quis passar!

Continue contribuindo com a galera. Obrigado.

Abraços

Olá, desculpe minha ignorância, mas estou tentando usar o seu modelo de AbstractTableModel, no entanto não consigo mudar o tamanho da culuna..Pode me dizer como faço para alterar esse tamanho?

Obrigada.

Oi Isabel,
O tamanho das colunas da tabela não tem relação alguma com o modelo que tal tabela usa.
Para mudar o tamanho de uma coluna vc usa o seguinte comando:

jTable1.getColumnModel().getColumn(INDICE).setPreferredWidth(width);

Onde “INDICE” pode ser um número que represente a coluna (a primeira da esquerda para a direita é a 0 (zero)) ou então uma String que seja igual ao nome da coluna (se na coluna estiver escrito ‘Nome’ passe a String “Nome”, exatamente igual).

Boa sorte.

Muito bom cara, eu sempre me mati com o DefaultTableModel, mas depois que comecei um projeto maior me deparei com problemas, mas apartir de hoje escreverei minhas proprias tablemodel

Muito legal o post, vou complementar o link da minha assinatura do GUJ para apontar pra cá também.
Só uma correção no português. Embora extensão seja com “x”, estender e estenda é com “s”.

Opa… errinhos de português me perseguindo.. ehhehehe
Já está tudo certo, obrigado.
=]

Parabéns!!! Exemplo simples de entender e muito prático!
Não utilizarei mais DefautTableModel!!!
:)

Qual seria o método correspondente da abstractTableModel para excluir linhas? Na DefaultTM, era setNumRow – deprecated – e a atual setRowCount. Outro problema da Abstract, que talvez tb seria da default, é que todos dados buscados, são inseridos, enquanto que na Default tem como fazer melhor isso. O que é melhor? Preencher as tabelas e transportar dados pra outra tela a partir da tabela e instancia de objetos. Ou quando mudar para outra tela, faz a consulta novamente nos sql com nova entrada de dados do objeto selecionado?

Desconsidere. A consulta MySql nao é feita diretamente na sua tableModel. Vou ver se altero a minha AbstractTableModel. Fica melhor.

Cara está pedindo para implementar
public int compare(Object o1, Object o2) {
throw new UnsupportedOperationException(“Not supported yet.”);
}

Cara na classe ProdutoTableModel está pedindo para implementar :

public int compare(Object o1, Object o2) {
throw new UnsupportedOperationException(“Not supported yet.”);
}

Está pedindo para implementar esses métodos pois criamos uma classe “inline” dentro dos métodos ordernarPorNome() e ordenarPorQuantidade().
Caso você queria pode criar uma classe separada e apenas mandar um objeto dela no Collections.sort(..)

Saab

Adorei o Site, simples e muito objetivo nos temas.

Coloquei nos meus favoritos.

Valeu!!!

Gostei muito do post, excelente abordagem do tema. Sou novo em Java e já estava ficando desanimado com o fato de usar DefaultTableModel, realmente ele torna a vida bem mais difício, mas com seu post tudo ficou bem mais fácil. Parabéns!!!
Aproveito para deixar uma sugestão para um novo post, falar um pouco sobre o ObjectTableModel do Mark Vasconcelos, dei uma olhada no blog dele (http://markytechs.wordpress.com/2009/05/29/objecttablemodel/) mas ainda não consegui compreender bem, acredito que você abordaria com muita propriedade o assunto.

Sem mais, valeu pelo tutorial e muito sucesso.

Muito legal o tutorial, deu uma ajuda num projeto pra faculdade.

Caros amigos, gostaria de acrescentar a este tutorial algo que me tirou um dia de trabalho por conta de algumas linhas de código que estão muito bem colocadas neste tutorial, mas que necessitavam de pequenos ajustes.

A classe como está, não atualiza a informação necessária para que os ouvintes que implementam TableModelListener saibam qual modificação foi feita na tabela, ou seja, se foi INSERT, UPDATE, DELETE. Por isso se tentarmos sobrescrever o método tableChanged() para saber qual modificação foi feita na tabela, ele só vai informar a modificação padrão (UPDATE). Para resolver isto deve-se reescrever as seguintes linhas:

public void inserir(Produto p) {
produtos.add(p);

//fireTableDataChanged(); altera para:
fireTableRowsInserted(produtos.indexOf(p), produtos.indexOf(p));
}
public void excluir(int pos) {
produtos.remove(pos);

//fireTableDataChanged(); altera para:
fireTableRowsDeleted(pos, pos);
}

public void excluir(Produto p) {
produtos.remove(p);

//fireTableDataChanged(); altera para:
fireTableRowsDeleted(produtos.indexOf(p), produtos.indexOf(p));
}

Em fim, espero ter contribuído para o enriquecimento deste belo tutorial.

Valeu Heberts!!

Muito obrigado pela contribuição e parabéns pelas pesquisas e pelo resultado, muito interessante teu código.

=D

Olá, gostaria de lhe parabenizar pelo artigo e gostaria da sua ajuda para me ajudar a entender alguns probleminhas que tive ao usar o seu exemplo para aprender, a Default Table Model realmente não facilita a vida de nós programadores. Como sou iniciante, peço que tenha um pouco de paciência comigo, afinal, ninguém nasceu sabendo. Em alguns trechos do seu código o NetBeans acusou alguns erros que não me permitiram rodar a sua aplicação tais como:

public Produto getCliente(int pos) {
if (pos >= produtos.size()) {
return null;
}

return produtos.get(pos);
}

A variável “produtos” é do tipo List, como declarado no topo do código fonte, o método que eu suponho ser, “getProduto” e não “getCliente” deveria retornar um tipo “Produto”, o Netbeans não aceitou esse tipo de retorno.

public void ordenarPorNome() {
//ordena pelo nome
Collections.sort(produtos, new Comparator() {

public int compare(Produto o1, Produto o2) {
return o1.getNome().compareTo(o2.getNome());
}
});

//avisa que a tabela foi alterada
fireTableDataChanged();
}

Nos métodos de ordenação a IDE informa algum problema que eu não consegui entender, portanto queria que você me esclarecesse essa parte do código por favor.
No mais, acho que caso você esclareça essas minhas dúvidas eu irei aprender a criar meu próprio model e fugir definitivamente do DTM.
desde já agradeço.

Fala Marcelo,
Valeu por informar os erros cara, tudo falta de atenção minha, desculpa… Já arrumei tudo no post. =D
Segue uma explicação deles:

Sobre o primeiro problema, o método estava com o nome errado mesmo, deveria ser getProduto.. Isso concerteza foi devido a um ctrl+c, ctrl+v.. hehehe
Agora sobre uma reclamação da IDE, provavelmente é porquê eu tinha esquecido de informar qual o tipo da lista.
Antes a declaração do atributo produtos estava assim: private List produtos;
Agora está assim: private List produtos;
Eu explícitamente falo que o tipo dos objetos dessa lista vai ser Produto. Quando o tipo não é informado o tipo padrão é Object.
Sacou o erro? Na declaração do método falamos que o retorno é um Produto porém como a lista não estava tipada (informando explicitamente o tipo Produto), era retornado o tipo Object.

Sobre as ordenações, acontece o mesmo erro… Faltou informar qual o tipo dos objetos que eu ia utilizar dentro da classe comparadora (Comparator).
O método estático sort() da classe Collections tem uma sobrescrita (override) que permite eu passar 2 parâmetros:
1 – A lista que eu quero ordenar
2 – A classe que vai ser utilizada para a ordenação de tal lista
A classe que vai ser utilizada deve ser uma implementação da interface Comparator, que define o método compare(), que é utilizado no algorítmo de ordenação.
Apenas para evitar a criação de uma nova classe (ProdutoOrdenadorQuantidade.java por exemplo) foi criada uma “classe inline”, ou seja, dentro do próprio método nós criamos uma nova classe que implementa a interface Comparator.
O problema é que caso não seja informado qual o tipo da classe que implementa essa interface (como estava antes: new Comparator() {…}) o padrão é Object, então o método compare() deveria receber dois Object: compare(Object o1, Object o2), o que estava gerando erro na IDE.
Depois que informamos o tipo da classe que implementa a interface (new Comparator() {…}) o método compare() passa a esperar dois objetos Produto: compare(Produto p1, Produto p2). =]

Brigadão por informar minhas faltas de atenção Marcelo.
Boa sorte ai nos estudos.
[]s,
Saab.

Fala Saab
Antes de tudo obrigado pelo post, esta ajudando muitos!
Tenho pesquisado porém, sem sucesso sobre como manipular um campo booleano (checkbox) dentro da classe AbstractTableModel se puder postar um pequeno exemplo!?

Grande abraço!

Comecei a usar o tableModel sem ser o default, mas agora está sendo atualizado 544 vezes.
O que eu faço?

Olá Saad , segui seu exemplo de criação da tableModel deu certo. blz.
Porem eu gostaria de saber para retornar a linha selecionada, depois de uma filtrada a tableModel inicial.
Eu criei um jTextField que filtra a TableModel, ela filtra normal porem quando eu fecho o jDialog, a linha que apresenta no jFramePrincipal é o índice da TableModel inicial sem filtrar. Como resolvo isso.?
Parabens pelo post.

Como eu faço para coloca um ckeckbox hein? eu tava tentando colocar aqui, eu até consegui fazer que o checbox aparecesse, mas quando clico nele, nada acontece.

bom pessoal,depois de pesquisar muito, encontrei uma possível solução, só que eu não testei pois vi que seria meio complicado para mim, e além do mais, da maneira do que eu estou fazendo, não valeria a pena. de qualquer forma, se alguém sabe de uma forma mais simples de colocar um checkebox na tabela, ficaria muito grato.

bom aqui vai a solção que eu encontrei:

http://www.guj.com.br/java/88484-adicionar-linha-por-linha-em-jtable-com-checkbox-resolvido

Leave a comment