In software programming (especially Graphical User Interface or GUI programming), many times, we need to load data from file or database into a table or list to be displayed to user on computer screen. Most of the time, the size of the data can be determined only during runtime. The data could be very small to a few rows of record and could be very large to a few millions rows of record.
The main problem facing by most of the programmers is that, the program cannot load all the data into memory because the computer has limited memory. Although the price of RAM for a computer today is pretty cheap but adding more RAMs into a computer does not solve the problem in long term because you will get more and more data when the time past. Some more, it is not efficient to load all data once into memory because it will take a lot of time to load all the data (imagine to load a million of records) and slow down the overall process and performance of the computer (other programs and operating system need to use memory too!).
One of the common way to solve this problem is to load the relevant data when the system needed. If you study how JList
or JTable
works, JList
or JTable
will only render the data that displayed on screen. The data requested by JList
or JTable
during runtime is retrieved from the list’s or table’s model. To utilize the model feature, you can write your own class that extends AbstractListModel
or AbstractTableModel
so that you can provide the needed data when necessary. The following example shows the usage of AbstractTableModel
.
private class SimpleModel extends AbstractTableModel { public int getRowCount() { /* return total rows of your data */ } public int getColumnCount() { /* return total columns of your data, normally fixed value */ } public Object getValueAt(int rowIndex, int columnIndex) { /* retrieve the relevant data from file or database */ } } ... /* To use the model */ JTable table = new JTable(); table.setModel(new SimpleModel());
Although the “load when necessary” solution does solve the problem but it is still inefficient. Imagine if someone keep scrolling the big list of JTable or JList or keep moving mouse over or clicking on the records in the JTable
or JList
. Each of these actions will trigger the JTable
or JList
to re-render the screen and therefore make a data request through the data model. At the end, it will slow down the overall performance because of file or database (worse if over a network) reading activities.
So now we see another problem. We cannot load all the data into memory because computer has limited memory and we cannot keep reading data because it is inefficient to do so. What we can do now?
The best solution to the above problem is we load some of the data into memory and load the data from disk into memory again when it is not in the memory. We also need to decide when to dispose some of the data when we have loaded the maximum data into memory. The following parameters are to be decided by programmer in order to implement the solution:
- Maximum memory used to cached the data loaded from disk – normally I will allow the program to use about 10Mb of the memory
- When and what data to be disposed when the memory cached reached the maximum – normally I will dispose the least recent used data
Below is the sample solution for a handling a large JTable
data:
private class BetterModel extends AbstractTableModel { private final static int COLUMN_COUNT = 4; private final static int MEMORY_SIZE = 10 * 1024 * 1024; private final static int RECORD_SIZE = COLUMN_COUNT * 200; private int rowCount; private RecordCache cache; public BetterModel() { cache = new RecordCache(MEMORY_SIZE/RECORD_SIZE); rowCount = /* load row count from file or database */ } public int getRowCount() { return rowCount; } public int getColumnCount() { return COLUMN_COUNT; } public Object getValueAt(int rowIndex, int columnIndex) { Integer row; Record record; row = new Integer(rowIndex); record = cache.get(row); if (record == null ) { // Load from file or database if record is not found in memory record = getRecord(rowIndex); // Save in to cache cache.put(row, record); } // Return the data at rowIndex and columnIndex return record.columnData[columnIndex]; } public Record getRecord(int rowIndex) { /* return record loaded from disk or database */ } private class Record { public String columnData[]; } private class RecordCache extends LinkedHashMap<Integer,Record> { private int cacheSize; public RecordCache (int size) { cacheSize = size; } protected boolean removeEldestEntry(Entry<Integer, Record> eldest) { if (size() > ldcSize) { // Release some memory eldest.getValue().columnData = null; return true; } return false; } } }
You can also further enhance the above solution by paginate the records. For example 50 records in one page. In the above example, it will take about 40k of memory size per page. This is because reading 40k at once from disk or network is much more efficient than reading 800 bytes from disk or network for 50 times. After you have paginate your records, make sure you map the row index to the relevant page index (e.g. row index at 55 is at page index 1).
David says
Useful information! Tanks for sharing this solution.