• Keine Ergebnisse gefunden

Die Datenstrukuren der in Abbildung 8 vorgestellten Attribute der KlasseGraph sind maßgebend f¨ur die Effizienz des Programms, weshalb sie in diesem Abschnitt kurz erl¨autert werden sollen.

Prim¨ar werden Lists und Maps verwendet, welche in Java beides abstrakte

Klassen sind; das heißt sie stellen nur die Methoden in

”abstrakter“ Form zur Verf¨ugung, nicht aber die Implementierung selbst. Es stehen mehrere implemen-tierte Klassen wie beispielsweise AbstractList, ArrayList, LinkedList, HashMap,TreeMap, u.v.m. zur Verf¨ugung, unter denen man die w¨ahlen kann, die dem Zweck des eigenen Programms am Besten unterst¨utzend beiwirkt.

F¨ur die sp¨atere Implementierung wurde sich f¨ur die Klassen ArrayList und HashMap entschieden. Eine ArrayList realisiert in Java ein dynamisches Ar-ray, was es insbesondere f¨ur die Menge der Beschriftungen L erm¨oglicht, flexibel auf die einzelnen Elemente zuzugreifen und sie abzuspeichern. Klar ist, dass ledig-lich Strings eingetragen werden und die Anzahl dieser vorher nicht bekannt ist, weshalb sich ein dynamisches Array quasi aufdr¨angt. F¨ur die Liste der Wurzel-knoten wurde aus dem selben Grund ebenfalls eine solche Datenstruktur gew¨ahlt.

Wirkliche Relevanz allerdings hat die Entscheidung bez¨uglich der Adjazenzliste, bei der die angesprochene HashMap ausgew¨ahlt wurde. Hierbei wird f¨ur jeden Knotennamen v (Schl¨ussel) eine ArrayList (Wert) angelegt, in der die von v ausgehenden Kanten abgelegt sind. In der Instanz der Klasse Edge findet sich dann die entsprechende Beschriftung derselbigen.

Die HashMap ist deshalb f¨ur die Realisierung der Adjazenzliste so gut geeig-net, weil sie f¨ur die Basisoperationen (also Einf¨ugen mit put() und Auslesen mit get()) inO(1), also konstanter Zeit, reagiert. Hierzu unterstellt man allerdings eine hinreichend gute Hashfunktion, die die Elemente gleichm¨aßig unter den Buckets verteilt. Diese ist wiederum in der Implementierung gegeben und sorgt daf¨ur, dass m¨oglichst wenige Kollisionen auftreten, wobei eine Kollision dann stattfin-det, wenn zwei Werte denselben Hashwert besitzen. Außerdem vergr¨oßert sich die Hashtabelle jedes Mal um den Faktor zwei, wenn sie zu 75% gef¨ullt ist, so dass man auch hier nicht platzbeschr¨ankt ist.

Weitere Details zu den Datenstrukturen sind im Kapitel 5 nachzulesen. Zun¨achst ist noch die beabsichtigte Fehlerbehandlung zu beschreiben.

Aufgrund der Tatsache, dass es in Java sogenannte Exceptions gibt, also Ausnah-meregelungen, die im Falle eines Fehlers aufgerufen werden und eine Information uber diesen Fehler leicht in verschiedene Programmebenen transportieren k¨¨ onnen, ist es naheliegend, dieses Konzept im Rahmen des Isomorphie-Checkers zu verwen-den und die Fehlerbehandlung unproblematisch dar¨uber abzuwickeln. Einleitend f¨ur das nachfolgende Kapitel, in dem es um die tats¨achlichen Implementierungen geht, ist der in Abbildung 11 gezeigte Code-Ausschnitt, der in dieser Form real im Programm eingesetzt wird:

1 p u b l i c c l a s s E x c e p t i o n s e x t e n d s E x c e p t i o n {

2 p u b l i c E x c e p t i o n s (S t r i n g s ) t h r o w s F i l e N o t F o u n d E x c e p t i o n , U n s u p p o r t e d E n c o d i n g E x c e p t i o n , I O E x c e p t i o n {

3 s u p e r( s ) ;

4

5 // Set t i m e s t a m p and o p e n f i l e

6 S t r i n g U N I X _ D A T E _ F O R M A T = " EEE MMM dd y y y y HH : mm : ss zzz ";

7 D a t e now = new D a t e () ;

8

9 S i m p l e D a t e F o r m a t f o r m a t t e r = new S i m p l e D a t e F o r m a t ( U N I X _ D A T E _ F O R M A T ) ;

10

11 // O p e n f i l e and a p p e n d e r r o r m e s s a g e

12 try( P r i n t W r i t e r out = new P r i n t W r i t e r (new B u f f e r e d W r i t e r ( new F i l e W r i t e r (" e r r o r s . log ", t r u e) ) ) ) {

13 out . p r i n t l n (" [ " + f o r m a t t e r . f o r m a t ( now ) + " ] " + s ) ;

14 }

15 c a t c h ( I O E x c e p t i o n ex ) {

16 }

17 }

18 }

