Eine Suchmethode im Hintergrund laufen lassen (Threads?)

Status
Nicht offen für weitere Antworten.

jacquipre

Mitglied
Hallo....

Ich habe ein Programm geschrieben, dass etwas durchsucht (ArrayLists). Als Ergebnis erhält man eine ArrayList. Diese wird dann im Fenster (in einer Tabelle) dargestellt, allerdings erst, wenn alle Suchergebnisse gefunden wurden.
Das will ich ändern: die Suche soll im Hintergrund laufen, und die Tabelle soll schon alle bisher gefundenen Ergebnisse darstellen. Nun frage ich mich (und euch), wie man das am besten macht. Ich habe schon an Threads gedacht, bin
aber auf diesem Gebiet nicht so bewandert und weiß nicht, wo ich da ansetzen soll.

Vielleicht mal etwas zur Programmstruktur:
Es gibt eine Gui Klasse.
Diese ruft (nach Klick auf den Suchen-Button) die Methode search einer anderen Klasse auf
und übergibt Suchkriterien (Arrays). Diese andere Klasse sucht nun die Ergebisse und speichert
diese nach und nach in eine ArrayList. Am Ende der Suche wird diese zurückgegeben und von
der Gui Klasse an die Tabelle geschickt.

Hier mal Auszüge aus dem Code:

In der GUI Klasse:
Code:
private void performSearch() 
  {
	  // ......

	  // search - Hier passiert die eigentlich Suche
	  resultItems = searchService.searchDoitItem(currentFilter, selectedSortItems);
	  
	 // ...Tabelle wird vorbereitet....
	 // ....

         // Ergebnisse werden dem TableModel übergeben
	  resultTableModel.setResultItems(resultItems);
	  	  
	  //....
  }

In der SuchService Klasse:
Code:
public ArrayList<ResultItem> searchDoitItem(ArrayList<Filter> searchFilters, ArrayList<SortItem> sortItems) 
	{
		// ... Initialisierungen, etc.....

		Iterator<String> it = p.iterator();
		while(it.hasNext())
		{
			String id = it.next();
			String type = getType(id);
			
			// if no filters are set, show all
			if (searchFilters.size() == 0)
			{
                              
                               // Hier wird ein Suchergebnis hinzugefügt !!!
				results.add(getResultItemById(id, sortItems, type));
			}
			else 
			{
                               // Hier wird ein Suchergebnis hinzugefügt !!!
				if (checkNodeByAllFilters(id, searchFilters, type))
					results.add(getResultItemById(id, sortItems, type));
			}
		}
		return results;
	}

Die Gui Klasse soll also merken, wenn ein weiteres Suchergebnis gefunden wurde
und ihre Tabelle aktualisieren.

Vielleicht fällt euch was ein.

Danke,
Jacqui.
 
Threads ist eine gute Idee. Den Algorithmus der Suche steckst du in ein Runnable-Objekt. Der Algorithmus ruft jedesmal wenn er was gefunden hat die Methode "EventQueue.invokeLater" auf, und übergibt der Methode ein neues Runnable. Dieses Runnable fügt dann schliesslich dem TableModel der JTable das neuer Ergebnis hinzu (Swing ist nicht threadsicher, desshalb muss man "invokeLater" verwenden).

Siehe auch den obersten Eintrag
 
Hm...okay.

Das heißt jetzt also was genau?

1. Meine Suchklasse implementiert jetzt Runnable?
2. Und meine jetztige searchDoitItem(...) Methode gibt es nicht mehr?
3. Der Algorithmus wird jetzt in der zu implementierenden run methode gesteckt?
4. Wird ein Ergebnis gefunden, wird InvokeLater aufgerufen und ein neues Runnable übergeben?

An dieser Stelle weiß ich nicht weiter.
In der Suchklasse gibt es jetzt diese run Methode, die ständig neue Runnables erzeugt. Aber wie
sehen die genau aus? Und wie werden diese dem TableModel übergeben? Das befindet sich doch
in der GUI Klasse.
 
jacquipre hat gesagt.:
1. Meine Suchklasse implementiert jetzt Runnable?
Ja. Wobei: der Suchalgorithmus sollte auch direkt eine eigene Klasse sein. Man könnte das zwar in das "searchDoitItem" reinbasteln, übersichtlicher würde es damit leider nicht...
jacquipre hat gesagt.:
2. Und meine jetztige searchDoitItem(...) Methode gibt es nicht mehr?
Die kann bleiben. Nur kreiert sie jetzt das Runnable mit der Suche, und startet den Thread.
jacquipre hat gesagt.:
3. Der Algorithmus wird jetzt in der zu implementierenden run methode gesteckt?
Jop
jacquipre hat gesagt.:
4. Wird ein Ergebnis gefunden, wird InvokeLater aufgerufen und ein neues Runnable übergeben?
Genau

