A Custom Table Model

Besides problems with sorting numeric data, the DefaultTableModel has some other drawbacks. If the data being stored in a JTable is associated with a java object, you will have to work harder to synchronize changes to the JTable with the underlying data. Suppose you have an ArrayList of some Java objects. You have to convert this data into Object[][] to populate the JTable. If you make changes to the JTable, you have to remember to make corresponding changes in the ArrayList of the underlying data. That same type of synchronization must occur if you change the underlying data and you want the JTable to be updated.

A way to make this synchronization easier, is to use a custom table mode, by extending the javax.swing.table.AbstractTableModel class. An abstract class can consist of regular methods that you inherit if they are public. But, an abstract class will also have abstract methods that must be implemented by the class that extends the abstract class.

The AbstractTableModel class has the following abstract methods:


public int getRowCount();
public int getColumnCount();
public Object getValueAt(int row, int column);

There are additional methods you may want to add, but your class that extends AbstractTableModel must implement these methods. Before we get to this, we need a simple storage class. Here is "SimpleBook.java", from the previous page on JTables


public class SimpleBook {
   private String title;
   private String author;
   private double price;
   public SimpleBook(String t, String a, double pr) {
      title = t.trim();
      author = a.trim();
      price = pr;
   }
   public String getTitle() {
      return title;
   }
   public String getAuthor() {
      return author;
   }
   public double getPrice() {
      return price;
   }
}

Here is the file "SimpleBookList.java", also from the previous page on JTables with some modifications.


import java.util.ArrayList;
import java.io.*; //File,FileReader,FileNotFoundException,BufferedReader,IOException
public class SimpleBookList {
   private ArrayList<SimpleBook> bookList;
   public SimpleBookList() {
      bookList = new ArrayList<SimpleBook>();
   }
   public void add(SimpleBook sb) {
      bookList.add(sb);
   }
   public ArrayList<SimpleBook> getBooks() {
      return bookList;
   }
   public void readFromCSV(String filename) {
      File file = new File(filename);
      FileReader reader = null;
      try {
         reader = new FileReader(file);
      }
      catch (FileNotFoundException e) {
         e.printStackTrace();
         System.exit(1);
      }
      BufferedReader infile = new BufferedReader(reader);
      String line = "";
      try {
         boolean done = false;
         while (!done) {
            line = infile.readLine();
            if (line == null) {
               done = true;
            }
            else {
               String[] tokens = line.trim().split(",");
               String title = tokens[0].trim();
               String author = tokens[1].trim();
               double price = Double.parseDouble(tokens[2].trim());
               SimpleBook sb = new SimpleBook(title,author,price);
               bookList.add(sb);
            }
         }
      }
      catch (IOException e) {
         e.printStackTrace();
         System.exit(1);
      }
   }
}

The new lines, lines 11-13, are shown with a different background. Also, the convert2Data() method has been removed because it won't be needed with the custom table model.

A class that extends javax.swing.table.AbstractTableModel

Here is the file "SimpleBookTableModel.java":


import javax.swing.table.AbstractTableModel; 
import java.util.ArrayList; 
public class SimpleBookTableModel extends AbstractTableModel { 
   private String[] columnNames = {"Title","Author","Price"}; 
   private ArrayList<SimpleBook> myList; 
   public SimpleBookTableModel(SimpleBookList bkList) { 
      myList = bkList.getBooks(); 
   } 
   public int getColumnCount() { 
      return columnNames.length; 
   } 
   public int getRowCount() { 
      int size; 
      if (myList == null) { 
         size = 0; 
      } 
      else { 
         size = myList.size(); 
      } 
      return size; 
   } 
   public Object getValueAt(int row, int col) { 
      Object temp = null; 
      if (col == 0) { 
         temp = myList.get(row).getTitle(); 
      } 
      else if (col == 1) { 
         temp = myList.get(row).getAuthor(); 
      } 
      else if (col == 2) { 
         temp = new Double(myList.get(row).getPrice()); 
      } 
      return temp; 
   } 
   // needed to show column names in JTable 
   public String getColumnName(int col) { 
      return columnNames[col]; 
   } 
   public Class getColumnClass(int col) { 
      if (col == 2) { 
         return Double.class; 
      } 
      else { 
         return String.class; 
      } 
   } 
}

As mentioned earlier, any class that extends the AbstractTableModel class must implement the following methods:


public int getColumnCount()
public int getRowCount()
public Object getValueAt(int row, int col)

These are the absolute minimum necessary methods to display data in the JTable. However, the getColumnName() method is also needed if you want to display column headings. In addition, the getColumnClass() method is also needed if you want to sort properly by any column other than a column with a String type. So, these methods are important as well.

If you are going to allow removing rows from the table, you would include a method for doing that in this class as well. That requires modifying the internal data for the model (in this case the variable myList) and calling the fireTableRowsDeleted(index, index) method.

For the SimpleBookTableModel to work, an ArrayList<SimpleBook> object must be returned from a SimpleBookList object. The constructor defined on lines 6-8 has been written to take this into account.

A GUI program that uses this custom table model with a JTable

Here is the program "CustomJTable.java". It is modified from "BasicJTable.java".


import javax.swing.JTable; 
import javax.swing.JFrame; 
import javax.swing.JPanel; 
import javax.swing.JScrollPane; 
import java.awt.event.ActionListener; 
import java.awt.event.ActionEvent; 
import java.awt.BorderLayout; 
import java.awt.Dimension;
public class CustomJTable extends JFrame implements ActionListener { 
   private SimpleBookTableModel tableModel; 
   private JTable table; 
   private SimpleBookList myList;
   public CustomJTable(String title) { 
      super(title); 
      setBounds(10,10,400,300); 
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      myList = new SimpleBookList();
      myList.readFromCSV("books.csv");
      tableModel = new SimpleBookTableModel(myList); 
      table = new JTable(tableModel); 
      table.setAutoCreateRowSorter(true);
      JScrollPane scrollPane = new JScrollPane(table); 
      scrollPane.setPreferredSize(new Dimension(380,280)); 
      JPanel panel = new JPanel(); 
      panel.add(scrollPane); 
      add(panel,BorderLayout.CENTER); 
   } 
   public void actionPerformed(ActionEvent ae) { 
    
   } 
   public static void main(String[] args) { 
      CustomJTable myApp = new CustomJTable("Custom JTable"); 
      myApp.setVisible(true); 
   } 
}

The new or modified lines, shown with a different background, are line 9, 10, 13, 19 and 32-33.

Line 9 has the class name changed to CustomJTable. Line 10 defines a new instance variable, tableModel, that is used to hold the SimpleBookCustomTable object.

Line 13 has the constructor name changed to match the class name. Line 19 constructs the table model by passing the SimpleBookList object to the SimpleBookCustomTable constructor.

Lines 32 and 33 have been changed to use the name of this new class.

If you run this program, you will see that the Price column sorts correctly now.

Updating the JTable

Here are a few events that cause an update the JTable: