Fehlermeldungen

Fehlermeldungen beim Compilieren

  1. Missing return statement (in Eclipse: „This method must return a result of type...“)

    Diese Fehlermeldung tritt auf, wenn eine Methode deklariert, einen bestimmten Rückgabetyp zu haben (also nicht als void deklariert ist), der Compiler aber nicht sicherstellen kann, dass tatsächlich ein entsprechender Wert zurückgegeben wird.

    Im einfachsten Fall liegt das daran, dass schlicht eine entsprechende Return-Anweisung fehlt.

    Es kann aber auch sein, dass die Return-Anweisung zwar vorhanden ist, die Methode aber auch regulär beendet werden kann, ohne dass die Anweisung ausgeführt wird. Ein typisches Beispiel ist das folgende, in dem im Fall einer Division durch 0 die auftretende Exception abgefangen wird – die Methode also nicht abrupt beendet wird, sondern regulär – aber im Catch-Block eben kein Wert zurückgegeben wird:

        int dividiere(int dividend, int divisor) {
            try {
                return dividend / divisor;
            } catch (ArithmeticException e) {
                e.printStackTrace();
            }
        }
  2. Non-static method /field ... cannot be referenced from a static context (in Eclipse: „Cannot make a static reference to the non-static method / field ...“)

    Siehe dazu im Kapitel „Variable / Attribut / Klassenattribut / lokale Variable“ den Abschnitt „Warum darf eine Klassenmethode nur auf Klassenattribute zugreifen?“ (das dort bzgl. Attributen Gesagte gilt analog für Exemplarmethoden) sowie das komplette Kapitel „Verwendung von static“.

  3. „The method m() of type X should be tagged with @Override since it actually overrides a superinterface / superclass method“ (Warnung in Eclipse)

    Seit Java 1.6 gibt es in Java die Möglichkeit, sog Annotations zu verwenden. Von diesen sind einige bereits „in Java eingebaut“, eine davon ist @Override. Wenn Sie diese Annotation vor eine Methodendeklaration setzen, dokumentieren Sie damit explizit, dass Ihre Methode eine Methode eines Supertyps überschreibt. Sollte das dann aber gar nicht der Fall sein, etwa, weil Sie versehentlich eine andere Parameteranzahl oder andere Parametertypen gewählt haben als bei der Methode, die Sie überschreiben wollten, wird der Compiler Ihnen eine Fehlermeldung geben.

    Um nun das volle Potential von @Override auszunutzen, ist es sinnvoll, diese Annotation konsequent zu verwenden und sich ein fehlendes @Override bei einer Methode, welche eine Superklassenmethode überschreibt, zumindest als Warnung melden zu lassen. Bei dem Eclipse-Workspace, den Sie auf http://feu.mpaap.de/eclipse/index.html herunterladen können, ist diese Einstellung bereits vorgenommen und wenn Sie in einem Subtyp einer Methode eines Supertyps überschreiben, ohne diese Absicht durch @Override explizit zu dokumentieren, erhalten Sie die oben genannte Warnung.

  4. Beim Versuch, aus einer lokalen Klasse auf eine lokale Variable des Blocks zuzugreifen, in dem die lokale Klasse definiert ist, erhalten Sie eine Fehlermeldung "Local variable X defined in an enclosing scope must be final or effectively final" oder ähnlich.

    Diese Meldung erhalten Sie, weil der Zugriff aus einer lokalen Klasse auf Variablen des umgebenden Blocks nur erlaubt ist, wenn diese als final deklariert sind (bis incl. Java 7) oder zumindest effektiv final sind (seit Java 8), ihnen also auf jeden Fall nach ihrer Initialisierung nichts mehr zugewiesen wird.

    Warum diese Einschränkung existiert, ist nicht ganz einfach zu verstehen und geht über den Kursstoff hinaus. Da die Frage aber immer wieder aufkommt, hier der Versuch einer Erklärung:


    Die Java-VM ist eine sog. Stackmachine. Das bedeutet vereinfacht, dass jeder Methodenaufruf seinen eigenen Block auf einem Stapel bekommt, in welchem er seine lokalen Variablen hält und mit dem er arbeiten kann. Kehrt eine Methode zurück, kann dieser sog. Stack-Frame entsorgt werden. Das heißt, dass alle methodenlokalen Variablen nur bis zum Ende ihrer Methode existieren.

    Normalerweise ist das kein Problem, weil man von außen ohnehin nicht auf die lokalen Variablen einer Methode zugreifen kann. Wenn nun aber ein Objekt einer lokalen inneren Klasse (ab hier LIC abgekürzt) erzeugt wird, wird dieses im Normalfall länger leben, als die Methode, in der es deklariert wurde.

    Es könnte also passieren, dass ich aus einer Methode des LIC-Objekts auf eine lokale Variable der "umgebenden Methode" zugreife, die zu diesem Zeitpunkt schon gar nicht mehr existiert. Angenommen, man hat in der Methode eine lokale Variable x, die ein Objekt y referenziert. Wenn nun eine Methode des LIC-Objekts versucht auf x zuzugreifen, will sie ja letztlich nur das dahinterstehende Objekt y erreichen. Das kann sie aber, wenn der Zugriff über x erfolgt, nur, solange x noch existiert. Das bedeutet aber, dass das Lesen des Wertes von x nicht erst zu dem Zeitpunkt erfolgen darf, an dem die Methode der LIC ausgeführt wird, denn dann könnte x bereits nicht mehr existieren. Dieses Problem kann man lösen (und hat man gelöst), indem man schon bei der Erzeugung des Objekts der LIC dort eine lokale Kopie von x anlegt. Damit kann der *Wert* von x (also die Referenz auf y) das Ende der Methode der umgebenden Klasse überleben.

    Dies bringt nun allerdings ein weiteres Problem mit sich: Im weiteren Verlauf der "äußeren" Methode könnte sich nach der Erzeugung eines Objekts der LIC der Wert von x ändern, so dass die zuvor angelegte Kopie (im Objekt der LIC) einen anderen Wert hätte, als das aktuelle (oder nach dem Beenden der Methode als das letzte gültige) x. Um die draus resultierenden Zweideutigkeiten zu vermeiden, hat man festgelegt, dass von innerhalb einer LIC aus nur auf als final deklarierte Variablen der umgebenden Methode zugegriffen werden kann. Damit steht der Wert von x für dessen gesamte Lebensdauer fest und ist immer identisch mit dem der Kopie, die bei der Instantiierung der LIC angelegt wurde. Auf diese Weise kann niemand überhaupt erst auf die Idee kommen, dem LIC-Objekt den Wert der Variablen "unter den Füßen zu ändern.


    Mit Java 8 hat sich an der Einschränkung zwar faktisch nichts geändert, allerdings reicht es jetzt, dass die Variablen "effektiv final" sind. Als final deklariert werden mssen sich nicht mehr, stattdessen gewährleistet der Compiler, dass nach ihrer Initialisierung keine Zuweisungen mehr erfolgen.

