Ich traf letztens auf einen sehr spezielles Java Problem, bei dem es dann zu Problemen bei der Serialisierung von Objekten kommt.
Ziel sollte es sein eine innere Klasse zu serialisieren während die äußere Klasse eine grafische Oberfläche und mehrere Ereignishandler beinhaltet und den JFileChooser verwendet. Dies schlägt fehl.
Hier der stark vereinfachte, aber trotzdem komplett lauffähige Aufbau eines solchen fehlerhaften Programms: (Video unten)
import java.awt.Color; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import javax.swing.JFileChooser; import javax.swing.JFrame; public class GraphEditorGerüst extends JFrame implements MouseListener { ArrayList<Kreis> kreise = new ArrayList<Kreis>(); // globale Kreisvariable public GraphEditorGerüst(){ super("GraphEditor"); setSize(150, 100); addMouseListener(this); } public class Kreis implements Serializable { // innere, zu serialisierende, Klasse public static final long serialVersionUID = 3L; public int x, y, dx, dy, id, idcounter = 0; public Color c = Color.BLACK; public Kreis(int x, int y, int dx, int dy, Color c) { this.x = x; this.y = y; this.dx = dx; this.dy = dy; this.c = c; this.id = ++idcounter; } } // Ereignishandler mouseClicked public void mouseClicked(MouseEvent me){ // ein Dummykreis erstellen, der nachher geschrieben wird kreise.add(new Kreis(0,0,0,0,Color.BLACK)); // neues JFileChooser Objekt JFileChooser fc = new JFileChooser(); // JFileChooser Dialog anzeigen und Rückgabewert speichern int fcreturn = fc.showDialog(this, "Speichern"); // wenn auf "Speichern" geklickt wurde if (fcreturn == JFileChooser.APPROVE_OPTION) { // ausgewählte Datei in ein File Objekt speichern File savefile = fc.getSelectedFile(); try { FileOutputStream fos = new FileOutputStream(savefile); ObjectOutputStream oos = new ObjectOutputStream(fos); for (Kreis k : kreise) { // hier tritt der Fehler auf oos.writeObject(k); } oos.close(); fos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args){ // Erzeugen eines Objektes von GraphEditorGerüst GraphEditorGerüst test = new GraphEditorGerüst(); test.setVisible(true); } @Override public void mouseEntered(MouseEvent e) {} @Override public void mouseExited(MouseEvent e) {} @Override public void mousePressed(MouseEvent e) {} @Override public void mouseReleased(MouseEvent e) {} }
Sieht an sich eigentlich unproblematisch aus.
Die innere Klasse soll serialisiert werden, hat das Interface implementiert und nutzt definitiv nur serialisierbare Datentypen und Elemente. Die äußere Klasse kümmert sich um die GUI und einige Ereignishandler. Einer dieser Handler enthält den JFileChooser zum Auswählen einer Datei, in die ein ObjectOutputStream nun die serialisierte innere Klasse schreiben soll. Es kommt beim Speichern, genauer gesagt bei der Ausführung von oos.writeObject, zu folgendem Fehler:
java.io.NotSerializableException: javax.swing.plaf.metal.MetalFileChooserUI
alertnativ auch auf
javax.swing.plaf.basic.BasicFileChooserUI$AcceptAllFileFilter
mit folgender Stack Trace:
Den Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.
java.io.NotSerializableException: javax.swing.plaf.metal.MetalFileChooserUI at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source) at java.io.ObjectOutputStream.writeSerialData(Unknown Source) at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source) at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.writeObject(Unknown Source) at javax.swing.event.EventListenerList.writeObject(Unknown Source) at sun.reflect.GeneratedMethodAccessor6.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at java.io.ObjectStreamClass.invokeWriteObject(Unknown Source) at java.io.ObjectOutputStream.writeSerialData(Unknown Source) at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source) at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source) at java.io.ObjectOutputStream.defaultWriteObject(Unknown Source) at javax.swing.JComponent.writeObject(Unknown Source) at sun.reflect.GeneratedMethodAccessor10.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at java.io.ObjectStreamClass.invokeWriteObject(Unknown Source) at java.io.ObjectOutputStream.writeSerialData(Unknown Source) at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source) at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source) at java.io.ObjectOutputStream.writeSerialData(Unknown Source) at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source) at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source) at java.io.ObjectOutputStream.defaultWriteObject(Unknown Source) at java.awt.Window.writeObject(Unknown Source) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at java.io.ObjectStreamClass.invokeWriteObject(Unknown Source) at java.io.ObjectOutputStream.writeSerialData(Unknown Source) at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source) at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.writeObject(Unknown Source) at java.awt.Window.writeObject(Unknown Source) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at java.io.ObjectStreamClass.invokeWriteObject(Unknown Source) at java.io.ObjectOutputStream.writeSerialData(Unknown Source) at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source) at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source) at java.io.ObjectOutputStream.writeSerialData(Unknown Source) at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source) at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.writeObject(Unknown Source) at java.util.ArrayList.writeObject(Unknown Source) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at java.io.ObjectStreamClass.invokeWriteObject(Unknown Source) at java.io.ObjectOutputStream.writeSerialData(Unknown Source) at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source) at java.io.ObjectOutputStream.writeObject0(Unknown Source) at java.io.ObjectOutputStream.writeObject(Unknown Source) at GraphEditor.actionPerformed(GraphEditor.java:191) at javax.swing.AbstractButton.fireActionPerformed(Unknown Source) at javax.swing.AbstractButton$Handler.actionPerformed(Unknown Source) at javax.swing.DefaultButtonModel.fireActionPerformed(Unknown Source) at javax.swing.DefaultButtonModel.setPressed(Unknown Source) at javax.swing.AbstractButton.doClick(Unknown Source) at javax.swing.plaf.basic.BasicMenuItemUI.doClick(Unknown Source) at javax.swing.plaf.basic.BasicMenuItemUI$Handler.mouseReleased(Unknown Source) at java.awt.Component.processMouseEvent(Unknown Source) at javax.swing.JComponent.processMouseEvent(Unknown Source) at java.awt.Component.processEvent(Unknown Source) at java.awt.Container.processEvent(Unknown Source) at java.awt.Component.dispatchEventImpl(Unknown Source) at java.awt.Container.dispatchEventImpl(Unknown Source) at java.awt.Component.dispatchEvent(Unknown Source) at java.awt.LightweightDispatcher.retargetMouseEvent(Unknown Source) at java.awt.LightweightDispatcher.processMouseEvent(Unknown Source) at java.awt.LightweightDispatcher.dispatchEvent(Unknown Source) at java.awt.Container.dispatchEventImpl(Unknown Source) at java.awt.Window.dispatchEventImpl(Unknown Source) at java.awt.Component.dispatchEvent(Unknown Source) at java.awt.EventQueue.dispatchEventImpl(Unknown Source) at java.awt.EventQueue.access$000(Unknown Source) at java.awt.EventQueue$3.run(Unknown Source) at java.awt.EventQueue$3.run(Unknown Source) at java.security.AccessController.doPrivileged(Native Method) at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source) at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source) at java.awt.EventQueue$4.run(Unknown Source) at java.awt.EventQueue$4.run(Unknown Source) at java.security.AccessController.doPrivileged(Native Method) at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source) at java.awt.EventQueue.dispatchEvent(Unknown Source) at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source) at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source) at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source) at java.awt.EventDispatchThread.pumpEvents(Unknown Source) at java.awt.EventDispatchThread.pumpEvents(Unknown Source) at java.awt.EventDispatchThread.run(Unknown Source)
Video des Beispielcodes oben:
Hier die Problemanalyse:
1.) Die innere Klasse mit dem static Modifier versehen.
2.) Kein JFileChooser verwenden.
3.) Keine ereignisverarbeitenden Methoden in der Klasse verwenden, die das zu serialisierende Objekt – oder eine innere Klasse mit diesem Objekt – enthält.
4.) Auslagern der inneren, zu serialisierenden, Klasse.
5.) Auslagern aller Ereignisempfänger und Auslagern der inneren, zu serialisierenden, Klasse.
Die erste Lösung geht wohl am schnellsten und hilft in den meisten Fällen ohne weitere Probleme zu generieren. Die zwei nächsten Lösungen schränken die Möglichkeiten ein. Die vierte Lösung ist am einfachsten umzusetzen; nur die innere, zu serialisierende, Klasse wird in eine eigene Class Datei ausgelagert und entsprechend verwendet. Die fünfte Lösung lagert zusätzlich noch alle Ereignishandler aus (was sowieso nicht schlecht ist, da es zur logischen Trennung und Übersichtlichkeit beiträgt), kann aber, je nach Größe des Projekts, relativ aufwändig werden.
Hier der Code eines komplett lauffähigen Programms, bei der die zu serialisierende Klasse ausgelagert wurde:
GraphEditor.java:
Den Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.
import java.awt.BorderLayout; import java.awt.Color; import java.awt.FlowLayout; import java.awt.Graphics; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; public class GraphEditor extends JFrame implements MouseListener, MouseMotionListener, ActionListener { //Globale Variablen JTextField txt = new JTextField(10); JPanel panel1 = new JPanel(); //Globale Variablen Kreis gezogenerKreis = null; //Kreis-Objekt zum Zwischenspeichern des bewegten Kreises //Kreisdurschnitt final int dX = 16, dY = 16; //Erstkonfiguration und Koordinatenkorrekturen int korrekturX = 0, korrekturY = 0; int korrekturX1 = 0, korrekturY1 = 0, korrekturX2 = 0, korrekturY2 = 0, configclickcounter = 1; boolean configured = false; //ArrayList zum Aufnehmen der Kreis-Objekte ArrayList<Kreis> kreise = new ArrayList<Kreis>(); //Konstruktor der Klasse GraphEditor public GraphEditor(){ //Aufruf des Konstruktors der Oberklasse super("GraphEditor"); //Schließen des Fensters setDefaultCloseOperation(DISPOSE_ON_CLOSE); //Menüleiste JMenuBar mbar = new JMenuBar(); this.setJMenuBar(mbar); //Menü JMenu mGraph = new JMenu("Graph"); JMenu mVertex = new JMenu("Vertex"); //Menüelemente JMenuItem mGraphOpen = new JMenuItem("open"); JMenuItem mGraphNew = new JMenuItem("new"); JMenuItem mGraphSave = new JMenuItem("save"); JMenuItem mVertexInsert = new JMenuItem("insert"); JMenuItem mVertexMove = new JMenuItem("move"); JMenuItem mVertexDelete = new JMenuItem("delete"); JMenuItem mVertexNone = new JMenuItem("none"); //Hinzufügen der Elemente zur Menüleiste mGraph.add(mGraphNew); mGraph.add(mGraphOpen); mGraph.add(mGraphSave); mVertex.add(mVertexInsert); mVertex.add(mVertexMove); mVertex.add(mVertexDelete); mVertex.add(mVertexNone); mbar.add(mGraph); mbar.add(mVertex); //Haupt-Panel this.setLayout(new BorderLayout()); panel1.setBackground(Color.white); this.add(panel1, BorderLayout.CENTER); panel1.addMouseListener(this); panel1.addMouseMotionListener(this); //unteres Panel für Label und Textfeld JPanel panel2 = new JPanel(); this.add(panel2, BorderLayout.SOUTH); //Label JLabel label = new JLabel("Selected Action", JLabel.RIGHT); panel2.setLayout(new GridLayout(1,2,5,0)); panel2.add(label); //Panel für Textfeld JPanel panel3 = new JPanel(); panel3.setLayout(new FlowLayout(3)); panel2.add(panel3); //Textfeld txt.setText("none"); txt.setDisabledTextColor(Color.black); txt.setEnabled(false); panel3.add(txt); //ActionListener mVertex.addActionListener(this); // vertex: mVertexInsert.addActionListener(this); mVertexDelete.addActionListener(this); mVertexMove.addActionListener(this); mVertexNone.addActionListener(this); mGraph.addActionListener(this); // graph: mGraphNew.addActionListener(this); mGraphOpen.addActionListener(this); mGraphSave.addActionListener(this); //ActionCommands mVertex.setActionCommand("vertex"); // vertex: mVertexInsert.setActionCommand("insert"); mVertexDelete.setActionCommand("delete"); mVertexMove.setActionCommand("move"); mVertexNone.setActionCommand("none"); mGraph.setActionCommand("graph"); // graph: mGraphNew.setActionCommand("new"); mGraphOpen.setActionCommand("open"); mGraphSave.setActionCommand("save"); } //Überschriebene Paint-Methode zum Zeichnen der Kreise public void paint(Graphics g) { super.paint(g); for (Kreis k: kreise) { g.setColor(k.c); g.fillOval(k.x - k.dx/2 + korrekturX, k.y - k.dy/2 + korrekturY, k.dx, k.dy); } } //ActionEvent public void actionPerformed(ActionEvent ae){ //Zwischenspeichern String action = ae.getActionCommand(); //werden unabhängig von der Erstkonfiguration immer ausgeführt if(action == "delete"){ txt.setText("Vertex: delete"); } if(action == "move"){ txt.setText("Vertex: move"); } if(action == "none"){ txt.setText("none"); } //wenn insert zum ersten Mal ausgewählt wird: Infofenster anzeigen if (configured == false && action == "insert") { int wahl = JOptionPane.showConfirmDialog(this, "" + "Bei der ersten Verwendung des Programms muss eine Konfiguration erfolgen. \n" + "Bitte klicken Sie an eine Stelle mittig im Zeichenbereich und danach\n " + "exakt in den weißen Punkt in der Mitte des gezeichneten Kreises!", "Konfiguration nötig!", JOptionPane.OK_CANCEL_OPTION); if (wahl == JOptionPane.OK_OPTION) { txt.setText("Vertex: insert"); } } //wenn Erstkonfiguration schon abgeschlossen ist else if (configured == true) { if(action == "insert"){ txt.setText("Vertex: insert"); } } //Löschen der Kreise aus der Liste if(action == "new") { kreise.clear(); repaint(); } //Speichern der gezeichneten Kreise if(action == "save") { JFileChooser fc = new JFileChooser(); int fcreturn = fc.showDialog(this, "Speichern"); if (fcreturn == JFileChooser.APPROVE_OPTION) { File savefile = fc.getSelectedFile(); try { FileOutputStream fos = new FileOutputStream(savefile); ObjectOutputStream oos = new ObjectOutputStream(fos); for (Kreis k : kreise) { oos.writeObject(k); } oos.close(); fos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } //Öffnen einer bereits gespeicherten Datei mit Punkten if(action == "open") { kreise.clear(); JFileChooser fc = new JFileChooser(); int fcreturn = fc.showDialog(this, "Öffnen"); if (fcreturn == JFileChooser.APPROVE_OPTION) { File openfile = fc.getSelectedFile(); fc = null; FileInputStream fis; ObjectInputStream ois; try { fis = new FileInputStream(openfile); ois = new ObjectInputStream(fis); try { while (true) { kreise.add((Kreis) ois.readObject()); repaint(); } } catch (EOFException e) { System.out.println("Dateiende erreicht"); ois.close(); fis.close(); } } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } repaint(); } //MouseEvents public void mouseClicked(MouseEvent me){ repaint(); int cursorX = me.getX(); int cursorY = me.getY(); //Erstkonfiguration der Koordinatenkorrektur starten, wenn noch nicht erfolgt if (configured == false) { //erster Klick der Erstkonfiguration, Textfeld mit "Vertex: insert" nur, wenn der Benutzer im Dialog "OK" geklickt hat if (configclickcounter == 1 && txt.getText().equals("Vertex: insert")) { //erstes Koordinatenpaar lesen korrekturX1 = me.getX(); korrekturY1 = me.getY(); configclickcounter++; /* Kreis mit den unkonfigurierten Standardkoordinaten zeichnen; * dieser wird links oberhalb des Klickpunktes erscheinen, je nach Betriebssystem(/-version) * und Windows-Design unterschiedlich! * Ein kleiner weißer Kreis symbolisiert die Mitte, die angeklickt werden muss. */ kreise.add(new Kreis(cursorX, cursorY, dX, dY)); kreise.add(new Kreis(cursorX, cursorY, 2, 2, Color.WHITE)); //zweiter Klick der Erstkonfiguration } else if (configclickcounter == 2 && txt.getText().equals("Vertex: insert")) { //Konfigurationskreise löschen kreise.clear(); repaint(); //zweites Koordinatenpaar lesen korrekturX2 = me.getX(); korrekturY2 = me.getY(); //Korrekturen errechnen korrekturX = korrekturX1 - korrekturX2; korrekturY = korrekturY1 - korrekturY2; //Konfiguration beenden, Infodialog anzeigen configured = true; JOptionPane.showConfirmDialog(this, "" + "Die Konfiguration ist abgeschlossen. \n" + "Koordinatenkorrektur für X: " + korrekturX + " | Y: " + korrekturY, "Konfiguration abgschlossen!", JOptionPane.OK_CANCEL_OPTION); } } //Erstkonfiguration beendet else if (configured == true){ if(txt.getText().equals("Vertex: insert")){ kreise.add(new Kreis(cursorX, cursorY, dX, dY)); } } if(txt.getText().equals("Vertex: delete")){ for(Kreis k : kreise){ if(cursorX <= (k.x + dX/2.0) && cursorX >= (k.x - dX/2.0) && cursorY <= (k.y + dY/2.0) && cursorY >= (k.y - dY/2.0)){ kreise.remove(k); break; } } } } public void mousePressed(MouseEvent me){ if(txt.getText().equals("Vertex: move")){ int cursorX = me.getX(); int cursorY = me.getY(); for(Kreis k : kreise){ if(cursorX <= (k.x + dX/2.0) && cursorX >= (k.x - dX/2.0) && cursorY <= (k.y + dY/2.0) && cursorY >= (k.y - dY/2.0)){ gezogenerKreis = k; } } } } public void mouseEntered(MouseEvent arg0){} public void mouseExited(MouseEvent arg0){} public void mouseReleased(MouseEvent arg0){ gezogenerKreis = null; } //MouseMotionEvents public void mouseDragged(MouseEvent me) { if(txt.getText().equals("Vertex: move")){ if(gezogenerKreis != null) { gezogenerKreis.x = me.getX(); gezogenerKreis.y = me.getY(); repaint(); } } } public void mouseMoved(MouseEvent e){} //Main-Methode public static void main(String[] args){ //GraphEditor-Objekt GraphEditor ui = new GraphEditor(); //Setzen der Größe und Sichtbarkeit des GraphEditor-Objekts ui.setSize(550, 350); ui.setVisible(true); } }
Kreis.java:
Den Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.
import java.awt.Color; import java.io.Serializable; public class Kreis implements Serializable { //Innere Kreis-Klasse public static final long serialVersionUID = 3L; public int x, y, dx, dy, id; public Color c = Color.BLACK; int counter = 0; // 2 verschieden überladene Konstruktoren ermöglichen die freie Farbwahl für jeden einzelnen Kreis public Kreis(int x, int y, int dx, int dy, Color c) { this.x = x; this.y = y; this.dx = dx; this.dy = dy; this.c = c; this.id = ++counter; } public Kreis(int x, int y, int dx, int dy) { this.x = x; this.y = y; this.dx = dx; this.dy = dy; } @Override public String toString() { return "x:"+ x + " y:" + y + " dx:" + dx + " dy:" + dy + " color:" + c.toString()+ "\n"; } }
Der Grund, dass die äußere Klasse mit serialisiert wird, liegt darin, dass die innere Klasse keine static class ist und nur innerhalb einer Instanz der äußeren Klasse existieren kann. Die einfachste Lösung müsste daher eigentlich nur sein, aus
„public class Kreis implements Serializable“ „public static class Kreis implements Serializable“. Durch das verschieben der Klasse in eine eigene Datei wird das gleiche bewirkt.
Tatsächlich…
ich bin ja mal etwas enttäuscht über die scheiß Arbeit, die ich mir hier gemacht habe 😀
Und darüber, dass das nicht mal irgendwo im Netz einfach stehen kann…
Danke 😉
Innere Klassen sind generell schlechter Stil 🙂
Nein im Ernst, sobald eine Klasse etwas komplexer wird und mehr als nur ein Datenhalter ist, sollte davon abgesehen werden, das als eine innere Klasse zu implementieren.
Philip bringts ganz gut auf den Punkt.