Suche Ursache für NPE - woher kommt sie? (Hilfe beim Debugging)

White_Fox

Top Contributor
Guten Abend allerseits

Ich bekomme hier eine höchst verwirrende NPE. Im Bild ist ein Debuggerscreenshot mitsamt dem Inhalt aller aktuell gültiger Variablen.

Die Klasse, in der der Fehler auftritt, ist die Klasse Component. Sie soll alle Eigenschaften, die ein Bauteil haben kann, halten. Es gibt zwei Sorten von Eigenschaften: temporäre und persistente. Die Methode, die im Screenshot zu sehen ist, soll eine HashMap<String, String> zurückliefern wobei 'Key' der Name und 'Value' der Wert der Eigenschaft ist, Art der Eigenschaften (also temporär oder persistent) werden mit einem Parameter PropertyScope scope ausgewählt. Es geht hier speziell um elektronische Bauteile.
Typische Beispiele (Name - Wert):
-Bauteilwert - 10Ω
-Dokumentpfad - C:\Pfad\zum\Datenblatt.pdf
-...

Alle Property-Objekte werden in einer private HashMap<PropertyScope, HashMap<String, ComponentProperty>> properties; gehalten. In der Methode, in der die NPE geworfen wird und die im Screenshot zu sehen ist, soll zunächst mit PropertyScope scope die gewünschten Eigenschaften ausgewählt werden - und hier tritt der Fehler auf (grün markierte Zeile).

Wie man unten in den Variablen sehen kann, ist properties nicht null. Die HashMap existiert und enthält zwei Einträge, je einen für PropertyScope.TEMPORARY und für PropertyScope.PERSISTENT. Einträge für temporäre Eigenschaften gibt es nicht (aber auch dort ist nix null, dort ist lediglich eine leere HashMap), der Eintrag für persistente Eigenschaften ist jedoch nicht leer, dort gibt es was.

Soweit ist alles, wie es sein soll. Jedoch:

Der Methodenparameter PropertyScope scope enthält als Wert PropertyScope.PERSISTENT. Folglich müßte ich in Zeile 243 eine HashMap<String, ComponentProperty> scopeFilteredProperties erhalten. Eine HashMap mit sechs Einträgen. Stattdessen liefert der Ausdruck properties.get(scope) in Zeile 243 null zurück, so daß es beim ersten Aufruf der for-each-Schleife zu einer NPE kommt.

Jetzt frag ich mich nur: Warum kommt da null zurück? Ich habe den Ausdruck ja im Debugger nochmal eingegeben - da wird null bestätigt. Aber in der Variablenansicht sieht man ja, daß da nicht null zurückkommen sollte. Und der Vergleich mit scope.equals(PropertyScope.PERSISTENT) liefert ja auch true zurück.

Noch etwas zum Umstand des Aufrufes: Das Component-Objekt ist vorher von meiner ObjectProcessorklasse deserialisiert worden. Ich habe daraufhin mal einen Test geschrieben mit einer HashMap in einer HashMap und Enumeration als Key. Dieser Test lief erfolgreich. Und im Debugger sieht ja sonst auch alles gut aus.
Die Zeile, in der ich angehalten habe, ist die Wurzel allen Übels im Stacktrace, die NPE kommt also nicht von einem höheren/unteren Methodenaufruf.

Hat jemand eine Idee?
 

Anhänge

  • Debugger.PNG
    Debugger.PNG
    97,5 KB · Aufrufe: 50
Zuletzt bearbeitet von einem Moderator:
Ja, ich würde da auch so wie LimDul erwarten, dass es da Probleme mit hashcode und/oder equals gibt. Daher findet er den Key auch nicht in der HashMap und es kommt null zurück.
 
In der Enumeration habe ich weder equals() noch hashCode() überschrieben.

Ist das bei Enums notwendig? Ich bin der Meinung, das schon ein paar Mal so benutzt zu haben - jedoch ohne NPE. Ich kann es ja mal probieren...
 