jacquipre hat gesagt.:
An dieser Stelle weiß ich nicht weiter.
In der Suchklasse gibt es jetzt diese run Methode, die ständig neue Runnables erzeugt. Aber wie
sehen die genau aus?
Code:
public class Search implements Runnable{
  public void run(){
     while( ... ){
        // bla bla bla
        X result = ... // ein Element gefunden
        PutCommand cmd = new PutCommand( result );
        EventQueue.invokeLater( cmd );
     }
  }

  private class PutCommand implements Runnable{
      private X result;
      public PutCommand( X result ){
         this.result = result;
      }
      public void run(){
          // Das Resultate "result" der GUI übergeben.
          // Z.B. könnte "Search" eine Instanzvariable für das TableModel haben.
      }
  }
}

jacquipre hat gesagt.:
Und wie werden diese dem TableModel übergeben? Das befindet sich doch
in der GUI Klasse.
Ok: wenn es dir unangenehm ist, dass das Model im Programm herumschwirrt, kannst du dir eine Klasse basteln, welche eine Methode besitzt, die einen Eintrag für das Model nimmt, und diesen Eintrag dann dem Model hinzufügt (einfach direkt die passenden Methoden des Models aufruft). Dann kannst du diese "Übersetzerklasse" herumschicken (und musst keine Angst haben, dass jemand was böses mit dem Model machen kann).

Derzeit kennt die GUI den "Standort" des Suchalgorithmuses. Andernfalls könnte die GUI ja gar nichts suchen lassen... Dann gib beim Aufruf der Suche noch das Model (oder die "Übersetzerklasse") mit, und der Algorithmus kennt die GUI. Mach dir keine Sorgen wegen MVC oder sowas, sobald man die EventQueue verwendet, kann man so einen Algorithmus so oder so für nichts anderes als das GUI gebrauchen :wink:
 
Hallo...

vom Prinzip her funktioniert das jetzt so, allerdings bekomme ich nun immer eine Exception in meiner TableSorter-Klasse. Das liegt wahrscheinlich an dieser Thread-Sache.

Es ist jetzt so:

In meiner GUI-Klasse übergebe ich nun auch der Suchmethode den TableAdaptor resultTableModel:

Code:
private void performSearchThread() 
  {
	  // ... bla bla bla
	 
          // hier wird die Suche ausgeführt
          // ich übergebe resultTableModel (Typ TableAdaptor)
	  searchService.searchDoitItemThread(currentFilter, selectedSortItems, resultTableModel);
	  
	  // ...bla bla bla
  }

In meiner SearchServiceKlasse wird nun ein Thread gestartet und ein neues Runnable-Objekt erzeugt:

Code:
public void searchDoitItemThread(ArrayList<Filter> searchFilters, ArrayList<SortItem> sortItems, TableAdaptor table) 
{
		
	Thread t = new Thread(new Search(searchFilters, sortItems, table, serviceBroker, rootName, getDataService()));
	t.start();
}

Die Search-Klasse implementiert das Runnable-Interface und hat den Such-Algorithmus:

Code:
public class Search implements Runnable 
{
	// ... ein paar Instanzvariablen...

	protected TableAdaptor resultTableModel;
	
        // Konstruktor
	public Search(ArrayList<Filter> searchF, ArrayList<SortItem> sortI, TableAdaptor ta, ServiceBroker sb, String root, DataService ds)
	{
		// ...
	}
	
	public void run() 
	{
        	// ... Suchalgorithmus.... bla bla bla
		
		// ein Ergebnis gefunden 		
		ResultItem result = getResultItemById(id, sortItems, type);

                // zu einer Liste hinzufügen
		results.add(result);
               
                // neues Runnable Objekt
		PutCommand cmd = new PutCommand(results);
		EventQueue.invokeLater(cmd);
	}
       
        // .....

        // noch eine Klasse für den Table Adaptor
	private class PutCommand implements Runnable
	{
		private ArrayList<ResultItem> results;
		
		public PutCommand(ArrayList<ResultItem> results)
		{
			this.results = results;
		}

		
		public void run() 
		{
			resultTableModel.setResultItems(results);
			
		}
		
	}
}

Soweit so gut.

Nun bekomme ich aber folgende Fehlermeldung:
Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: 2
at de.hsbremen.doit.search.TableSorter.modelIndex(TableSorter.java:283)
at de.hsbremen.doit.search.TableSorter.getValueAt(TableSorter.java:331)
(manchmal auch ein anderer Index)

Das sind folgende Methoden in der TableSorter-Klasse:

Code:
public void setValueAt(Object aValue, int row, int column) {
        tableModel.setValueAt(aValue, modelIndex(row), column);
    }

 public int modelIndex(int viewIndex) 
    {
        return getViewToModel()[viewIndex].modelIndex;
    }