Abbildung 11: Exceptions.java

Der Klassenname Exceptions ist willk¨urlich gew¨ahlt und spielt keine wichtige Rolle – entscheidender ist, dass die Klasse von Exception abgeleitet wird und damit alle ihre Methoden erbt. ¨Uber die Methode super() wird der Konstruktor der Basisklasse aufgerufen und die Fehlermeldung weitergereicht. Anschließend wird der aufgetretene Fehler in eine Datei errors.logmit der aktuellen Zeit und der Fehlermeldung eingetragen, um gegebenenfalls sp¨ater eine Fehleranalyse zu betreiben.

Nachdem nun die Algorithmen, die Datenstrukturen und auch die Verwendung des entstehenden Programms pr¨asentiert wurden, kann sich im folgenden Kapitel nun der tats¨achlichen Implementierung gewidmet werden.

5 Implementierungen

Die zur Implementierung notwendige Spezifikation wurde in den vorangegangenen Abschnitten deutlich gemacht. Wie bereits erw¨ahnt wird die Programmiersprache Java in der Version 7 Update 51 verwendet.

Grunds¨atzlich sind in Java zusammenh¨angende (Teil-)Programme mittels soge-nannter packages miteinander verbunden. Dadurch wird erreicht, dass alle zu-geh¨origen Dateien einen gemeinsamen Namensraum verwenden und auf die Klas-sen der jeweils anderen Dateien zugreifen k¨onnen. Aufgrund der Tatsache, dass Klassen, die einem package p nicht zugeh¨orig sind, keinen Zugriff auf die in p befindlichen Daten haben, wird das Konzept der Kapselung, das eigentlich dem objekt-orientierten Paradigma zu eigen ist, hier eine Ebene h¨oher realisiert.

Im Folgenden wird das package CheckIsomorphism implementiert, das die in den vorherigen Kapiteln diskutierten Details verwirklicht und insbesondere die Algorithmen aus Kapitel 3 zur Verf¨ugung stellt. Von den von Java bereits zur Verf¨ugung gestellten packages werden prim¨ar java.util und java.io verwen-det, weil diese wichtige Datenstrukturen inne haben und den Umgang mit Dateien flexibel erm¨oglichen.

Neben der Implementierung der Algorithmen werden stellenweise interessante und wichtige Funktionen hervorgehoben, die der Realisierung der Datenstruktur bzw.

des Interpreters zugeh¨orig sind.

5.1 Graph.java

In Abbildung 8 ist die Datenstruktur f¨ur Graphen mittels eines UML-Diagramms gezeigt, die in der Datei Graph.java umgesetzt wurde.

Bez¨uglich der Einbindung externer packages wurden aus java.util die Klassen ArrayList,Date und HashMapverwendet, deren Einsatz anhand implemen-tierter Methoden beispielhaft aufgezeigt wird.

In der KlasseGraphbefinden sich, alsInner Classes realisiert,Node undEdge mit ihren zugeh¨origen Methoden zur Verwaltung ihrer Attribute. Da diese Me-thoden haupts¨achlich Getter-/Setter sind, wird auf eine detaillierte Vorstellung verzichtet und f¨ur genauere Informationen bez¨uglich der Aufgaben dieser Metho-den auf die Abschnitte 4.1.1 und 4.1.2 verwiesen.

Detaillierter hingegen sollen einige Methoden der Klasse Graph diskutiert wer-den. Der KonstruktorGraph() initialisiert ¨uber das Schl¨usselwortnewseine Attri-bute, so dass einer Graph-Instanz f¨ur die weitere Verwendung zweiArrayLists Labels und Roots und drei HashMaps NodeLabels, NodeMapping und

Adjacen-cyList zur Verf¨ugung stehen. Das AttributCountEdges wird auf 0 gesetzt.

