allgemein
objekt-orientierter entwurf
programmierung
qualitätssicherung / testen

bugTeaser  #003: Der tückische Zuhörer  
Schwierigkeitsgrad  mittel 
Lösung veröffentlicht am   07.03.2005  

Beobachtbare Symptome

Es kommt vor, dass die Ausführung von clear() auf manchen Elements-Instanzen zu einer NullPointerException führt, deren Ursache zunächst unklar ist. Als Folge dieser Exception, die aufgrund eines systemweiten Exception-Handlers lediglich protokolliert wird ohne die Anwendung zu beenden, blockieren alle weiteren Zugriffe auf die betroffene Elements-Instanz. Zudem werden bestimmte Anzeigen in der Benutzerschnittstelle nicht korrekt aktualisiert und zeigen den ursprünglichen Inhalt der betroffenen Elements-Instanz.

Erklärung

Aufgrund der geschilderten Symptome stellen sich mehrere Fragen: An welcher Stelle kommt es zu einer NullPointerException? Warum funktioniert der Zugriff auf die Elements-Instanz nicht mehr, wenn clear() eine Exception wirft? Warum werden Teile der Benutzerschnittstelle nicht korrekt aktualisiert?

Schaut man sich den Code an, so wird rasch klar, dass die Exception durch einen fehlerhaft implementierten Listener verursacht wird. Da sich die Elements-Klasse nicht gegen Exceptions im Listener-Code schützt, bricht die Verarbeitung ab. Ist der fehlerhafte Listener nicht der letzte Eintrag in der Listener-Liste, so werden alle nachfolgenden Listener nicht mehr angestossen. Das erklärt, warum Teile der Benutzerschnittstelle u.U. nicht korrekt aktualisiert werden.

Bleibt die Frage, warum der weitere Zugriff auf eine Elements-Instanz blockiert nachdem clear() mit einer Exception beendet worden ist. Das liegt wiederum daran, dass Elements sich nicht gegen mögliche Exceptions im Listener-Code schützt. Der Aufruf von lock() schützt die Elements-Instanz vor konkurrierenden Zugriffen, unlock() hebt die entsprechende Sperre wieder auf. Kommt es während Listener-Ausführung zu einer Exception wird die clear() Methode beendet, ohne dass der nötige unlock() Aufruf erfolgt. Jeder weitere Zugriff auf diese Elements-Instanz wird fortan nicht mehr funktionieren. Daraus ergibt sich die erste wichtige Anpassung am ursprünglichen Code: Die Listener-Benachrichtigung wird in einen try/finally-Block verpackt, wobei das unlock() im finally-Block zu liegen kommt. Damit wird sicher gestellt, dass in die Elements-Instanz in jedem Fall wieder frei gegeben wird, egal wie die Listener-Benachrichtigung und die jeweiligen Listener umgesetzt wurden. Die clear() Methode sieht dann so aus:

public void clear() {
  lock();
  try {    
    notifyBeforeClear();
    removeElements();
    notifyAfterClear();
  finally {
    unlock();
  }
}

Nun müssen wir noch dafür sorgen, dass garantiert alle Listener selbst dann benachrichtigt werden, wenn einer der Listener eine Exception wirft. Hierzu muss der Aufruf der jeweligen Listener-Methode in einen try/catch-Block eingekapselt werden. Der catch-Block wird dabei in der einfachsten Variante einfach leer implementiert, wodurch die Exception einfach geschluckt wird; besser wäre hier sicher das Loggen der Exception. Das nachträgliche Werfen einer Exception, um zu signalisieren, dass einer der Listener Probleme verursacht hat, lohnt sich dagegn meist nicht. Sollte man sich aber dennoch dazu entschliessen, ist durch den in clear() eingefügten try/finally-Block immerhin gewährleistet, dass auch in diesem Fall Elements-Instanzen weiterhin korrekt verwendet werden können.

private void notify(IAction action) {
  IElementListener[] listeners = getListeners();
  for (int i = 0; i < listeners.length; i++) {
    try {
      action.execute(listeners[i]);
    } catch (Exception e) {
      log(e);
    }
  }    
}

Es leuchtet ein, dass jede Implementierung eines Observables grundsätzlich anfällig auf Fehler im Callback-Code des Observers/Listeners ist. Umso erstaunlicher ist es daher, dass diesem Punkt nur selten Beachtung geschenkt wird. Selbst die Observable-Implentierung von Java schützt sich nicht gegen Fehler in den Observer-Implementierungen.

Über diesen bugTeaser diskutieren.

Druckbare Version