Bei Enums kann man equals und HashCode nicht überschreiben da diese final sind. Statt HashCode wäre evtl. auch EnumMap sinnvoller.

Aber das erklärt das Verhalten gerade nicht. Die Enum hat nur einfach die Keys und ist damit auch unveränderlich?
 
Ja.

Das ist die Klasse, mehr ist da nicht. Keine weiteren Methoden oder Parameter.
Java:
public enum PropertyScope {
    TEMPORARY,
    PERSISTANT
}
 
Zwar keine wirkliche Antwort auf das Problem; aber möglicherweise verschwindet es dadurch schon:
Die Map würde ich durch eine eigene Klasse ertsetzen, etwa:
Java:
class Properties {
  Map<String, ComponentProperty> persistent;
  Map<String, ComponentProperty> temporary;
  Map<String, ComponentProperty> getByScope(PropertyScope scope) {
    if (scop == TEMPORARY) return temporary;
    ...
  } 
}

Falls irgendwann deutlich mehr scopes möglich sind, kann man das intern immer noch durch eine Map ersetzen.


Mit Java 14 gibt es die neue Option "-XX:+ShowCodeDetailsInExceptionMessages" (Quelle ). Vielleicht hilft deren Ausgabe weiter.
Seit Java 15 standardmäßig aktiv 🙂
 
Wie hast du die HashMap gefüllt?

Die hashcode müssen nur innerhalb eines Laufes konstant sein. Wenn er da die HashCodes aus einer Datei mit einliest, dann könnte der HashCode falsch sein weil der bei einem früheren Lauf anders war ...

Aber das ist jetzt blindes raten, was das Problem sein könnte ...
 
Die hashcode müssen nur innerhalb eines Laufes konstant sein. Wenn er da die HashCodes aus einer Datei mit einliest, dann könnte der HashCode falsch sein weil der bei einem früheren Lauf anders war ...
Wäre auch meine einzige Idee das könnte passieren, wenn man die Map selbst per Reflection (de-) serialisiert und dabei ignoriert, dass man mit Maps arbeitet. Das gleiche Problem dürfte man bei einigen JDK-Klassen haben, und ich bin einfach mal davon ausgegangen, dass @White_Fox das passend behandelt.
 
@mrBrown
Danke, ich werde das mal im Hinterkopf behalten. Aber aktuell wird es mein wahrscheinlich Problem nicht lösen (warum kommt gleich).

Wie hast du die HashMap gefüllt?
Das ganze Objekt (inkl. der HashMap) wird von meiner ObjectProcessorklasse zusammengebaut. Da wird die Map nicht befüllt, sondern die Membervariablen der Instanz per Reflection reingefummelt. Die Objekte selber werden mit Objenesis instanziert.
Aber wie gesagt - ich habe das in einem Unittest schon nachgestellt, da hat es funktioniert.

Was ich noch vergessen habe zu erwähnen:
Das Programm ist ja dazu da, um aus minimalen Informationen sehr viele Daten zu generieren. Es gibt einmal die sogenannte StampPage, in dieser werden besagte Minimalinformationen eingegeben.

Dann gibt es die sogenannte ComponentPage - dieser Teil enthält die erzeugten Daten.

Wenn ich stampPageseitig Daten eingebe, speichere und wieder lade, funktioniert das wie erwartet. Wenn ich zwischendurch Daten erzeuge und dann speichere, tritt beim Laden der Bibliothek der Fehler auf. Beim Erzeugen der Daten jedoch nicht, da funktioniert das an dieser Stelle.


PS:
Und da ich den letzten Post von @mrBrown jetzt erst lese scheint der Fehler tatsächlich noch in meiner Deserialisierung zu liegen. Wenn da irgendwo ein Integer als Instanzvariable herumfleucht, wird das Integer gespeichert und später wieder reingeschrieben.