Fehlermeldungen beim Ausführen

  1. NullPointerException (NPE)

    Eine NPE tritt genau dann auf, wenn man versucht, das „Objekt hinter einer Referenz“ anzusprechen, also zu „dereferenzieren“, man aber in Wirklichkeit die leere Referenz „null“ vor sich hat, es also gar kein referenziertes Objekt gibt.

    Dieses Dereferenzieren findet statt, wenn auf eine Methode oder ein Attribut eines Objekts zugegriffen werden soll. Man kann den Punkt als Dereferenzierungsoperator lesen, analog zu den entsprechenden Operatoren z.B. in C oder Pascal. Außerdem findet eine Dereferenzierung statt, wenn man auf ein Element eines Arrays zugreift: Auch ein Array ist ja ein Objekt und eine Referenz, von der man glaubt, sie verweise auf einen Array, kann natürlich auch den Wert null haben.

    Wenn man eine NullPointerException bekommt, ist der erste Schritt also, herauszufinden, welche Referenz, die man zu dereferenzieren versucht, eigentlich den Wert null hat. Dazu bietet es sich an, die betreffende Zeile so zu zerlegen, dass man pro Zeile nur noch eine Dereferenzierung hat. Lautet die Zeile, in der die NPE auftritt beispielsweise

        String name = allPersons.getPersonArray()[2].getName();

    so gibt es drei Dereferenzierungen und man könnte die Zeile wie folgt zerlegen:

        Person[] persons = allPersons.getPersonArray();
        Person person = persons[2];
        String name = person.getName();

    Führt man nun das Programm erneut aus, kann man diesmal an der Zeilennummer der Exception genau erkennen, welche Referenz den Wert null hatte. Dann muss man „nur“ noch herausfinden, warum sie diesen Wert hat.

    Ein paar typische Ursachen:

    • Man hat ein Attribut eines Referenztyps zwar deklariert, aber nicht initialisiert, ihr also kein Objekt zugewiesen. Damit hat es per automatischer Initialisierung den Wert null.

    • Ein Attribut eines Referenztyps sollte an einer anderen als der Deklarationsstelle initialisiert werden, dabei wurde aber versehentlich eine erneute Deklaration vorgenommen, also eine lokale Variable deklariert. Die Initialisierung betrifft dann natürlich diese lokale Variable; das Attribut behält den Wert null.

    • Ein Referenztyp-Array wurde zwar erzeugt, die einzelnen Objekte aber nicht. Damit haben die Elemente des Arrays per automatischer Initialisierung den Wert null. Näheres siehe hier.

    • Man will auf ein Attribut zugreifen, hat aber versehentlich eine lokale Variable oder einen formalen Parameter gleichen Namens deklariert. Diese Variable verdeckt das Attribut, so dass man auf die lokale Variable bzw. den formalen Parameter zugreift, die dann natürlich einen anderen Wert hat, als man erwartete, z.B. null.

  2. ArrayIndexOutOfBoundsException (AIOOBE)

    Siehe hierzu im Kapitel „Arrays“ den Abschnitt „Ich bekomme eine ArrayIndexOutOfBoundsException. Was hat es damit auf sich?

  3. ClassNotFoundException (CNFE)

    Eine CNFE bedeutet, dass die Laufzeitumgebung eine Klasse nicht laden konnte, welche sie zur Ausführung des Programms benötigte.

    Die häufigste Ursache dieser Exception ist ein falsch gesetzter Classpath. Der Classpath sagt der Laufzeitumgebung, wo sie nach Klassen zu suchen hat. Ist der Classpath falsch gesetzt, werden z.B. Klassen im aktuellen Verzeichnis nicht gefunden, wo normalerweise automatisch gesucht wird.

    Die systemweite Umgebungsvariable CLASSPATH sollte aber aus verschiedenen Gründen ohnehin nicht gesetzt werden. Wenn zu bestimmten Zwecken eine explizite Angabe des Classpath nötig ist, sollte dazu beim Start des Programms der Aufrufparameter -classpath verwendet werden.

    Eine weitere Ursache besteht darin, dass bei der Exemplarerzeugung per Reflection (z.B. im Adressbuchbeispiel aus der Newsgroup) der Methode forName() der Klasse Class ein String als Klassenname übergeben wird, dem die Laufzeitumgebung keine Klasse zuordnen kann. Wenn es sich nicht um einen einfachen Tippfehler handelt, wurde hier meist die Package-Angabe vergessen, die zum vollständigen Klassennamen aber dazugehört.

    Im Kontext des Kurses 1618 begegnet einem die CNFE meist im Zusammenhang mit RMI (Kurseinheit 7). Dazu einige Erläuterungen:

    Damit die RMI-Registry (welche selbst ein Java-Programm ist) die benötigten Server-Klassen findet, gibt es mehrere Möglichkeiten:

    • Man benötigt die RMI-Registry nur für genau ein Serverprogramm

      Dann kann man sie aus dem Verzeichnis heraus starten, aus dem man auch das Serverprogramm startet (unter der Annahme, dass über diesen Pfad auch das Remote-Interface erreichbar ist). Einfacher ist es dann aber, die Registry direkt aus dem Serverprogramm zu starten. Dazu genügt folgende Zeile:

          java.rmi.registry.LocateRegistry.createRegistry(1099);

      Nachteil: Diese RMI-Registry wird zusammen mit dem Serverprogramm beendet, deswegen ist diese Lösung ungeeignet, wenn mehrere Programme die Registry nutzen sollen.

    • Die Registry wird für mehrere Serverprogramme verwendet.

      In diesem Fall ist das Serverprogramm dafür zuständig, der Registry mitzuteilen, wo die von ihr benötigten Klassen liegen. Dazu übergibt man beim Start des Serverprogramms diese Information mit Hilfe des VM-Parameters -Djava.rmi.server.codebase. Das könnte z.B. so aussehen:

          java -Djava.rmi.server.codebase=file:/D:/1618/bin/pufferServer.RingPufferServer

      Wenn es sich bei der übergebenen Codebase um ein Verzeichnis handelt, muss an dessen Ende ein Slash stehen.

    In der Praxis wird man oft wesentlich komplexere Szenarien haben, bei denen es erwünscht ist, dass die zum Client und/oder zum Server gehörigen Klassen dynamisch von der jeweils anderen oder von dritter Seite geladen werden können. RMI unterstützt derlei durch die Möglichkeit, Klassen dynamisch von Servern laden zu können. Einen guten überblick über das dynamische Laden von Klassen im Kontext von RMI bietet diese Seite. Das dort Beschriebene geht allerdings weit über die Anforderungen des Kurses hinaus.

  4. IllegalMonitorStateException (IMSE)

    Eine IMSE tritt genau dann auf, wenn ein Thread wait(), notify() oder notifyAll() auf einem Objekt aufruft, dessen Monitor dieser Thread nicht besitzt. Der Aufruf der besagten Methoden richtet sich ja immer an ein ganz bestimmtes Objekt, auf dessen Warteschlange er sich bezieht.

    Der einfachste solche Fall ist, dass man eine der drei Methoden in einem Bereich aufruft, der überhaupt nicht synchronisiert ist.

    Dann gibt es den Fall, dass man eine der drei Methoden zwar in einem synchronisierten Bereich aufruft, aber auf einem anderen Objekt als dem (bzw. einem von denen) auf dem (bzw. denen) der Bereich synchronisiert ist.

    Von diesem zweiten Fall gibt es noch eine besonders subtile Variante, nämlich dass man einer der Methoden zwar auf der Variablen aufruft, über die auch die Synchronisation erfolgte, diese Variable aber inzwischen ein anderes Objekt referenziert. Um dies zu verhindern, werden zur Synchronisation, so weit sie nicht auf „this“ erfolgt, meist als final deklarierte Variablen (also Konstanten) verwendet, da sich hier das referenzierte Objekt nicht ändern kann. Vorsicht in diesem Zusammenhang bei Arrays: Die Deklaration einer Variablen eines Array-Typs als final verhindert ja nicht, dass den Elementen des Arrays neue Werte zugewiesen werden.