dynamisches Suchfeld (ala Thunderbird) / Filtern einer JTable mit sehr vielen Daten

shutdown

Erfahrenes Mitglied
Hi!

Ich hätte eine Frage zu einem meiner Codes.
Und zwar habe ich ein Suchfeld wie im Thunderbird geschrieben, das die Eingaben entgegen nimmt und sobald die Eingabe abgeschlossen ist, sofort die zugrunde liegende Tabelle aktualisiert.

Hier mal ein paar Code-Ausschnitte - die Frage kommt dann danach:

Das Suchfeld ist eine eigene Klasse - hier ein Ausschnitt aus dem Konstruktor:

Code:
this.getDocument().addDocumentListener(this);

		updateInvoker = new Thread() {
			public void run() {
				while(true) {
					/**
					 * Wenn die letzte Änderung länger als 200 msek zurückliegt, Update übermitteln.
					 */
					if(lastChange != 0) {
						if(System.currentTimeMillis() - lastChange > 200) {
							// aktualisieren
						}
					}	
					
					/**
					 * Wenn die letzte Änderung länger als 750 msek zurückliegt, diesen Thread schlafen legen. 
					 */
					if(System.currentTimeMillis() - lastChange > 750) {
						lastChange = 0;
						this.suspend();
					}
				}				 				
			}
		};		
		updateInvoker.start();
		updateInvoker.suspend();

Zur Erklärung: Das Suchfeld schickt das Signal zum aktualisieren erst, wenn seit der letzten Eingabe 200 ms zurückliegt. Wenn's zu lange dauert, dann legt sich der Thread wieder schlafen um das System zu entlasten.


Wenn sich am Suchfeld was ändert - z.B. was eingegeben wird:

Code:
public void insertUpdate(DocumentEvent arg0) {
		lastChange = System.currentTimeMillis();
		updateInvoker.resume();
	}

dann wird der Thread wieder aufgeweckt und das Spiel geht von vorne los.

Nun meine Frage/Problem:
Gibt es da eine elegantere Lösung?
Problem ist nämlich, dass ich diese 200 ms durch Ausprobieren und Herantasten an eine typische Schreibgeschwindigkeit herausgefunden habe.

Es gibt jetzt aber hin und wieder den Fall, dass genau eine Geschwindigkeit getroffen wird, bei der nicht die gesamte Eingabe tatsächlich auch übermittelt wird, weil der Thread schon wieder schläft, aber nicht mehr mit aufgeweckt wird.

Vielen Dank
shutdown
 
@spinner
das ist ein Listener - nur etwas anders (Grund sh. folgendes)

@thomas
ich will die Datensätze nicht löschen.
Folgendes:
- es sind ein paar tausend Einträge
- diese muss ich mir übers Netzwerk holen
- und dann auch noch sortieren

Es dauert also schon verdammt lange, diese Tabelle überhaupt erst aufzubauen - darum will ich auch nichts mehr löschen.
Aber wenn ich jetzt als Beispiel mal nach "Apfel" suche:
Ein normaler Listener geht jetzt her:
- und vergleicht mehrere tausend Zeilen mit "A"
- und vergleicht mehrere tausend Zeilen mit "p"
- und vergleicht mehrere tausend Zeilen mit "f"
- und vergleicht mehrere tausend Zeilen mit "e"
- und vergleicht mehrere tausend Zeilen mit "l"
und zeigt dann jeweils die anderen Datensätze nicht mehr an.
Jetzt probier das mal aus und tipp den Apfel im normalen Schreibtempo ein - ein Herzinfarkt für den Listener.

Meine Variante macht nun folgendes:
- "A" - warten - "p" - warten - "f" - warten - "e" - warten - "l" - warten
- und vergleicht mehrere tausend Zeilen mit "Apfel"
und zeigt dann jeweils die anderen Datensätze nicht mehr an.

Also wesentlich effizienter und ressourcenschonender.
Nur habe ich jetzt halt folgendes Problem:
Bei einem gewissen Schreibtempo übernimmt er zum Filtern nicht "Apfel" sonder z.B. "Apfe", obwohl im Suchfeld "Apfel" steht.
 
Hallo,

hier mal ein Beispiel für das schnelle Filtern einer JTable mit mehreren 100000 Einträgen:
Java:
/**
 * 
 */
package de.tutorials;

import java.awt.BorderLayout;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.table.DefaultTableModel;

/**
 * @author Tom
 */
public class FilteredJTableWithLargeDataSupportExample extends JFrame {

  FilterableJTable table;
  JTextField txtFilter;
  JLabel lblFilteredCount;