Wenn ein enum zur Laufzeit einen Hashcode liefert und zur Laufzeit eines anderen Laufes einen anderen - ja, dann würde das mein Problem erklären. Dann würde der alte Hashcode zurückgeschrieben werden, aber das Enum liefert mittlerweile irgendetwas anderes. Und möglicherweise würde es auch eklären, warum mein Unittest den Fehler nicht gezeigt hat.

Ach du grüne Neune...wie soll ich das denn auflösen? Dem Integer sehe ich ja nicht an ob es ein Hash ist oder eine andere Bedeutung hat.
 
Ach du grüne Neune...wie soll ich das denn auflösen? Dem Integer sehe ich ja nicht an ob es ein Hash ist oder eine andere Bedeutung hat.
Behandle mindestens Map und alle Collections ganz explizit und lass sie nicht auf dem Default-Weg serialisieren 🙂

Du könntest Maps zB immer in ein "MapEntry"-Array überführen, und dieses dann auf dem "normalen" Weg serialisieren – beim deserialisieren musst du dann nur wieder aus dem MapEntry-Array eine Map machen.

EDIT 1:
Danke, ich werde das mal im Hinterkopf behalten. Aber aktuell wird es mein wahrscheinlich Problem nicht lösen (warum kommt gleich).
BTW: zumindest an dieser einen Stelle hätte es das Problem gelöst (weil an er Stelle eben keine Map sondern ein POJO gewesen wäre) 🙂 Das gleiche Problem wäre dann aber unter Garantie irgendwo anders aufgetreten


EDIT 2:
Das ganze Objekt (inkl. der HashMap) wird von meiner ObjectProcessorklasse zusammengebaut. Da wird die Map nicht befüllt, sondern die Membervariablen der Instanz per Reflection reingefummelt. Die Objekte selber werden mit Objenesis instanziert.
Aber wie gesagt - ich habe das in einem Unittest schon nachgestellt, da hat es funktioniert.

Und auch zu dem Unit-Test die Erklärung: Für die meisten sinnvoll mit einer Map benutzbaten Datentypen ist der HashCode aus den Attributen ableitbar – daher ist der HashCode auch über verschiedene JVM-Instanzen hinweg konstant. Für Enums gilt grad das aber nicht, die nutzen einfach den "normalen" HashCode von Object, was auch kein Problem ist, da es ja nur eine einzelne Instanz geben kann, es müssen auch nicht verschiedene "gleiche" Isntanzen den gleichen HashCode haben. Da es aber den Identity-HashCode nutzt, ist das über JVM-Instanzen hinweg nicht konstant.

In deinem Test fällt das nicht auf, da sie alle in einer JVM-Instanz (bzw unter einem einzelnem ClassLoader) laufen – bei "echter" Serialisierung knallt es dann aber.

Simulieren könnte man das in Tests, indem man explizite ClassLoader nutzt, wird aber dann deutlich umständlicher (wenn auch ziemlich interessant, rein von der technischen Umsetzung)
 
Zuletzt bearbeitet:
An der Stelle dann ggf. den Enum wegwerfen und einen eigenen Enum zu Fuss machen. Mal eine Stelle, wo ein Singelton vielleicht Sinn ergibt
Das ist doch gar nicht notwendig. Lediglich die HashMap taugt nichts in so einem Szenario. Wenn man eine Enum als Key hat, dann nimmt auch eine EnumMap. Diese arbeitet dann mit ordinal statt mit dem hashcode. Damit ist das Problem dann auch Geschichte.
 
Das ist doch gar nicht notwendig. Lediglich die HashMap taugt nichts in so einem Szenario. Wenn man eine Enum als Key hat, dann nimmt auch eine EnumMap. Diese arbeitet dann mit ordinal statt mit dem hashcode. Damit ist das Problem dann auch Geschichte.
Oder eben noch besser die Map explizit behandeln 🙂 (und für diesen einen Fall gar keine Map nutzen 😛)

Das irgendwann Enums in einer HashMap liegen kann man ja nicht ausschließen, von daher ist das sowieso nötig.
 
