Technischer Neustart: Verwendung von Zensical statt MkDocs

This commit is contained in:
2025-12-10 17:17:38 +01:00
commit e24deec4b4
28 changed files with 2764 additions and 0 deletions

232
docs/zentralabitur/baum.md Normal file
View 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:
![](img/baum2.gif)
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.
![](img/baum2.gif)
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.
![](img/baum2.gif)
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:
![](img/baum1.png)
```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());
}
}
```

View 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:
![](img/automat.png)
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; }
}
}
```

View 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
View 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:
![](img/graph.png)
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";
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View 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)

View 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();
}
```

View 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");
}
}
```