  public FilteredJTableWithLargeDataSupportExample() {
    super("FilteredJTableWithLargeDataSupportExample");
    setDefaultCloseOperation(EXIT_ON_CLOSE);

    this.txtFilter = new JTextField(20);

    txtFilter.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        if (txtFilter.getText().length() > 0) {
          table.getModel().filter(0, txtFilter.getText());
          table.updateUI();
        } else {
          if (null != table.getModel()) {
            table.getModel().setRowMapping(null);
            table.updateUI();
          }
        }
      }
    });

    add(txtFilter, BorderLayout.NORTH);

    this.table = new FilterableJTable(new FilteredTableModel());

    JScrollPane scrollPane = new JScrollPane(this.table);

    add(scrollPane, BorderLayout.CENTER);

    lblFilteredCount = new JLabel("");

    add(lblFilteredCount, BorderLayout.SOUTH);

    pack();
    setVisible(true);
  }


  /**
   * @param args
   */
  public static void main(String[] args) {
    new FilteredJTableWithLargeDataSupportExample();
  }

  class FilterableJTable extends JTable {
    public FilterableJTable(FilteredTableModel filteredTableModel) {
      super(filteredTableModel);
    }


    @Override
    public FilteredTableModel getModel() {
      return (FilteredTableModel) super.getModel();
    }
  }

  class FilteredTableModel extends DefaultTableModel {
    List<TreeSet<ColumnValue>> columnValueIndex;
    int[] rowMapping;


    public FilteredTableModel() {
      setDataVector(createRowData(), createColumnNames());
    }


    public void filter(int columnIndex, String filterText) {
      String startFilter = filterText.toLowerCase();
      String endFilter = startFilter+Character.MAX_VALUE;

      SortedSet<ColumnValue> filteredColumnValues = columnValueIndex.get(columnIndex).subSet(
        new ColumnValue(startFilter, -1), new ColumnValue(endFilter, -1));

      rowMapping = new int[filteredColumnValues.size()];

      System.out.println("rowMappingSize: " + rowMapping.length);

      int currentRowMappingIndex = 0;

      for (ColumnValue columnValue : filteredColumnValues) {
        rowMapping[currentRowMappingIndex++] = columnValue.getRowIndex();
      }

      lblFilteredCount.setText("Filtered: " + rowMapping.length + " out of " + super.getRowCount());
    }


    @Override
    public Object getValueAt(int row, int column) {
      if (null != rowMapping) {
        return super.getValueAt(rowMapping[row], column);
      }
      return super.getValueAt(row, column);
    }


    @Override
    public int getRowCount() {
      if (null != rowMapping) {
        return rowMapping.length;
      } else {
        return super.getRowCount();
      }
    }


    private Object[] createColumnNames() {
      return new Object[] { "A", "B" };
    }


    private Object[][] createRowData() {
      System.out.println("Generating row data...");

      Random randomizer = new Random();
      Object[][] rowData = new Object[131072][2];
      columnValueIndex = new ArrayList<TreeSet<ColumnValue>>();
      for (int i = 0; i < 2; i++) {
        columnValueIndex.add(new TreeSet<ColumnValue>());
      }

      for (int currentRow = 0; currentRow < rowData.length; currentRow++) {
        int randomValue = randomizer.nextInt(Integer.MAX_VALUE);
        String aColumnValue = randomValue + "A" + currentRow;
        rowData[currentRow][0] = aColumnValue;
        columnValueIndex.get(0).add(new ColumnValue(aColumnValue.toLowerCase(), currentRow));
        String bColumnValue = randomValue + "B" + currentRow;
        rowData[currentRow][1] = bColumnValue;
        columnValueIndex.get(1).add(new ColumnValue(bColumnValue.toLowerCase(), currentRow));
      }

      return rowData;
    }


    /**
     * @return the rowMapping
     */
    public int[] getRowMapping() {
      return rowMapping;
    }


    /**
     * @param rowMapping the rowMapping to set
     */
    public void setRowMapping(int[] rowMapping) {
      this.rowMapping = rowMapping;
    }

  }

  static class ColumnValue implements Comparable<ColumnValue> {
    String value;
    int rowIndex;


    /**
     * @param value
     * @param rowIndex
     */
    public ColumnValue(String value, int rowIndex) {
      super();
      this.value = value;
      this.rowIndex = rowIndex;
    }


    /**
     * @return the value
     */
    public String getValue() {
      return value;
    }


    /**
     * @param value the value to set
     */
    public void setValue(String value) {
      this.value = value;
    }


    /**
     * @return the rowIndex
     */
    public int getRowIndex() {
      return rowIndex;
    }


    /**
     * @param rowIndex the rowIndex to set
     */
    public void setRowIndex(int rowIndex) {
      this.rowIndex = rowIndex;
    }


    /*
     * (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + rowIndex;
      result = prime * result + ((value == null) ? 0 : value.hashCode());
      return result;
    }


    /*
     * (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
      if (this == obj) return true;
      if (obj == null) return false;
      if (getClass() != obj.getClass()) return false;
      final ColumnValue other = (ColumnValue) obj;
      if (rowIndex != other.rowIndex) return false;
      if (value == null) {
        if (other.value != null) return false;
      } else if (!value.equals(other.value)) return false;
      return true;
    }


    public int compareTo(ColumnValue o) {
      return this.value.compareTo(o.value);
    }
  }
}

Der Trick hierbei ist, dass das Filtern über eine Baumstruktur (unser TreeSet -> columnValueIndex) erledigt wird. Durch diese Strutktur ist ein effizientes Filtern ohne Probleme möglich, da hier immer nur im entsprechenden Teilbaum gesucht werden muss der noch das Filterkriterium erfüllt.

Gruß Tom
 

Neue Beiträge

Zurück