Technischer Neustart: Verwendung von Zensical statt MkDocs
This commit is contained in:
232
docs/zentralabitur/baum.md
Normal file
232
docs/zentralabitur/baum.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# 2. Baumstrukturen
|
||||
|
||||
Bäume gehören zu den wichtigsten Datenstrukturen der Informatik. Sie finden Anwendung in vielen Bereichen, z.B. als Suchbäume zum schnellen Finden von Elementen in geordneten Mengen.
|
||||
|
||||
|
||||
|
||||
## Binärbaum
|
||||
|
||||
Der Binärbaum wird in der Regel als rekursive Datenstruktur betrachtet: Ein Binärbaum besteht demnach aus einem Element (genauer einem Objekt einer zuvor festgelegten Klasse), einem linken und einem rechten Teilbaum. Eine Ordnung ist für einen Binärbaum nicht erforderlich.
|
||||
|
||||
|
||||
### Einen Binärbaum aufbauen
|
||||
|
||||
Für ein erstes Beispiel soll der folgende Baum aufgebaut werden:
|
||||
|
||||

|
||||
|
||||
Zur Vereinfachung wird als Grundklasse für den Baum die Klasse *String* definiert, d.h. die Elemente des Beispielsbaums werden als Zeichenketten gespeichert. Sicherlich wäre eine Modellierung als Zahl naheliegend, doch dann wäre der Einsatz der Wrapper-Klasse Integer (vgl. Abschnitt [Objekte als Datenspeicher](linear.md#objekte-als-datenspeicher)) oder der Entwurf einer neuen Klasse erforderlich.
|
||||
|
||||
Der folgende Quellcode baut den Baum *bottom-up* (d.h. von unten nach oben) auf. Eine andere Vorgehensweise ist nicht möglich, da beispielsweise der Gesamtbaum (vgl. Element 23) nur dann erzeugbar ist, wenn linker und rechter Teilbaum (vgl. Elemente 17 und 38) bereits existieren.
|
||||
|
||||
```java
|
||||
BinaryTree<String> k5 = new BinaryTree<String>("5");
|
||||
BinaryTree<String> k14 = new BinaryTree<String>("14");
|
||||
BinaryTree<String> k13 = new BinaryTree<String>("13",k5,k14);
|
||||
BinaryTree<String> k19 = new BinaryTree<String>("19");
|
||||
BinaryTree<String> k17 = new BinaryTree<String>("17",k13,k19);
|
||||
|
||||
BinaryTree<String> k39 = new BinaryTree<String>("39");
|
||||
BinaryTree<String> k40 = new BinaryTree<String>("40",k39,null);
|
||||
BinaryTree<String> k24 = new BinaryTree<String>("24");
|
||||
BinaryTree<String> k38 = new BinaryTree<String>("38",k24,k40);
|
||||
|
||||
tree = new BinaryTree<String>("23",k17,k38);
|
||||
```
|
||||
|
||||
|
||||
### Einen Binärbaum durchsuchen (Tiefensuche)
|
||||
|
||||
Beim Durchsuchen eines Binärbaums kommt zunächst die Tiefensuche in Betracht. Hier wird der Baum rekursiv durchsucht, wobei dem linken Teilbaum Vorrang vor dem rechten Teilbaum gegeben wird.
|
||||
|
||||
Der Zeitpunkt der Verarbeitung des aktuellen Elements (hier angedeutet durch den Ausdruck auf dem Bildschirm) vor dem ersten Rekursionsaufruf, zwischen den beiden Aufrufen oder nach dem zweiten Rekursionsaufruf beeinflusst zusätzlich die Reihenfolge der Verarbeitung. Die drei Varianten werden als *PreOrder*, *InOrder* bzw. *PostOrder* bezeichnet.
|
||||
|
||||
Die am häufigsten eingesetzte Variante ist der InOrder-Durchlauf, da er die Elemente in ihrer natürlichen Reihenfolge verarbeitet. Im obigen Beispiel, das eine Anordnung von Zahlen im Baum vorsieht, würden die Elemente deshalb auch in aufsteigender Reihenfolge abgearbeitet.
|
||||
|
||||
```java
|
||||
public void inorder(BinaryTree<String> tree) {
|
||||
|
||||
if (!tree.isEmpty()) {
|
||||
|
||||
inorder(tree.getLeftTree());
|
||||
System.out.println(tree.getContent()); // Ausdrucken
|
||||
inorder(tree.getRightTree());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Einen Binärbaum durchsuchen (Breitensuche)
|
||||
|
||||
Als Alternative zur Tiefensuche steht die Breitensuche zur Verfügung, die die Elemente ebenenweise abarbeitet.
|
||||
|
||||

|
||||
|
||||
Für den Beispielbaum würde die Reihenfolge der Verarbeitung dann *23, 17-38, 13-19-24-40, 5-14-39* lauten. Um die Ebenen abzubilden, wird eine [Schlange](linear.md#stack-und-queue) als Hilfsdatenstruktur verwendet.
|
||||
|
||||
```java
|
||||
public void levelorder(BinaryTree<String> tree) {
|
||||
|
||||
Queue<BinaryTree<String>> q = new Queue<BinaryTree<String>>();
|
||||
q.enqueue(tree);
|
||||
|
||||
while (!q.isEmpty()) {
|
||||
BinaryTree<String> t = q.front();
|
||||
q.dequeue();
|
||||
|
||||
System.out.println(t.getContent()); // Ausdrucken
|
||||
|
||||
if (!t.getLeftTree().isEmpty()) {
|
||||
q.enqueue(t.getLeftTree());
|
||||
}
|
||||
if (!t.getRightTree().isEmpty()) {
|
||||
q.enqueue(t.getRightTree());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Einen Binärbaum verarbeiten (mit Rückgabewert)
|
||||
|
||||
Zum Abschluss soll hier ein komplexeres Beispiel präsentiert werden. Zur Illustration dient der nachfolgende Baum. Die Aufgabe besteht darin, die Tiefe des Baums (d.h. die Anzahl der benutzten Ebenen, im Beispiel 4) zu berechnen.
|
||||
|
||||

|
||||
|
||||
Die kompakte rekursive Methode hat es durchaus in sich: Ist der aktuelle Teilbaum leer, so ist seine Tiefe 0. Ansonsten ist seine Tiefe um 1 größer als die maximale Tiefe des linken oder des rechten Teilbaums - je nachdem, welcher von beiden die größere Tiefe besitzt.
|
||||
|
||||
```java
|
||||
public int depth(BinaryTree<String> tree) {
|
||||
if (tree.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
int left = depth(tree.getLeftTree());
|
||||
int right = depth(tree.getRightTree());
|
||||
|
||||
if (left>right) {
|
||||
return left+1;
|
||||
}
|
||||
else {
|
||||
return right+1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Suchbaum
|
||||
|
||||
Der Suchbaum stellt einen guten Kompromiss aus den Datenstrukturen Feld und Liste dar: Er ist dynamisch, erlaubt also jederzeit nachträgliches Einfügen oder Löschen ohne zusätzlichen Speicherbedarf. Er ermöglicht zugleich eine schnelle Suche, da die Idee der binären Suche übertragbar ist, wodurch sich logarithmischer Suchaufwand ergibt.
|
||||
|
||||
|
||||
### Eine Klasse zum Suchen vorbereiten
|
||||
|
||||
Voraussetzung für den Aufbau eines Suchbaums ist eine Ordnung der zu verwaltenden Elemente. Für die Klasse *BinarySearchTree* wird die erforderliche Ordnung so realisiert, dass für die Basisklasse der zu speichernden Objekte gefordert wird, dass sie das Interface *ComparableContent* implementiert. In diesem Interface wird mit den drei Methoden *isLess()*, *isGreater()* und *isEqual()* die Vergleichbarkeit der Elemente verankert.
|
||||
|
||||
```java
|
||||
public interface ComparableContent<ContentType> {
|
||||
|
||||
public boolean isGreater(ContentType pContent);
|
||||
|
||||
public boolean isEqual(ContentType pContent);
|
||||
|
||||
public boolean isLess(ContentType pContent);
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Die folgende Beispielklasse *Entry* speichert für jedes Objekt exemplarisch ein ganzzahliges Attribut, das über eine get-Methode abgefragt werden kann. In den drei Vergleichsmethoden muss ein Objekt die Frage beantworten, wie es sich selbst gegenüber einem zweiten Objekt (hier *pContent*) der gleichen Klasse einordnet.
|
||||
|
||||
```java
|
||||
|
||||
public class Entry implements ComparableContent<Entry> {
|
||||
|
||||
private int wert;
|
||||
|
||||
public Entry(int pWert) {
|
||||
this.wert = pWert;
|
||||
}
|
||||
|
||||
public boolean isLess(Entry pContent) {
|
||||
return wert < pContent.getWert();
|
||||
}
|
||||
|
||||
public boolean isEqual(Entry pContent) {
|
||||
return wert == pContent.getWert();
|
||||
}
|
||||
|
||||
public boolean isGreater(Entry pContent) {
|
||||
return wert > pContent.getWert();
|
||||
}
|
||||
|
||||
public int getWert() {
|
||||
return wert;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Einen Suchbaum aufbauen
|
||||
|
||||
Steht die Basisklasse, so steht dem Aufbau des Suchbaums nichts mehr im Wege. Im nachfolgenden Beispiel werden nacheinander die Elemente 45, 25 usw. in den Suchbaum eingefügt. Für das geordnete Einfügen sorgt die Methode *insert*.
|
||||
|
||||
Somit ergibt sich der folgende Beispielsuchbaum:
|
||||
|
||||

|
||||
|
||||
```java
|
||||
Entry int1 = new Entry(45);
|
||||
Entry int2 = new Entry(25);
|
||||
Entry int3 = new Entry(65);
|
||||
Entry int4 = new Entry(15);
|
||||
Entry int5 = new Entry(35);
|
||||
|
||||
tree = new BinarySearchTree<Entry>();
|
||||
|
||||
tree.insert(int1);
|
||||
tree.insert(int2);
|
||||
tree.insert(int3);
|
||||
tree.insert(int4);
|
||||
tree.insert(int5);
|
||||
```
|
||||
|
||||
|
||||
### In einem Suchbaum suchen
|
||||
|
||||
Um nun ein Element im Suchbaum zu suchen, ist zunächst ein Such-Objekt der Basisklasse (in unserem Beispiel *Entry*) anzulegen. Kann die Methode *search()* ein passendes Objekt im Baum finden, so wird dieses Objekt zurückgegeben. Bleibt die Suche erfolglos, so gibt sie *null* zurück. Eine Fallunterscheidung kann die beiden Fälle trennen.
|
||||
|
||||
```java
|
||||
Entry suchitem = new Entry(77);
|
||||
|
||||
Entry ergitem = tree.search(suchitem);
|
||||
|
||||
if (ergitem!=null) {
|
||||
System.out.println("Ja");
|
||||
}
|
||||
else {
|
||||
System.out.println("Nein");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Einen Suchbaum durchlaufen
|
||||
|
||||
Analog zum Binärbaum kann auch der Suchbaum rekursiv durchsucht werden. Diese Möglichkeit ist aber vorrangig für Debugging-Zwecke (z.B. zum Ausdrucken des Suchbaum-Inhalts) interessant.
|
||||
|
||||
```java
|
||||
private void durchlaufeBaum(BinarySearchTree<Entry> mytree) {
|
||||
|
||||
if (!mytree.isEmpty()) {
|
||||
|
||||
durchlaufeBaum(mytree.getLeftTree());
|
||||
System.out.println(mytree.getContent());
|
||||
durchlaufeBaum(mytree.getRightTree());
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
60
docs/zentralabitur/compilerbau.md
Normal file
60
docs/zentralabitur/compilerbau.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 4. Compilerbau (LK)
|
||||
|
||||
Im Themengebiet der Theoretischen Informatik (meist abkürzend als Automaten bezeichnet) muss im LK auch der Bau eines Compilers behandelt werden. Obwohl beim Compilerbau zur Beschreibung formaler Sprachen wie z.B. Programmiersprachen meist kontextfreie Sprachen eingesetzt werden, wird für das Zentralabitur lediglich verlangt, einen Parser für eine reguläre Sprache entwickeln zu können.
|
||||
|
||||
An dieser Stelle werden wir einen einfachen Parser entwickeln, der genau die Sprache akzeptiert, die auch der folgende Endliche Automat akzeptiert:
|
||||
|
||||

|
||||
|
||||
Die schrittweise Umsetzung nummeriert die Zustände durch und repräsentiert sie als fortlaufende int-Zahlen. Da die Eingaben Ziffern sind, kann die Eingabe ganz einfach Buchstabe für Buchstabe verarbeitet werden. Jeder Zustandswechsel findet sich in einer der geschachtelten switch-Anweisungen wieder. (*Bemerkung:* Die switch-Anweisungen werden lediglich verwendet, weil sie eine übersichtliche Lösung ermöglichen. Analog können selbstverständlich auch geschachtelte if-else-Anweisungen zum Einsatz kommen.)
|
||||
|
||||
```java
|
||||
public class Automat {
|
||||
|
||||
public boolean parse(String wort) {
|
||||
|
||||
int zustand = 0; // Startzustand
|
||||
int zaehler = 0; // Beginn beim 0-ten Symbol
|
||||
|
||||
while (zaehler < wort.length()) {
|
||||
|
||||
char symbol = wort.charAt(zaehler); // aktuelles Symbol
|
||||
|
||||
switch (zustand) {
|
||||
case 0: { switch (symbol) {
|
||||
case '0': { zustand = 1; break; }
|
||||
case '1': { zustand = 0; break; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: { switch (symbol) {
|
||||
case '0': { zustand = 2; break; }
|
||||
case '1': { zustand = 0; break; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2: { switch (symbol) {
|
||||
case '0': { zustand = 3; break; }
|
||||
case '1': { zustand = 0; break; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 3: { switch (symbol) {
|
||||
case '0': { zustand = 3; break; }
|
||||
case '1': { zustand = 0; break; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
zaehler = zaehler + 1; // naechstes Symbol
|
||||
|
||||
}
|
||||
|
||||
if (zustand==3) { return true; } // Endzustand?
|
||||
else { return false; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
33
docs/zentralabitur/datenbanken.md
Normal file
33
docs/zentralabitur/datenbanken.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# 5. Datenbanken und Java
|
||||
|
||||
Der Zugriff auf eine relationale Datenbank ist in Java fest eingebaut. Dieser Vorgang ist so komplex, dass er für den Schulgebrauch durch die beiden Klassen *DatabaseConnector* und *QueryResult* gekapselt wird.
|
||||
|
||||
Das folgende Beispiel zeigt, wie eine beliebige SQL-Anfrage an eine vorhandene MySQL-Datenbank "Millionär" weitergeleitet und das tabellarische Ergebnis Zeile für Zeile auf dem Bildschirm ausgedruckt wird:
|
||||
|
||||
```java
|
||||
import db.*;
|
||||
|
||||
public class Datenbanktest {
|
||||
|
||||
public void testeAnfrage(String anfrage) {
|
||||
DatabaseConnector con = new DatabaseConnector("localhost",3306,"millionaer","root","root");
|
||||
con.executeStatement(anfrage);
|
||||
QueryResult res = con.getCurrentQueryResult();
|
||||
if (res != null) {
|
||||
for (int i = 0; i < res.getColumnCount(); i++) {
|
||||
System.out.print(res.getColumnNames()[i]+"\t");
|
||||
}
|
||||
System.out.println();
|
||||
for (int j = 0; j < res.getRowCount(); j++) {
|
||||
for (int i = 0; i < res.getColumnCount(); i++) {
|
||||
System.out.print(res.getData()[j][i]+"\t");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
} else {
|
||||
System.out.println(con.getErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
268
docs/zentralabitur/graph.md
Normal file
268
docs/zentralabitur/graph.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# 3. Graphstrukturen (LK)
|
||||
|
||||
Als Spezialfall eines Graphen betrachten wir hier den ungerichteten, gewichteten Graphen. D.h. eine Kantenverbindung zwischen zwei Knoten wird grundsätzlich in beide Richtungen interpretiert und jeder Kante wird ein Gewicht (z.B. die Weglänge oder die benötigte Zeit) zugeordnet. Durch die Gesamtheit aller Kanten ergibt sich der Graph.
|
||||
|
||||
|
||||
## Eine Datenstruktur für Graphen
|
||||
|
||||
Um Graphen zu modellieren, stehen die drei Klassen *Vertex*, *Edge* und *Graph* zur Verfügung.
|
||||
|
||||
|
||||
### Einen Graphen aufbauen
|
||||
|
||||
Der folgende Quelltext baut mithilfe der drei Klassen einen einfachen Beispielgraphen auf:
|
||||
|
||||

|
||||
|
||||
Im Quelltext ist deutlich zu sehen, dass die Knoten und Kanten des Graphen getrennt erzeugt und dem Graphen zugewiesen werden.
|
||||
|
||||
```java
|
||||
Graph g = new Graph();
|
||||
|
||||
Vertex v1 = new Vertex("1");
|
||||
Vertex v2 = new Vertex("2");
|
||||
Vertex v3 = new Vertex("3");
|
||||
|
||||
g.addVertex(v1);
|
||||
g.addVertex(v2);
|
||||
g.addVertex(v3);
|
||||
|
||||
Edge e1 = new Edge(v1,v2,5);
|
||||
Edge e2 = new Edge(v1,v3,7);
|
||||
Edge e3 = new Edge(v2,v3,13);
|
||||
|
||||
g.addEdge(e1);
|
||||
g.addEdge(e2);
|
||||
g.addEdge(e3);
|
||||
```
|
||||
|
||||
|
||||
### Beispiel: Nächster Nachbar
|
||||
|
||||
Als exemplarische Anwendung der drei Klassen wird hier in einem gegebenem Graphen für einen gegebenen Knoten der nächstgelegene Nachbar bestimmt, d.h. die ID desjenigen Knoten, der mit dem gegebenen Knoten direkt verbunden ist und dessen zugehörige Kante das geringste Gewicht aufweist.
|
||||
|
||||
Dazu wird mithilfe der Methode *getNeighbours()* zunächst eine Liste aller Nachbarknoten erfragt. Diese Liste wird nun durchlaufen. Dabei wird jeweils überprüft, ob das bisherige minimale Kantengewicht von der aktuellen Kante (vom Ausgangsknoten zum Nchbarknoten) unterboten werden kann.
|
||||
|
||||
```java
|
||||
public String findeNaechstenNachbarn(String ID) {
|
||||
|
||||
Vertex startnode = g.getVertex(ID);
|
||||
if (startnode==null) { return "Knoten unbekannt"; }
|
||||
else {
|
||||
List<Vertex> l = g.getNeighbours(startnode);
|
||||
l.toFirst();
|
||||
String next_neighbour = "kein Nachbar";
|
||||
double min = 10000.0;
|
||||
while (l.hasAccess()) {
|
||||
Vertex node = l.getContent();
|
||||
Edge e = g.getEdge(startnode,node);
|
||||
double distance = e.getWeight();
|
||||
if (distance<min) {
|
||||
min = distance;
|
||||
next_neighbour = node.getID();
|
||||
}
|
||||
l.next();
|
||||
}
|
||||
return next_neighbour;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Strategien zum Graphdurchlauf
|
||||
|
||||
Ziel des Graphendurchlauf ist es, alle Knoten eines Graphen nach einer festgelegten Strategie zu besuchen. Das Besuchen steht dabei stellvertretend für die Verarbeitung des Knotens, in den folgenden Quellcode-Beispielen wird dazu lediglich die ID des aktuelle besuchten Knotens ausgedruckt.
|
||||
|
||||
Beim Durchlauf von Graphen kommen analog zu [Bäumen](baum.md) wieder die beiden Strategien der Tiefensuche und der Breitensuche in Betracht. Die Tiefensuche kann entweder rekursiv oder mithilfe eines Stacks umgesetzt werden. Um die Analogie zur Breitensuche herausarbeiten zu können, wird an dieser Stelle ein Stack verwendet. Das im nächsten Abschnitt beschriebene Backtracking, das auf der Tiefensuche basiert, wird dagegen vollständig rekursiv realisiert.
|
||||
|
||||
|
||||
### Tiefensuche
|
||||
|
||||
Zentrales Hilfsmittel der Tiefensuche ist ein Stack. Er sorgt dafür, dass ausgehend vom aktuellen Knoten immer der letzte Nachbar weiter verfolgt wird. (*Bemerkung:* Die Festlegung auf den letzten Nachbarn wurde hier willkürlich getroffen. Da es keine Anordnung von Nachbarknoten gibt, hat diese Festlegung keine Auswirkung auf die Funktionsfähigkeit des Verfahrens, sondern lediglich auf die Reihenfolge der besuchten Knoten.)
|
||||
|
||||
Zu Beginn wird der Startknoten auf den Stack gelegt, anschließend alle seine noch nicht besuchten Nachbarn. Da nun der oberste Knoten vom Stack genommen und weiter verarbeitet wird, geht die Verarbeitung also mit dem zuletzt auf den Stack gelegten Nachbarknoten weiter.
|
||||
|
||||
```java
|
||||
public void sucheTief(Graph g, String startid) {
|
||||
|
||||
g.setAllVertexMarks(false);
|
||||
|
||||
Vertex startnode = g.getVertex(startid);
|
||||
Stack<Vertex> s = new Stack<Vertex>();
|
||||
s.push(startnode);
|
||||
|
||||
while (!s.isEmpty()) {
|
||||
|
||||
Vertex aktuell = (Vertex) s.top();
|
||||
if (!aktuell.isMarked()) { // VERARBEITEN
|
||||
System.out.println(aktuell.getID());
|
||||
}
|
||||
aktuell.setMark(true);
|
||||
|
||||
List<Vertex> l = g.getNeighbours(aktuell);
|
||||
l.toFirst();
|
||||
boolean gefunden = false;
|
||||
|
||||
while (l.hasAccess()) { // nicht-markieter Knoten vorhanden?
|
||||
Vertex nachbar = l.getContent();
|
||||
if (!nachbar.isMarked()) {
|
||||
s.push(nachbar);
|
||||
gefunden = true;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
l.next();
|
||||
}
|
||||
}
|
||||
|
||||
if (!gefunden) { s.pop(); }
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Breitensuche
|
||||
|
||||
Im Unterschied zur Tiefensuche wird bei der Breitensuche eine Schlange als Hilfsdatenstruktur eingesetzt. Da für jeden aktuellen alle noch nicht besuchten Nachbarn an die Schlange angehängt werden, ergibt sich eine "ebenenweise" Abarbeitung des Graphen.
|
||||
|
||||
```java
|
||||
private Queue<Vertex> nichtDoppeltEinfuegen(Queue<Vertex> q, Vertex node) {
|
||||
|
||||
Queue<Vertex> qneu = new Queue<Vertex>();
|
||||
boolean insert = true;
|
||||
|
||||
while (!q.isEmpty()) {
|
||||
Vertex n = q.front();
|
||||
q.dequeue();
|
||||
if (n.getID().equals(node.getID())) {
|
||||
insert = false;
|
||||
}
|
||||
qneu.enqueue(n);
|
||||
}
|
||||
|
||||
if (insert) { qneu.enqueue(node); }
|
||||
|
||||
return qneu;
|
||||
}
|
||||
```
|
||||
|
||||
Die Hilfsmethode, die das nicht-doppelte Einfügen in die Schlange realisiert, sei hier der Vollständigkeit halber mit angegeben:
|
||||
|
||||
```java
|
||||
private Queue<Vertex> nichtDoppeltEinfuegen(Queue<Vertex> q, Vertex node) {
|
||||
|
||||
Queue<Vertex> qneu = new Queue<Vertex>();
|
||||
boolean insert = true;
|
||||
|
||||
while (!q.isEmpty()) {
|
||||
Vertex n = q.front();
|
||||
q.dequeue();
|
||||
if (n.getID().equals(node.getID())) {
|
||||
insert = false;
|
||||
}
|
||||
qneu.enqueue(n);
|
||||
}
|
||||
|
||||
if (insert) { qneu.enqueue(node); }
|
||||
|
||||
return qneu;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Backtracking
|
||||
|
||||
Der folgende Backtracking-Algorithmus bestimmt alle Wege, die von einem gegebenen Startknoten zu einem gegebenen Zielknoten führen. Es ist leicht einzusehen, dass dieser Algorithmus exponentielle Laufzeit haben muss.
|
||||
|
||||
Die Startmethode trifft alle Vorbereitungen, um das Backtracking in Gang zu setzen.
|
||||
|
||||
```java
|
||||
public void sucheWeg(Graph g, String vonID, String nachID) {
|
||||
|
||||
g.setAllVertexMarks(false);
|
||||
|
||||
Vertex vonKnoten = g.getVertex(vonID);
|
||||
Vertex nachKnoten = g.getVertex(nachID);
|
||||
|
||||
List<Vertex> knotenliste = new List<Vertex>();
|
||||
vonKnoten.setMark(true); // Markiere den Startknoten
|
||||
knotenliste.append(vonKnoten);
|
||||
|
||||
backtrack(g, vonKnoten, nachKnoten, knotenliste); // Starte die Rekursion!
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Die zentrale Idee des Algorithmus besteht darin, dass ausgehend von einem bislang ermittelten Pfad mit seinem bisherigen Endknoten ein Weg beginnend mit dem bisherigen Endknoten und endend mit dem gegebenen Endknoten gefunden werden soll. Der rekursive Algoritthmus sorgt dafür, dass für jedes Zwischenergebnis (also jeden Zwischenpfad) alle weiteren möglichen Pfade gesucht werden. Dabei werden nur neue (Nachbar-)Knoten berücksichtigt, die noch nicht besucht worden sind.
|
||||
|
||||
Auf das eigentliche Backtracking (Speichere die Länge des bislang kürzesten gefundenen Weges und verwerfe Alternativwege, deren Länge bereits größer ist.) wird hier zur Vereinfachung verzichtet.
|
||||
|
||||
```java
|
||||
private void backtrack(Graph g, Vertex vonKnoten, Vertex nachKnoten, List<Vertex> weg) {
|
||||
|
||||
if (vonKnoten == nachKnoten) { // Ziel schon erreicht?
|
||||
String hilf = druckeWegAus(g, weg);
|
||||
System.out.println(hilf);
|
||||
}
|
||||
else {
|
||||
|
||||
List<Vertex> nachbarKnoten = g.getNeighbours(vonKnoten);
|
||||
nachbarKnoten.toFirst();
|
||||
|
||||
while (nachbarKnoten.hasAccess()) { // Bearbeite alle Nachbarknoten
|
||||
|
||||
Vertex knoten = nachbarKnoten.getContent();
|
||||
|
||||
if (!knoten.isMarked()) {
|
||||
knoten.setMark(true);
|
||||
weg.append(knoten);
|
||||
|
||||
backtrack(g, knoten, nachKnoten, weg); // Suche ueber diesen Nachbarn weiter nach dem Ziel
|
||||
|
||||
knoten.setMark(false);
|
||||
weg.toLast();
|
||||
weg.remove();
|
||||
}
|
||||
|
||||
nachbarKnoten.next();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Zum Ausdrucken eines vollständig gefundenen Pfades vom Start- zum Endknoten wird eine Hilfsmethode verwendet, die hier der Vollständigkeit halber mit angegeben ist.
|
||||
|
||||
```java
|
||||
private String druckeWegAus(Graph g, List<Vertex> wegliste) {
|
||||
|
||||
// Bestimme zunaechst die Weglaenge
|
||||
double wegLaenge = 0;
|
||||
wegliste.toFirst();
|
||||
Vertex wegKnoten1 = wegliste.getContent();
|
||||
wegliste.next();
|
||||
|
||||
while (wegliste.hasAccess()) {
|
||||
Vertex wegKnoten2 = wegliste.getContent();
|
||||
Edge e = g.getEdge(wegKnoten1, wegKnoten2);
|
||||
double distanz = e.getWeight();
|
||||
wegLaenge = wegLaenge + distanz;
|
||||
wegKnoten1 = wegKnoten2;
|
||||
wegliste.next();
|
||||
}
|
||||
|
||||
// Baue Zeichenkette zusammen
|
||||
wegliste.toFirst();
|
||||
String s = wegLaenge + ": ";
|
||||
|
||||
while (wegliste.hasAccess()) {
|
||||
Vertex wegKnoten = wegliste.getContent();
|
||||
s = s + wegKnoten.getID()+" ";
|
||||
wegliste.next();
|
||||
}
|
||||
|
||||
return s+"\n";
|
||||
}
|
||||
```
|
||||
BIN
docs/zentralabitur/img/automat.png
Normal file
BIN
docs/zentralabitur/img/automat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/zentralabitur/img/baum1.png
Normal file
BIN
docs/zentralabitur/img/baum1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/zentralabitur/img/baum2.gif
Normal file
BIN
docs/zentralabitur/img/baum2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/zentralabitur/img/graph.png
Normal file
BIN
docs/zentralabitur/img/graph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
10
docs/zentralabitur/index.md
Normal file
10
docs/zentralabitur/index.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# II. Zentralabitur NRW
|
||||
|
||||
Der vorliegende Bereich bespricht ausführlich den Umgang mit den Zentralabiturklassen in der Qualifikationsphase.
|
||||
|
||||
- [Lineare Datenstrukturen](linear.md)
|
||||
- [Baumstrukturen](baum.md)
|
||||
- [Graphstrukturen (LK)](graph.md)
|
||||
- [Compilerbau (LK)](compilerbau.md)
|
||||
- [Datenbanken und Java](datenbanken.md)
|
||||
- [Netzwerkprogrammierung (LK)](netzwerke.md)
|
||||
306
docs/zentralabitur/linear.md
Normal file
306
docs/zentralabitur/linear.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# 1. Lineare Datenstrukturen
|
||||
|
||||
Kommen Datenstrukturen zum Einsatz, geht es meist darum, beliebig viele Objekte in einer passenden Datenstruktur zu verwalten, z.B. alle Schüler einer Schule. In einem solchen Falle macht es keinen Sinn, alle Eigenschaften eines Schülers als primitive Datentypen zu modellieren und für jede Eigenschaft ein eigenes Feld o.ä. zu bilden. Deshalb wird zu Beginn dieses Kapitels zunächst erläutert, wie Objekte als Datensatzspeicher verwendet werden können. Im Anschluss daran werden die linearen Datenstrukturen Feld, Liste, Stack und Queue behandelt.
|
||||
|
||||
|
||||
|
||||
## Objekte vs. primitive Datentypen
|
||||
|
||||
|
||||
### Objekte als Datenspeicher
|
||||
|
||||
In diesem Abschnitt möchten wir eine Schülerverwaltung modellieren. Dazu entwerfen wir eine eigene Klasse *Schueler* und modellieren zunächst die benötigten Eigenschaften als Attribute. Bei Bedarf kommen noch Methoden zum Ändern und Abfragen der Attribute hinzu.
|
||||
|
||||
Im vorliegenden Beispiel sind die beiden Attribute Alter und Name definiert worden. Beide müssen im beim Erzeugen eines Schüler-Objekts übergeben werden (vgl. Konstruktor), beide können mithilfe der set-Methoden auch nachträglich verändert und mithilfe der get-Methoden jederzeit abgefragt werden.
|
||||
|
||||
```java
|
||||
public class Schueler {
|
||||
|
||||
private String name;
|
||||
private int alter;
|
||||
|
||||
public Schueler(String name, int alter) {
|
||||
this.name = name;
|
||||
this.alter = alter;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setAlter(int alter) {
|
||||
this.alter = alter;
|
||||
}
|
||||
|
||||
public int getAlter() {
|
||||
return alter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Objekte einer Klasse, die wie in diesem Beispiel lediglich als Datenspeicher fungieren und selbst keine oder fast keine eigene Programmlogik besitzen, werden in Java als **Beans** ("Kaffeebohnen") bezeichnet.
|
||||
|
||||
|
||||
### Wrapper-Klassen
|
||||
|
||||
Die einzige Datenstruktur, die in Java neben Objekten auch primitive Datentypen verwalten kann, ist das Feld. Alle anderen Datenstrukturen (z.B. Liste, Stack, Queue usw.) erlauben lediglich das Speichern von Objekten.
|
||||
|
||||
Doch was ist zu tun, wenn es für eine Anwendung ausreicht, lediglich Zahlen (z.B. ISBN-Nummern oder Schuhgrößen) zu speichern? Muss dann erst umständlich eine wie im Schüler-Beispiel beschriebene eigene Klasse entworfen werden?
|
||||
|
||||
Für diese seltenen Fälle sind in Java so genannte **Wrapper-Klassen** eingebaut.
|
||||
|
||||
primitiver Datentyp | Wrapper-Klasse | Service-Methode
|
||||
-----|-----|-----
|
||||
int | Integer | intValue()
|
||||
double | Double | doubleValue()
|
||||
char | Character | charValue()
|
||||
boolean | Boolean | boolValue()
|
||||
|
||||
Beispiel: Ein primitiver int-Wert 17 wird in einem Objekt der Klasse Integer "verpackt" (Boxing).
|
||||
|
||||
```java
|
||||
int i = 17;
|
||||
Integer iobj = new Integer(i); // Boxing
|
||||
```
|
||||
|
||||
Um ihn anschließend wieder aus dem Objekt herauszulesen (Unboxing), steht für jede Wrapper-Klasse eine entsprechende Service-Methode (vgl. Tabelle) zur Verfügung.
|
||||
|
||||
```java
|
||||
int j = iobj.intValue(); // Unboxing
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Feld
|
||||
|
||||
Die naheliegende Datenstruktur zur Verwaltung von Objekten ist das Feld. Es ist in Java bereits eingebaut und ist recht einfach zu nutzen.
|
||||
|
||||
|
||||
### Verwendung eines Felds
|
||||
|
||||
An dieser Stelle soll das Beispiel einer Schülerverwaltung wieder aufgegriffen werden. Eine entsprechende Klasse Schüler ist bereits im vorherigen Abschnitt (vgl. [Objekte als Datenspeicher](#objekte-als-datenspeicher)) entwickelt worden.
|
||||
|
||||
Die Erzeugung und Verwendung eines Feld über Schüler-Objekten verläuft analog zur Verwendung eines Felds über primitiven Datentypen (vgl. auch Abschnitt [Felder](../grundlagen/javagrundlagen.md#felder)).
|
||||
|
||||
Wie im Quelltext zu sehen ist, werden zunächst die Schüler-Objekte erzeugt und anschließend wie in einem Feld üblich an die entsprechenden Positionen gesetzt. Die Methode *istVorhanden()* zeigt exemplarisch den Durchlauf durch ein Feld.
|
||||
|
||||
```java
|
||||
public class Schuelerverwaltung {
|
||||
|
||||
private Schueler[] meineschueler;
|
||||
|
||||
public Schuelerverwaltung() {
|
||||
Schueler s1 = new Schueler("Otto",14);
|
||||
Schueler s2 = new Schueler("Susi",13);
|
||||
Schueler s3 = new Schueler("Hans",15);
|
||||
|
||||
meineschueler = new Schueler[3];
|
||||
meineschueler[0] = s1;
|
||||
meineschueler[1] = s2;
|
||||
meineschueler[2] = s3;
|
||||
}
|
||||
|
||||
public boolean istVorhanden(String suchname) {
|
||||
for (int i=0; i<meineschueler.length; i++) {
|
||||
Schueler derschueler = meineschueler[i];
|
||||
String name = derschueler.getName();
|
||||
if (name.equals(suchname)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Diskussion
|
||||
|
||||
Vorteile:
|
||||
|
||||
- Das Feld ist in Java eingebaut und kann ohne zusätzlichen Aufwand eingesetzt werden.
|
||||
- Der Zugriff ist auf jede Feldposition möglich. Damit kann wieder eine schnelle binäre Suche mit logarithmischem Aufwand realisiert werden.
|
||||
|
||||
Nachteile:
|
||||
|
||||
- Das Feld ist statisch, d.h. seine Größe muss beim Erzeugen festgelegt werden. Dadurch könnte es entweder viel zu groß (Speicherverschwendung) oder viel zu klein (kein nachträgliches Einfügen möglich) sein.
|
||||
|
||||
Dem Nachteil der statischen Größe begegnen dynamische Datenstrukturen wie die Liste (vgl. Abschnitt [Liste](#liste)). Die ideale Kombination von schneller Suche und Dynamik stellt der Suchbaum dar (vgl. Abschnitt [Suchbaum](baum.md#suchbaum)).
|
||||
|
||||
|
||||
|
||||
## Liste
|
||||
|
||||
Die Liste ist eine lineare Datenstruktur, die sequentiell (d.h. von vorn nach hinten) durchlaufen wird. Jedes Element der Liste hat lediglich Zugriff auf seinen Nachfolger, wodurch sich eine verkettete Struktur ergibt.
|
||||
|
||||
Die Liste ist dynamisch, da an jeder Position neue Elemente in die Kette eingefügt werden können. Eine Suche auf einer Liste hat lineare Laufzeit, da nur der vollständige Durchlauf von vorn nach hinten möglich ist.
|
||||
|
||||
Die Abiturklasse *List* ist generisch, d.h. bereits beim Erzeugen wird festgelegt, von welcher Klasse die Objekte sein müssen, die in der Liste verwaltet werden sollen. Dabei ist es natürlich möglich (wenn auch nur selten sinnvoll), auch Objekte von Unterklassen dieser Klasse zu verwalten.
|
||||
|
||||
|
||||
### Eine Liste aufbauen
|
||||
|
||||
Im folgenden Beispiel wird beim Erzeugen der Liste festgelegt, dass Objekte der Klasse *String* verwaltet werden sollen. Mit Hilfe der Methode *append* werden die Zeichenketten jeweils an das Ende der Liste gehängt, wodurch sich die Reihenfolge *Babsi - Franzi - Susi* ergibt.
|
||||
|
||||
```java
|
||||
String s1 = "Babsi";
|
||||
String s2 = "Franzi";
|
||||
String s3 = "Susi";
|
||||
|
||||
List<String> l = new List<String>();
|
||||
l.append(s1);
|
||||
l.append(s2);
|
||||
l.append(s3);
|
||||
```
|
||||
|
||||
### Eine Liste durchlaufen
|
||||
|
||||
Beim Durchlaufen einer Liste (z.B. um eine Suche zu realisieren) ist ein Sprung an den Anfang der Liste mithilfe der Methode *toFirst()* erforderlich. Anschließend hilft die Anfrage *hasAccess()* dabei zu erfragen, ob noch ein Listenelement verfügbar ist oder durch das Weiterlaufen durch die Liste mit der Methode *next()* bereits das Ende der Liste erreicht ist.
|
||||
|
||||
Wird die Zeichenketten-Liste aus dem vorherigen Abschnitt zugrunde gelegt, so lautet die Ausgabe der Schleife: *Babsi -> Franz -> Susi -> ENDE!*
|
||||
|
||||
```java
|
||||
l.toFirst();
|
||||
|
||||
while (l.hasAccess()) {
|
||||
String s = l.getContent();
|
||||
System.out.print(s+" -> ");
|
||||
l.next();
|
||||
}
|
||||
|
||||
System.out.println("ENDE!");
|
||||
```
|
||||
|
||||
|
||||
### Eine Liste verändern
|
||||
|
||||
Für das nächste Beispiel stellen wir uns eine Musiksammlung vor, in der Objekte der Klasse *Titel* verwaltet werden. Für jedes Titelobjekt ist eine Bewertung zwischen 1 und 5 angegeben, die durch die Methode *getBewertung()* erfragt werden kann.
|
||||
|
||||
Damit durchläuft die Schleife die Liste aller Titelobjekte und löscht mit der Methode *remove()* diejenigen Titel, die eine Bewertung 1 erhalten haben. Zusätzlich wird die Anzahl der gelöschten Objekte in der Variablen *counter* mitgezählt.
|
||||
|
||||
```java
|
||||
l.toFirst();
|
||||
int counter = 0;
|
||||
|
||||
while (l.hasAccess()) {
|
||||
|
||||
Titel t = l.getContent();
|
||||
if (t.getBewertung()==1) {
|
||||
l.remove();
|
||||
counter++;
|
||||
}
|
||||
else {
|
||||
l.next();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### In eine sortierte Liste einfügen
|
||||
|
||||
Eine Sortierung einer Liste kann mitunter ebenfalls sinnvoll sein. In unserem Beispiel der Musiksammlung könnten die Titelobjekte sortiert nach der Bewertung abgelegt werden. Damit die Sortierung der Liste nicht verloren geht, muss dem Benutzer der Musiksammlung eine eigene Methode zum sortierten Einfügen eines neuen Titels zur Verfügung gestellt werden.
|
||||
|
||||
Die Schleife wird solange durchlaufen, bis ein Titel gefunden wurde, der eine höhere oder gleich hohe Bewertung als der neue Titel besitzt. An dieser Stelle wird das zugehörige Titelobjekt mit der Methode *insert* vor dem aktuellen Listenobjekt eingefügt.
|
||||
|
||||
```java
|
||||
public void fuegeTitelSortiertEin(Titel t, List l) {
|
||||
l.toFirst();
|
||||
while (l.hasAccess()) {
|
||||
Titel aktuell = l.getContent();
|
||||
if (t.getBewertung() <= aktuell.getBewertung()) {
|
||||
l.insert(t);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
l.next();
|
||||
}
|
||||
}
|
||||
l.append(t);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Stack und Queue
|
||||
|
||||
Als Spezialfall der Liste sind sich Stack und Queue sehr ähnlich, weshalb sie oft gemeinsam besprochen werden. Die Queue (Schlange) ist eine FIFO-Datenstruktur (first in, first out) - die Objekte werden in der Reihenfolge verwaltet, in der sie in die Datenstruktur eingefügt worden sind. Direkter Zugriff besteht nur auf das vorderste Element der Schlange (den Kopf), eingefügt wird immer hinten (am Schlangenende). Der Stack (Stapel) hingegen wird als LIFO-Datenstruktur (last in, first out) bezeichnet. Hier werden neue Objekte immer oben auf den Stapel gelegt, direkter Zugriff besteht ebenfalls nur auf das oberste Objekt des Stapels.
|
||||
|
||||
|
||||
### Einen Stack aufbauen
|
||||
|
||||
Wie bei einer Liste ist beim Erzeugen eines Stacks zunächst die Klasse der Objekte anzugeben, die im Stack verwaltet werden sollen. Im nachfolgenden Beispiel werden drei Zeichenketten auf den Stack gelegt. Durch die LIFO-Struktur ergibt sich auf dem Stapel von oben nach unten gesehen die Abfolge *Hans - Maria - Manfred*.
|
||||
|
||||
```java
|
||||
String s1 = "Manfred";
|
||||
String s2 = "Maria";
|
||||
String s3 = "Hans";
|
||||
|
||||
Stack<String> s = new Stack<String>();
|
||||
|
||||
s.push(s1);
|
||||
s.push(s2);
|
||||
s.push(s3);
|
||||
```
|
||||
|
||||
|
||||
### Einen Stack durchlaufen
|
||||
|
||||
Der Stapel kann mit einer Schleife leicht von oben nach unten durchlaufen werden.
|
||||
|
||||
!!! Hinweis
|
||||
Dabei wird der Stapel abgebaut, d.h. der Zugriff auf die Objekte geht verloren. Ist dies unerwünscht, müssen die einzelnen Objekte während des Durchlaufs in einer anderen Datenstruktur zwischengespeichert werden.
|
||||
|
||||
```java
|
||||
while (!s.isEmpty()) {
|
||||
|
||||
String aktuell = s.top();
|
||||
|
||||
System.out.println(aktuell);
|
||||
|
||||
s.pop();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Eine Queue aufbauen
|
||||
|
||||
Wie bei einer Liste ist beim Erzeugen einer Queue zunächst die Klasse der Objekte anzugeben, die in der Queue verwaltet werden sollen. Im nachfolgenden Beispiel werden drei Zeichenketten in die Schlange eingefügt. Durch die FIFO-Struktur ergibt sich in der Schlange die Abfolge *Manfred - Maria - Hans*.
|
||||
|
||||
```java
|
||||
String s1 = "Manfred";
|
||||
String s2 = "Maria";
|
||||
String s3 = "Hans";
|
||||
|
||||
Queue<String> s = new Queue<String>();
|
||||
|
||||
s.enqueue(s1);
|
||||
s.enqueue(s2);
|
||||
s.enqueue(s3);
|
||||
```
|
||||
|
||||
|
||||
### Eine Queue durchlaufen
|
||||
|
||||
Eine Schlange kann mit einer Schleife leicht von vorn nach hinten durchlaufen werden.
|
||||
|
||||
!!! Hinweis
|
||||
Dabei wird die Schlange abgebaut, d.h. der Zugriff auf die Objekte geht verloren. Ist dies unerwünscht, müssen die einzelnen Objekte während des Durchlaufs in einer anderen Datenstruktur zwischengespeichert werden.
|
||||
|
||||
```java
|
||||
while (!s.isEmpty()) {
|
||||
|
||||
String aktuell = s.front();
|
||||
|
||||
System.out.println(aktuell);
|
||||
|
||||
s.dequeue();
|
||||
}
|
||||
```
|
||||
165
docs/zentralabitur/netzwerke.md
Normal file
165
docs/zentralabitur/netzwerke.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# 6. Netzwerkprogrammierung (LK)
|
||||
|
||||
Der nachfolgende Abschnitt gliedert sich nach den drei Klassen der Netzwerkbibliothek (Connection, Client und Server), die im LK die Grundlage von Client-Server-Projekten darstellt.
|
||||
|
||||
|
||||
## Die Klasse Connection
|
||||
|
||||
Objekte der Klasse Connection ermöglichen eine Netzwerkverbindung zu einem Server mittels TCP/IP-Protokoll. Nach Verbindungsaufbau können Zeichenketten (Strings) zum Server gesendet und von diesem empfangen werden.
|
||||
|
||||
### TimeClient
|
||||
|
||||
Im folgenden Beispiel wird Kontakt zu einem öffentlichen Time-Server aufgenommen und ohne das Versenden einer eigenen Nachricht eine Nachricht des Servers (die aktuelle Zeit) abgewartet.
|
||||
|
||||
```java
|
||||
import netz.*;
|
||||
|
||||
public class TimeClient {
|
||||
|
||||
public String getTime() {
|
||||
Connection con = new Connection("time.fu-berlin.de",13);
|
||||
return con.receive();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### EchoClient
|
||||
|
||||
Im Gegensatz zum vorherigen Beispiel wird zunächst eine eigene Nachricht zum (Echo-)Server geschickt, bevor erneut auf Antwort des Servers gewartet wird.
|
||||
|
||||
```java
|
||||
import netz.*;
|
||||
|
||||
public class EchoClient1 {
|
||||
|
||||
Connection con;
|
||||
|
||||
public void starteClient(String pIPAdresse, int pPort) {
|
||||
con = new Connection(pIPAdresse, pPort);
|
||||
}
|
||||
|
||||
public String sendeNachricht(String pNachricht) {
|
||||
con.send(pNachricht);
|
||||
return con.receive();
|
||||
}
|
||||
|
||||
public void beendeVerbindung() {
|
||||
con.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Die Klasse Client
|
||||
|
||||
Objekte von Unterklassen der abstrakten Klasse Client ermöglichen Netzwerkverbindungen zu einem Server mittels TCP/IP-Protokoll. Nach Verbindungsaufbau können Zeichenketten (Strings) zum Server gesendet und von diesem empfangen werden, wobei der Nachrichtenempfang nebenläufig geschieht. Jede empfangene Nachricht wird einer Ereignisbehandlungsmethode übergeben, die in Unterklassen implementiert werden muss (vgl. Methode processMessage).
|
||||
|
||||
### EchoClient
|
||||
|
||||
Das folgende Beispiel zeigt eine Umsetzung des Echo-Clients als Unterklasse der Klasse Client. Zu beachten ist hier das Überlagern der Methode processMessage.
|
||||
|
||||
```java
|
||||
import netz.*;
|
||||
|
||||
public class EchoClient2 extends Client {
|
||||
|
||||
public EchoClient2(String pIPAdresse, int pPortNr) {
|
||||
super(pIPAdresse, pPortNr);
|
||||
}
|
||||
|
||||
public void processMessage(String pMessage) {
|
||||
System.out.println(pMessage);
|
||||
}
|
||||
|
||||
public void sendeNachricht(String pNachricht) {
|
||||
this.send(pNachricht);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Die Klasse Server
|
||||
|
||||
Objekte von Unterklassen der abstrakten Klasse Server ermöglichen das Anbieten von Serverdiensten, so dass Clients Verbindungen zum Server mittels TCP/IP-Protokoll aufbauen können. Verbindungsannahme, Nachrichtenempfang und Verbindungsende geschehen nebenläufig. Auf diese Ereignisse muss durch Überschreiben der entsprechenden Ereignisbehandlungsmethoden reagiert werden (vgl. Methoden processNewConnection, processMessage und processClosingConnection).
|
||||
|
||||
### EchoServer
|
||||
|
||||
Das fgolgende Beispiel zeigt eine Umsetzung eines Echo-Servers als Unterklasse der Klasse Server. Zu beachten ist hier das Überlagern der drei genannten Methoden.
|
||||
|
||||
```java
|
||||
import netz.*;
|
||||
|
||||
public class EchoServer extends Server {
|
||||
|
||||
public EchoServer (int pPortNum) {
|
||||
super(pPortNum);
|
||||
}
|
||||
|
||||
public void processNewConnection(String pClientIP, int pClientPort) {
|
||||
System.out.println("! Neue Verbindung " + pClientIP + ":" + pClientPort);
|
||||
}
|
||||
|
||||
public void processMessage(String pClientIP, int pClientPort, String pMessage) {
|
||||
System.out.println(">>" + pClientIP + ":" + pClientPort + " : " + pMessage);
|
||||
this.send(pClientIP, pClientPort, pMessage);
|
||||
}
|
||||
|
||||
public void processClosingConnection(String pClientIP, int pClientPort) {
|
||||
System.out.println("! Abmeldung Client " + pClientIP + ":" + pClientPort);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### RateServer
|
||||
|
||||
Das abschließende Beispiel setzt einen einfachen Zahlenraten-Server als Client-Server-Lösung mit beliebig vielen Mitspielern um.
|
||||
|
||||
```java
|
||||
import netz.*;
|
||||
import java.util.*;
|
||||
|
||||
public class RateServer extends Server {
|
||||
private Random zufall;
|
||||
private int ratezahl;
|
||||
|
||||
public RateServer(int pPortNr) {
|
||||
super(pPortNr);
|
||||
zufall = new Random();
|
||||
ratezahl = zufall.nextInt(1000)+1;
|
||||
System.out.println("Der Server ist gestartet. PortNr: "+pPortNr);
|
||||
}
|
||||
|
||||
public void processClosingConnection(String pClientIP, int pClientPort) {
|
||||
System.out.println(""+pClientIP+" "+pClientPort+" hat sich abgemeldet.");
|
||||
}
|
||||
|
||||
public void processMessage(String pClientIP, int pClientPort, String pNachricht) {
|
||||
try {
|
||||
int zahl = Integer.parseInt(pNachricht);
|
||||
if (zahl == ratezahl) {
|
||||
send(pClientIP, pClientPort,pClientIP+" "+pClientPort+"+ok Herzlichen Glückwunsch");
|
||||
System.out.println("Gewinner: " + pClientIP + ":" + pClientPort);
|
||||
ratezahl = zufall.nextInt(1000)+1;
|
||||
sendToAll("Neues Spiel");
|
||||
System.out.println("Neues Spiel gestartet");
|
||||
} else {
|
||||
if (zahl < ratezahl) {
|
||||
send(pClientIP, pClientPort,"+ok "+zahl+" ist zu klein");
|
||||
} else {
|
||||
send(pClientIP, pClientPort,"+ok "+zahl+" ist zu gross");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
send(pClientIP, pClientPort, "-err Bitte Zahl zwischen 1 und 1000 schicken");
|
||||
}
|
||||
System.out.println(""+pClientIP+" "+pClientPort+" "+pNachricht);
|
||||
}
|
||||
|
||||
public void processNewConnection(String pClientIP, int pClientPort) {
|
||||
send(pClientIP, pClientPort, "+ok Willkommen. "+pClientIP+" "+pClientPort);
|
||||
send(pClientIP, pClientPort, "Bitte Zahl schicken");
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user