In den Methoden addLabel(String label), getLabelPosition(String label) und get-LabelByIndex(int index), die zur Verwaltung der Beschriftungen dienen, werden haupts¨achlich Funktionen der Klasse ArrayList verwendet. Interessanter ist dementsprechend die MethoderemoveLabel(String label), weil hier vor der tats¨ achli-chen Entfernung der Beschriftung aus der MengeLdie Kanten modifiziert werden m¨ussen, die diese Beschriftung bisher getragen haben. Zus¨atzlich m¨ussen anschlie-ßend nun eventuell vorhandene redundante Kanten entfernt werden:

1 p u b l i c b o o l e a n r e m o v e L a b e l (S t r i n g l a b e l) {

Zun¨achst werden alle Knoten und Kanten in der Adjazenzliste durchlaufen und die Beschriftungen derjenigen auf die BeschriftungnewlabelinLge¨andert, die vorher mit label beschriftet waren (vgl. Zeile 4 - 9). Nun kann es aber sein, dass es von Knoten u zu Knoten v bereits eine Kante mit Beschriftung label gab und somit gerade eine redundante erzeugt wurde. Genau diese Redundanz muss aufgel¨ost werden:

c u r r e n t _ f r o m ) &&

Dazu wird f¨ur jeden Knoten die ihm zugeh¨orige Kantenliste durchlaufen (eine Kante von u nach v wird in der Liste von u gespeichert) und es wird ¨uberpr¨uft, ob es eine andere Kante mit derselben Beschriftung gibt (und gegebenenfalls ent-fernt). Es reicht bei (i+ 1) zu beginnen und zu stoppen, sobald eine solche Kante gefunden wurde, weil es h¨ochstens eine davon geben kann und es in der Situation, dass es zwei Kanten (u, v, l) auf Position 1 und (u, v, l) auf Position 10 gibt, keinen Unterschied macht, welche davon behalten wird. Nat¨urlich muss beim Entfernen darauf geachtet werden, dass die Grad-Zahlen der Knoten aktualisiert werden und auf die jetzt eventuell vorhandene Wurzel-Eigenschaft ¨uberpr¨uft wird.

Eine ¨ahnliche Routine wird in der FunktionchangeEdgeLabel() verwendet, bei der es ebenso auftreten kann, dass pl¨otzlich zwei Kanten gleich beschriftet sind.

Ansonsten ist die Verwaltung der Knoten und Kanten mittels der Adjazenzliste vom Typ HashMap<Node, List<Edge>> recht simpel: Jeder Knoten erh¨alt genau einen Eintrag in dieser Liste, wobei Name und Beschriftung in der Instanz Node verwaltet werden. Jede ausgehende Kanten des Knotens wird in seine zu-geh¨orige Liste vom Typ Edge eingetragen (in den Instanzen werden auch Ein-gangsknoten und Beschriftung der Kante gespeichert). Eine Modifikation ist also

nicht aufwendig. Insbesondere muss beim L¨oschen eines Knotens nichts weiter beachtet werden, weil die inzidenten Kanten automatisch durch Entfernen des Eintrags f¨ur den Knoten aus der Adjazenzliste entfernt werden.

Selbstverst¨andlich sind die Methoden robust, d.h. gegen unpassende Benutzerein-gaben gesichert: So kann beispielsweise nicht zwei Mal die selbe Kante mit der selben Beschriftung oder zwei Mal der gleiche Knoten eingetragen werden.

Weiterhin lesen die MethodenprintGraph(),printNodes(),printEdges()und print-Labels() nur die Datenstrukturen aus und stellen die Daten formatiert auf der Kommandozeile dar.

Interessant ist noch die FunktionexportGraph(String graph, String filename), die eine vorhandene Graphinstanz in eine Datei externalisiert:

1 p u b l i c v o i d e x p o r t G r a p h (S t r i n g graph , S t r i n g f i l e n a m e ) {

Abbildung 13: Methode: Graph.exportGraph(String graph, String filename)

In Zeile 2 - 6 werden nur Initialisierungen gemacht und die Datei ge¨offnet – diese wird unter dem Dateinamenfilename.graph im Ordner/graphs/ abgelegt. An-schließend werden die Befehle eingef¨ugt, die der Interpreter verarbeiten kann, um eine Graph-Instanz zu rekonstruieren (vgl. Abschnitt 4.2). Dazu werden lediglich die Datenstrukturen durchlaufen und entsprechend extrahiert.

Wie eine solche Instanz wieder importiert werden kann, soll Teil des n¨achsten Abschnitts sein.