In der GUI-Klasse initialisiere ich folgendes:

Code:
TableAdaptor resultTableModel = new TableAdaptor(null, null);
TableSorter sorter = new TableSorter(resultTableModel);
resultTable = new JTable(sorter);
sorter.setTableHeader(resultTable.getTableHeader());

Er will auf einen Index zu greifen, den es nicht gibt? Oder wie?
Die TableSorter-Klasse habe ich aus den Java Beispielen der Doku.
 
Also wenn die Klasse aus der Doku ist, wird sie vermutlich nicht fehlerhaft sein. Mein erster Kandidat für den Fehler wäre die Methode "setResultItems". Bastelt die Methode irgendwie komisch an dem Model herum?

Achja: die ArrayList mit den Resultaten in dem "PutCommand" muss unabhängig von der anderen Liste sein. Andernfalls wird die Liste verändert, während jemand damit arbeitet... Ich würde sowieso nur das neue Element in "PutCommand" hinzufügen. Ansonsten wird die Tabelle immer wieder sortiert, nicht sehr effizient :wink:
 
Ok...ich hab's nochmal versucht und nun gehts, ohne Exception. Der Fehler lag dann wohl daran, dass ich wirklich immer ne komplette Ergebnisliste übergeben habe.

In PutCommand sieht das nun so aus:
Code:
private ResultItem result;
		
public PutCommand(ResultItem res)
{
	this.result = res;
}

public void run() 
{
	resultTableModel.setSingleResultItem(this.result);
			
}

Und im TableAdaptor so:
Code:
private ArrayList<ResultItem> results = new ArrayList<ResultItem>();
	
public void clearResultList()
{
	this.results.clear();
}

	
public void setSingleResultItem(ResultItem result)
{
	this.results.add(result);
	fireTableDataChanged();
}

Vorher hatte ich dort die Methode:
Code:
public void setResultItems(ArrayList<ResultItem> ri) 
{
	this.results = ri;
	fireTableDataChanged();
}

Das hatte wohl immer die Liste neu gesetzt. Nicht so effizient 🙂

Wenn ich jetzt noch einen Abbrechen-Button einbauen will, d.h. die Suche soll unterbrochen werden, dann
stoppe ich den Thread einfach und in meiner Result-Liste stehen alle bis dahin gefundenen Ergebnisse?
 
Ja.
Aber stoppe den Thrad nicht mit "stop()", benutz "interrupt()", und frag in der Suche immer wider nach, ob "isInterrupted()" wurde.
Die Methode "stop" ist ein bisschen brutal und kann Fehler nach sich ziehen.
 
Hm...nun habe ich doch nochmal eine Frage.

Das mit dem Interrupt kriege ich nicht so ganz hin.

Es ist so, dass ich in der Gui-Klasse zwei Buttons habe, der search button startet die Suche, der Cancel Button
soll die Suche abbrechen. Der searchButton ruft bei Klick die performSearch-Methode auf, der cancel Button die cancelSearch-Methode.
Nur leider will das bei mir nicht ganz funktionieren.

Hier mal der Code:

GUI Klasse
------------
Code:
private void performSearchThread() 
{
	  // ...bla bla bla
	  
	  // search
	  searchService.searchDoitItemThread(currentFilter, selectedSortItems, resultTableModel);
}

private void cancelSearch()
{
	 searchService.cancelSearch(); 
}

SearchService
----------------
Code:
public void searchDoitItemThread(ArrayList<Filter> searchFilters, ArrayList<SortItem> sortItems, TableAdaptor table)  
{
	// clear list
	table.clearResultList();
	Search searchThread = new Search(searchFilters, sortItems, table, serviceBroker, rootName, getDataService());
	Thread t = new Thread(searchThread);
	t.start();
		
}

public void cancelSearch()
{
		
}

Search
--------
Code:
public void run() 
{
	// ...bla bla bla 

	while(!Thread.currentThread().isInterrupted())
	{
		while(...) // noch zu durchsuchende Dateien vorhanden
		{
                         
			ResultItem result = getResultItemById(id, sortItems, type);
			PutCommand cmd = new PutCommand(result);
			EventQueue.invokeLater(cmd);
		}

		// search ready, interrupt thread
		Thread.currentThread().interrupt();
	}
}

Wie kann ich nun hier meine Buttons unterbringen?
Ich habe gedacht, in der SearchService Klasse, den Thread zu interrupten, abe wie?
Kann man t als Instanzvariable nehmen? Dann könnte ich doch in der cancelSearch-Methode
t.interrupt() aufrufen? Nur leider funktioniert das nicht so....
 
Ja, du musst "t" als Instanzvariable nehmen. Mehr weiss ich nicht, das sollte eigentlich funktionieren :-/
 
Status
Nicht offen für weitere Antworten.

Neue Themen


Zurück
Oben