![]() |
|
![]() |
![]() |
||||
|
|||||
Recent ArticlesClojure is a relatively new language to appear on the Java Virtual Machine (JVM), although it draws on very mature roots in the form of the LISP langu ... Should You Care About Requirements Engineering? Recently, I (Adil) was invited to participate in a one day seminar on the subject of Requirements Engineering. Whilst I have no direct experience of t ... Tips for Setting Up Your First Business Website To attract all potential customers to your business you need a presence on the web. The problem is that if you haven't set up a website before, you p ... LISP is a general-purpose programming language and is the second-oldest programming language still in use, but how much do you know about it? Did you ... Open Source Tools for Developers: Why They Matter From a developer's point of view use of open-source tools has advantages beyond the obvious economic ones. With the open-source database MySQL in mind ... |
A TableModel with ClassIntroduction
Anybody who has used Java's The
public interface TableModel {
public int getRowCount();
public int getColumnCount();
public String getColumnName(int col);
public Class getColumnClass(int col);
public boolean isCellEditable(int row, int col);
public Object getValueAt(int row, int col);
public void setValueAt(Object aValue, int row, int col);
public void addTableModelListener(TableModelListener l);
public void removeTableModelListener(TableModelListener l);
}
The documentation for the
public Class getColumnClass(int col) {
return Object.class;
}
It returns the A Naive Approach
The first step is to make the return value in some way dependent on the contents of the
public Class getColumnClass(int col) {
if (getRowCount() == 0) {
return Object.class;
} else {
Object cellValue = getValueAt(0, col);
return cellValue.getClass();
}
}
Note the defensive style used to protect against the case when the table has no rows. This approach is fine provided the cells of your column are all of the same class (or, more precisely, assignable to the class of the first cell). This covers a large number of cases and has the advantage that it returns a value in constant time, regardless of the number of rows in the table. However, it is not general enough. There will be cases when the class of the first cell is not representative of the whole column, and this approach therefore returns the wrong result. Handling such cases requires more effort. Computing the Correct Class
As an example, suppose we have three instances a, b, c of the respective classes
A sketch of an algorithm to determine the least general unifier is as follows. We begin by not knowing the least general unifier, so we assign a So the general decision procedure is to carry out the first of the following actions that applies:
A method that encodes this decision procedure is as follows:
public Class getColumnClass(int column) {
int rowCount = getRowCount();
Class leastGeneralUnifier = null;
for (int row = 0; row < rowCount; row++) {
Object value = getValueAt(row, column);
if (value != null) {
Class cls = value.getClass();
if (leastGeneralUnifier == null) {
leastGeneralUnifier = cls;
} else if (leastGeneralUnifier.isAssignableFrom(cls)) {
; // do nothing
} else if (cls.isAssignableFrom(leastGeneralUnifier)) {
leastGeneralUnifier = cls;
} else { // traverse upwards in the class hierarchy
leastGeneralunifier = traverseForClass(leastGeneralUnifier, cls);
}
}
}
if (leastGeneralUnifier == null) {
leastGeneralUnifier = Object.class;
}
return leastGeneralUnifier;
}
The source code that performs the traversal up the class hierarchy described in the last step of the decision procedure is as follows:
protected Class traverseForClass(Class start, Class unifyWith) {
if (start == Object.class) { // halt the recursion
return Object.class;
} else {
if (start.isAssignableFrom(unifyWith)) {
return start;
} else { // recurse up the tree
return traverseForClass(start.getSuperclass(), unifyWith);
}
}
}
Keeping the Correct Class
In general we should inspect all the values of the column and derive a least general, but nevertheless unifying, class. The problem is that for large tables, this may be computation intensive, so it shouldn't be performed too often. Requests to
We should give special consideration to changing cell values, as in practise a change to a single cell value would not often merit the recalculation of the column class from all column cells. Instead, whenever a cell's value is changed, we should test whether the new value is of the same class as the currently cached column class. If it is (or if the new value is
The following methods use a column-class cache and update those values in response to changes to the table. They use a table model's standard method signatures. Note that for significant changes, such as a structural changes, or a number of rows being updated, the whole cache is reinitialised, so that the column class is recomputed on receiving the next request. When a cell is updated, the
public void fireTableDataChanged() {
initialiseClassCache();
super.fireTableChanged(new TableModelEvent(this));
}
public void fireTableStructureChanged() {
initialiseClassCache();
super.fireTableChanged(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
}
public void fireTableRowsUpdated(int firstRow, int lastRow) {
initialiseClassCache();
super.fireTableChanged(new TableModelEvent(this, firstRow, lastRow,
TableModelEvent.ALL_COLUMNS, TableModelEvent.UPDATE));
}
public void fireTableRowsDeleted(int firstRow, int lastRow) {
initialiseClassCache();
super.fireTableChanged(new TableModelEvent(this, firstRow, lastRow,
TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE));
}
public void fireTableCellUpdated(int row, int column) {
Class currentUnifier = getColumnClass(column);
Object value = getValueAt(row, column);
if (value != null) {
Class newCellClass = value.getClass();
if (newCellClass == currentUnifier) {
// leave the column class unaffected
} else if (newCellClass.isAssignableFrom(currentUnifier)) {
setCachedColumnClass(column, newCellClass);
} else {
setCachedColumnClass(column, computeColumnClass(column));
}
}
super.fireTableChanged(new TableModelEvent(this, row, row, column));
}
The caching and lazy evaluation of the class is implemented by the following:
private boolean[] dirtyColumnClass;
private Class[] columnClass;
protected boolean isColumnClassDirty(int column) {
return dirtyColumnClass[column];
}
protected void setColumnClassDirty(int column, boolean value) {
dirtyColumnClass[column] = value;
}
protected Class getCachedColumnClass(int column) {
return columnClass[column];
}
protected void setCachedColumnClass(int column, Class cls) {
columnClass[column] = cls;
}
public Class getColumnClass(int column) {
if (isColumnClassDirty(column)) {
setCachedColumnClass(column, computeColumnClass(column));
setColumnClassDirty(column, false);
}
return getCachedColumnClass(column);
}
Conclusion
We can be truer to the interface of a Firstly, computing the column class in the way I have described takes up some CPU cycles at runtime in a way that "compiled" knowledge of a column's class does not. Furthermore, the number of CPU cycles that it takes up is dependent upon the number of rows in the table. (This is logical: the more values a column contains, the more values need to be inspected to determine the most specific class for the column.) In other words, it could be inefficient for very large tables. At the time of writing, I have not tested the algorithm against larger tables, but my belief is that for all practical purposes the algorithm should not be limiting. Nevertheless, this may have been an important consideration for the Java developers at Sun.
Secondly, the implementation of Simon White |