Also...ich habe die HashMap mal durch eine EnumMap ersetzt. Mit dem Ergebnis, daß Objenesis die EnumMap nicht instanzieren konnte und eine Exception geworfen hat. Keine NPE, sondern Zugriffsverletzung, aber ich habe diese jetzt jedoch nicht weiter nachverfolgt. Ich würde EnumMap ja gerne dafür verwenden - ist ja schließlich genau dafür gemacht worden, aber so...

Ich werde die Sache jetzt (vorläufig) so lösen, daß ich evt. vorhandene Componentobjekte vor dem Serialisieren einfach entferne. Dies hatte ich ursprünglich sowieso so vor, da die ComponentPage ausschließlich generierte Daten enthält. Wenn die weg sind ist für den Benutzer nichts verloren...lediglich der Kompiliervorgang muß ausgeführt werden und die Daten sind wieder da.
Wie gesagt: Ich hatte das sowieso vor, da beim aktuellen Stand die Datenmenge explodieren würde. Ich habe aktuell einige Bauteiltabellen, da sind vielleicht vier (nicht selten auch einige mehr) Strings mit >100 Zeichen drin - dergleiche bei jeweils 10.000 Bauteilen (was wiederum sehr schnell sehr viel mehr werden kann). Die würden alle zigfach redundant serialisiert werden und um das zu vermeiden wollte ich der ObjectProcessor-Klasse noch einen Stringpool mitgeben.

Ich habe aktuell aber die Schnauze von ObjectProcessor erstmal voll, ich will was haben was wenigstens rudimentär funktioniert.


Das irgendwann Enums in einer HashMap liegen kann man ja nicht ausschließen, von daher ist das sowieso nötig.
Richtig, und daher hagelt es jetzt erstmal ToDo-Einträge. Das werde ich früher oder später noch beheben...aber dazu muß ich erstmal darüber nachdenken, wie am Besten. Und mir dann den Aufbau der Collections genauer ansehen. Und die Collections in der SE sind ja nicht die einzigen, in Apache Commons gibt es auch ein paar sehr interessante Sachen.

Vielen Dank an alle, die sich das Problem mal angesehen haben. 🙂
 
Also...ich habe die HashMap mal durch eine EnumMap ersetzt. Mit dem Ergebnis, daß Objenesis die EnumMap nicht instanzieren konnte und eine Exception geworfen hat. Keine NPE, sondern Zugriffsverletzung, aber ich habe diese jetzt jedoch nicht weiter nachverfolgt. Ich würde EnumMap ja gerne dafür verwenden - ist ja schließlich genau dafür gemacht worden, aber so...
EnumMap lässt sich nicht "einfach so" initialisieren, sondern nur mit spezifischer Angabe der Enums – daran dürfte Objenesis scheitern.

Richtig, und daher hagelt es jetzt erstmal ToDo-Einträge. Das werde ich früher oder später noch beheben...aber dazu muß ich erstmal darüber nachdenken, wie am Besten. Und mir dann den Aufbau der Collections genauer ansehen. Und die Collections in der SE sind ja nicht die einzigen, in Apache Commons gibt es auch ein paar sehr interessante Sachen.
Der innere Aufbau der Collections dürfte sogar fast egal sein, eigentlich alle kann man in ein Array überführen (welches man dann serialisiert) und andersrum auch wieder aus einem Array neu erstellen (beim deserialisieren).

Das JDK allein bringt ja duzende Listen mit, die alle einzeln zu behandeln wird ein Ding der ummöglichkeit 🙂
 
Hm...ich habs mir noch nicht genauer angesehen, aber ArrayList müßte z.B. ohne Sonderbehandlung durchlaufen.

Aber ich denke ich muß zumindest die Collections herausfiltern, die exzessiv mit Hashs arbeiten...da wird die HashMap nicht die Einzige Klasse sein. Mal sehen...ist ja nicht mein Problem, sondern das des Zukunfts-White_Foxs.
 

Zurück
Oben