Skript: Fortgeschrittene Programmierung, Algorithmen und Datenstrukturen

Kursname: WWIIBI_ 202 Programmierung und Programmiertechniken 

Kompetenzziele

Die Studierenden kennen die Grundprinzipien der Programmierung und Objektorientierung und können diese in einer adäquaten Programmiersprache anwenden.
Sie sind in der Lage, einfache Problemstellungen algorithmisch zu formulieren, Algorithmen mit den Sprachelementen der Programmiersprache adäquat umzusetzen und Programme zu implementieren, zu testen und anzuwenden. Die Grundprinzipien der Objektorientierung können an modellhaften Szenarien analysiert und implementiert werden.
Die Studierenden sind in der Lage, eine objektorientierte Programmiersprache anzuwenden und damit auch komplexe Problemstellungen algorithmisch zu behandeln und anwenderfreundlich und effizient umzusetzen. Sie sind in der Lage, Algorithmen in verschiedenen Darstellungsarten zu verstehen und ihre Effizienz bzw. Qualität zu beurteilen, aber auch selbstständig Algorithmen und dazu erforderliche Datenstrukturen zu entwickeln und implementieren. Die Studierenden können angewandte Problemstellungen analysieren und bekannte Algorithmen und Datenstrukturen effizienzorientiert darauf anwenden und falls notwendig an die Problemstellungen anpassen.

Lehrinhalte

Fortgeschrittene Programmierung, Algorithmen und Datenstrukturen

Objektorientierte Programmierung mit

Algorithmen und Datenstrukturen

Fragen zur Selbstkontrolle

Online Skript

Referenzen

 

Ablauf der Vorlesung

Zeitliche Aufteilung der Themen auf die Vorlesungsblöcke. Das thematisch organisierte Inhaltsverzeichnis des Skripts ist hier zufinden.

1. Abstrakte Klassen (Wdh.), Interfaces(1), Sortieren(1)

2. Assertions, Ausnahmen(1), Interfaces(2)

3. Ausnahmen(2), Swing(1)

4. Swing(2), Suchen(1)

5. Sortieren(2), Suchen(2)

6. Listen, Stapel (Stack), Warteschlangen

7. Bäume

8. Generische Klassen, Collections und Iteratoren

9. Nebenläufiges Programmieren

 

Programmieren

 Dieser Abschnitt behandelt fortgeschrittene Javakonzepte:

Schnittstellen (Interfaces)

Javaklassen enthalten Methoden und Datenfelder. Die öffentlich zugänglichen Methoden und Datenfelder bilden die Schnittstellen einer Klasse. Schnittstellen sind wichtige Hilfsmittel bei Entwurf komplexer Softwaresysteme. Sie erlauben es unterschiedliche Subsysteme definiert zu koppeln (Coupling). Die Wechselwirkung zwischen diesen Subsystemen wird auch Coherence oder Cohesion genannt.

Schnittstellen unterstützen das Konzept des Information Hiding und sollten möglichst stabil sein. Stabile Schnittstellen erlauben es mehreren Teams unabhängig von einander zu arbeiten. Sie erlauben eine Trennung von

  • Spezifikation und
  • Implementierung

Schnittstellen (engl. Interfaces) sind ein wichtiges Sprachmittel beim Entwurf von Softwaresystemen.

Sie enthalten daher nur die für die Spezifikation wichtige Elemente:

  • Methodendeklarationen
  • öffentliche Attribute

Klassen hingegen enthalten die Spezifikation, alle Attribute sowie zusätzlich die Implementierung der Methoden durch die Methodenrümpfe.

Javaschnittstellen (Interfaces)

Java bietet im Gegensatz zu C++ das Schlüsselwort interface zur Beschreibung von Schnittstellen. Java-Interfaces muss man gedanklich als abstrakte Klassen verstehen, die selbst über keine Implementierung von Methoden verfügen.

Klassen enthalten die Spezifikation und zusätzlich die Implementierung der Methoden durch die Methoden Rümpfe/Blöcke. Schnittstellen in Java enthalten (in der Regel, siehe unten) nur die Spezifikation.

Eigenschaften Klasse abstrakte Klasse Schnittstelle
Öffentliche Attribute Ja Ja Ja
Private Attribute Ja Ja Nein
Methodenköpfe (die Spezifikation) Ja Ja Ja
Methodenrumpf Ja Nein (bei abstr. Meth.) Nein (in der Regel)
kann von Klasse erben Ja Ja Nein
kann von Schnittstelle erben Nein Nein Ja
kann eine oder mehrere Schnittstelle implementieren Ja Ja Nein

 Für Java-Schnittstellen gilt:

  • alle Attribute müssen öffentliche, finale und initialisierte Konstanten sein
  • alle Methoden sind abstrakt (abstract), öffentlich (public), nicht final und nicht statisch
  • sie geben keine Konstruktoren vor. Die Gründe sind:
    • Konstruktoren haben immer den Namen der aktuellen Klasse.
    • Ein Konstruktor in einer Implementierung kann nicht den Namen der implementierten Schnittstelle besitzen.

Notation einer Java-Schnittstelle

[public] interface Interface-name [extends Interface1, Interface2, ... ];
  • Das Schlüsselwort public erlaubt wie auch bei Klassen das Interface ausserhalb des zugehörigen Pakets zu benutzen
  • dem Schlüsselwort interface folgt der Name der Schnittstelle und
  • eine optionale Liste von Schnittstellen von denen die Schnittstelle erbt. Diese Liste von Schnittstellen wird mit dem Schlüsselwort extends eingeleitet

Schnittstellen können nur von Schnittstellen abgeleitet werden.

Klassen können nur von Klassen abgeleitet werden. Klassen implementieren jedoch beliebig viele Schnittstellen durch das Schlüsselwort implements.

[public|private|protected] class Klassenname1 extends Klassenname2 [implements Interface1, Interface2, ....]

Die Klasse Klassenname1 wird aus Klassenname2 abgeleitet und implementiert (Schlüsselwort implements) optional eines oder mehrere Interfaces.

Beispiel

Personen (class Person), sowie Lieferanten (class Supplier) implementieren das Interface BankAccount. BankAccount muss mit einer IBAN Nummer und dem Namen einer Bank umgehen können. Das Interface macht keinerlei Vorgaben zum Setzen dieses Daten. Das Erfassen dieser Daten obliegt dem Implementierer der Schnittstelle.

interface BankAccount {
   public long iban();
   public String bank();
}

class Person implements BankAccount {
   private String myIban;
   private String myBank;
   public name()  { // Implementierung
   }
   public long iban() {
      ...
      return myIban;
   }
   public String bank() {
      ...
      return myBank;
   }
}

class Supplier implements BankAccount {
   private String supplierIban;
   private String supplierBank;
   public String getLicense() { //Implementierung
      }
   public long iban() {
      ...
      return supplierIban;
   }
   public String bank() {
      return supplierBank;
   }
}
...
BankAccount b1 = new Person();
BankAccount b2 = new Supplier();
...
System.out.println(b1.bank());
System.out.println(b2.bank());

Die Klasse Person implementiert alle Methoden von BankAccount und verhält sich in allen Aspekten wie das Interface.

Für Klassen die Schnittstellen(Interfaces) implementieren gilt:

  • sie können beliebig viele Interfaces gleichzeitig implementieren.
  • sie implementieren alle Methoden und Attribute des Interface
    • implementieren sie nicht alle Aspekte des Interface müssen sie als abstrakte Klasse deklariert werden. Erst abgeleitete Klassen die alle Aspekte der Schnittstelle zur Verfügung stellen müssen nicht mehr abstrakt sein.

Schnittstellen und Vererbung

  • Schnittstellen können von Schnittstellen erben (Schlüsselwort extends)
    • Dies deutet für den Entwickler, dass er alle Methoden der Vererbungshierarchie implementieren muss. In diesen Aspekt verhalten sich Schnittstellenmethoden wie abstrakte Methoden.
  • Klassen können nicht von Schnittstellen erben. Sie implementieren Schnittstellen!

UML Notation

In UML Klassendiagrammen wird die Java Interface-beziehung als Verfeinerung mit gestrichelten Pfeilen gekennzeichnet: Alternativ kann man die zu implementierende Klasse auch mit einem Strich und einem Kreis darstellen:

UML Diagramm mit Java Schnittstellen

alternative Schnittstellenbeschreibung in UML

Anwendung von Schnittstellen (Interfaces)

Schnittstellen (Schlüsselwort interface) und Vererbung (Schlüsselwort extends) sind zwei Vererbungskonzepte in Java. Sie haben unterschiedliche Zielsetzungen bezüglich Strukturierung und Planung.

  • Schnittstellen (Interfaces)
    • fördern modularen Aufbau auf Softwaresystemen
      • erlauben unabhängige Implementierung und vereinfachen den Austausch von Schnittstellenimplementierungen
    • fordern nur die Implementierung bestimmter Methoden.
    • entkoppeln Systeme und fördern die genaue Spezifikation von benötigten Diensten (Verträge!)
    • schaffen Strukturen die in späteren Phasen der Softwareentwicklung die Komplexität der Implementierung senken
  • Vererbung
    • erleichert Codewiederverwendung
    • führt zu einer engeren Kopplung von Systemen.
      • Da immer eine Implementierung der Oberklasse genutzt werden muss. Man muss z.Bsp. den Konstruktor der Oberklasse immer nutzen
      • Eine Unterklasse muss immer alle Eigenschaften der Oberklasse aufweisen
  Vererbung (extends) Schnittstelle (interface)
Spezifikation wird von der Oberklasse vorgegeben wird vom Interface vorgegeben
Programmcode wird von der Oberklasse vorgegeben.
wird von einer abstrakten Oberklasse gefordert
Interface fordert eine Implementierung
Mehrfachvererbung/Implementierung nein ja
Abhängigkeit der Unterklasse/Interfaceimplementierung hängt stark von der Oberklasse ab hängt schwächer vom zu implementierenden Interface ab

Implikationen

Schnittstellen (Interfaces) müssen Aufgrund ihrer großen Bedeutung für den Entwurf von Softwaresystemen sehr sorgsam entworfen werfen. Änderungen in Schnittstellen führen dazu, dass alle Klassen die eine Schnittstelle  (Interface) implementieren, bei einer Änderung vom Laufzeitsystem nicht mehr akzeptiert werden. Bei einer Schnittstellenänderung müssen alle Klassen der neuen Schnittstelle angepasst und neu übersetzt werden. 

Schnittstellen versus Vererbung mit Polymorphismus

Schnittstellen sind oft eine bessere Lösung für langlebige Softwaresysteme. Die enge Kopplung an die vererbenden Basisklassen werden sich ändernden Anforderungen auch oft ein Hindernis. Der Vorteil alle ererbten Methoden direkt benutzen zu können, kann hier auch zum Nachteil werden, insbesondere wenn Klassenhierarchien tief und unübersichtlich sind.

Die Konzepte von

  • Assoziation und
  • Schnittstellen

führen oft zu einem initial höheren Implementierungsaufwand. Sie bieten jedoch eine bessere Entkopplung von Softwarekomponenten. 

Weiterführende Quellen: "Why extends is evil"

Genau hingeschaut: Methodenimplementierungen von Schnittstellen

Weiter oben wird behauptet, dass in der Deklaration einer Schnittstelle keine Implementierung von Methoden möglich sind. Diese Aussage ist seit Java 8 so nicht mehr korrekt. Sie gilt aber nach wie vor für sehr, sehr viele Anwendungsfälle.

Die neuen Möglichkeiten von "default" Methoden in Schnittstellen werden im Rahmen dieser Vorlesung nicht beachtet. Man sollte diesen Sonderfall kennen. Für das grundlegende Verständnis des Schnittstellenkonzepts ist dieser "exotische" Anwendungsfall aber nicht notwendig.

Hintergrund

Vor Java 8 waren alle Methoden eines Interfaces abstrakt. Das heißt, es lag nur der Methodenkopf mit der Signatur vor. Seit Java 8 kann man in Interfaces

  • default-Methoden implementieren
  • statische Methoden implementieren

Die Notwendigkeit dieser Konzepte entstanden durch die Integration von Lamdba-Ausdrücken (JSR 335) . Man benötigt diese Konzepte um die Rückwärtskompatibilität zu älteren Javaimplementierungen zu gewährleisten. Es beruht auf dem Problem der Java Interface-Evolution bei der Benutzung der Collection Klassen in Lamdba-Ausdrücken. Angelika Langer hat dieses Problem sehr gut in einem deutschsprachigen Internetartikel im Java Magazin beschrieben.

Beispiele Schnittstellenanwendung

 Serialisierung

Serialisierung ist ein Beispiel bei dem man gut sieht wie eine Klasse durch Implementierung einer Schnittstelle wichtige Eigenschaften gewinnen man. Man kann sie in einer Datei speichern oder in einen beliebigen Stream stecken!

Java bietet die Möglichkeit Objekte im Hauptspeicher in einen seriellen Datenstron zu konvertieren den man zum Beispiel in Dateien schreiben kann. Diese Technologie nennt man Serialisierung(engl. Serialization siehe Wikipedia). Java kann Klassen serialisieren die die Schnittstelle Serializable implementieren.

Java kann Objekte serialisieren wenn

  • die entsprechende Klasse die Schnittstelle Serializable implementiert und
  • wenn alle Attribute die auf Objekte zeigen auch die Schnittstelle Serializable implementieren

Das folgende Beispielprogramm ist in der Lage eine Person und ihre Adresse in eine Datei zu schreiben und wieder zu lesen.

Alles was man tun muss um die Klassen Person und Adresse serialisierbar zu machen ist die Schlüsselworte "implements Serializable" hinzuzufügen

 Heap Diagramm mit vier Objekten zur Serialisierung
  1. Im Hauptprogramm wird eine Person Urmel angelegt die auf eine Adresse Lummerland zeigt
  2. Die Methode Methode schreiben() schreibt alle von p referenzierten Objekte in eine Datei serialisiert.ser
  3. Die Methode lesen() liest die Person und alle referenzierten Objekte aus der Datei zurück
  4. p1 zeigt nun auf eine neue Person mit eigenen referenzierten Objekt Adresse

 

 Überlegungen:

  • Die Schnittstelle Serializable fordert keine Methoden zu implementieren!
  • Warum sind 4 Objekte in der Datei?
  • Was würde geschehen wenn die die Klasse Person mit Stammbaum aus der ersten Vorlesung serialisieren würde?
  • Was würde geschehen wenn man weitere Attribute mit Basistypen zu den Klassen hinzufügt?
  • Was geschieht wenn man auf eine Klasse referenziert die nicht serialisierbar ist?

Klasse Serialisierung

package Kurs2.Schnittstelle;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
*
* @author stsch
*/
public class Serialisierung {
/**
* Erzeuge eine Person und die Adresse. Schreibe sie in eine
* Datei und lese sie aus der Datei
* @param args
*/
public static void main(String[] args) {
final String meineDatei = "serialisiert.ser";
// Erzeuge das Objekt der Person und das Objekt mit der Adresse
Person p = new Person("Urmel", new Adresse("Lummerland"));
System.out.println("Zu schreibende Person " + p.name +
" wohnt in " + p.wohnort.stadt);
// Schreibe die Objekte in eine Datei
schreiben(p, meineDatei);
Person p1 = (Person) lesen(meineDatei);
System.out.println("Gelesene Person " + p1.name +
" wohnt in " + p1.wohnort.stadt);
System.out.println("Die geschrieben Adresse und die gelesene"+
" Adresse " + p.wohnort.stadt + " sind" +
(p.wohnort==p1.wohnort? " " : " nicht ") +
"identisch");
}
/**
* Diese Methode schreibt ein beliebiges serialisierbares Objekt
* in eine Datei
* @param o Objekt welches in eine Datei geschrieben wird.
* Es muss serialisierbar sein!
* @param datei Die Datei in die geschrieben werden soll
*/
public static void schreiben(Object o, String datei) {
try {
// Erzeuge einen Stream der in eine Datei geleitet wird
FileOutputStream fileOut = new FileOutputStream(datei);
// Erzeuge einen Strem der Objekte in einen Filestrem leitet
ObjectOutputStream out = new ObjectOutputStream(fileOut);
// Schreibe ein beliebiges Objekt in diesen Objectstream
out.writeObject(o);
// Schliesse Stream
out.close();
// Schliesse Dateistream. Die letzten Bytes werden so raus geschrieben
fileOut.close();
System.out.println("Serialisierte Daten sind gespeichert in Datei "
+ datei);
} catch (IOException i) { // Hier können Ausnahmen auftreten
i.printStackTrace();
}
}
/**
*
* @param datei die Datei aus der gelesen werden soll
* @return
*/
public static Object lesen(String datei) {
System.out.println("Lesen aus " + datei);
Object o;
try { // Hier können Ausnahmen Auftreten
// Öffne Datei aus der gelesen werden soll
FileInputStream fileIn = new FileInputStream(datei);
// Erzeuge Objectstream der aus Datei liest
ObjectInputStream in = new ObjectInputStream(fileIn);
//Lies Objekt aus Stream
o = in.readObject();
// Schließe Objectstream
in.close();
// Schließe Datei
fileIn.close();
} catch (IOException i) { // Wird ausgeführt wenn Probleme auftreten
i.printStackTrace();
return null;
} catch (ClassNotFoundException c) {
System.out.println("Gelesene Klasse nicht gefunden");
c.printStackTrace();
return null;
}
return o;
}

}

Klasse Adresse

package Kurs2.Schnittstelle;
import java.io.Serializable;
/**
 *
 * @author stsch
 */
public class Adresse implements Serializable {
    String stadt;
    public Adresse (String s) {
        stadt =s;
    }    
}

Klasse Person

package Kurs2.Schnittstelle;
import java.io.Serializable;
/**
 *
 * @author stsch
 */
public class Person implements Serializable {
    String name;
    Adresse wohnort;
    public Person(String n, Adresse w) {
        wohnort = w;
        name=n;
    }    
}

Weiterführende Übungen und Überlegungen zu diesem Beispiel

  1. Was geschieht wenn zwei Personen auf das gleiche Adressobjekt zeigen? Wird die Adresse zweimal gespeichert oder nur einmal? Selbst wenn die Adresse nur einmal gespeichert wird. Was geschieht beim Lesen der serialisierten Datei. Wird das Addressobjekt verdoppelt?
  2. Was geschieht beim Übersetzen und Ausführen wenn man den Term "implements Serializable" in der Klasse Adresse oder Person weglässt?
  3. Versioning Problem: Was geschieht wenn man die Objekte in eine Datei serialisiert. Dann ein Attribut zu einer Klasse hinzufügt oder weglässt. Dann die serialisierten Objekt wieder einliest?
  4. Was muss man tun wenn man ein Attribut (zBsp. stadt) nicht speichern möchte? Suchen Sie in der Java-Spezifikation oder mit einer Suchmaschine...

Übung: Schnittstellen und abstrakte Klassen

Übung 1: Implementiere die Klasse Dollar aus der abstrakten Klasse Number

Die Java Laufzeitumgebung bietet die abstrakte Klasse Number aus der alle Zahlentypen abgeleitet werden.

Implementieren Sie eine Klasse Dollar die aus der Klasse Number abgeleitetet wird.

  • Die Klasse soll aus Sicherheitsgründen mit unveränderbaren Attributen belegt sein. Der Wert des Objekts darf nur im Konstruktur gesetzt werden.
  • Im Konstruktor soll der Betrag nach Dollar und Centbeträgen getrennt erfasst werden. Unsinnige Centbeträge sollen auf Null Cent gesetzt werden.

Vorgehen:

  • Arbeiten Sie am besten im Paket Kurs2.Schnittstelle. Beim Kopieren sind dann keine Anpassungen notwendig.
  • Nutzen Sie die Klasse Kurs2.SchnittstelleTestDollar zum Testen Ihrer Implementierung.
  • Legen Sie eine Klasse Kurs2.Schnittstelle.Dollar an, die aus der Klasse Number abgeleitet wird.
  • Tipp: Verwaltung des Betrags. Es ist einfacher den gesamten Betrag als Centbetrag in einem long zu speichern
  • Geben Sie bei allen Methoden die ganze Zahlen implementieren nur den Dollarbetrag aus
  • Geben Sie bei den Fließkommamethoden den Dollar und Centbetrag auf 2 Nachkommastellen aus (Rundungsfehler können ignoriert werden)

Tipp: Vorsicht beim Berechnen des Centbetrag im Konstruktor bei negativen Werten. Es gilt:

  • $3.25 = 3 *100 + 25 cents
  • $-3.25 = -3 * 100 - 25 cents
  • daher gilt die Rechenregel cents = signum(dollar)*( abs(dollar)*100+abs(cents) )

Nutzen Sie hierfür die statischen Importdeklarationen:

import static java.lang.Math.abs;
import static java.lang.Math.signum;

Das Testprogramm TestDollar

package Kurs2.Schnittstelle;
public class TestDollar {
/**
*
* Das Hauptprogramm benötigt keine Parameter
* Es testet die wichtigsten Eigenschaften der Klasse Dollar
*/
public static void main(String[] args) {
System.out.println("Phase 1: Einfache Tests");
Dollar konto1 = new Dollar(172, 12);
long d1 = konto1.longValue();
System.out.println("Dollar.longValue().Sollwert 172; Istwert: " + d1);
double d2 = konto1.doubleValue();
System.out.println("Dollar.doubleValue().Sollwert 172.12; Istwert: " + d2);
float d3 = konto1.floatValue();
System.out.println("Dollar.floatValue().Sollwert 172.12; Istwert: " + d3);
System.out.println("Phase 2: Härtere Tests");
Dollar konto2 = new Dollar(-380, 25);
d1 = konto2.longValue();
System.out.println("Dollar.longValue().Sollwert -380; Istwert: " + d1);
d2 = konto2.doubleValue();
System.out.println("Dollar.doubleValue().Sollwert -380.25; Istwert: " + d2);
d3 = konto2.floatValue();
System.out.println("Dollar.floatValue().Sollwert -380.25; Istwert: " + d3);
Dollar konto3 = new Dollar (-382,225);
d1 = konto3.longValue();
System.out.println("Dollar.longValue().Sollwert -382; Istwert: " + d1);
d2 = konto3.doubleValue();
System.out.println("Dollar.doubleValue().Sollwert -382; Istwert: " + d2);
d3 = konto3.floatValue();
System.out.println("Dollar.floatValue().Sollwert -382; Istwert: " + d3);
}
}

Die Ausgaben sollten etwa wie folgt aussehen:

Phase 1: Einfache Tests
Dollar.longValue().Sollwert 172; Istwert: 172
Dollar.doubleValue().Sollwert 172.12; Istwert: 172.12
Dollar.floatValue().Sollwert 172.12; Istwert: 172.12
Phase 2: Härtere Tests
Dollar.longValue().Sollwert -380; Istwert: -380
Dollar.doubleValue().Sollwert -380.25; Istwert: -380.25
Dollar.floatValue().Sollwert -380.25; Istwert: -380.25
Dollar.longValue().Sollwert -382; Istwert: -382
Dollar.doubleValue().Sollwert -382; Istwert: -382.0
Dollar.floatValue().Sollwert -382; Istwert: -382.0

Übung 2: Implementieren von Schnittstellen

Implementieren Sie die Klasse Euro. Die Klasse Euro

  • wird aus der Klasse Number abgeleitet
  • implementiert die Schnittstelle Comparable
  • implementiert die SchnittStelle Waehrung
  • soll noch die Methode public String toString() überschreiben um einen formatierten Wert ausgeben zu können.

Hinweise zur Implementierung der abstrakten Klasse Number

Kopieren Sie sich alle abstrakten Methoden der Klasse Number in die Klasse Dollar! Somit stellen Sie sicher, dass die Signatur identisch ist und das Überschreiben funktioniert.

Hinweise zur Implementierung der Schnittstelle Waehrung

Geben Sie bei der Methode symbol() ein "€" Zeichen zurück.

Implementierung der Methode mult(double faktor).

Diese Methode dient der Zinsberechnung. Durch die Multiplikation mit dem Wert 1.03 soll man den Wert um 3% Vergrößeren oder Verkleinern können.

  • Erzeugen Sie ein neues Objekt vom Typ Euro und geben Sie es direkt zurück. Verändern Sie da aktuelle Objekt nicht.
  • Berechnen des Centbetrags: Nehmen Sie den Absolutwert ihres neu berechneten Centbetrags und dann davon den Rest einer Division durch 100 (Modulo). Negative Beträge werden so korrekt berechnet.

Hinweise zu Implementierung der Schnittstelle Comparable()

Diese Schnittstelle erlaubt vielen Hilfsklassen in Java eine Sortierung vorzunehmen. Die Methode gibt abhängig vom Vergleich der Objekte negative, postive oder einen Nullwert zurück. Die Logik des Vergleichs liegt in der Verantwortung des Programmieres.

Tipp: Sie müssen in der Lage sein einen Euro gegen ein beliebiges Objekt zu vergleichen!

  • Prüfen Sie den Typ des Objekts vor dem Vergleich (siehe Kapitel Polymorphismus)
    • Geben Sie einen konstanten Betrag (-1 oder 1) zurück wenn Sie die Typen nicht vergleichen können
  • Falls Sie sicher sind, dass Sie Euro mit Euro vergleichen, können das Signum der Differenz des Wert in Cent benutzen.

Die Schnittstelle Waehrung

Benutzen Sie dies Klasse

package Kurs2.Schnittstelle;
/**
*
* @author sschneid * @ version 1.1
*/
public interface Waehrung {
/**
*
* @return Währungssymbol
*/
public String symbol();
/**
* Multipliziert den Wert des Objekts mit der Fließkommazahl
*
* @param f
* @return neues Objekt welches das Produkt enthält
*/
public Waehrung mult(double f);
}

Ein Testprogramm TestEuro

package Kurs2.Schnittstelle;
import java.util.Arrays;
/**
*
* @author sschneid
* @version 1.1
*/
public class TestEuro {
public static void main(String[] args) {
System.out.println("Phase 1: Einfache Tests");
Euro konto1 = new Euro(172, 12);
long d1 = konto1.longValue();
System.out.println(" Euro.longValue().Sollwert 172; Istwert: " + d1);
double d2 = konto1.doubleValue();
System.out.println(" Euro.doubleValue().Sollwert 172.12; Istwert: " + d2);
float d3 = konto1.floatValue();
System.out.println(" Euro.floatValue().Sollwert 172.12; Istwert: " + d3);
System.out.println(" Euro.symbol().Sollwert €; Istwert: " + konto1.symbol());
System.out.println(" Euro.toString().Sollwert 172.12€; Istwert: " + konto1);
System.out.println("Phase 2: Härtere Tests");
Euro konto2 = new Euro(-380, 25);
d1 = konto2.longValue();
System.out.println(" Euro.longValue().Sollwert -380; Istwert: " + d1);
d2 = konto2.doubleValue();
System.out.println(" Euro.doubleValue().Sollwert -380.25; Istwert: " + d2);
d3 = konto2.floatValue();
System.out.println(" Euro.floatValue().Sollwert -380.25; Istwert: " + d3);
Euro konto3 = new Euro (-382,225);
d1 = konto3.longValue();
System.out.println(" Euro.longValue().Sollwert -382; Istwert: " + d1);
d2 = konto3.doubleValue();
System.out.println(" Euro.doubleValue().Sollwert -382; Istwert: " + d2);
d3 = konto3.floatValue();
System.out.println(" Euro.floatValue().Sollwert -382; Istwert: " + d3);
System.out.println("Phase 3: Multiplikation testen");
Waehrung konto10 = new Euro(1,23);
double m1 = 2;
Waehrung konto11 = konto10.mult(m1);
System.out.println(" Euro.mult(): "+konto10 +" * "+ m1 + " = " + konto11);
Waehrung konto20 = new Euro(1,98);
double m2 = 2;
Waehrung konto21 = konto20.mult(m2);
System.out.println(" Euro.mult(): "+konto20 +" * "+ m2 + " = " + konto21);
Waehrung konto30 = new Euro(-1,98);
double m3 = 2;
Waehrung konto31 = konto30.mult(m3);
System.out.println(" Euro.mult(): "+konto30 +" * "+ m3 + " = " + konto31);
Waehrung konto40 = new Euro(-1,98);
double m4 = -2;
Waehrung konto41 = konto40.mult(m4);
System.out.println(" Euro.mult(): "+konto40 +" * "+ m4 + " = " + konto41);
Waehrung konto50 = new Euro(10,10);
double m5 = -2.01;
Waehrung konto51 = konto50.mult(m5);
System.out.println(" Euro.mult(): "+konto50 +" * "+ m5 + " = " + konto51);
Waehrung konto60 = new Euro(10,1);
double m6 = -2.001;
Waehrung konto61 = konto60.mult(m6);
System.out.println(" Euro.mult(): "+konto60 +" * "+ m6 + " = " + konto61);
System.out.println("Phase 4: Einfache Tests für Comparable");
if (konto1.compareTo(konto2) >0)
{System.out.println(" "+konto1 + " ist groeßer als " + konto2);}
else
{System.out.println(" "+konto1 + " ist nicht groeßer als " + konto2);}
if (konto3.compareTo(konto2) >0)
{System.out.println(" "+konto3 + " ist groeßer als " + konto2);}
else
{System.out.println(" "+konto3 + " ist nicht groeßer als " + konto2);}
sortierTest();
}
public static void sortierTest() {
System.out.println("Phase 4: Testen der Schnittstelle Comparable");
Euro[] bank = {
new Euro(99,99),
new Euro(66,66),
new Euro(33,33),
new Euro(11,11),
new Euro(22,22),
new Euro(88,88),
new Euro(55,55),
new Euro(22,22),
new Euro(44,44),
new Euro(0,0)};
System.out.println(" Unsortiertes Feld:");
for (Euro konto : bank) System.out.println(" "+konto);
// Diese Methode sortiert ein Feld in seiner natürlichen Ordnung
// Die ntürliche Ordnung wird durch die Schnittstelle Comparable
// festgelegt
Arrays.sort(bank);
System.out.println(" Sortiertes Feld:");
for (Euro konto : bank) System.out.println(" "+konto);
}

Lösung: Schnittstellen und abstrakte Klassen

Lösung Übung 1: Klasse Dollar

package Kurs2.Schnittstelle;
import static java.lang.Math.signum;
/**
*
* @author sschneid
* @version 1.2
*/
public class Dollar extends Number{
public final long cents;
public Dollar(int dollars, int cents) {
// Ignoriere Centsbetrag wenn er nicht im richtigen Intervall ist
if ((cents<0) || (99<cents)) cents=0;
if (dollars == 0)
this.cents = cents;
else
// Signum ist notwendig da
// -2.20= -2 - 0.2 sind. Falsch: -2 + 0.2 ergibt -1.8!
this.cents = dollars*100+cents*(long)signum(dollars);
}
@Override
public int intValue() {
return (int)cents/100;
}
@Override
public long longValue() {
return cents/100;
}
@Override
public float floatValue() {
return cents/100f;
}
@Override
public double doubleValue() {
return cents/100d;
}
}

Lösung Übung 2: Klasse Euro

package Kurs2.Schnittstelle;
import static java.lang.Math.abs;
import static java.lang.Math.signum;
/**
*
* @author sschneid
* @version 1.2
*/
public class Euro extends Number implements Waehrung, Comparable{
/**
* Der gesamte Betrag wird intern in Vents verwaltet
*/
public final long cents;

public Euro(long euros, long cents) {
// Ignoriere Centsbetrag wenn er nicht im richtigen Intervall ist
if ((cents<0) || (99<cents)) cents=0;
if (euros == 0)
this.cents = cents;
else
// Signum ist notwendig da
// -2.20= -2 - 0.2 sind. Falsch: -2 + 0.2 ergibt -1.8!
this.cents = euros*100+cents*(long)signum(euros);
}
@Override
public int intValue() {
return (int)cents/100;
}
@Override
public long longValue() {
return cents/100;
}
@Override
public float floatValue() {
return cents/100f;
}
@Override
public double doubleValue() {
return cents/100d;
}
@Override
public String symbol() {
return "€";
}
@Override
public String toString() {
// Füge eine Null bei Centbeträgen zwischen 0 und 9 eine
String leerstelle = ((abs(cents)%100)<10) ? "0" : "";
return Long.toString(cents/100L) + "." + leerstelle +
Long.toString(abs(cents%100L)) + symbol();
}
@Override
public Waehrung mult(double d) {
long temp;
temp = (long)(cents *d);
return new Euro(temp/100L,abs(temp%100L));
}
@Override
public int compareTo(Object o) {
int result;
if (o instanceof Euro) {
Euro e = (Euro) o;
result = (int)(this.cents-e.cents);
}
else {result = -1;} // Alles was kein Euro ist, ist kleiner
return result;
}
}

Lernziele (Schnittstellen)

Am Ende dieses Blocks können Sie:

  • den Zusammenhang zwischen "Information Hiding" und Java Schnittstellen erklären
  • die in Java-Schnittstellem vorkommenden Komponenten von Klassen nennen
  • die Unterschiede und Gemeinsamkeiten von Javaklassen und Javaschnittstellen erklären
  • die Syntax einer Javaschnitstelle nennen
  • die UML Notation für Schnittstellen aufzeichnen und anwenden
  • beim Entwurf von Anwendungen Gründe für die Wahl von Vererbung mit Polymorphismus oder die Wahl von Schnittstellen nennen.

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: Fragen zu Java Schnittstellen (Interfaces)

Assertions

 

 

Assertions (engl. Versicherung, Zusage) erlauben den Entwicklern für Testzwecke bestimmte Randbedingungen zu prüfen die immer erfüllt sein sollen.

Assertions geben dem Entwickler die Möglichkeit logische Bedingungen für die folgenden Fälle zu Programmieren:

  • Interne Invarianten
  • Invarianten im Kontrollfluß
  • Vorbedingungen, Nachbedingungen, Klasseninvarianten

Durch die Implementierung dieser Invarianten kann der Entwickler die Qualität seiner Implementierung erhöhen, da Bedingungen geprüft werden können die nie verletzt sein sollen.

Der Unterschied zu Ausnahmen (Exceptions) besteht darin, dass Assertions immer gelten sollten und diese daher nicht im Normalfall kontrolliert werden müssen, da sie den Programmablauf nur unnötig verlangsamen würden. Assertions haben eine große Ähnlichkeit mit Ausnahmen sie dienen jedoch unterschiedlichen Zwecken:

Vergleich Assertions und Ausnahmen
  Assertion Ausnahme (Exception)
Einsatzbereich Nur wenn die Logik des Programm in den Augen des Entwicklers verletzt wird Jederzeit da sie Teil der regulären Ablaufsteuerung sind
Auswirkungen bei der normalen Programmausführung Keine. Sie werden nicht geprüft! Können immer Auftreten.
Sichtbarkeit für Endanwender Nie: Wenn das Programm nicht explizit mit entsprechenden Optionen (-ea) gestartet wurde Nur wenn sie nicht abgefangen und behandelt werden
Zielgruppe Helfen dem Entwickler bei der Fehlersuche (auch beim Endanwender)

Zusammenarbeit der Entwickler: Teil der regulären externen Spezifikation von Klassen

Endanwender: Klassifikationschema für Fehlermeldungen bei Programmabbrüchen (Unbehandelte Ausnahmen)

Theoretischer Hintergrund Erlauben das Implementieren von Invarianten oder Vor- und Nachbedingungen von Schleifen und Routinen

Elegantes Konstrukt zum Verlassen von Blöcken zur Behandlung von seltenen Ereignissen.

Implementierter Code wird übersichtlicher da man nicht bei jeder Operation einzeln Sonderfäller prüfen muss

Assertions bieten die folgenden Vorteile für den Entwickler:

  • Der Entwickler kann Annahmen von denen er ausgeht als logische Ausdrücke implementieren und ist nicht auf Kommentare angewiesen.
  • Die Konsistenzprüfungen werden im Normalfall nicht abgearbeitet und produzieren daher keinerlei Laufzeitkosten für den Anwender
  • Sie geben dem Entwickler die Möglichkeit auch nach der Auslieferung seiner Anwendung zusätzliche Informationen durch Einschalten des Assertionchecking zu erhalten.
    • Hintergrund: Bei C und C++ Anwendungen werden bei Kundenproblemen oft spezielle Programme mit extra Debuginformationen ausgeliefert. Dies ist bei Java nicht nötig

Notation

Einfache Variante einer Assertion:

assert Ausdruck1;

Der Ausdruck Audruck1 wird ausgewertet. Hat das Ergebnis den Wert true (wahr) so ist die Zusage wahr und das Programm weiter ausführt. Trifft die Zusage (Assertion) nicht zu wird das Programm mit einem AssertionError abgebrochen (falls es den entspechenden Optionen zum Checken der Assertions aufgerufen wurde).

Eine zweite Syntaxvariante ist:

assert Ausdruck1: Ausdruck2;

Hier fährt das Programm wie im Fall zuvor mit der Ausführung fort wenn Ausdruck1 wahr ist. Ist Ausdruck1 jedoch unwahr wird Ausdruck2 ausgewertet und der entsprechenden Instanz von AssertionError als Parameter mitgegeben und dann in der Fehlermeldung mit ausgegeben. Dieser Rückgabewert unterstützt den Entwickler bei der Analyse des aufgetretenen Fehlerfalls. 

Einschalten der Prüfungen im Laufzeitsystem

Das Prüfen von Assertions kann beim Starten einer Javaanwendung mit den Optionen -ea bzw. -enableassertions eingeschaltet werden oder mit der Option -da bzw. -disableassertions ausgeschaltet werden. Die Option wird vor dem Klassennamen dessen main() Methode gestartet werden soll angegeben:

java -ea Klassenname1
java -ea paketname1... Klassenname1
java -ea paketname1.Klassename2 Klassenname1

java -da Klassenname1
java -da paketname1... Klassenname1 java -da paketname1.Klassename2 Klassenname1java 

 Wichtig: Die Notation mit den drei Punkten  paketname1... ist teil der Aufrufsyntax. Mit ihr werden die Assertions für alle Klassen in einem Paket angeschaltet.

Anwendungsbeispiele

Überprüfen korrekter Wertebereiche

Prüfen des Personenalters bei Rentenberechnungen

assert ((personenAlter>0) && (personenAlter<150)); 
assert (rentenEintrittsAlter>0); 

Die gleichen Assertions in der Variante mit einer Ausgabe für die Konsole

assert ((personenAlter>0) && (personenAlter<150)): personenAlter; 
assert (rentenEintrittsAlter>0): "negatives Renteneintrittsalter "+ rentenEintrittsAlter;  

Das Kontrollieren des Werts eines Monats:

int monat;
...
switch (monat)
{
  case 1: case 2: case 3: System.out.println("Q1");
      break;
  case 4: case 5: case 6: System.out.println("Q2");
      break;
  case 7: case 8: case 9: System.out.println("Q3");
      break;
  case 10: case 11: case 12: System.out.println("Q4");
      break;
  default: assert false;
}

Prüfen eines Kontrollflusses

In einen Programm soll eine der Bedingungen Ausdruck1 oder Ausdruck2 immer erfüllt sein.

void testMethode()
{
   for (int k = 0; k<= 99; k++)
   {
      if (k == 50)
         return;
   }
   assert false;
}

Die Assertion kann Prüfen ob ein Fehlerfall vorliegt.

Geschichtlicher Hintergrund

Assertions wurden in Java durch JSR 42 (A Simple Assertion Facility) in JDK 1.4 eingeführt. Dies führt zu einem gewissen Kompatiblitätsproblem:

  • Quellcode der das Schlüsselwort assert als normalen Bezeichner in JDK 1.3 verwendete wird in JDK 1.4 nicht übersetzen da das Schlüsselwort nicht als Namen akzeptiert wird
  • Quellcode der für JDK 1.4 geschrieben wurde wird nicht unter JDK 1.3 übersetzen, da javac in JDK 1.3 nicht der Syntax von assertions umgehen kann.

Übungen (Assertions)

Übung 1: Einfügen von Assertions

Nutzen Sie das Beispiel aus dem Abschnitt zu Schnittstellen.

UML Diagramm Euro

Modifizieren Sie die Klasse Euro so, daß

  • Beim Setzen des Centbetrags eine Assertion geworfen wird falls der Centbetrag nicht im korrekten Wertebereich ist. Nutzen Sie die erweiterte Syntax um eine vernünftige Fehlermeldung auszugeben.
  • Melden  Sie beim Multiplizieren eines Eurobetrags einen Faktor von Null (0) mit Hilfe einer Assertion. Nutzen Sie die erweiterte Syntax um eine Fehlermeldung auszugeben.

Starten Sie das Programm mit der Klasse TestEuro: Es sollte wie zuvor funktionieren

Starten Sie die Klasse TestEuro so, daß Assertions beachtet werden.

  • Welche Option muß man beim Programmstart einfügen?
  • Modifizieren Sie das Testprogramm so, dass die Assertion für die Multiplikation ausgelöst wird. Der Fall einer Multiplikation mit Null wird im aktuellen Testprogramm nicht getestet.

Lösungen (Assertions)

Übung 1: Einfügen von Assertions

Klasse Euro

package Kurs2.Schnittstelle;
import static java.lang.Math.abs;
import static java.lang.Math.signum;
/**
*
* @author sschneid
* @version 1.1
*/
public class Euro extends Number implements Waehrung, Comparable{
/**
* Der gesamte Betrag wird intern in Cents verwaltet
*/
public final long cents; public Euro(long euros, long cents) { assert ((cents>=0) && (cents < 101)): "Cents Bereichsverletzung";
// Ignoriere Centsbetrag wenn er nicht im richtigen Intervall ist
if ((cents<0) || (cents>=100))
cents=0;
this.cents = (abs(euros)*100+cents) *(long)signum(euros);
}
@Override
public int intValue() {
return (int)cents/100;
}
@Override
public long longValue() {
return cents/100;
}
@Override
public float floatValue() {
// Signum und Absolutwert sind notwendig
// da -2.20= -(2 + 0.2) sind. Falsch: -2 + 0.2 ergibt -1.8!
return (float)cents/100f;
}
@Override
public double doubleValue() {
// Signum und Absolutwert sind notwendig
// da -2.20= -(2 + 0.2) sind. Falsch: -2 + 0.2 ergibt -1.8!
return (double)cents/100d;
}
@Override
public String symbol() {
return "€";
}
@Override
public String toString() {
// Füge eine Null bei Centbeträgen zwischen 0 und 9 eine
String leerstelle = ((abs(cents)%100)<10) ? "0" : "";
return Long.toString(cents/100L) + "." + leerstelle +
Long.toString(abs(cents%100L)) + symbol();
}
@Override
public Waehrung mult(double d) { assert (d!=0): "Multplikation mit " + d + "nicht erlaubt";
long temp;
temp = (long)((double)cents *d);
return new Euro(temp/100L,abs(temp%100L));
}
@Override
public int compareTo(Object o) {
int result;
if (o instanceof Euro) {
Euro e = (Euro) o;
result = (int)(this.cents-e.cents);
}
else {result = -1;} // Alles was kein Euro ist, ist kleiner
return result;
}
}

 

Lernziele (Assertions)

Am Ende dieses Blocks können Sie:

  • ... mit Hilfe von Java-Assertions Invarianten der Anwendung implementieren
  • ... die Auswirkung von Annahmen (Assertions) auf die Wartbarkeit von Anwendungen beschreiben
  • ... Javaannahmen (Assertions) zur Laufzeit gezielt an und ausschalten
  • ... den Einsatz von Assertions (Annahmen) abwägen gegen die Verwendung von Ausnahmen (Exceptions)

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: Fragen zu Annahmen (Assertions))

Ausnahmen (Exceptions)

Notwendigkeit von Ausnahmebehandlung

Beim Ausführen von Programmen können Ereignisse auftreten die nicht zum normalen Ablauf gehören und trotzdem sinnvoll behandelt werden müssen. Nicht normaler Ablauf bedeutet hier, dass diese Ausnahmen nicht direkt von den Eingaben oder Anweisungen des Programmcodes hervorgerufen werden. Beispiele hierfür sind

  • Division einer Ganzzahl durch Null
  • Lesen aus einer Datei die nicht vorhanden ist
  • Fehlende Resourcen wie Hauptspeicher
  • Verlust einer Netzwerkverbindung
  • Feldüberlauf etc.

Es ist guter (und erforderlicher) Programmierstil mit solchen Ausnahmen umgehen zu können und sie angemessen behandeln.

Zur Erkennung und Behandlung solcher abnormaler Situationen gibt es eine Reihe von Möglichkeiten die von der gewählen Technologie abhängen können:

  • Präventives Kontrollieren von potentiellen Aushnahmen
    • Beispiel: Prüfung von Eingabewerten oder Zwischenwerten von Berechnungen auf potentielle Sonderdebingungen wie Nullwerte oder Feldgrenzenüberläufe
  • Fehlercoderückgaben bei jedem Operationsaufruf
    • Unixkommandos geben Beispielsweise immer einen Fehlerwert mit aus. Ist dieser ungleich Null liegt ein bestimmter Fehler (Ausnahme) vor
  • Unterbrechungsgesteuerte (Interrupthandling) Ausnahmen
    • Der reguläre Programmablauf wird verlassen und einer anderen Stelle fortgesetzt.

Bei der Behandlung von Ausnahmen steht der Entwickler in der Regel vor dem folgenden Zielkonflikten

Zielkonflikte
niedriger Implementierungsaufwand mit hohem Fehlerrisiko hoher Implementierungsaufwand mit geringem Fehlerrisiko
übersichlicher aber wenig robuster Code unübersichtlicher (und fehleranfälliger) Code mit vielen Präventivüberprüfungen

Moderne Programmiersprachen und -umgebungen bieten eine Infrastruktur um die diese Zielkonflikte zu mildern. Die Technologie einer Rückgabe von Fehlerzuständen bei jeder einzelnen Operation macht es seht unwahrscheinlich, dass ein Entwickler bei jeder einzelnen Operation diesen Wert auswertet und individuelle Maßnahmen ergreift.

Die Programmiersprache Java unterstützt den Entwickler mit einem mächtigen Konzept zur Ausnahmebehandlung (Exceptionhandling) mit dem man Unterstützung in den folgenden Bereichen bekommt:

  • Möglichkeit Entwickler zur Behandlung von Ausnahmen zu zwingen
  • Möglichkeit  potentielle Ausnahmen in größeren Codeblöcken an einer Stelle zu behandlen
  • Objektorientierte Konzepte um Ausnahmehierarchien zu modellieren und damit die Behandlung ganzer Klasse von Ausnahmen zu erlauben

Beispiel einer Ausnahme in Java

Die Zuweisung des Ausdrucks auf die Variable c löst eine Ausnahme aus, da hier durch Null dividiert wird:

package Kurs2.Exception;
public class Beispiel {
public static void main(String args[]) {
int a = 18;
int b = 6;
int c = (a+b)/(3*b-18); int d = 17; // wird nicht erreicht
}
}

Bei der Division durch Null erzeugt Java ein Ausnahmeobjekt in dem die Ausnahme beschrieben ist. Wird die Ausnahme nicht vom Entwickler behandelt nutzt Java diese Information. Gibt sie auf der Konsole aus und beendet das Programm.

Exception in thread "main" java.lang.ArithmeticException: / by zero
at Kurs2.Exception.Beispiel.main(Beispiel.java:6)

Syntax einer Java-Ausnahme auf der Konsole

Syntax einer Ausnahme auf der Konsole

Die Klassenhierarchie des Java-Laufzeitsystems

Java verwendet eine Reihe von Klassen zur Verwaltung von Ausnahmen. Bei Auftreten von Ausnahmen werden Instanzen dieser Klassen erzeugt. Mit Hilfe dieser Instanzen kann man Ausnahmen analysieren und behandeln. Anbei ein Auszug der Klassenhierarchie des Java-Laufzeitsystems:

Exceptionklassen des Java-laufzeitsystems

Exceptions (Ausnahmen) in UML

UML benutzt Unterbechungskanten um Ausnahmen in Aktivitätsdiagrammen zu beschreiben. Unterbrechungskanten werden als Pfeile in Form eines Blitzes gezeichnet. Die Ausnahmebedingung wird in rechteckigen Klammern an der Unterbrechungskante dokumentiert. Da folgende Beispiel zeigt das Sortieren von Personen welches abgebrochen wird wenn eine Person eine leere Zeichenkette für den Nachnamen besitzt.

UML Notation mit Pfeil in Blitzform

UML Notation mit Aktivitätspfeil
und separatem Blitzsymbol

 

Java-Ausnahmen (Exceptions) behandeln

Java zwingt den Entwickler zur Behandlung von Ausnahmen wenn er Methoden aufruft die zu "Checked Exceptions" führen können.

Entwickler haben hier zwei Lösungsoptionen

  • die Möglichkeit die Behandlung einer Ausnahme an den Aufrufer der eigenen Methode zu delegieren
  • die Behandlung der Ausnahme im aktuellen Block selbst zu implementieren.

Der Javaübersetzer wird den quellcode nicht übersetzen falls der Entwickler keine Maßnahme ergriffen. Die zwangsweise Behandlung von "Checked Exceptions" ist Teil des Schnittstellenvertrags zur Benutzung einer Methode.

Behandeln einer Ausnahme mit einem try-catch Block

Java erlaubt die Ausnahmebehandlung mit Hilfe von try-catch-finally Programmblöcken die aus drei Teilen bestehen:

Block Anzahl Inhalt 
try einmal Block enthält den eigentlichen Anwendungscode. Er wird "versucht" (try) komplett auszuführen. In ihm können gewisse Ausnahmen auftreten. Er wird verlassen wenn wenn eine Ausnahme auftritt
catch einmal pro Exceptiontyp Block wird ausgeführt wenn eine bestimmte Ausnahme auftritt. Er "fängt" die Ausnahme und behandelt sie.
finally einmal, optional Block wird unbedingt ausgeführt. Er wird entweder nach dem regulären durchlaufen des try-Blocks oder nach dem Durchlaufen des catch-Blocks ausgeführt. Hier werden typischerweise Resourcen wie offene Dateien geschlossen und wieder freigegeben.

Der try Block wird beim Auftreten einer Ausnahme direkt verlassen und es wird in den catch-Block gesprungen.

Die Java hat die folgende Syntax:

try
{
  ... // in diesem Block wird der reguläre Code implementiert
      // Dieser Code kann an beliebigen Stellen Ausnahmen auslösen
}
catch (ExceptionTypeA myName1)
{
  ... // dieser Block wird aufgerufen wenn im try-Block eine Ausnahme
      // vom Typ ExceptionTypeA auslöst
}
catch (ExceptionTypeB myName2)
{
  ... // dieser Code wird aufgerufen wenn im try-Block eine Ausnahme
      // vom Typ ExceptionTypeB auslöst
}
finally
{
   ...// Dieser Block ist optional
       // er wird immer aufgerufen. Er hängt nicht vom Auslösen einer Ausnahme ab
}

Ablaufsteuerung

Wichtig: "Geworfene" Ausnahmen führen zum Beenden des Programmes falls sie nicht innerhalb eines try-Blocks mit der entsprechenden catch-Klausel und passendem Exception-typ gefangen und bearbeitet werden! Das Javaprogramm wird jede weitere Ausführung einstellen und die entsprechenden Ausführungsblöcke verlassen. Es wird auf der Kommandozeile das Auftreten der entsprechende Ausnahme dokumentieren.

Der try-Block kann jedoch in einem beliebigen äusseren Block implementiert sein. Das heißt, Methoden die einen solchen Codepfad direkt oder indirekt aufrufen, müssen in einem solchen try-catch-Block eingehüllt sein.

Ablauf mit finally Block Ablauf ohne finally Block
Aktivitätsdiagramm try-catch-finally Block Aktivitätsdiagramm try-catch Block;

 

Beispiel 1: NumberFormatException

Im folgenden Beispiel wird das erste Kommandozeilenargument in eine ganze Zahl umgewandelt. Hier kann eine NumberFormatException Ausnahme bei der Umwandlung der Eingabezeichenkette args[0] auftreten. Tritt diese Ausnahme auf, wird der Variablen firstArg eine Null zugewiesen.

public class Main {
    public static void main(String[] args) {
    int firstArg=0;
    int stellen=0;
    if (args.length > 0) {
        try {
            firstArg = Integer.parseInt(args[0]);
        } 
        catch (NumberFormatException e) {
            firstArg = 0;
        }
        finally {
           System.out.println("Die Zahl firstArg = " + firstArg + " wurde eingelesen");
        }
    }
}
}

In diesem Beispiel wurde die Größe des Eingabefeldes präventiv mit einer if Bedingung abgeprüft.

Beispiel 2: ArrayIndexOutOfBoundsException

Die präventive Prüfung der Feldgröße kann aber auch in einem zweiten catch Block zum gleichen try Block durchgeführt werden.

public class Main {

    public static void main(String[] args) {
        int firstArg = 0;
        int stellen = 0;
        try {
            firstArg = Integer.parseInt(args[0]);
        } catch (ArrayIndexOutOfBoundsException e) {
            firstArg = -1;
            System.out.println("Argument auf Position "
                    + e.getMessage()
                    + " nicht vorhanden");
        } catch (NumberFormatException e) {
            firstArg = 0;
        } finally {
            System.out.println("Die Zahl firstArg = " + firstArg + " wurde eingelesen");
        }
    }
}

Das Behandeln des Feldüberlaufs mit Hilfe eines catch-Blocks ist die flexiblere Lösung da sie jeglichen Feldüberlauf innerhalb des try-Blocks fangen kann.

Fehleranalyse im catch-Block

Dem catch-Block wird eine Instanz der bestimmten Klasse der Ausnahme mitgegeben. In Beispiel 2 wird die Ausnahmeinstanz e benutzt um mit Hilfe der Methode e.getMessage() die Postion im Feld auszulesen die die ArryIndexOutOfBoundsException Ausnahme auslöste.

Behandeln einer Ausnahme durch Delegation an die nächst äussere Methode

Die zweite Möglichkeit eines Entwicklers besteht darin, die Methode nicht direkt zu behandeln und das Behandeln der Ausnahme dem Aufrufer der akuellen Methode zu überlassen. Dies geschieht in dem man im Kopf der Methode dokumentiert welche Ausnahmen durch den Aufruf einer Methode ausgelöst werden können. Die Methode stringLesen2() kann zum Beispiel zum Auslösen einer IOException Ausnahme führen:

public static String StringLesen2() throws IOException{
...
}

Methoden können mehr als eine Ausnahme auslösen. Die Syntax des Methodenkopfs ist die folgende:

[Modifikatoren] Rückgabewert Name-der-Methode ([Parameter]) throws Name-Ausnahme [, Name-Ausnahme]

Der Aufrufer der Methode muss dann die Behandlung selbst durchführen.

Hinweis: Die Angabe der Ausnahmen die von einer Methode ausgelöst haben keine Bedeutung bei der Unterscheidung von überladenen Methoden.

Beispiel

Im folgenden Fall werden zwei Methoden zum Einlesen einer Konsoleneingabe gezeigt.

Die Methode stringLesen1() behandelt die IOException Ausnahme selbst. Der Aufrufer im main() Hauptprogramm muss keine Maßnahmen ergreifen.

Due Methode stringLesen2() behandelt die Ausnahme nicht und "exportiert" sie an den Aufrufer. Hier muss das Hauptprogramm main() die Ausnahme selbst.

package Kurs2.Script.Ausnahmen;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

public static void main(String[] args) {
String eingabe = "";
eingabe = StringLesen1();
System.out.println(eingabe);
try {
eingabe = StringLesen2();
} catch (IOException e) {
eingabe = " KEINE EINGABE BEI ZWEITEM AUFRUF;";
}
System.out.println(eingabe);
}

public static String StringLesen1() {
String input = "Konsoleneingabe(1): ";
// liest einen vom Benutzer eingegebenen Text (String) ein
BufferedReader keyboard =
new BufferedReader(new InputStreamReader(System.in));
try {
input = input + keyboard.readLine() + ";";
} catch (IOException e) {
input = input + " KEINE EINGABE;";
}
return input;
}

public static String StringLesen2() throws IOException{
String input = "Konsoleneingabe(2): ";
// liest einen vom Benutzer eingegebenen Text (String) ein
BufferedReader keyboard =
new BufferedReader(new InputStreamReader(System.in));
input = input + keyboard.readLine() + ";";
return input;
}
}

 

Wichtig
Eine Methode kann nur "Checked Exceptions" auslösen die von ihr in der throws Klausel angegeben wurden.

Abarbeitung von mehreren catch-Klauseln

Java sucht bei mehreren catch-Klauseln (Handler) für eine Ausnahmebehandlung alle Klauseln von oben nach untern. Die erste Klausel mit einem passenden Typ der Ausnahme wird dann aufgerufen.

Aufgrund des Polymorphismus kann es mehrere Möglichkeiten geben. Klauseln die für eine Oberklasse einer Ausnahme geschrieben wurden werden benutzt werden wenn eine speziellere Ausnahme abgehandelt werden muss.

Beim Benutzen von mehreren catch Klauseln sollten daher die speziellen Ausnahmen vorne stehen und die allgemeinen Ausnahmen am Ende.

Finally Block (Vertiefung)

Der finally Block wird immer ausgeführt. Dies im Extremfall zu ungewöhnlichen Situationen führen. Analysieren Sie das folgende Programm:

package Kurs2.Exception;

class BeispielFinally {
public static int finallyTest (int a) {
int result=0;

try {
result = 17/a; // Hier kann eine ArithmeticException ausgelöst werden
}
catch (ArithmeticException e) {
return 0;
}
finally {
if (result==result) //Dieser Vergleich wird benötigt um den Parser zu überlisten
return 1;
}
System.out.println("Methode beendet");
return result;
}
public static void main(String[] args) {
int x=0;
System.out.println(finallyTest(0));
}
}

Es erzeugt die folgende Konsolenausgabe:

1

 

Java-Ausnahmen deklarieren und werfen

Werfen von Ausnahmen

Java erlaubt nicht nur die vom Laufzeitsystem erzeugten Ausnahmen zu behandeln. Java erlaubt es auch selbst Ausnahmen zur Erzeugen. Dies geschieht durch das Erzeugen einer Instanz einer Klasse die aus der Klasse Throwable abgeleitet ist. Im folgenden Beispiel geschieht dies, in dem vor einer Division auf eine mögliche Division durch Null geprüft wird :

package Kurs2.Exception;

import java.lang.IllegalArgumentException;

public class Grundrechenarten {
public static int division(int a, int b) {
IllegalArgumentException ex;
if (b==0) {
ex = new IllegalArgumentException("Division durch 0!");
throw ex;
}
return a/b;
}

public static void main(String[] args) {
int c = division(3,0);
}

}

Das Werfen einer eigenen Ausnahme erfolgt in zwei Schritten:

  • Dem Erzeugen einer Instanz der Ausnahmeklasse. Hier kann ein Text mit Informationen im Konstruktor mitgegeben werden.
  • Dem Werfen der Ausnahme mit dem Schlüsselwort throw und der Angabe eines Ausnahmeobjekts

Beim Ausführen des oben gezeigten Programms wird die folgende Meldung ausgegeben:

Exception in thread "main" java.lang.IllegalArgumentException: Division durch 0!
at Kurs2.Exception.Grundrechenarten.division(Grundrechenarten.java:9)
at Kurs2.Exception.Grundrechenarten.main(Grundrechenarten.java:16)

Da das Ausnahmeobjekt in der Regel nicht weiter oder wieder verwendet wird, kann man das Erzeugen des Objekts direkt mit dem Werfen der Ausnahme verbinden:

throw new IllegalArgumentException("Division durch 0!");

Selbstimplementierte Ausnahmeklassen

Java erlaubt auch das Implementieren eigener Ausnahmeklasse. Man kann zum Beispiel die Klasse IllegalArgumentException zu einer eigenen Klasse ableiten:

package Kurs2.Exception;

public class DivisionException extends IllegalArgumentException {
int dividend;
int divisor;

public DivisionException(int a, int b) {
super ("Versuch von Division " + a + "/" + b);
dividend =a;
divisor = b;
}
}

Das Hauptprogramm muss entsprechend angepasst werden:

package Kurs2.Exception;

import java.lang.IllegalArgumentException;

public class Grundrechenarten {

public static int division2(int a, int b) {
DivisionException ex;
if (b==0) {
ex = new DivisionException(a,b);
throw ex;

}
return a/b;
}

public static void main(String[] args) {
int c = division2(3,0);
}

}

Es wird dann eine angepasste Fehlermeldung geworfen:

Exception in thread "main" Kurs2.Exception.DivisionException: Versuch von Division 3/0
at Kurs2.Exception.Grundrechenarten.division2(Grundrechenarten.java:18)
at Kurs2.Exception.Grundrechenarten.main(Grundrechenarten.java:25)
Java Result: 1

 

"Checked" und "Unchecked Exceptions"

Die Klasse Throwable

Die Klasse Throwable ist die Basisklasse der gesamten Ausnahmehierarchie. Instanzen von ihr sorgen dafür das Ausnahmen "geworfen" werden, die dann behandelt werden können.

Alle Klassen in der Throwable Hierarchie können den Grund für das Werfen einer Ausnahme im Konstruktor als Zeichenkette beschreiben.

Die Klasse Error

Die Klasse Error wird für Probleme der Java virtuellen Maschine selbst verwendet. Diese Fehler sollten nicht auftreten. Fehler die durch diese Klasse gemeldet werden sind in der Regel so schwerwiegend, dass keine weitere Fortführung des Programms sinnvoll ist.

Ein Beispiel hierfür ist das Fehlen von freiem Hauptspeicher. Er wird mit einem OutOfMemoryError Fehler gemeldet. Eine sinnvolle Behandlung dieses Fehlers ist nur schwer möglich. Selbst ein Berichten über die Details dieses Problem benötigt Speicherplatz zum Verwalten von Zeichenketten. Der Versuch einer Behandlung eines solchen Fehlers ist nur sinnvoll wenn man in der Lage ist, in nativen Bibliotheken die die Anwendung selbst verwaltet unmittelbar auf C/C++ Ebene Speicher freizugeben.

Da alle Fehler in dieser Klassenhierarchie (Errror) nicht direkt von der entsprechenden Sektion im Programm abhängen und sehr selten sind, zählen diese Fehler zu den "unchecked Exceptions". Ein Entwickler muss Codestrecken nicht mit einem try-catch Block versehen und eine Behandlung implementieren.

Wichtig

Alle abgeleiteten Klassen der Klasse Error sind "unchecked exceptions".

Implikation: Sie müssen nicht mit try-catch Blöcken abgesichert werden.

Hintergrund: Fehler in dieser Kategorie können in jeder Java-Codestrecke auftreten. Der Entwickler müsste seinen gesamten Code mit try-catch Blöcken versehen. Hinzukommt, dass eine Behandlung in der Regel nicht möglich ist. Try-catch Blöcke sind allso nicht sinnvoll für diese Kategerie von Ausnahmen.

Beispiele
Oberklasse Error Bedeutung
InstantiationError Versuch des Erzeugens einer Instanz einer abstrakten Klasse oder einer Schnittstelle
StackOverflowError Überlauf des Stapel (Stack). Typische Ursachen sind Programm mit extrem vielen und tiefen Methodenaufrufen oder ein nicht terminierendes rekurvsives Programm
OutOfMemoryError Der Javaprozess hat keinen freien Hauptspeicher mehr und kann keine weiteren Objekte allokieren

 

Die Klasse Exception

Aus der Klasse Exception werden alle Ausnahmen abgeleitet, die ein Entwickler zur Laufzeit behandeln kann. Alle Klassen in dieser Unterhierarchie sind "Checked Exceptions" mit Ausnahme der Klasse RuntimeException. Dies bedeutet Sie müssen mit try-catch Blöcken behandelt werden.

Die Klasse RuntimeException

In der Hierarchie der Klasse RuntimeException sind alle Ausnahmen zu finden die im "normalen" Betrieb des Laufzeitsystems auftreten können. Sie sind auch "unchecked Exceptions" die man nicht im Code behandeln muss, da sie jederzeit auftreten können. 

Wichtig

Alle abgeleiteten Klassen der Klasse RuntimeException sind "unchecked exceptions".

Implikation: Sie müssen nicht mit try-catch Blöcken abgesichert werden.

 

Einige Ausnahmeklassen aus dieser Hierarchie sind:

Beispiele
Oberklasse RuntimeException Bedeutung
ArithmeticException Division durch Null bei Ganzzahlwerten.
NegativeArraySizeException Versuch der Erzeugung eines Feldes mit einer negativen Größe.
NullPointerException Versuchter Zugriff auf eine Instanz deren Referenz mit null belegt ist.

 

Das Konzept von "Checked" und "Unchecked Exceptions"

Die Unterscheidung dieser beiden Arten von Ausnahmen hat für den Entwickler die Konsequenz, dass er bei Aufrufen von Methoden die zu "Checked Exceptions" führen, eine entsprechende Behandlung bei der Implementierung durchführen muss. Behandlung bedeutet. "Unchecked Exceptions" müssen nicht in der Implementierung berücksichtig und behandelt werden.

Checked Exception

Bei Aufrufen von Methoden die zu "Checked Exceptions" führen können hat der Entwickler zwei Möglichkeiten:

  • Aufruf der entsprechen Methode innerhalb eines try-catch Blocks und der entsprechenden Behandlung der Ausnahme 
  • Weiterreichen der Ausnahme indem die gesamte Methode das Schlüsselwort throws und die Ausnahme benutzt um die Behandlung der Ausnahme an den aufrufenden Block weiterreicht.

Wird eine "checked Exception nicht mit einem try-catch Block behandelt so besteht auch die Möglichkeit die Ausnahme nicht zu behandeln. In diesem Fall muss die Ausnahme aus der aktuellen Methode herausgereicht werden. Hierfür muss die Methode im Kopf der entsprechenden Methode mit Hilfe des Schlüsselwort throws nach Aussen weitergereicht werden. Die Syntax des Methodenkopfs ist:

qualifier [return-parameter] methoden-name [extends oberklasse] (parameter-liste) {throws eine-ausnahme} {...}

Ein konkreter Methodenkopf ist z. Bsp.

public boolean istKleinerAls (Object s) throws NachnameExceptionRT {...} 

Beispiel: Die Klasse BufferReader.readLine() wird benötigt um Konsoleneingaben zu Lesen.

Unchecked Exception

"Unchecked Exception" sind sehr vielfältig und sie können in fast jeder Zeile Code auftreten.  Diese Ausnahmen müssen nicht zwingend mit try-catch Blöcken abgefangen werden.

Der Entwickler muss bei "Unchecked Exceptions" selbst die Entscheidung treffen welche Codestrecken das Potential haben eine Ausnahme auszulösen.

Das Entwurfsziel besteht darin sicher zu codieren und den Code noch lesbar zu gestalten.

Beispiel:

Eine potentielle Division durch Null kann an sehr vielen Stellen auftreten. Müsste man jede Division durch Null mit einem try-catch Block ummanteln wäre das sehr aufwendig und der Code wird sehr schlecht lesbar sein.

In der folgenden Tabelle können in den ersten beiden Fällen keine Divisionen durch Null auftreten. Im dritten Fall kann eine solche Division abhängig vom Eingabewert c auftreten. Hier obliegt es dem Entwickler das Risiko einer Division durch Null abzuschätzen und die Division eventuell mit einem try-catch Block zu schützen.

Fall 1: kein Risiko Fall 2: kein Risiko Fall 3: Risiko einer Ausnahme
public int test(int a) {
   int b = a / 2;
   return b;
}
public int test(int a) {
   int c = 2;
   int b = a / c;
   return b;
}
public int test(int a, int c) {
   int b = a / c;
   return b;
}

 

 

Weiterreichen von Ausnahmen

Java erlaubt es nicht nur in catch Blöcken Ausnahmen zu behandeln, Java erlaubt es in einem catch Block die Ausnahme zu bearbeiten und anschließend trotzdem unbehandelt weiter zureichen.

Hierdurch kann eine Aktion durchgeführt werden und die finale Lösung des Problems einem äusserem Block überlassen werden.

try {
   DemoException einProblem = new DemoException ("Anwendungsfehler 17");
   throw einProblem;
}
catch (DemoException e)
{
 //Analyse der Ausnahme
 if (e.getMessage().equals("Anwendungsfehler 17"))
    throw e;
}

Hinweis: Die Anwendung muss nicht notwendigerweise die gleiche Ausnahme weiterreichen.

Es ist auch möglich eine beliebige neue Ausnahme zu werfen. Dies kann nützlich sein, wenn man nur noch anwendungsspezifische Ausnahmen nach aussen reichen will, um andere Klassen von Ausnahmen zu verbergen.

Der try-with-resources Befehl

Dies ist eine weiterführendes Thema...

Den try-with-resources Befehl kann man seit Java 7 benutzen. Er erlaubt das Schliesen von Ressourcen und ersetzt damit den finally Block.

Java erlaubt es mit externen Ressourcen wie Dateien oder Streams umzugehen indem man Sie öffnet. Diese Ressourcen müssen aber auch wieder geschlossen werden, da das Betriebsystem hierfür Ressourcen allokieren muss.

Vor Java 7 schloß man Ressourcen im folgenden Stil:

BufferedReader buffr = new BufferedReader(new FileReader(path));
try {
   return buffr.readLine();
   } finally {
     // Wird auf jeden Fall ausfgeführt!
     if (buffr != null) {buffr.close()};
   }

Ab Java 7 kann man das eleganter beschreiben:

try (BufferedReader buffr =
   new BufferedReader(new FileReader(path))) {
      return buffr.readLine();
   }

Um diesen Stil zu verwenden, muss die Klasse die Schnittstelle java.lang.AutoCloseable b.z.w. java.io.Closeable  implementieren.

Dieser try block unterdrückt die geworfene Ausnahme!

Mehr Informationen gibt es im folgenden Oracle Tutorial.

Übungen (Ausnahmen)

Übung 1: Werfen einer Ausnahme

Verfeinern Sie die Implementierung der Klasse Person. Werfen Sie eine Ausnahme der Klasse UnsupportedOperationException  wenn beim Vergleich in der Methode compareTo() eine leere Zeichenkette ("") als Nachname implementiert ist.

Nutzen Sie die Klasse Person:

package Kurs2.Exception;
import java.text.Collator;
import java.util.Arrays;
import java.util.Locale;
public class Person implements Comparable {
private String nachname;
private String vorname;
private static int maxLaenge = 10; // Maximale Laenge von Zufallszeichenketten
private static Locale myLocale = Locale.GERMANY; // Zu verwendende Sortierordnung
/**
* Konstruktor der Klasse Person
* @param nn Nachname
* @param vn Vorname
*/
public Person(String nn, String vn) {
nachname = nn;
vorname = vn;
}
/**
*
* @param s Objekt mit dem verglichen wird
* @return -1 wenn der Eingabewert kleiner ist
* 0 wenn der Eingabewert gleich ist
* +1 wenn der Eingabewert größer ist
*/
@Override
public int compareTo(Object s) {
int result = -1;
Person p;
if (s.getClass() == this.getClass()) {
p = (Person) s;
// Konfiguriere die Sortierordnung
Collator myCollator = Collator.getInstance(myLocale);
myCollator.setStrength(Collator.TERTIARY);
result = myCollator.compare(nachname, p.nachname);
if (result ==0)
result = myCollator.compare(vorname, p.vorname);
}
return result;
}
/**
* Erlaubt den Vor- und Nachnamen als Text auszudrucken
* @return
*/
@Override
public String toString() {
return nachname + ", " + vorname;
} /** * * @param args wird nicht ausgewertet */

public static void main(String[] args) {
// Aufzählende Iniatialisierung eines Felds
Person[] psn = {
new Person("Schneider", "Hans"),
new Person("Schmidt", "Hans"),
new Person("Schneider", "Gretel"),
new Person("Schmitt", "Hans"),
new Person("Meier", "aschenputtel"),
new Person("", "Aschenputtel")
};
Arrays.sort(psn);
System.out.println("Sortiert:");
for ( Person p : psn) {
System.out.println(p);
}
}
}

Spoileralam für versierte Programmier; nicht weiterlesen!

Man kann einen leeren String mit dem folgenden Test erkennen:

if (p.vorname.equals(""))

Übung 2: Werfen einer selbstimplementierten Ausnahme

Implementieren Sie eine Klasse Kurs2.Exception.NachnameException mit den folgenden Eigenschaften:

  • Abgeleitet aus der Klasse RuntimeException oder CheckedException
    • Aus welcher der beiden Klassen können Sie Ihre Ausnahme nicht ableiten. Warum nicht? Wo gibt es einen Konflikt
       
  • Erfasst den Vornamen als Text der Ausnahme
  • Speichern des Vornamens für potentielle Wiederverwendung bei der Analyse der Ausnahme

Vorlage für Implementierung. Wählen Sie die passende der beiden Klassen aus der Sie die Ausnahme ableiten

package Kurs2.Exception;
public class NachnameException extends [RuntimeException|CheckedException] {
    ...
    public NachnameException(String vn) {...}
    }

Nutzen sie die Klasse Person der Übung 1. Passen Sie die Klasse Person so an, dass die neue Ausnahmenklasse aufgerufen wird.

Übung 3: Werfen einer selbstimplementierten Ausnahme und Behandeln

Behandeln Sie die Ausnahme in der main() Methode und geben Sie eine passende Konsolenausgabe.

Optionale Übung

Falls Ihre selbst geschriebene Ausnahme einen Zeiger auf das Objekt mit einem Null-String verwaltet, können Sie diesen Null-String mit etwas anderem ersetzen und den Sortiervorgang erneut aufrufen!

Lösungen (Ausnahmen)

Übung 1: Werfen einer Ausnahme

Hinweis: Alle Klassen dieser Übung werden im Paket Kurs2.Exception neu implementiert. Sie haben daher andere import Kommandos als die Klassen im Paket Kurs2.Sort.

Klasse Person (nur die Methode mit den erforderlichen Änderungen)

...
/**
*
* @param o
* @return -1 wenn der Eingabewert kleiner ist
* 0 wenn der Eingabewert gleich ist
* +1 wenn der Eingabewert größer ist
*/
@Override
public int compareTo(Object s) {
int result = -1;
PersonUebung1 p;
if (s.getClass() == this.getClass()) {
p = (PersonUebung1) s;
if (p.vorname.equals("")) {
throw new UnsupportedOperationException("Vorname fehlt");
}
if (nachname.equals("")) {
throw new UnsupportedOperationException("Nachname fehlt");
}

// Konfiguriere die Sortierordnung
Collator myCollator = Collator.getInstance(myLocale);
myCollator.setStrength(Collator.TERTIARY);
result = myCollator.compare(nachname, p.nachname);
if (result ==0)
result = myCollator.compare(vorname, p.vorname);
}
return result;
}
...

 

Übung 2: Werfen einer selbstimplementierten Ausnahme

Klasse NachnameException:

package Kurs2.Exception;

public class NachnameException extends RuntimeException {
public String vorname;

public NachnameException(String vn) {
super(vn + " hat keinen Nachnamen");
vorname = vn;
}

}

In der Klasse Person der vorherigen Übung muss die Methode compareTo() auch angepasst werden:

...

/**
*
* @param o
* @return -1 wenn der Eingabewert kleiner ist
* 0 wenn der Eingabewert gleich ist
* +1 wenn der Eingabewert größer ist
*/
@Override
public int compareTo(Object s) {
int result = -1;
PersonUebung2 p;
if (s.getClass() == this.getClass()) {
p = (PersonUebung2) s;
if (p.vorname.equals("")) {
throw new UnsupportedOperationException("Vorname fehlt");
}
if (nachname.equals("")) {
throw new NachnameException(vorname);
}
// Konfiguriere die Sortierordnung
Collator myCollator = Collator.getInstance(myLocale);
myCollator.setStrength(Collator.TERTIARY);
result = myCollator.compare(nachname, p.nachname);
if (result ==0)
result = myCollator.compare(vorname, p.vorname);
}
return result;
}

...

Übung 3: Werfen einer selbstimplementierten Ausnahme und Behandeln

Klasse NachnameExceptionChecked

package Kurs2.Exception;

public class NachnameExceptionChecked extends Exception {
public String vorname;
public Boolean zweitesArgument;
/**
*
* @param vn Vorname des Objekts ohne Nachnamen
* @param zweitesArg ist wahr wenn das zweite Objekt beim Vergleich
* keinen Nachnamen hat. Ist falsch wenn das erste Objekt keinen
* Nachnamen hat
*/
public NachnameExceptionChecked(String vn, Boolean zweitesArg) {
super(vn + " hat keinen Nachnamen");
vorname = vn;
zweitesArgument = zweitesArg;
}

}

Klasse Person

Die Methode main():

public static void main(String[] args) {
Person[] psn = new Person[6];
psn[0] = new Person("Schneider", "Hans");
psn[1] = new Person("Schmidt", "Hans");
psn[2] = new Person("Schneider", "Gretel");
psn[3] = new Person("Schmitt", "Hans");
psn[4] = new Person("Meier", "aschenputtel");
psn[5] = new Person("", "Aschenputtel");
try {
Arrays.sort(psn);
}
catch (NachnameException e) {
System.out.println("Das Feld konnte nicht sortiert werden, da " +
e.vorname + " keinen Nachnamen besitzt");
}

System.out.println("Sortiert:");
for ( PersonUebung3 p : psn) {
System.out.println(p);
} }

 

Beispielprogramme aus der Vorlesung

In diesem Beispiel wird die Klasse Konto Schritt für Schritt weiterentwickelt um die den Einsatz von Assertions und Ausnahmen zu diskutieren.

Die durchnummierten Klassen repräsentieren verschiedene Enwticklungsstufen. Sie erlauben es wieder den Einstieg zu finden falls man beim Mittippen nicht mehr mitgekommen ist.

Klasse Konto

Beispiel eines Bankkontos. In der main() Methode werden einfache Überweisungsoperationen ausgeführt.

Welche inkorrekten Aufrufe können in dieser Klasse erfolgen?

package Kurs2.Exception.Demo;
public class Konto {
int betrag;
public Konto(int startBetrag) {
betrag = startBetrag;
}
private void einzahlen(int b) {
betrag += b;
}
private void auszahlen(int b) {
betrag -= b;
}
public void ueberweisenAuf (Konto b, int wert) {
auszahlen(wert);
b.einzahlen(wert);
}
@Override
public String toString() {return betrag + " Euro";} /** * Die main Methode sei eine Methode die von einem Team gepflegt wird * welches nichts von der internen Implementierung der Klasse weis. * Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse * implementiert * @param args */ public static void main(String[] args) { Konto a1 = new Konto(500); Konto a2 = new Konto(100); a2.ueberweisenAuf(a1, 50); System.out.println("a1: "+ a1 + "; a2= " +a2); a2.ueberweisenAuf(a1, -500); System.out.println("a1: "+ a1 + "; a2= " +a2); }

}

Klasse Konto1

package Kurs2.Exception.Demo;
public class Konto1 {
int betrag;
public Konto1(int startBetrag) {
betrag = startBetrag;
}
private void einzahlen(int b) {
betrag += b;
}
private void auszahlen(int b) {
betrag -= b;
}
public void ueberweisenAuf (Konto1 b, int wert) {
auszahlen(wert);
b.einzahlen(wert);
}
@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto1 a1 = new Konto1(500);
Konto1 a2 = new Konto1(100);
a2.ueberweisenAuf(a1, 50);
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, -500);
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
}

Klasse Konto2

In diesem Beispiel werden die Methoden so modifiziert, dass sie einen Wahrheitswert zurückgeben der über die korrekte Ausführung informiert.

Welche Nachteile hat eine solche Implementierung?

package Kurs2.Exception.Demo;
public class Konto2 {
int betrag;
public Konto2(int startBetrag) {
betrag = startBetrag;
}
private boolean einzahlen(int b) {
if (b>=0) {
betrag += b;
return true;
}
else return false;
}
private boolean auszahlen(int b) {
if (b>=0) {
betrag -= b;
return true;
}
else return false;
}

public boolean ueberweisenAuf (Konto2 b, int wert) {
boolean korrekt;
korrekt =
auszahlen(wert);
if (korrekt)
korrekt=
b.einzahlen(wert);
return korrekt;
}

@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nicht die interne Implementierung der Klasse.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto2 a1 = new Konto2(500);
Konto2 a2 = new Konto2(100);
boolean korrekt;
korrekt = a2.ueberweisenAuf(a1, 50);
if (korrekt)
System.out.println("a1: "+ a1 + "; a2= " +a2);
else
System.out.println("Fehler; a1: "+ a1 + "; a2= " +a2);

korrekt = a2.ueberweisenAuf(a1, -500);
if (korrekt)
System.out.println("a1: "+ a1 + "; a2= " +a2);
else
System.out.println("Fehler; a1: "+ a1 + "; a2= " +a2);

}
}

Kasse Konto3

In dieser Implementierung werden "Assertions" verwendet.

Die Verwendung der Assertion wird mit Hilfe der Option -ea aktiviert.

Man startet das Programm mit dem Befehl

$ java -ea Kurs2.Exception.Demo.Konto3

Jetzt werden die Assertions ausgeführt und geprüft. Das Programm wird bei der ersten Verletzung einer Assertion beendet.

package Kurs2.Exception.Demo;
public class Konto3 {
int betrag;
public Konto3(int startBetrag) {
betrag = startBetrag;
}
private void einzahlen(int b) {
assert (b>=0): "Versuch " + b + " zu einzuzahlen";
betrag += b;
}
private void auszahlen(int b) {
assert (b>=0): "Versuch " + b + " zu auszuzahlen";
betrag -= b;
}
public void ueberweisenAuf (Konto3 b, int wert) {
auszahlen(wert);
b.einzahlen(wert);
}
@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto3 a1 = new Konto3(500);
Konto3 a2 = new Konto3(100);
boolean korrekt;
a2.ueberweisenAuf(a1, 50);
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, -500);
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
}

Tipp für Netbeansbenutzer

Auf Eclipse ist sinngemäss das gleiche zu tun.

Einschalten des Assertionchecking für die Klasse Konto3:

  1. Rechtsklicken und halten  auf dem Projektsymbol (Kafeetassensymbol) in dem sich die Klasse Konto3 befindet.
  2. Auswahl des letzten Menüpunkts "Properties".
  3. Auswahl der Kategory "Run"
  4. In "Configuration" den "New" Knopf drücken
  5. Im modalen Dialog einen Konfigurationsname wählen (z.Bsp. Konto3Assertion)
  6. Feld "Main Class" belegen in dem man den Knopf rechts davon "Browse" drückt und die gewünschte Klasse im Projekt wählt
  7. Es werden alle Klassen im Projekt angezeigt, die als Startprogramm dienen können. Wählen Sie die Klasse Konto3 aus.
  8. Fügen sie im Textfeld "VM Options" "-ea" um Assertions einzuschalten.
  9. Speichern Sie den Dialog duech Drücken der Taste "OK"

Starten des Programms

  1. Rechtsklicken und halten auf dem Projektsymbol (Kafeetassensymbol) in dem sich die Klasse Konto3 befindet.
  2. Auswahl des letzten Menüpunkts "Set Configuration".
  3. Wählen Sie die im Vorgängerschritt angelegte Konfiguration
  4. Starten Sie das Programm mit Rechtsklicken und halten auf dem Projektsymbol (Kafeetassensymbol) in dem sich die Klasse Konto3 befindet.
  5. Wählen Sie "Run" aus

Das Programm kann jetzt wahrscheinlich auch mit der Ikone mit dem großen grünen Dreieck in der Menüleiste gestartet werden.

Klasse Konto4

Das Verwenden von Assertions in den privaten Methoden einzahlen() und auszahlen() ist sinnvoll, da die beiden Methoden nur innerhalb der Klasse aufgerufen werden können. Der Entwickler der Klasse kann garantieren, dass sie immer mit korrekten Werten aufgerufen werden. Ein negativer Betrag als Parameter soll nicht vorkommen.

In der folgenden Klasse wir die Methode ueberweisenAuf() auch mit einer Assertion geschützt. Die Methode ist öffentlich und kann auch von externen Klassen aufgerufen werden.

Warum ist dies keine gute Implementierung?

package Kurs2.Exception.Demo;
public class Konto4 {
int betrag;
public Konto4(int startBetrag) {
betrag = startBetrag;
}
private void einzahlen(int b) {
assert (b>=0);
betrag += b;
}
private void auszahlen(int b) {
assert (b>=0);
betrag -= b;
}
public void ueberweisenAuf (Konto4 b, int wert) {
assert(wert>=0): "Versuch einer negativen Überweisung von " + wert ;
auszahlen(wert);
b.einzahlen(wert);
}
@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto4 a1 = new Konto4(500);
Konto4 a2 = new Konto4(100); boolean korrekt; a2.ueberweisenAuf(a1, 50); System.out.println("a1: "+ a1 + "; a2= " +a2); a2.ueberweisenAuf(a1, -500); System.out.println("a1: "+ a1 + "; a2= " +a2); } }

Klasse Konto5

Die Klasse Konto5 erweitert die Implementierung. Es wird ein Überweisungslimit eingeführt. Das Überweisungslimit wird im Konstruktor erfasst. Es sollen keine Überweisungen mit einem höheren Betrag als dem Überweisungslimit ausgeführt.

package Kurs2.Exception.Demo;
public class Konto5 {
int betrag;
private int ueberweisungsLimit;
private static final int MINLIMIT =1;
public Konto5(int startBetrag, int ll) {
betrag = startBetrag;
if (ll>0) ueberweisungsLimit=ll;
else ueberweisungsLimit = MINLIMIT;
}
private void einzahlen(int b) {
assert (b>=0);
betrag += b;
}
private void auszahlen(int b) {
assert (b>=0);
betrag -= b;
}
public void ueberweisenAuf (Konto5 b, int wert) {
assert(wert>=0);
auszahlen(wert);
b.einzahlen(wert);
}
@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto5 a1 = new Konto5(500, 50);
Konto5 a2 = new Konto5(100, 80);
boolean korrekt;
a2.ueberweisenAuf(a1, 50);
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, -500);
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, 100);
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
}

Klasse Konto6

In dieser Implementierung werden inkorrekte Überweisungen mit negativen Beträgen oder Beträgen jenseits des Überweisungslimit mit Hilfe von unterschiedlichen Rückgabewerten und Konstanten gemeldet.

Die gewählte Implementierung vermeidet Fehler und meldet diese ausführlich.

Die Behandlung der Fehlerwerte ist jedoch aufwändig.

package Kurs2.Exception.Demo;
public class Konto6 {
int betrag;
private int ueberweisungsLimit;
private static final int MINLIMIT =1;
public static final int OK = 0;
public static final int NEGATIVERWERT = 1;
public static final int LIMITUEBERSCHRITTEN = 2;
public Konto6(int startBetrag, int ll) {
betrag = startBetrag;
if (ll>0) ueberweisungsLimit=ll;
else ueberweisungsLimit = MINLIMIT;
}
private void einzahlen(int b) {
assert (b>=0);
betrag += b;
}
private void auszahlen(int b) {
assert (b>=0);
betrag -= b;
}
public int ueberweisenAuf (Konto6 b, int wert) {
if (wert < 0) return NEGATIVERWERT;
else
if ((wert > ueberweisungsLimit )|| (wert > b.ueberweisungsLimit ))
return LIMITUEBERSCHRITTEN;
else {

auszahlen(wert);
b.einzahlen(wert);
return OK;
}

}
@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto6 a1 = new Konto6(500, 50);
Konto6 a2 = new Konto6(100, 80);
int status;
status = a2.ueberweisenAuf(a1, 50);
if (status != OK)
System.out.println("Fehler: " + status);

else
System.out.println("a1: "+ a1 + "; a2= " +a2);
status = a2.ueberweisenAuf(a1, -500);
if (status != OK)
System.out.println("Fehler: " + status);
else

System.out.println("a1: "+ a1 + "; a2= " +a2);
status = a2.ueberweisenAuf(a1, 100);
if (status != OK)
System.out.println("Fehler: " + status);
else

System.out.println("a1: "+ a1 + "; a2= " +a2);
}
}

Klasse Konto7

In dieser Implementierung wird die Ausnahme IllegalArgumentException aus den Javastandardklassen verwendet.

package Kurs2.Exception.Demo;
public class Konto7 {
int betrag;
private int ueberweisungsLimit;
private static final int MINLIMIT =1;
public static final int OK = 0;
public static final int NEGATIVERWERT = 1;
public static final int LIMITUEBERSCHRITTEN = 2;
public Konto7(int startBetrag, int ll) {
betrag = startBetrag;
if (ll>0) ueberweisungsLimit=ll;
else ueberweisungsLimit = MINLIMIT;
}
private void einzahlen(int b) {
assert (b>=0);
betrag += b;
}
private void auszahlen(int b) {
assert (b>=0);
betrag -= b;
}
public void ueberweisenAuf (Konto7 b, int wert) {
if (wert < 0) throw new IllegalArgumentException("Negativer Wert " + wert);
else
if (wert > ueberweisungsLimit )
throw new IllegalArgumentException("Limit ueberschritten " + wert
+ "; Limit: " + ueberweisungsLimit);
else
if (wert > b.ueberweisungsLimit )
throw new IllegalArgumentException("Limit ueberschritten " + wert
+ "; Limit: " + b.ueberweisungsLimit);

else {
auszahlen(wert);
b.einzahlen(wert);
}
}
@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto7 a1 = new Konto7(500, 50);
Konto7 a2 = new Konto7(100, 80);
int status;
a2.ueberweisenAuf(a1, 50);
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, -500);
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, 100);
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
}

Klasse Konto8 und Ueberweisungsausnahme

 In dieser Implementierung werden eigens implementierte Ausnahmen (Exceptions) verwendet um eine Ausnahmebehandlung durchzuführen.

Klassenhierarchie der neuen Ausnahme

package Kurs2.Exception.Demo;
public class Konto8 {
int betrag;
private int ueberweisungsLimit;
private static final int MINLIMIT =1;
public static final int OK = 0;
public static final int NEGATIVERWERT = 1;
public static final int LIMITUEBERSCHRITTEN = 2;
public Konto8(int startBetrag, int ll) {
betrag = startBetrag;
if (ll>0) ueberweisungsLimit=ll;
else ueberweisungsLimit = MINLIMIT;
}
private void einzahlen(int b) {
assert (b>=0);
betrag += b;
}
private void auszahlen(int b) {
assert (b>=0);
betrag -= b;
}
public void ueberweisenAuf (Konto8 b, int wert) {
if (wert < 0) throw new UeberweisungsAusnahme("Negativer Wert" + wert);
else
if (wert > ueberweisungsLimit )
throw new UeberweisungsAusnahme("Limit ueberschritten " + wert
+ "; Limit: " + ueberweisungsLimit);
else
if (wert > b.ueberweisungsLimit )
throw new UeberweisungsAusnahme("Limit ueberschritten " + wert
+ "; Limit: " + b.ueberweisungsLimit);
else {
auszahlen(wert);
b.einzahlen(wert);
}
}
@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto8 a1 = new Konto8(500, 50);
Konto8 a2 = new Konto8(100, 80);
int status;
a2.ueberweisenAuf(a1, 50);
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, -500);
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, 100);
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
}

Die Klasse Ueberweisungsausnahme

Die Klasse wird aus der Klasse IllegalArgumentException abgeleitet. Sie muss nicht behandelt werden da diese Ausnahme eine Spezialisierung der RuntimeException ist. Das Auftreten der Ausnahme im Hauptprogramm kann aber behandelt werden.

package Kurs2.Exception.Demo;
public class UeberweisungsAusnahme extends IllegalArgumentException {
public UeberweisungsAusnahme(String text) {
super(text);
}
}

Klasse Konto9

In der Klasse Konto9 wird in der main() Methode eine geworfene Ausnahme behandelt.

package Kurs2.Exception.Demo;
public class Konto9 {
int betrag;
private int ueberweisungsLimit;
private static final int MINLIMIT =1;
public static final int OK = 0;
public static final int NEGATIVERWERT = 1;
public static final int LIMITUEBERSCHRITTEN = 2;
public Konto9(int startBetrag, int ll) {
betrag = startBetrag;
if (ll>0) ueberweisungsLimit=ll;
else ueberweisungsLimit = MINLIMIT;
}
public void einzahlen(int b) {
assert (b>=0);
betrag += b;
}
public void auszahlen(int b) {
assert (b>=0);
betrag -= b;
}
public void ueberweisenAuf (Konto9 b, int wert) {
if (wert < 0) throw new UeberweisungsAusnahme("Negativer Wert" + wert);
else
if (wert > ueberweisungsLimit )
throw new UeberweisungsAusnahme("Limit ueberschritten " + wert
+ "; Limit: " + ueberweisungsLimit);
else
if (wert > b.ueberweisungsLimit )
throw new UeberweisungsAusnahme("Limit ueberschritten " + wert
+ "; Limit: " + b.ueberweisungsLimit);
else {
auszahlen(wert);
b.einzahlen(wert);
}
}
@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto9 a1 = new Konto9(500, 50);
Konto9 a2 = new Konto9(100, 80); int status; a2.ueberweisenAuf(a1, 50); System.out.println("a1: "+ a1 + "; a2= " +a2); try { a2.ueberweisenAuf(a1, -500);
}
catch (UeberweisungsAusnahme ue) {
System.out.println("Fehler aufgetreten" + ue.getMessage());
System.out.println ("Operation nicht ausgeführt");
}

System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, 100);
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
}

Klasse Konto10

In der Klasse Konto10 wird zwischen einer allgemeinen ÜberweisungsAusnahme und einer LimitAusnahme unterschieden.

Klassenhierarchie mit Ausnahmen 

package Kurs2.Exception.Demo;
public class Konto10 {
int betrag;
private int ueberweisungsLimit;
private static final int MINLIMIT =1;
public static final int OK = 0;
public static final int NEGATIVERWERT = 1;
public static final int LIMITUEBERSCHRITTEN = 2;
public Konto10(int startBetrag, int ll) {
betrag = startBetrag;
if (ll>0) ueberweisungsLimit=ll;
else ueberweisungsLimit = MINLIMIT;
}
public void einzahlen(int b) {
assert (b>=0);
betrag += b;
}
public void auszahlen(int b) {
assert (b>=0);
betrag -= b;
}
public void ueberweisenAuf (Konto10 b, int wert) {
if (wert < 0) throw new UeberweisungsAusnahme("Negativer Wert" + wert);
else {
int maxlimit = (ueberweisungsLimit<b.ueberweisungsLimit) ?
ueberweisungsLimit: b.ueberweisungsLimit;
if (wert > maxlimit )
throw new LimitAusnahme("Limit ueberschritten " + wert,
maxlimit);

else {
auszahlen(wert);
b.einzahlen(wert);
}
}
}
@Override
public String toString() {return betrag + " Euro";}
/**
* Die main Methode sei eine Methode die von einem Team gepflegt wird
* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto10 a1 = new Konto10(500, 50);
Konto10 a2 = new Konto10(50, 80);
int status;
a2.ueberweisenAuf(a1, 50);
System.out.println("a1: "+ a1 + "; a2= " +a2);
try {
a2.ueberweisenAuf(a1, -500);
}
catch (UeberweisungsAusnahme ue) {
System.out.println("Fehler aufgetreten" + ue.getMessage());
System.out.println ("Operation nicht ausgeführt");
}
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, 100);
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
}

Klasse LimitAusnahme

Diese Klasse kann das Limit welches verletzt wurde verwalten. Hierdurch können Nutzer mehr Informationen erhalten und intelligenter reagieren.

package Kurs2.Exception.Demo;
public class LimitAusnahme extends UeberweisungsAusnahme {
public int limit;
public LimitAusnahme(String text, int ll) {
super(text);
limit = ll;
}
}

Klasse Konto11

Die Klasse Konto11 reagiert auf eine Limitverletzung durch mehrere Überweisungen die unter dem Limit bleiben.

package Kurs2.Exception.Demo;
public class Konto11 {
int betrag;
private int ueberweisungsLimit;
private static final int MINLIMIT =1;
public static final int OK = 0;
public static final int NEGATIVERWERT = 1;
public static final int LIMITUEBERSCHRITTEN = 2;
public Konto11(int startBetrag, int ll) {
betrag = startBetrag;
if (ll>0) ueberweisungsLimit=ll;
else ueberweisungsLimit = MINLIMIT;
}
private void einzahlen(int b) {
assert (b>=0);
betrag += b;
}
private void auszahlen(int b) {
assert (b>=0);
betrag -= b;
}
public void ueberweisenAuf (Konto11 b, int wert) {
if (wert < 0) throw new UeberweisungsAusnahme("Negativer Wert" + wert);
else {
int maxlimit = (ueberweisungsLimit<b.ueberweisungsLimit) ?
ueberweisungsLimit: b.ueberweisungsLimit;
if (wert > maxlimit )
throw new LimitAusnahme("Limit ueberschritten " + wert,
maxlimit);
else {
auszahlen(wert);
b.einzahlen(wert);
}
}
}
@Override
public String toString() {return betrag + " Euro";} /** * Die main Methode sei eine Methode die von einem Team gepflegt wird

* welches nichts von der internen Implementierung der Klasse weis.
* Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse
* implementiert
* @param args
*/
public static void main(String[] args) {
Konto11 a1 = new Konto11(500, 50);
Konto11 a2 = new Konto11(100, 80);
int status;
a2.ueberweisenAuf(a1, 50);
System.out.println("a1: "+ a1 + "; a2= " +a2);
try {
a2.ueberweisenAuf(a1, -500);
}
catch (UeberweisungsAusnahme ue) {
System.out.println("Fehler aufgetreten " + ue.getMessage());
System.out.println ("Operation nicht ausgeführt");
}
System.out.println("a1: "+ a1 + "; a2= " +a2);
int betrag = 100;
try {
a2.ueberweisenAuf(a1, betrag);
}
catch (LimitAusnahme ue) {
// Splitte Überweisung in mehrere Ueberweisungen unterhalb des Limits
int dasLimit = ue.limit;
while (betrag > dasLimit) {
betrag = betrag - dasLimit;
a2.ueberweisenAuf(a1, dasLimit);
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
a2.ueberweisenAuf(a1, betrag);
}

System.out.println("a1: "+ a1 + "; a2= " +a2);
}
}

Klasse Konto12

In der Klasse Konto12 wird eine andere Ausnahme bei einem Überweisungsfehler geworfen. Die Ausnahmeklasse UeberweisungsAusnahme12 ist aus der Klasse Exception abgeleitet.

Klassenhierarchie von Ausnahmen

Da die Ausnahme in der Methode ueberweisenAuf() geworfen wird muss

  • ... dies im Kopf der Methode dokumentiert werden
  • ... bei der Verwendung der Methode ueberweisenAuf() immer ein tra catch Block implementiert werden.
package Kurs2.Exception.Demo;
public class Konto12 {
int betrag;
private int ueberweisungsLimit;
private static final int MINLIMIT =1;
public static final int OK = 0;
public static final int NEGATIVERWERT = 1;
public static final int LIMITUEBERSCHRITTEN = 2;
public Konto12(int startBetrag, int ll) {
betrag = startBetrag;
if (ll>0) ueberweisungsLimit=ll;
else ueberweisungsLimit = MINLIMIT;
}
private void einzahlen(int b) {
assert (b>=0);
betrag += b;
}
private void auszahlen(int b) {
assert (b>=0);
betrag -= b;
}
public void ueberweisenAuf(Konto12 b, int wert) throws UeberweisungsAusnahme12 {
if (wert < 0) throw new UeberweisungsAusnahme12("Negativer Wert" + wert);
else {
int maxlimit = (ueberweisungsLimit<b.ueberweisungsLimit) ?
ueberweisungsLimit: b.ueberweisungsLimit;
if (wert > maxlimit )
throw new LimitAusnahme("Limit ueberschritten " + wert,
maxlimit);
else {
auszahlen(wert);
b.einzahlen(wert);
}
}
}
@Override
public String toString() {return betrag + " Euro";} /** * Die main Methode sei eine Methode die von einem Team gepflegt wird * welches nichts von der internen Implementierung der Klasse kennt. * Die Methode wurde nur aus Gründen der Kompaktheit in dieser Klasse * implementiert * @param args
*/
public static void main(String[] args) {
Konto12 a1 = new Konto12(500, 50);
Konto12 a2 = new Konto12(100, 80);
int status;
try {
a2.ueberweisenAuf(a1, 50);
System.out.println("a1: "+ a1 + "; a2= " +a2);
a2.ueberweisenAuf(a1, -500);
System.out.println("a1: "+ a1 + "; a2= " +a2);
int betrag = 100;
try {
a2.ueberweisenAuf(a1, betrag);
}
catch (LimitAusnahme ue) {
// Splitte Überweisung in mehrere unter des Limits
int dasLimit = ue.limit;
while (betrag > dasLimit) {
betrag = betrag - dasLimit;
a2.ueberweisenAuf(a1, dasLimit);
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
a2.ueberweisenAuf(a1, betrag);
}
System.out.println("a1: "+ a1 + "; a2= " +a2);
}
catch (UeberweisungsAusnahme12 ue) {
System.out.println("Fehler aufgetreten " + ue.getMessage());
System.out.println ("Eine Überweisung wurde nicht ausgeführt");
}

}
}

Klasse Ueberweisungsausnahme12

Die Klasse wird aus der Klasse Exception abgeleitet. Sie ist eine "checked Exception" und muss behamdelt oder deklariert werden

package Kurs2.Exception.Demo;
public class UeberweisungsAusnahme12 extends Exception {
public UeberweisungsAusnahme12(String text) {
super(text);
}
}

 

Lernziele (Ausnahmen)

Am Ende dieses Blocks können Sie:

  • ...  abschätzen wann Sie im Programmablauf Ausnahmen einsetzen um den Kontrollfluß übersichtlich zu gestalten
  • ...  wissen Sie wie Sie auf Ausnahmen reagieren können ohne das die Anwendung beendet wird
  • ...  die Syntax  von try-catch Blöcken anwenden um Ausnahmen zu behandeln
  • ...  mit Hilfe der Exception-Klasshierarchie entscheiden, welcher try-catch Block beim Auftreten einer Ausnahme ausgeführt wird
  • ...  zwischen checked- und unchecked Exceptions unterscheiden und die entsprechenden Implementierungsmaßnahmen treffen.
  • ...  selbst Ausnahmen werfen
  • ...  selbst Ausnahmeklassen implementieren 
  • ... die folgenden Klassen in ihre Vererbungshierarchie einordnen: Throwable, Error, Exception, RuntimeException
  • ... try-catch Blöcke mit mehren catch Blöcken interpretieren

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: Fragen zu Ausnahmen (Exception)

Oberflächenprogrammierung mit Swing

Grafische Benutzeroberflächen in Java

  • GUI: englische Abkürzung für "Graphical User Interface" oft auch mit UI (User Interface) abgekürzt
  • Ziel graphischer Benutzeroberflächen:
    • Intuitiv bedienbare Benutzerschnittstellen für ungeübte Benutzer
    • Optionen zu effizienten Benutzung von erfahrenen Benutzern (Kommandos auf Befehlstasten, Tabulatoren zum Bewegen zwischen Eingabefeldern etc.)
  • Vorteil von Java-GUIs
    • Plattformunabhängigkeit (write once, run anywhere)

Entwicklungsgeschichte der grafischen Benutzerschnittstellen in Java (JRE)

Swing ist das Ergebnis einer Evolution der grafischen Benutzerschnittstellen die seit 1996 immer weiterentwickelt wurden. Der geschichtliche Überblick ist hilfreich beim Verstehen der recht komplexen Paketstrukturen von Swing und AWT.

Java 1.0: Another Windowing Toolkit (AWT)

Die ursprüngliche Grafikbibliothek AWT sollte die folgenden Anforderungen erfüllen

  • einfach zu verstehen und zu Programmieren ("Volks GUI")
    • weniger komplex als das damals populäre X11, Motif von Unix-Workstations
    • unabhängig von Windows (Rechte, Lizensen!)
    • Kleine Teilmenge der wichtigsten GUI Elemente
  • geplanter Einsatz Browser-Applets und TV-Settop Boxen (Auf Neudeutsch "Kabeltuner")
    • Internet Browsers zu mehr Interaktion und mehr Intelligenz zu verhelfen
  • leicht auf unterschiedlichen Betriebssytemen zu implementieren

AWT hat daher die folgenden Eigenschaften

  • nur wenige GUI Komponenten
  • HeavyWeight Implementierung (direkte Abbildung auf Betriebssystemkomponenten)
  • Threadsicher
  • keine Model-View-Controller Architektur

Java 1.1: AWT + Java Foundation Classes (JFC)

Das AWT musste um eine objektorientierte Ereignissteuerung erweitert werden da das Implementieren von GUIs mit vielen Komponenten sehr unübersichtich war

Gleichzeitig wurde ein vollkommenes Neudesign mit einem optionalen Package (javax Pakete) mit dem Projektnamen Swing (engl. Schaukel) vorgestellt. Der offizielle Name war "Java Foundation Classes".

Da JFC zum Zugriff auf die Betriebssytemkomponenten AWT benutzen muss, ist es von AWT abhängig. JFC ist eine "light weight" Implementierung und daher stärker vom gegebenen Betriebssytemen entkoppelt.

/sites/default/files

Duke auf Schaukel

Java 1.2: JFC/Swing Standard GUI

  • Jahr 1998

JFC (Swing) muss nicht mehr separat geladen werden und ist Bestandteil des Standard JREs. Es verdrängt AWT mehr und mehr. Seine wichtigsten Eigenschaften sind

Swing wurde seit JRE 1.2 kontinuierlich weiterentwickelt und erhielt in jeder neuen JRE Version zusätzliche Eigenschaften.

Die Demoanwendung SwingSet2 ist eine gute Referenzanwendung in der sehr viele Komponenten verwendet werden (Quellen der Demoanwendung)

"Lightweight" versus "Heavyweight" Implementierungen

Man spricht von Heavyweight Komponenten wenn zu ihrer Implementierung die vom Betriebssytem zu Verfügung gestellten Komponenten verwendet werden.

In einer Heavyweight Implementierung wird z.Bsp. zur Anzeige einer Menüauswahliste direkt die Menüauswahlliste des Betriebssytem verwendet.

Man spricht von einer Lightweight Implementierung wenn eine Grafikbibliothek vom Betriebsystem nur einen Pixelbereich auf dem Bildschirm nutzt und dann die Komponenten selbst zeichnet (rendered). Bei einer Lightweigtimplemenierung muss die Anwendung zum Beispiel eine Menüauswahlliste selbst auf dem Bildschirm zeichnen und selbst darauf achten, welche Bereiche des Fenster überschrieben werden.

Abwägung
  Vorteile Nachteile
Heavyweight
  • Schnell, automatische Grafikbeschleunigung
  • Perfekte Abstimmung mit anderen grafischen Elementen des OS
  • Look & Feel des Betriebssystems
  • Plattformabhängig
    • Ereignisse
    • Komponenten
    • Look & Feel
Lightweight
  •  Plattformunabhängig
    • einheitliches Modell für Ereignisse
    • Look & Feel frei wählbar
    • Es können Ereignisse und Komponenten modelliert werden die eine Plattform nicht bietet
  • Plattform kennt Ereignisse und Komponenten nicht
  • Grafikbeschleunigung kann fehlen (nicht mehr relevant in den aktuellen Java Versionen)
  • Komplexere Interaktion mit grafischen Komponenten anderer Programme (Zwischenablagen, Verdecken von Bereichen etc.)

 

Anmerkung: Die historischen (vor 2005) Probleme von Swing sind in den aktuellen Javaversion nicht mehr vorhanden. Moderne Garbage-Kollektoren blockieren die Benutzerschnittstellen nicht mehr sichtbar. Die aktuelle 2D Bibliothek von Java zeichnet in der Regel ausreichend schnell und benutzt auf den gängigen Plattformen (Windows, Mac, Linux) die Betriebssystemoptimierungen für Fonts, 2D- und 3D-Operationen. JavaFX stellt auch die nötigen hardwareunterstützten Operationen für Bildtransformationen und Filme zur Verfügung.

Implementieren von einfachen Swingkomponenten

Swing-Komponenten

Eine sehr anschauliche Übersicht über die existierenden Swing-Komponenten kann man in de Oracle Tutorials finden.

Die SwingSet2 Demo zeigt viele Komponenten in einer einzigen Anwendung. Man startet Sie nach dem Herunterladen von Google Code so:

java -jar SwingSet2.jar
Definition
Komponente (In Java Swing)
Komponenten sind grafische Bereiche die mit dem Benutzer interagieren können.

 

Toplevel Container

Swing verfügt über drei "Top-level-Container" in denen Benutzeroberflächen implementiert werden können:

  • JFrame: reguläre freistehende Programmfenster mit (optionaler) Menüleiste
  • JApplet; Programmfenster die auf einer html Seite plaziert werden. Ihre Lebensdauer hängt von der html Seite ab
  • JDialog: Eine Dialog-Box für modale Dialoge wie z.Bsp. Fehlermeldungen
  • JWindow: Ein freistehendes Fenster ohne Menüleisten, ohne Buttons zur Verwaltung des Fensters
Definition
Container (In Java Swing)
Ein Container ist eine Komponente die andere AWT Kompomenten enthalten kann.

Fenster: Klasse JFrame

Fenster können mit der Klasse JFrame erzeugt werden. Das folgende Beispielprogramm erzeugt ein einfaches Fenster der Größe 300 Pixel x 100 Pixel mit einem Titel in der Kopfleiste:

package Kurs2.swing;

import javax.swing.JFrame;

public class JFrameTest {
    public static void main(String[] args) {
        JFrame myFrame = new JFrame("Einfaches Fenster");
        myFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Beende Anwendung bei Schließen des Fensters
        myFrame.setSize(300,100); // Fenstergroesse 300x100 Pixel
        myFrame.setVisible(true); // Mache Fenster sichtbar
    }
}

Erzeugt das folgende Fenster (MacOS):

Die Klasse JFrame erbt die Eigenschaften von Component, Container, Window und Frame aus dem AWT Paket:

 

Einige weitere wichtige Methoden sind in der API Dokumentation des Skripts beschrieben. Die vollständge Beschreibung findet man in der Java API Dokumentation.

 Ein JFrame besteht aus mehreren Ebenen in denen unterschiedliche grafische Objekt positioniert sind:

Die Verwendung der unterschiedlichen Ebenen ist ein fortgeschrittenes Thema und wird im Rahmen des Kurses nicht behandelt. Es ist jedoch wichtig zu wissen, dass diese verschiedenen Ebenen existieren. Das Glass ane erlaubt beispielsweise eine unsichtbare Struktur über das GUI zu legen und damit zum Beispiel Mausereignisse abzufangen. Mehr Informationen sind z. Bsp. in der Oracle Dokumentation zu finden.

Buttons: Die Klasse JButton

Buttons können leicht mit der Klasse JButton erzeugt werden. Sie können mit der add() Methode dem Fenster hinzugefügt werden da das JFrame als Container in der Lage ist Komponenten aufzunehmen.

Beispiel: JButton in einem Applet

Applet Quellcode
package Kurs2.Swing;

import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JApplet;
import javax.swing.JButton;

public class ButtonJApplet extends JApplet implements ActionListener {

private JButton myButton;
/**
* zaehlt Tastendrücke
*/
private int zaehler=0;

public ButtonJApplet() {
myButton = new JButton();
myButton.setText("Click mich");
myButton.addActionListener(this);

Container myPane = getContentPane();
myPane.add(myButton);
}

public void actionPerformed(ActionEvent e) {
myButton.setText("Kicher (" + ++zaehler + ")");
}
}

Beispiel: JButton in einem JFrame

package Kurs2.swing;

import javax.swing.JButton;
import javax.swing.JFrame;

public class JFrameTest {
public static void main(String[] args) {
JFrame myFrame = new JFrame("Einfaches Fenster");
myFrame.setVisible(true);
myFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
myFrame.add(new JButton("Click mich ich bin ein JButton!"));
myFrame.setSize(300,100);
myFrame.setVisible(true);
}
}

Ergibt das folgende Fenster:

 Swing bietet eine Reihe von Varianten von Buttons an auch in Menüleisten verwendet werden können. Die entsprechenden Klassen werden von der Klasse AbstractButton abgeleitet:

Textfelder: Klasse JTextComponent und JTextField

Swing erlaubt die Verwaltung von Eingabetextfeldern durch die Klasse JTextField. Die Klasse JTextField ist die einfachste Möglichkeit Text auszugeben und einzugeben wie im folgenden Beispiel zu sehen ist.

Beispiel: JTextfield in JFrame

import javax.swing.JFrame;
import javax.swing.JTextField;

public class TextfeldTest {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Ein Fenster mit Textfeld");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(new JTextField("Editier mich. Ich bin ein JTexfield", 60));
        frame.setSize(300, 100);
        frame.setVisible(true);
    }
}

Das Programm erzeugt bei der Ausführung das folgende Fenster:

Beispiel: JTextField in einem Applet

Applet Quellcode

Hinweis: Eingabe mit "Enter"-Taste beenden.

package Kurs2.Swing;

import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JApplet;
import javax.swing.JTextField;

public class TextfieldJApplet extends JApplet implements ActionListener {

private JTextField myTextField;

public TextfieldJApplet() {

myTextField = new JTextField("Editier mich. Ich bin ein JTextfield", 60);
myTextField.addActionListener(this);
Container myPane = getContentPane();
myPane.add(myTextField);
}

public void actionPerformed(ActionEvent e) {
String eingabe = myTextField.getText();
StringBuffer st = new StringBuffer(eingabe.length());
for (int i = (eingabe.length()-1); i>=0;i--)
st.append(eingabe.charAt(i));
myTextField.setText(st.toString());
}
}

Swing bietet eine ganze Reihe von Möglichkeiten mit Texten umzugehen wie sich aus der Klassenhierarchie von JTtextComponent ergibt:

 

Beschriftungen: Klasse JLabel

Einfache, feste Texte für Beschriftungen werden in Swing mit der Klasse JLabel implementiert:

package Kurs2.Swing;

import javax.swing.JFrame;
import javax.swing.JLabel;

public class JLabelTest {

public static void main(String[] args) {
JFrame f = new JFrame("Das Fenster zur Welt!");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new JLabel("Hinweistext!"));
f.setSize(100, 80);
f.setVisible(true);
}
}

Das Programm erzeugt bei der Ausführung das folgende Fenster:

 

Container, Behälter für andere Komponenten: Klasse JPanel

Zum Anordnen und Positionieren von Komponenten wird die Klasse JPanel verwendet. Sie ist ein Behälter der andere Komponenten aus der JComponent-Hierarchie verwalten kann. Dies sind z.Bsp.:

  • andere JPanels
  • JButton
  • JTextField
  • JLabel etc.

JPanels haben zwei weitere wichtige Eigenschaften

  • sie können nur ihren eigenen Hintergrund zeichnen
  • sie benutzen Layoutmanager um die Positionierung der JComponenten durchzuführen

Jede Instanz eines JPanels hat einen eigenen Layoutmanager. Da typischerweise ein Layoutmanager nicht ausreicht, werden JPanels und deren Layoutmanager geschachtelt.

Beispiel: JTextfield in JFrame

import javax.swing.JFrame;
import javax.swing.JTextField;

public class TextfeldTest {
   public static void main(String[] args) {
      JFrame frame = new JFrame("Ein Fenster mit Textfeld");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.add(new JTextField("Editier mich. Ich bin ein JTextfield", 60));
      frame.setSize(300, 100);
      frame.setVisible(true);
  }
}

Das Programm erzeugt bei der Ausführung das folgende Fenster:

Beispiel: JTextField in einem Applet

Applet Quellcode

Hinweis: Eingabe mit "Enter"-Taste beenden.

package Kurs2.Swing;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

public class JPanelJApplet extends JApplet implements ActionListener {
private JTextField myTextField;

public JPanelJApplet() {
myTextField =
new JTextField("Editier mich. Ich bin ein JTextfield", 25);
myTextField.addActionListener(this);

JButton myButton = new JButton("Click mich");
myButton.addActionListener(this);

JLabel myLabel = new JLabel("Ich bin ein JLabel");

JPanel myPanel = new JPanel();
myPanel.add(myButton, BorderLayout.NORTH);
myPanel.add(myTextField, BorderLayout.CENTER);
myPanel.add(myLabel, BorderLayout.SOUTH);

Container myPane = getContentPane();
myPane.add(myPanel);
}

public void actionPerformed(ActionEvent e) {
String eingabe = myTextField.getText();
StringBuffer st = new StringBuffer(eingabe.length());
for (int i = (eingabe.length() - 1); i >= 0; i--)
st.append(eingabe.charAt(i));
myTextField.setText(st.toString());
}
}

 

Layoutmanager

Layoutmanager erlauben die Anordnung von Komponenten in einem Container. Abhängig vom gewählten Manager werden die Komponenten in ihrer gewünschten oder in einer gestreckten, bzw. gestauchten Form angezeigt. Die Feinheiten der Layout-Manager werden hier nicht behandelt. Es werden auch nur die wichtigsten Layout-Manager vorgestellt. Die Swingdokumentation ist für die Entwicklung von GUIs unerlässlich.

Definition: Layout-Manager
Ein Layout-Manager ist ein Objekt, welches Methoden bereitstellt, um die grafische Repräsentation verschiedener Komponenten innerhalb eines Container-Objektes anzuordnen

 

Wichtig: Eine absolute Positionierung von Komponenten ist ungünstig, da Fenster eine unterschiedliche Größe haben können.

Java biete eine Reihe von Layout-Managern. Im Folgenden wird eine Auswahl vorgestellt:

Flowlayout

Das FlowLayout ist das einfachste und Standardlayout. Es wird benutzt wenn kein anderer Layoutmanager angeben wird. In ihm werden die Komponenten in der Reihenfolge des Einfügens von links nach rechts eingefügt.

Strategie: Alle Komponenten werden in einer Reihe wie in einem Fließtext angeordnet. Reicht die gegebene Breite nicht erfolgt ein Umbruch mit einer neuen Zeile.

Dem JPanel jp wird im Folgenden kein expliziter LayoutManager mitgegeben um die 6 Knöpfe zu verwalten es wird der FlowLayoutmanager als Standardeinstellung verwendet:

package Kurs2.Swing;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
* Zeigt einen sehr einfachen FlowlayoutManager mit 5 Buttons
* @author stsch
*/
public class FlowlayoutTest {     /**
* Hauptmethode
* @param args Es werden keine Parameter ausgewertet
*/
public static void main(String[] args) {         JFrame f = new JFrame("FlowLayout");         JPanel jp = new JPanel();         for (char c = 0; c <= 5; ++c) { // Stecke 6 Buttons in das Panel             jp.add(new JButton("Button " + (char)('A'+c)));         }         f.add(jp); // Füge Panel zu Frame         //Beende Anwendung beim Schliesen des Fensters f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);         f.pack(); // Berechne Layout         f.setVisible(true);// Zeige alles an     } }

Ergibt ein Fenster mit dem folgenden Layout:

Bei mangelndem horizontalem Platz wird ein automatischer Umbruch in eine neue Zeile durchgeführt (siehe rechts). Flowlayout mit wenig Platz

Eigenschaften Flowlayout

  • Komponenten behalten ihre Wunschgröße
  • Zeilenumbruch bei mangelndem horizontalen Platz
  • aus den beiden ersten Eigenschaften ergibt sich, dass Komponenten eventuell nur teilweise angezeigt werden oder völlig verdeckt sein können!

Borderlayout

Der Borderlayoutmanager erlaubt das Gruppieren von Komponenten abhängig von der Richtung im Panel.

Strategie: Die zur Verfügung stehende Fläche wird in fünf Bereiche nach den Himmelsrichtungen aufgeteilt

  • NORTH (Oben)
  • SOUTH (Unten)
  • EAST (Rechts)
  • WEST (Links)
  • CENTER (Mitte)

Der Centerbereich ist der priorisierte Bereich.

Das BorderLayout ist die Standardeinstellung für die Klassen Window und JFrame.

Im nächsten Beispiel wir je ein Knopf (Button) in einem der Bereiche angelegt:

package Kurs2.Swing;
 
import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
 
public class BorderLayoutTest {
 
    public static void main(String[] args) {
        JFrame f = new JFrame("BorderLayout");
        JPanel jp = new JPanel();
 
        jp.setLayout(new BorderLayout());
        jp.add(new JButton("Norden"),BorderLayout.NORTH);
        jp.add(new JButton("Westen"),BorderLayout.WEST);
        jp.add(new JButton("Osten"),BorderLayout.EAST);
        jp.add(new JButton("Süden"),BorderLayout.SOUTH);
        jp.add(new JButton("Center"),BorderLayout.CENTER);
        f.add(jp);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.pack();
        f.setVisible(true);
    }
}

Zur Bestimmung der Position werden Konstanten mit den englischen Namen der Himmelsrichtung verwendet (Bsp. BorderLayout.NORTH). Beim Borderlayout ist zu beachten, dass Komponenten die oben, bzw. unten angeordnet werden über die ganze Breite des Containers gezeichnet werden. Komponenten die links und rechts angeordnet werden sind jedoch immer unter, bzw. über den Komponenten die oben und unten angelegt urden.

Eigenschaften Borderlayout

Siehe Fenster rechts mit größerem Fensterbereich:

  • Alle Komponenten füllen die jeweils gesamte Fläche aus
  • Die Bereiche imNorden und Süden haben die Wunschhöhe
  • Die Bereiche im Osten und Westen haben die Wunschbreite
  • Der Centerbereich erhält alle freien Flächen
BorderLayoutResize

 

BoxLayout

Das BoxLayout erlaubt das Anordnen von Komponenten in Zeilen oder Spalten. Durch das Verschachteln von BoxLayoutmanagern kann man ähnliche Effekte wie beim GridLayout erzielen. Man hat jedoch die Möglichkeit einzelne Zeilen oder Spalten individuell zu konfigurieren.

Das BoxLayout versucht alle Komponenten mit ihrer bevorzugten Breite bei der horizontalen Darstellung zu positionieren. Die Höhe aller Komponenten wird hier versucht auf Die Wunschhöhe der höchsten Komponente wird benutzt um die Gesamthöhe zubestimmen.

Bei der vertikalen Darstellung wird entsprechend versucht die bevorzugte Höhe zu verwenden. Bei der vertikalen Darstellung versucht der Layoutmanager alle Komponenten horizontal so weit wie die breiteste Komponente zu strecken.

Komponenten können aneinander

  • Linksbündig
  • Rechtsbündig
  • Zentriert

ausgerichtet werden

Das folgende Beispiel zeigt ein horizontales Boxlayout welches nach 2 Sekunden automatisch die Orientierung zu vertikal und dann wieder zurück triggert:

package Kurs2.Swing;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;

public class BoxLayoutTest {

public static void main(String[] args) {
int wartezeit = 2000; // in Millisekunden
JFrame f = new JFrame("BoxLayout");
JPanel jp = new JPanel();

// Erzeuge ein horizontales und ein vertikales BoxLayout
BoxLayout horizontal = new BoxLayout(jp, BoxLayout.X_AXIS);
BoxLayout vertikal = new BoxLayout(jp, BoxLayout.Y_AXIS);
jp.setLayout(horizontal);
for (char c = 0; c < 4; ++c) {

            jp.add(new JButton("Button " + (char) ('A' + c)));
        }
        JTextArea jta =new JTextArea(2,10);
        jta.append("JTextArea \nsecond row");
        jp.add(jta);
        f.add(jp);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.pack();
f.setVisible(true); // Warte 2 Sekunden try { Thread.sleep(wartezeit); // Wechsle fünfmal die Orientierung alle zwei Sekunden for (int i = 0; i < 5; i++) { jp.setLayout(vertikal); f.setTitle("BoxLayout - Vertikal"); f.pack(); Thread.sleep(wartezeit); jp.setLayout(horizontal); f.setTitle("BoxLayout - Horizontal"); f.pack(); Thread.sleep(wartezeit); } } catch (InterruptedException e) { // Mache nichts im Fall einer Ausnahme } } }

Ein horizontales Layout wird mit der Konstanten  BoxLayout.X_AXIS beim Konfigurieren des Layoutmanagers erzeugt:

Ein vertikales Layout wird beim Erzeugen des Layoutmanagers mit Hilfe der Konstanten BoxLayout.Y_AXIS konfiguriert:

Eigenschaften Boxlayout

  • Jede Komponente wird in ihrer Wunschgröße gargestellt
  • Die Größe des Containers ergibt sich aus der größten Wunschhöhe und der größten Wunschbreite

Hinweis: Die zweizeilige JTextArea im Beispiel oben hat eine andere Wunschgröße als die Knöpfe

GridLayout

Der GridLayoutmanager erlaubt die Anordnung von Komponenten in einem rechteckigen Raster (Grid: engl. Raster, Gitter). Der Gridlayoutmanager versucht die Komponenten von links nach rechts und von oben nach unten in das Raster einzufügen.

Strategie: Alle Zellen haben eine einheitliche Größe

Wird für die Größe der Zeilen oder Spalten eine 0 angegeben erlaubt dies das Anlegen beliebig vieler Element in den Spalten oder Zeilen.

Im folgenden Beispiel werden 11 Buttons und ein Textfeld in drei Reihen und vier Spalten angeordnet:

package Kurs2.Swing;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
public class GridLayoutTest {
  public static void main(String[] args) {
    JFrame f = new JFrame("GridLayout");
    JPanel jp = new JPanel();
    jp.setLayout(new GridLayout(3,4));
    for (char c = 0; c < 11; ++c) {
      jp.add(new JButton("Button " + (char)('A'+c)));
    }
    JTextArea jta =new JTextArea(3,10);
    jta.append("JTextArea \nsecond row\nthird row");
    jp.add(jta);
    f.add(jp);
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.pack();
    f.setVisible(true);
    }
}

Dies ergibt das folgende Fenster bei der Ausführung:

Eigenschaften des Gridlayouts

  • Alle Komponenten werden in die gleiche Größe gezwungen
  • Das initiale Layout berechnet die Zellengröße nach der breitesten und höchsten Komponente
  • Komponenten, die die Zellengröße nicht einhalten können, werden eventuell nicht vollständig angezeigt (Beispiel rechts)

 

gestauchtes Fenster:

 

 

Ereignisse und deren Behandlung

 GUI Programme und Ereignisverarbeitung

GUI Programme laufen nicht linear ab. Bei Benutzerinteraktionen muss das Programm in der Lage sein sofort einen bestimmten Code zur Behandlung auszuführen.

Programmaktionen werden durch Benutzeraktionen getriggert. Man spricht hier von einem ereignisgesteuerten Programmablauf.

Definition: Ereignis

Ein Ereignis ist ein Vorgang in der Umwelt des Softwaresystems von vernachlässigbarer Dauer, der für das System von Bedeutung ist.

Im Rahmen dieses Abschnitts sprechen wir von einer wichtigen Gruppe von Ereignissen den Benutzerinteraktionen:

Beispiele sind

  • Mausclick
  • Tasteneingabe
  • Menülistenauswahl
  • Zeigen auf einen Bereich des GUI
  • Texteingabe oder Veränderung

Der Programmablauf wird aber auch von anderen weniger offensichtlichen Benutzerintreaktionen gesteuert

  • Verdecken des Programmfensters durch ein anderes Fenster
  • Fenstermodifikationen
    • Vergößern, verkleinern
    • Bewegen,
    • Schließen eines Fenster
  • Bewegen der Maus über das Programmfenster ohne Klicken (nicht auf allen Plattformen)
  • Erlangen des Fokus auf einem Fenster

Ereignisklassen

Java benutzt Klassen zur Behandlung von Ereignissen (engl. Events) der Java-Benutzeroberflächen. Typische Klassen sind

Die Klassen enthalten die Beschreibung von GUI Ereignissen (z.Bsp. Maus wurde auf Position x=17, y=24 geklickt).

Sie korrelieren mit den Klassen die die grafischen Komponenten implementieren: Z.Bsp.

Erzeugen von Ereignissen und deren Auswertung

Ereignisse werden automatisch von den Swingkomponenten erzeugt. Dies ist die Klasse JComponent mit ihren abgeleiteten Klassen. Sie werden zum Beispiel erzeugt, wenn ein Benutzer die Schaltfläche eines JButton betätigt. Die Aufgabe die dem Entwickler verbleibt ist die Registrierung seiner Anwendung für bestimmte GUI-Ereignisse. Die Anwendung kann nur auf Ereignisse reagieren gegen die sich sich vorher registriert hat.

Der Entwickler kann dann nach der Registrierung eines bestimmten Ereignisses ist die Auswertung des Ereignisses vornehmen und auf das Ereignis mit der gewünschten Aktion reagieren.

Java verwendet hierzu die Ereignis-Delegation um die Komponente die das Ereignis auslöst von der Ereignisbehandlung zu entkoppeln.

  • Der Benutzer betätigt z.Bsp. eine Schaltfläche
  • Das Laufzeitsystem erkennt das Ereignis
  • Objekte die das Ereignis beobachten (engl. "Listener" in Java) werden aufgerufen und das Ereignis wird behandelt.
    • Ein Listener erhält vom Laufzeitsystem ein ActionEvent-Objekt mit allen Informationen die zum Ereignis gehören.

Die Listenerobjekte müssen sich mit Hilfe einer Registrierung bei den GUI Objekten anmelden:

  • Komponenten die Ereignisse erzeugen (Klasse JComponent) können, erlauben die Registrierung von "Listener" Objekten (engl. zuhören; nicht nur hören!)
    • die Registrierung erfolgt mit Methoden der Syntax addXXXListener(...) der GUI Komponenten
  • "Listener" Objekte implementieren das Java Interface ActionListener und damit die Methoden die beim Eintritt eines Ereignisses ausgeführt werden sollen.

Wichtig: Beziehung zwischen Listenerobjekt und GUI Objekt

Ein Listenerobjekt kann sich gegen ein oder mehrere GUI Objekte registrieren

  • Registriert man ein Listenerobjekt gegen genau ein GUI Objekt (Bsp. 1 Button <-> 1 Listenerobjekt)
    • muß man den Urheber des Ereignisses und den Ereignistyp nicht analysieren. Der Typ des Ereignis und das GUI Objekt sind bekannt
  • Registriert man ein Listenerobjekt gegen mehrere GUI Objekte (Bsp. 3 Buttons <-> 1 Listenerobjekt)
    • muß man im Listenerobjekt das übergebene Ereignisobjekt auf den Verursacher, das GUI Objekt, untersuchen
    • muß man ein Listenerinterface implementieren, dass auch die Ereignisse aller Objekte verarbeiten kann.
      • Beispiel: 1 Button und ein Textfeld werden von einem Listenerobjekt verwaltet. Man benötigt hier eine gemeinsame Listener-Oberklasse die beide graphische Objekte verwalten kann

Beispiel

Das folgenden Beispiel ist eine sehr einfache Implementierung eines JFrame mit einem Button und einem ActionListener:

package Kurs2.Swing;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;

public class ActionListenerBeispiel implements ActionListener {

public void actionPerformed(ActionEvent ae) {
//Ausgabe des zum ActionEvent gehörenden Kontexts
System.out.println("Aktion: " + ae.getActionCommand());
}

public static void main(String[] args) {
JFrame myJFrame = new JFrame("Einfacher ActionListener");
myJFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton jb = new JButton("Hier drücken");

ActionListenerBeispiel behandeln = new ActionListenerBeispiel();
jb.addActionListener(behandeln); // Füge Listener zu Button
myJFrame.add(jb); // Füge Button zu Frame
myJFrame.pack();
myJFrame.setVisible(true);
}
}

Die Klasse ActionListenerBeispiel implementiert die Methode actionPerformed() nach der Spezifikation eines ActionListener.

Das Programm startet in der main() Methode und erzeugt das folgende GUI:

 

  • Nach klicken des Button "feuert" das Buttonobjekt jb ein Ereignis (Event)
  • die Methode actionPerformed() des registrierten Listener behandeln wird mit einem Eventobjekt als Übergabeobjekt aufgerufen
  • Das Eventobjekt ae wird analysiert und das
  • Kommando wird als Text auf der Konsole ausgegeben:
Aktion: Hier drücken

Der Text "Hier drücken" wird ausgeben, da das Eventobjekt bei Buttons immer den Text des Buttons ausgibt.

Ereignisse (Events)

Es gibt eine reichhaltige Hierarchie von spezialisierten Event- und Listenerklassen. Hierzu sei auf die Java-Tutorials von Oracle verwiesen.

Weitere Events sind zum Beispiel:

  • ItemEvent: Z. Bsp. Analysieren von JCheckBox Komponenten
  • MouseEvent: Analysieren von Mausposition, Bewegung etc. 
  • ChangeEvent: Z. Bsp. Analysieren von Änderungen an einem JSlider

Der Swing "Event Dispatch Thread"

Damit Benutzeraktionen unabhängig vom normalen Programmablauf behandelt werden können, benutzt Swing eine eigene Ausführungseinheit, einen Thread, zum Bearbeiten der Benutzeraktionen. Dieser "Thread" ist ein Ausführungspfad der parallel zum normalen Programmablauf im main-Thread abläuft. Threads laufen parallel im gleichen Adressraum des Prozesses und haben daher Zugriff auf die gleichen Daten.

Blockieren des GUIs

Alle Aktionen der ActionListener werden vom "Swing-Event-Dispatch-Thread" ausgeführt. Diese Thread  arbeitet GUI Interaktionen ab während das Javaprogramm mit anderen Threads im Hintergrund weiterlaufen kann.

Wichtig

Alle Codestrecken von zur Behandlung von Ereignissen (Methode actionPerformed()) werden in nur einem Thread aufgerufen und blockieren alle anderen Behandlungen während sie ausgeführt werden.

Im Klartext: Das GUI wird während der Ausführungszeit einer Ereignisbehandlung nicht bedient und ist blockiert.

Vermeiden Sie aufwändige (=langlaufende)  Implementierungen in den actionPerformed() Methoden!

Beispiel

Das folgende Programm blockiert das GUI für 2 Sekunden. Die Blockade ist nach dem Klicken an der geänderten Farbe des Buttons zu erkennen. Er bleibt gedrückt:

package Kurs2.Swing;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;

public class ActionListenerBlockiert implements ActionListener {

public void actionPerformed(ActionEvent ae) {
//Ausgabe des zum ActionEvent gehörenden Kontexts
System.out.println("Aktion: " + ae.getActionCommand());
try {// Thread für 2s blockieren (schlafen)
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
JFrame myJFrame = new JFrame("Einfacher ActionListener");
myJFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton jb = new JButton("Hier drücken");
ActionListenerBlockiert behandeln = new ActionListenerBlockiert();
jb.addActionListener(behandeln); // Füge Listener zu Button
myJFrame.add(jb); // Füge Button zu Frame
myJFrame.pack(); // Berechne Layout
myJFrame.setVisible(true);
}
}

Synchronisation mit Swing GUIs

Wichtig
Die meisten Swing Komponenten sind nicht synchronisiert. Veränderungen an den Datenstrukturen durch nicht synchronisiertern Zugriff von anderen Threads können zu Inkonsistenzen führen.

 

 

Event-Interface und Event-Adapter

Die Implementierung eines einfachen ActionListener-Schnittstelle ist recht einfach, da nur eine Methode implementiert werden muss.

Viele nicht triviale ActionListener erfordern jedoch die Implementierung von mehreren Methoden aufgrund der Komplexität der entsprechenden Komponente. Ein Beispiel hierfür ist die Schnittstelle MouseListener. Sie erfordert die Implementierung der folgenden Methoden:

  • mouseClicked(MouseEvent e)
  • mouseEntered()
  • mouseExited()
  • mousePressed()
  • mouseReleased()

Dies ist aufwändig wenn man sich nur für ein bestimmtes Ereignis wie z. Bsp. mouseReleased() interessiert. Man muss alle Interfacemethoden implementieren. Vier der Methoden bleiben leer und sind überflüssig.

Listener-Adapter Klassen

Um diese unnötige Schreibarbeit zu vermeiden, stellt Swing sogenannte Adapterklassen als abstrakte Klassen mit leeren, aber schon implementierten Methoden zur Verfügung.

Vorteil: Der Entwickler kann seine Klasse aus der abstrakten Klasse ableiten und muss nur die Methoden für die Ereignisse implementieren die ihn interessieren.

Für die Implementierung von Maus-Events steht zum Beispiel die Klasse MouseAdapter zur Verfügung, die die entsprechenden Methoden implementiert.

Einige der Adapterklassen die Swing zur vereinfachten Implementierung von Ereignisschnittstellen (Event Interface) sind:

Beispiele von Adapterklassen
Spezialisierungen der Schnittsteller EventListener Adapterklasse
ComponentListener ComponentAdapter
ContainerListener ContainerAdapter
FocusListener FocusAdapter
KeyListener KeyAdapter
MouseListener MouseAdapter
MouseMotionListener MouseMotionAdapter
WindowListener WindowAdapter

Im folgenden Beispiel wird die Klasse MouseAdapterTest aus der abstrakten Klasse MouseAdapter abgeleitet um nur die Methode mouseClicked() implementieren zu müssen:

package Kurs2.Swing;

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JButton;
import javax.swing.JFrame;

public class MouseAdapterTest extends MouseAdapter {

public MouseAdapterTest() {
erzeugeGUI();
}

public static void main(String[] args) {
MouseAdapterTest mat = new MouseAdapterTest();
}

private void erzeugeGUI() {
JFrame myJFrame = new JFrame("Mouse Click Adapter Test");
myJFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton jb = new JButton("Hier drücken");
jb.addMouseListener(this);
myJFrame.getContentPane().add(jb);
myJFrame.pack();
myJFrame.setVisible(true);
}

@Override
public void mouseClicked(MouseEvent mEvent) {
System.out.println("MouseClick wurde auf Position ["
+ mEvent.getX() + ","
+ mEvent.getY() + "] "
+ mEvent.getClickCount() + " mal geklickt");
}
}

Das Programm registriert sich selbst gegen den Button und ist jetzt in der Lage, die Information des des Eventobjekts mEvent auszulesen (x,y Position, Anzahl der Mehrfachclicks).

Beispiel

Im Diagramm (unten) sind die beiden Möglichkeiten aufgeführt die ein Entwickler zum Auslesen eines Mauscklicks benutzen kann. Die von Java zur Verfügung gestellten Infrastruktur besteht aus dem zu implementierenden Interface MouseListener und einer abstrakten Klasse Mouseadapter:

Ein Entwickler der sich nur gegen das MouseClick Ereignis registrieren möchte, kann wie im Diagramm oben:

  • das Interface MouseListener in z.Bsp. der Klasse ListenerAufwaendig implementieren. Hier müssen alle 5 Methoden des Interface implementiert werden. Die vier nicht benötigten Methoden können als leere Methoden implementiert werden.
  • aus der abstrakten Klasse MouseAdapter eine Klasse wie z.Bsp. ListenerEinfach ableiten. In ihr muss man nur eine einzige Methode mouseClicked() durch Überschreiben implementieren. Die anderen vier Methoden sind schon in der Klasse MouseAdapter als leere Proformamethoden implementiert. Beim Ableiten aus der abstrakten Klasse MouseAdapter müssen nur die gewünschten Methoden überschrieben werden. Ein Konstruktor ist nicht nötig, da die Klasse MouseAdapter einen Default-Konstruktor besitzt.

Anbei die vollständige Implementierung der semantisch gleichwertigen Methoden:

Vergleich
Klasse ListenerAufwaendig Klasse ListenerEinfach
package Kurs2.Swing;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
 
public class ListenerAufwaendig implements MouseListener{
    @Override public void mouseClicked(MouseEvent mEvent) {
        System.out.println("MouseClick wurde auf Position ["
                + mEvent.getX() + ","
                + mEvent.getY() + "] "
                + mEvent.getClickCount() + " mal geklickt");
    }
    @Override public void mouseEntered(MouseEvent mEvent) 
        { /* leere Implementierung, erzwungen */ }
    @Override public void mouseExited(MouseEvent mEvent) 
        { /* leere Implementierung, erzwungen */ }
    @Override public void mousePressed(MouseEvent mEvent)
        { /* leere Implementierung, erzwungen */ }
    @Override public void mouseReleased(MouseEvent mEvent)
     { /* leere Implementierung, erzwungen */ }
}
package Kurs2.Swing;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
 
public class ListenerEinfach extends MouseAdapter{
    @Override public void mouseClicked(MouseEvent mEvent) {
        System.out.println("MouseClick wurde auf Position ["
                + mEvent.getX() + ","
                + mEvent.getY() + "] "
                + mEvent.getClickCount() + " mal geklickt");
    }
}
 
 
 
 
 
 
 
 

 

Anonyme und innere Klassen

Nach den bisher vorgestellten Prinzipien erfordert die Implementierung eines GUI die Implementierung von vielen Listenern und führt zur Erzeugung sehr vieler Klassen. Java erlaubt hier die Implementierung von anoymen, inneren Klassen die nur im Kontext einer bestimmten Klasse existieren und den Namensraum der Klassen nicht unnötig belasten.

Innere Klassen

Innere Klassen helfen es zu vermeiden, dass man Klassen veröffentlicht  die nur von genau einer anderen Klasse benutzt werden. Innere Klassen werden syntaktisch wie normale Klassen implementiert. Der einzige Unterschied ist, dass sie im Block einer äusseren Klasse implementiert werden. Sie werden in der äusseren Klasse mit dem gleichen Block der äusseren Klasse wie die Klassenvariablen und Methoden der äusseren Klasse implementiert.

Die in der Vorlesung vorgestellten inneren Klassen sind Elementklassen.

Elementklasse
Definition Elementklasse

Elementklassen sind wie Instanzmethoden und Instanzvariablen Elemente einer (anderen) Klasse. 

Sie werden auf der gleichen Blockebene wie Instanzmethoden und Instanzvariablen implementiert. Sie haben einen Zugriffschutz wie Instanzmethoden und Instanzvariablen.

Innere Klassen können auch als lokale Klasse innerhalb eines beliebigen Blocks implementiert werden. Diese Variante ist nicht Gegenstand der Vorlesung.

Die inneren Klassen gehören zum Paket der äusseren Klasse. Importkommandos müssen in der äusseren Klasse implementiert werden.

Besondere Eigenschaften von Elementklassen
  • Elementklassen und deren Instanzen können nur existieren, wenn ein Objekt der Sie umschliesenden Klasse existiert.
  • Elementklassen haben den vollen Zugriff auf die Instanzvariablen und -methoden des umgebenden Objekts!

Instanzen von Elementklassen sind Komponenten des umgebenden Objekts!

Das vorhergehende Beispiel kann jetzt wie folgt implementiert werden:

package Kurs2.Swing;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
public class MouseAdapterInnereKlasseTest {
/**
* Die innere Klasses MyMouseListener
*/
class MyMouseListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent mEvent) {
System.out.println("MouseClick wurde auf Position ["
+ mEvent.getX() + ","
+ mEvent.getY() + "] "
+ mEvent.getClickCount() + " mal geklickt");
}
}

/**
* Erzeuge GUI im Konstruktor
*/
public MouseAdapterInnereKlasseTest() {
JFrame myJFrame = new JFrame("Mouse Click Innere Klasse Test");
myJFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton jb = new JButton("Hier drücken");
jb.addMouseListener(new MyMouseListener());
myJFrame.getContentPane().add(jb);
myJFrame.pack();
myJFrame.setVisible(true);
}
public static void main(String[] args) {
MouseAdapterInnereKlasseTest mat =
new MouseAdapterInnereKlasseTest();
}
}

Die Unterschiede sind die Folgenden:

  • Die Klasse MouseAdapterInnereKlasseTest muss nicht mehr von der Klasse MouseAdapter abgeleitet werden oder eine Schnitstelle MouseEvent implementieren
  • Es wird eine eigene innere Klasse MyMouseListener implementiert die nicht ausserhalb der umgebenden Klasse bekannt ist.
  • Die Klasse MyMouseListener erbt (extends Schlüsselwort) von der Klasse MouseAdapter.
  • Beim Hinzufügen eines Listeners zum Button wird eine Instanz der inneren Klasse erzeugt.

Diese innere Klasse hat für den Entwickler den Vorteil, dass er keine neuen Klassen nach aussen hin bekanntmachen muss. Die Implementierung des Listeners erfolgt in der gleichen Datei. Die Implementierung des Listener ist also visuell näher als wenn sie in einer eigenen Klasse und einer eigenen Datei geschehen würde.

Objektabhängigkeit von Instanzen innerer Klassen

Im obigen Beispiel wird eine Instanz der Klasse MyMouseListener erzeugt. Dies ist nur erlaubt wenn das Objekt aus dem Kontext (Methode) eines Objekts der Klasse MouseAdapterInnereKlasseTest erzeugt wird. Dieses äussere Objekt existiert, da die innere Klasse im Konstruktor von MouseAdapterInnereKlasseTest erzeugt wird.

Der Code zum Erzeugen des GUI im Konstruktor kann nicht einfach in die statische main() Methode kopiert werden. Hier gibt es noch keinen Kontext zu einem Objekt der Klasse MouseAdapterInnereKlasseTest. Der javac Übersetzer wird einen Fehler melden.

Anonyme, innere Klasse

Für Swing wurde das Konzept der anonymen, inneren Klasse entwickelt.

Definition anonyme, innere Klasse

Eine anonyme, innere Klasse ist eine lokale Klasse ohne Namen die innerhalb eines Ausdrucks (Block) definiert und instanziiert wird.

Anonyme, innere Klassen haben keinen Namen und daher auch keine Konstruktoren. Sie werden typischerweise als Implementierungen für Adapterklassen oder Schnittstellen verwendet.

Beispiel

Anonyme, innere Klassen erlauben die benötigte Listenerimplementierung noch eleganter durchzuführen:

An der Stelle an der eine Instanz eines Listeners benötigt wird kann man auch direkt eine vollständige Klasse implementieren und instanziieren.

Das folgende Beispiel hat die gleiche Funktion wie das vorgehende Beispiel mit der Implementierung einer inneren Klasse. Es kommt aber ohne einen eigenen Klassennamen aus:

package Kurs2.Swing;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
public class MouseAdapterAnonymeInnereKlasseTest {
/**
* Erzeuge GUI im Konstruktor
*/
public MouseAdapterAnonymeInnereKlasseTest() {
JFrame myJFrame = new JFrame("Mouse Click Adapter Test");
myJFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton jb = new JButton("Hier drücken");
jb.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent mEvent) {
System.out.println("MouseClick wurde auf Position ["
+ mEvent.getX() + ","
+ mEvent.getY() + "] "
+ mEvent.getClickCount() + " mal geklickt");
}
}
);
myJFrame.getContentPane().add(jb);
myJFrame.pack();
myJFrame.setVisible(true);
}
public static void main(String[] args) {
MouseAdapterAnonymeInnereKlasseTest mat =
new MouseAdapterAnonymeInnereKlasseTest();
}
}

Die Unterschiede sind die Folgenden:

  • Die Klasse MouseAdapterInnereAnonymeKlasseTest muss nicht mehr von MouseAdapter abgeleitet werden oder einen MouseEvent implementieren
  • Beim Hinzufügen eines Listeners zum Button wird
    • mit dem new Operator eine Instanz einer anonymen Klasse angelegt, die die abstrakte Klasse MouseAdapter implementiert. Sie ist anonym da sie selbst keinen Namen besitzt.
    • die anonyme Klasse wird innerhalb der Klasse MouseAdapterInnereAnonymKlasseTest soweit wie nötig implementiert um aus der abstrakten Oberklasse eine normale Klasse zu erzeugen

Diese "Hilfskonstruktion" hat für den Entwickler eine Reihe von Vorteilen:

  • Das Ereignis kann textuell sehr nahe an der Erzeugung der Komponente implementiert werden. Der Code wird übersichtlicher
  • Es müssen keine neuen Klassen mit eigenen Namen erzeugt werden. Hierdurch wird die gesamte Klassenhierarchie übersichtlicher und deutlich kleiner.

Statische innere Klassen, nicht anonyme lokale Klassen

... sind nicht Gegenstand dieser Vorlesung.

 

Übungen (Swing)

Ausnahmefenster

Implementieren Sie ein Fenster mit Hilfe der Klasse JFrame zur Behandlung von Ausnahmen.

  • Das JFrame soll beim Auftreten einer Ausnahmen aufgerufen werden und den Namen der Ausnahme zeigen.
  • Das Programm soll dann auf Wunsch beendet werden oder es soll ein Stacktrace auf der Konsole angezeigt werden
  • Das Programm soll ein GIF aus dem Internet als Label verwenden

 Das Fenster soll in etwa wie das folgende GUI aussehen:

Verwenden Sie das Rahmenprogramm AusnahmeFenster.java welches eine Infrastruktur zum Testen zur Verfügung stellt:

package Kurs2.Gui;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URL;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;

public class AusnahmeFenster {
final private JFrame hf;
final private JButton okButton;
final private JButton exitButton;
final private Exception myException;
/**
* Aufbau des Fensters zur Ausnahmebehandlung
*
* @param fehlermeldung ein beliebiger Fehlertext der angezeigt wird
* @param e Die Ausnahme die angezeigt werden soll
*/
public AusnahmeFenster(String fehlermeldung, Exception e) {
JLabel logo;
JPanel buttonPanel;
myException = e;
// 1. Erzeugen einer neuen Instanz eines Swingfensters
System.out.println("Hier beginnt die Arbeit: Löschen Sie dieses Kommando");
// ...
hf = null;
// 3. Gewünschte Größe setzen
// 1. Parameter: horizontale Größe in Pixel: 220
// 2. Parameter: vertikale Größe: 230
// ...
// 8. Labelerzeugung
logo = meinLogo();
// 4. Nicht Beenden bei Schliessen des Fenster
// 5. Anlegen der Buttons
okButton = null;
exitButton= null;
// 10. Hinzügen der Eventbehandlung
// Tipp: Die Klasse muss noch das Interface ActionListener implementieren!
// ...
// 6. Aufbau des Panels
// ...
// 7. Aubau des ContentPanes
// ...
// 2.1 Das Layout des JFrame berechnen.
// ...
// 3. Gewünschte Größe setzen
// 1. Parameter: horizontale Größe in Pixel
// 2. Parameter: vertikale Größe
// ...
// 2.2 Sichtbar machen des JFrames. Immer im Vordergrund
// ...
// ...
}
/**
* Implementieren des Logos
* 9.ter Schritt
* @return Zeiger auf das Logoobjekt
*/
private JLabel meinLogo() {
URL logoURL;
JLabel logoLabel;
String myURL = "http://www.dhbw-mannheim.de/fileadmin/templates/default/img/DHBW_Header_Logo.gif";
try {
logoURL = new URL(myURL);
ImageIcon myImage = new ImageIcon(logoURL);
logoLabel = new JLabel(myImage);
} catch (java.net.MalformedURLException e) {
System.out.println(e);
System.out.println("Logo URL kann nicht aufgelöst werden");
logoLabel = new JLabel("Logo fehlt");
}
return logoLabel;
}
/**
* Behandlung der JButton-Ereignisse
* 11. ter Schritt
* @param e
*/
public void actionPerformed(ActionEvent e) {
//System.exit(0);
//System.out.println("OK Button clicked");
//myException.printStackTrace();
}
/**
* Hauptprogramm zum Testen des Ausnahmefensters
* @throws Exception
*/
public static void main(String[] args) {
AusnahmeFenster dasFenster;
try {
myTestMethod();
} catch (Exception e) {
dasFenster = new AusnahmeFenster("Hier läuft etwas schief", e);
}
}
/**
* Eine Testmethode die mit einer Division durch Null eine
* Ausnahme provoziert
* @throws Exception
*/
public static void myTestMethod() throws Exception {
int a = 5;
int b = 5;
int c = 10;
c = c / (a - b);
System.out.println("Programm regulär beendet");
}
}

Empfehlung: Bauen Sie das GUI schrittweise auf und testen Sie es Schritt für Schritt

  1. Erzeugen eines einfachen JFrame
  2. Sichtbarmachen des JFrame
  3. Größe des JFrames setzen
  4. Programm nach Schließen des JFrames weiterlaufen lassen
  5. Anlegen der Buttons
  6. Aufbau des Panels
  7. Verbinden von Buttons, Panel und JFrame
  8. Erzeugen des Labels mit dem GIF und Hinzufügen zum Pane
  9. Implementieren des Labels mit Logo
  10. Hinzufügen der ActionListener zu den Buttons
  11. Implementieren der Aktionen

Taschenrechner

Implementieren Sie einen Taschenrechner mit den vier Grundrechenarten.

Benutzen Sie hierfür die Aufgabenstellung der Universität Bielefeld von Herrn Jan Krüger:

Innere Klasse und anonyme Klasse

Das Programm der vorhergehenden Übung (Ausnahmefenster)  benutzt in der Musterlösung eine einzige Methode actionPerformed() in der Klasse AusnahmeFenster um die Aktionen der beiden Buttons auszuführen.

public class AusnahmeFensterFertig implements ActionListener {

...

public AusnahmeFenster(String fehlermeldung, Exception e) {
...
okButton = new JButton();
exitButton = new JButton();
// 10. Hinzügen der Eventbehandlung
okButton.addActionListener(this);
exitButton.addActionListener(this);
...
}
public void actionPerformed(ActionEvent e) {
JButton source = (JButton) (e.getSource());
if (source == exitButton) {
System.exit(0);
}
if (source == okButton) {
System.out.println("OK Button clicked");
myException.printStackTrace();
}
}

}

Aufgabe 1:

  • Implementieren Sie eine externe Klasse SystemExitListener die als Listener für den "Beenden" Button genutzt werden kann.
  • Ändern Sie die Klasse AusnahmeFenster derart,dass Sie für den "Beenden" Button nicht mehr das Ausnahmefenster als Objekt registrieren (this). Erzeugen Sie als registrierten Listener eine Instanz der Klasse SystemExitListener

Sie haben nun eine eigene Klasse mit einem eigenen Objekt zur Behandlung des "Beenden" Ereignisses

Weiterführend (optional)

Implementieren Sie einen Menüeintrag "Beenden" in einer Menüleiste mit dem EIntrag "Ablage".

Herangehen:

  • Nutzen Sie Ihre Klasse SystemExitListener
  • Nutzen Sie das Java API und erzeugen Sie
    • eine Menülisteneintrag durch eine Instanz der Klasse JMenuItem
    • registrieren Sie das Listenerobjekt in dieser Klasse
  • Erzeugen Sie eine Menüliste mit Hilfe der Klasse JMenu
    • Fügen Sie den Menülisteneintrag zum Menü hinzu
  • Erzeugen Sie eine Menüleiste mit Hilfe der Klasse JMenuBar
    • Fügen Sie das zu erzeugte Menü in die Menüleiste ein
    • Fügen Sie die Menüleiste zum JFrame hinzu.
Menüleiste

 

Aufgabe 2:

  • Die Klasse AusnahmeFenster soll NICHT mehr die Schnittstelle ActionListener implementieren.
  • Implementieren Sie eine innere Klasse SystemExitAction. Sie soll einen ActionListener implementieren, der das Programm beendet.
  • Ändern sie den Aufruf addActionListener() des exitButton so, dass er die Klasse SystemExitAction benutzt.
  • Ändern Sie den Aufruf addActionListener() des okButton so, dass eine anonyme innere Klasse aufgerufen wird die einen Stacktrace ausdruckt

Hierdurch entfällt die Analyse der auslösenden Aktion in der ursprünglichen Implementierung. Für jede Aktion wurde jetzt eine eigene Methode implementiert

Lösungen (Swing)

Ausnahmefenster

package Kurs2.Gui;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URL;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;


public class AusnahmeFenster implements ActionListener {
final private JFrame hf;
final private JButton okButton;
final private JButton exitButton;
final private Exception myException;
/**
* Aufbau des Fensters zur Ausnahmebehandlung
*
* @param fehlermeldung ein beliebiger Fehlertext der angezeigt wird
* @param e Die Ausnahme die angezeigt werden soll
*/
public AusnahmeFenster(String fehlermeldung, Exception e) {
JLabel logo;
JPanel buttonPanel;
myException = e;
hf=null;
okButton=null;
exitButton=null;
// 1. Erzeugen einer neuen Instanz eines Swingfensters
hf = new JFrame("Anwendungsfehler");
// 8. Labelerzeugung
logo = meinLogo();
// 4. Beenden bei Schliesen des Fenster
hf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 5. Anlegen der Buttons
okButton = new JButton();
okButton.setText("Stack Trace");
exitButton = new JButton();
exitButton.setText("Beenden");
// 10. Hinzügen der Eventbehandlung
okButton.addActionListener(this);
exitButton.addActionListener(this);
// 6. Aufbau des Panels
buttonPanel = new JPanel(new GridLayout(1, 0));
buttonPanel.add(exitButton);
buttonPanel.add(okButton);

JTextArea fehlertextArea = new JTextArea(2, 20);
fehlertextArea.append(fehlermeldung + "\n");
fehlertextArea.append("Exception: "+ myException);
// 7. Aubau des ContentPanes
Container myPane = hf.getContentPane();
myPane.setLayout(new BorderLayout());
myPane.add(logo, BorderLayout.NORTH);
myPane.add(fehlertextArea, BorderLayout.CENTER);
myPane.add(buttonPanel, BorderLayout.SOUTH);
// 2.1 Das Layout des JFrame berechnen.
hf.pack();
// 3. Gewünschte Größe setzen
// 1. Parameter: horizontale Größe in Pixel
// 2. Parameter: vertikale Größe
hf.setSize(350, 300);
// 2.2 Sichtbar machen des JFrames. Immer im Vordergrund
hf.setVisible(true);
hf.setAlwaysOnTop(true);
}
/**
* Implementieren des Logos
* 9.ter Schritt
* @return Zeiger auf das Logoobjekt
*/
private JLabel meinLogo() {
URL logoURL;
JLabel logoLabel;
String myURL = "http://www.dhbw-mannheim.de/fileadmin/templates/default/img/signet.gif";
try {
logoURL = new URL(myURL);
ImageIcon myImage = new ImageIcon(logoURL);
logoLabel = new JLabel(myImage);
} catch (java.net.MalformedURLException e) {
System.out.println(e);
System.out.println("Logo URL kann nicht aufgelöst werden");
logoLabel = new JLabel("Logo fehlt");
}
return logoLabel;
}
/**
* Behandlung der JButton Ereignisse
* 11. ter Schritt
* @param e
*/
public void actionPerformed(ActionEvent e) {
JButton source = (JButton) (e.getSource());
if (source == exitButton) {
System.exit(0);
}
if (source == okButton) {
System.out.println("OK Button clicked");
myException.printStackTrace();
}
}
/**
* Hauptprogramm zum Testen des Ausnahmefensters
* @throws Exception
*/
public static void main(String[] args) {
AusnahmeFenster dasFenster;
try {myTestMethod();}
catch (Exception e) {
dasFenster = new AusnahmeFenster("Hier läuft etwas schief",e);
}
}
/**
* Eine Testmethode die eine durch eine Division durch Null eine
* Ausnahme provoziert
* @throws Exception
*/
public static void myTestMethod() throws Exception {
int a = 5;
int b = 5;
int c = 10;
c = c / (a-b);
System.out.println("Programm regulär beendet");
}
}

Taschenrechner

Die Universität Bielefeld (Herr Jan Krüger) bieten die Lösung der Programmieraufgabe an unter:

Innere und anonyme Klasse

Aufgabe 1

Klasse SystemExitListener

package Kurs2.Gui;
 
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
   /**
   *
   * @author sschneid
   * Implementierung eines ActionListener der als Aktion die Anwendung
   * beendet
   */
   public class SystemExitListener implements ActionListener{
      @Override
      public void actionPerformed(ActionEvent e) {
         System.exit(0);
      }
   }

Einfügen eines Menüs (Optional)

import javax.swing.*;
...
JMenuItem jmi = new JMenuItem("Beenden");
jmi.addActionListener(new SystemExitListener());
JMenu jm = new JMenu("Ablage");
jm.add(jmi);
JMenuBar jmb = new JMenuBar();
jmb.add(jm);
hf.setJMenuBar(jmb);

Aufgabe 2

Hinweis: Die Klasse wurde in AusnahmeFensterInnerer umbenannt 

package Kurs2.Gui;
 
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.URL;
import javax.swing.*;
 
 
public class AusnahmeFensterInnere {
    private JFrame hf;
    private JButton okButton;
    private JButton exitButton;
    private Exception myException;
 
    public class SystemExitAction implements ActionListener{
    @Override
    public void actionPerformed(ActionEvent e) {
        System.exit(0);
        System.out.println(hf);
    }
 
}
    /**
     * Aufbau des Fensters zur Ausnahmebehandlung
     *
     * @param fehlermeldung ein beliebiger Fehlertext der angezeigt wird
     * @param e Die Ausnahme die angezeigt werden soll
     */
    public AusnahmeFensterInnere(String fehlermeldung, Exception e) {
                JLabel logo;
        JPanel buttonPanel;
        myException = e;
        // 1. Erzeugen einer neuen Instanz eines Swingfensters
        hf = new JFrame("Anwendungsfehler");
        // 8. Labelerzeugung
        logo = meinLogo();
        // 4. Beenden bei Schliesen des Fenster
hf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
        // 5. Anlegen der Buttons
        okButton = new JButton();
        okButton.setText("Stack Trace");
        exitButton = new JButton();
        exitButton.setText("Beenden");
        // 10. Hinzügen der Eventbehandlung
        okButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                myException.printStackTrace();
            }
            });
        exitButton.addActionListener(new SystemExitAction());
        // 6. Aufbau des Panels
        buttonPanel = new JPanel(new GridLayout(1, 0));
        buttonPanel.add(exitButton);
        buttonPanel.add(okButton);
 
        JTextArea fehlertextArea = new JTextArea(2, 20);
        fehlertextArea.append(fehlermeldung + "\n");
        fehlertextArea.append("Exception: "+ myException);
        // 7. Aubau des ContentPanes
        Container myPane = hf.getContentPane();
        myPane.add(logo, BorderLayout.NORTH);
        myPane.add(fehlertextArea, BorderLayout.CENTER);
        myPane.add(buttonPanel, BorderLayout.SOUTH);
        JMenuItem jmi = new JMenuItem("Beenden");
        jmi.addActionListener(new SystemExitListener());
        JMenu jm = new JMenu("Ablage");
        jm.add(jmi);
        JMenuBar jmb = new JMenuBar();
        jmb.add(jm);
        hf.setJMenuBar(jmb);
        // 2.1 Das Layout des JFrame berechnen.
        hf.pack();
        // 3. Gewünschte Größe setzen
        //     1. Parameter: horizontale Größe in Pixel
        //     2. Parameter: vertikale Größe
        hf.setSize(350, 300);
        // 2.2 Sichtbar machen des JFrames. Immer im Vordergrund
        hf.setVisible(true);
        hf.setAlwaysOnTop(true);
    }
 
    /**
     * Implementieren des Logos
     * 9.ter Schritt
     * @return Zeiger auf das Logoobjekt
     */
    private JLabel meinLogo() {
        URL logoURL;
        JLabel logoLabel;
        String myURL =
                "http://www.dhbw-mannheim.de/fileadmin/templates/default/img/signet.gif";
 
        try {
            logoURL = new URL(myURL);
            ImageIcon myImage = new ImageIcon(logoURL);
            logoLabel = new JLabel(myImage);
        } catch (java.net.MalformedURLException e) {
            System.out.println(e);
            System.out.println("Logo URL kann nicht aufgelöst werden");
            logoLabel = new JLabel("Logo fehlt");
        }
        return logoLabel;
    }
 
    /**
     * Behandlung der JButton Ereignisse
     * 11. ter Schritt
     * @param e
     */
    public void actionPerformed(ActionEvent e) {
        JButton source = (JButton) (e.getSource());
        if (source == exitButton) {
            System.exit(0);
        }
        if (source == okButton) {
            System.out.println("OK Button clicked");
            myException.printStackTrace();
        }
 
    }
 
    /**
     * Hauptporgramm zum Testen des Ausnahmefensters
     * @throws Exception
     */
    public static void main(String[] args) {
        AusnahmeFensterInnere dasFenster;
 
        try {myTestMethod();}
        catch (Exception e) {
            dasFenster = new AusnahmeFensterInnere("Hier läuft etwas schief",e);
            }
 
    }
 
    /**
     * Eine Testmethode die eine durch eine Division durch Null eine
     * Ausnahme provoziert
     * @throws Exception
     */
    public static void myTestMethod() throws Exception {
 
        int a = 5;
        int b = 5;
        int c = 10;
        c = c / (a-5);
        System.out.println("Programm regulär beendet");
    }
}
 

 

Lernziele (Swing, innere und anonyme Klassen)

Am Ende dieses Blocks können Sie:

  • ... einfache Swing Oberflächen mit
    • Komponenten,
    • Layoutmanagern
    • Eventlistenern
    • Fenster, Menüleisten, Menüeinträge implementieren
  • ... innere and anonyme Klassen benutzen um Eventlistener einfacher und übersichtlicher zu implementieren
  • ... die wichtigsten Layoutmanager von Swing benennen und kennen die Strategien nach denen sie das Layout für Komponenten berechnen

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten:Fragen zur graphischen Programmierung (Swing)

Pakete (Java Packages)

Duke mit Paketen

Javapakete (englisch packages) erlauben das Zusammenfassen von Klassen und "Interfaces" (Schnittstellen) in logischen Gruppen.

Das Gruppieren von Klassen und Schnittstellen (Interfaces) in Packete erlaubt:

  • Bildung von eigenen Namensräumen für Klassen und Schnittstellen (Interfaces)
  • Definierter Export von Klassen, bzw. "Information Hiding" von Klassen die nicht exportiert werden sollen
  • Definierter Import von Klassen von Fremdpaketen in eigene Klassen

Javapakete werden mit dem Schlüsselwort package deklariert und mit Hilfe des Schlüsselworts import importiert.

Jede Javaklasse die zu einem gegebenen Paket gehören soll, muss in einer Quelldatei stehen die mit dem Schlüsselwort package beginnt:

package DemoPackage;
...
class Demonstration1 {
...
}
...
class Demonstration2 {
...
}
Struktur von *.java Quelldateien
  • Datei beginnt optional mit Schlüsselwort package und Paketname
    • Alle Klassen und Interfaces in dieser Datei gehören zum entsprechenden Paket
  • gefolgt von einer oder mehreren Java Klassen oder "Interfaces"
  • genau eine Klasse darf als public deklariert sein.

 

Hinweis: Der Javaübersetzer javac wird für jede einzelne Klasse eine Datei mit dem Klassennamen und der Dateierweiterung .class erzeugen.

Alle Javaklassen die mit dem gleichen Paketnamen versehen sind gehören logisch zusammen. Dies bedeutet, dass diese Klassen typischerweise gemeinsam verwendet werden und das für sie ähnliche Zugriffsrechte gelten.

Alle Klassen für die kein Paket spezifiziert wurde, gehören automatisch dem namenlosen Standardpaket an.

Javapakete und Verzeichnisstrukturen

Bei der Verwendung von Javapaketen ist darauf zu achten, dass das Javalaufzeitsystem (Kommando java) standardmäßig davon ausgeht, dass eine *.class Datei in einem Unterverzeichnis zu finden ist, welches den Paketnamen besitzt. Der Javaübersetzer (javac) ignoriert jedoch in seinen Standardeinstellungen Unterverzeichnisse und speichert Javaklassen im aktuellen Verzeichnis.

Hierzu das folgende Beispiel mit der Datei HelloWorld.java:

package Demo;

class A {
   public static void main(String[] args) {
      B.printHelloWorld();
   }
}

class B {
   public static void printHelloWorld() {
      System.out.println("Hello World!");
   }
}

Es empfiehlt sich im Rahmen des Kurses die .java Dateien in einem Verzeichnis zu pflegen welches den Namen des verwendeten Pakets besitzt. Der Javaübersetzer javac sollte in dem Paketverzeichnis aufgerufen werden. Das Java Laufzeitsystem java sollte im darüber liegenden (allgemeineren) Verzeichnis aufgerufen werden.

Wichtig: In einer Quelldatei dürfen zwar mehrere Klassen vorhanden sein, es darf jedoch nur maximal eine Klasse als public deklariert sein.

Empfehlung: Im Normalfall ist es üblich in einer Datei nur eine Klasse zu implementieren. Die Datei sollte genau den Namen der Klasse tragen.

Steuern der Paketsuche durch "classpath"

Java findet die Standardklassen der SE Edition immer automatisch. Zum Finden von anwenderspezifischen Paketen wird in Java der classpath verwendet. classpath ist eine Umgebungsvariable des Betriebsystems die man setzen kann. Die Javakommandos werden dann diese Variable zum Suchen der Pakete verwenden. Die Art und Weise des Setzens dieser Variable sind Betriebssystem spezifisch. Unter Windows geschieht dies beispielsweise durch das folgende Kommando:

set classpath=.;D:\Paket1;D:\Paket2

Hier wird zuerst im aktuellen Verzeichnis (.) gesucht dann in den Verzeichnissen Paket1 und Paket2 des Laufwerks D.

Die Javakommandos verfügen auch über eine Option -classpath mit der man die Suchpfade für einen bestimmten Aufruf vorgeben kann. Die Syntax der -classpath Option kann man mit Hilfe des Kommandos java -help erfragen:

java -help
...
-classpath <class search path of directories and zip/jar files>
A : separated list of directories, JAR archives,
and ZIP archives to search for class files. ...

Namensräume

Klassen und Schnittstellen (Interfaces) in einem Paket bilden einen Namensraum. Dies bedeutet:

  • Nur Klassen aus dem gleichen Paket können ohne weiteres verwendet werden
  • Klassen und Interfacenamen innerhalb eines Pakets müssen eindeutig sein
  • Zur Verwendung von Klassen aus anderen Paketen muss
    • die Klasse für die gesamte Quelldatei importiert werden (expliziter Import). Beispiel
      • import Demo.B;
    • oder die Klasse muss mit dem Paket genau bei der Benutzung spezifiziert werden (impliziter Import). Beispiel:
      • Demo.B.printHelloWorld();
  • Es können Klassen mit dem gleichen Namen in unterschiedlichen Paketen vorkommen.

Export von Klassen und Schnittstellen(Interfaces)

Nur das Paket selbst bestimmt welche Klassen exportiert werden. Dies bedeutet, dass die entsprechenden Klassen von Aussen sichtbar und benutzbar sind.

Die Benutzung einer Klasse außerhalb eines Pakets wird duch das Schlüsselwort public vor dem Schlüsselwort class deklariert. Beispiel:

package Demo;

...
 public class A {
...

}

Import von Klassen und Schnittstellen(Interfaces)

Expliziter Import

Der explizite Import von Klassen eines anderen Pakets geschieht durch das Schlüsselwort import gefolgt vom Paketnamen und dem Klassennamen der vom Paketnamen durch den Punktoperator getrennt wird.

package DemoConsumer;
...
import Demo.A;
...
class Comsumer {
...
   A.eineMethode(); // Aufruf der Methode eineMethode() der Klassse A
...
}

Neben der Möglichkeit bestimmte Klassen eines Pakets zu importieren, kann man auch alle Klassen eines Pakets importieren. Beispiel:

package DemoConsumer;
...
import Demo.*;
...
class Comsumer {
...
   A.eineMethode();   // Aufruf der Methode Demo.A.eineMethode()
   B.andereMethode(); // Aufruf der Methode Demo.B.andereMethode()
...
}

Impliziter Import

Fremde Klassen können auch adhoc verwendet werden indem man den Klassennamen mit vorangestelltem Paketnamen und dem Punktoperator verwendet. Beispiel;

package DemoConsumer;
...
class Comsumer {
...
   Demo.A.main(); Aufruf der Methode main() der Klassse A
   Demo.B.printHelloWorld();
...
}

Der implizite Import ist nützlich wenn zwei Klassen mit identischem Namen aus zwei verschiedenen Paketen benutzt und unterschieden werden müssen. Beim freiwilligen, impliziten Import muss man zwischen den folgenden Vor- und Nachteilen abwägen:

  • Vorteil: Man benutzt nur die deklarierte Klasse, die Herkunft der Klasse ist für den Leser des Quellcodes direkt sichtbar
  • Nachteil: Bei jeder Verwendung muss der Paketnamen vorgestellt werden welches den "Textverbrauch" die langen Klassen- und Paketnamen erheblich steigern kann. Eine Programmzeile sollte nicht mehr als 80 Zeichen haben!

Statischer Import

Die bisher vorgestellten Importvarianten erlauben das Importieren einer oder mehrerer Klassen. Beim Benutzen von statischen Attributen, Konstanten und Methoden muss man bei Java jedoch immer den Klassennamen voranstellen (Beispiel: Math.cos() ).

Statische Importe erlauben diese statischen Elemente einer Klasse im Namensraum einer Datei direkt bekannt zu machen. Hiermit kann man auf statische Attribute, Konstanten, Methoden einer Klasse zugreifen wie auf die lokalen Objektelemente einer Klasse. Man erspart sich das explizite Nennen der Klasse.

Implementierung mit explizitem Import:

package test;
import java.lang.Math;
...
class calc {
   public void main(String[] args) {
      float x = Math.PI;
      float y = Math.cos(2*x);
   }
}

Mit Hilfe des des statischen Imports kann man z.Bsp. die Klasse Math importieren um deren statischen Elemente direkt benutzen zu können. Durch den statischen Import ergibt sich für das vorhergehende Beispiel die folgende Implementierung:

package test;
import static java.lang.Math.*;
...
class calc {
   public void main(String[] args) {
      float x = PI;
      float y = cos(2*x);
   }
}

Wichtig:

  • beim statischen Import müssen Klassen immer explizit angegeben werden.
  • Namenskonflikte werden vom Übersetzer beanstandet. Sie müssen dann durch einen impliziten Import aufgelöst werden.

Zugriffsrechte auf Methoden und Attribute

Klassen außerhalb eines Pakets können auf Methoden und Attribute von Klassen eines Pakets nur zugreifen insofern es sich selbst mit den Schlüsselworten public class selbst zum Export freigibt. Falls das der Fall ist kann auf Methoden und Attribute abhängig von der Schlüsselwörten public, protected, private zugregriffen werden. Hierfür gilt:

  • public: Zugriff innerhalb der Klasse, außerhalb der Klasse, inner- und außerhalb des Pakets
  • protected: Zugriff nur durch Klassen und Methoden des eigenen Pakets oder Methoden von Unterklassen
  • private: Zugriff nur innerhalb der Klasse
  • keine Angabe: Zugriff nur innerhalb des Pakets (Im folgenden Diagramm "package" genannt)
Zugriffsrechte abhängig von der Deklaration im Paket
Exportierendes Schlüsselwort Konsument außerhalb des Pakets Konsument innerhalb des Pakets Konsument in einer Unterklasse Konsument innerhalb der Klasse
"kein Schlüsselwort" (package) - Benutzung erlaubt Benutzung erlaubt Benutzung erlaubt
private - - - Benutzung erlaubt
protected Benutzung nur erlaubt wenn Klasse Unterklasse ist Benutzung erlaubt Benutzung erlaubt Benutzung erlaubt
public Benutzung erlaubt Benutzung erlaubt Benutzung erlaubt Benutzung erlaubt

Dies ergibt in Bezug auf die Benutzbarkeit von anderen Klassen die folgenden geschachtelten Mengen:

Reichweite der Benutzbarkeit in Abhängigkeit vom Schlüsselwort

 

 

Lernziele

Am Ende dieses Blocks können Sie:

  • .... mit Hilfe von Paketen verschiedene Namensräume für Klassennamen nutzen
  • ... erkennen welche Methoden und Klassen aus anderen Paketen verwendet werden dürfen
  • ... die Modifizierer public, protected, private verwenden um den Zugriff auf Klassen, Methoden und Attribute in Paketen zu kontrollieren
  • ... die Sonderformendes statischen und impliziten Imports anwenden

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten:

Generische Klassen (Generics)

Duke mit Templates

Generische (engl. generic)  Klassen, Schnittstellen und Methoden wurden in Java in der Version 5.0 eingeführt.

Generisch bedeutet in diesem Zusammenhang, dass die entsprechenden Klassen, Methoden und Schnittstellen parametrisierbare Typen verwenden. Ein Übergabetyp ist also nicht im Quellcode festgelegt, er kann bei der Verwendung zum Übersetzungszeitpunkt verschiedene Ausprägungen annehmen.

Das Konzept der generischen Klassen erhöht die Typsicherheit im Zusammenhang mit Polymorphismus und Vererbung. Eine Erhöhung der Typsicherheit bedeutet, dass der Entwickler weniger explizite Casts verwenden muss um Typkonversionen und Anpassungen zu erzwingen.

Definition
Generische Klasse
Verwendet eine Klasse formale Typ-Parameter so nennt man sie generische Klasse. Der formale Typ-Parameter ist ein symbolischer Typ der wie ein normaler Bezeichner in Java aufgebaut ist. Er wird nach dem Klassennamen in spitzen Klammern angegeben.

Am einfachsten lässt sich das Konzept an einem Beispiel einer Klasse Koordinate mit einem parametrisierbaren Typen <T> veranschaulichen:

public class Koordinate<T> {
   private T x;
   private T y;

   Koordinate(T p1, T p2) {
      x = p1;
      y = p2;
   }
   ...
}

In dieser Implementierung der Klasse wird der eigentliche Typ der Komponenten (x und y) der Klasse Koordinate nicht konkret festgelegt. Ein Konsument dieser Klasse kann sich entscheiden diese Implementierung für Integer, Float oder Double zu verwenden. Die allgemeine Syntaxregel für die Deklaration einer generischen Klasse lautet:

Syntaxregel
Deklaration einer generischen Klasse
class Klassenname < Typvariablenliste > {

 

Will man die Implementierung der generischen Klasse Koordinate für große Fließkommazahlen verwenden, so benutzt man die folgende Syntax:

Koordinate<Double> eineKoordinate = new Koordinate<Double>(2.2d, 3.3d);

Der Typ ist parametrisierbar und wird Teil des Variablennamens bzw. des Klassennamens. Die allgemeine Syntax zum Erzeugen eines Objekts einer generischen Klasse lautet:

Syntaxregel
Instanziieren einer generischen Klasse
new Klassenname < Typliste > ( Parameterliste);

 

Will man kleinere Fließkommazahlen benutzen, so kann man die Klasse Koordinate auch mit dem Typ Float parametrisieren:

Koordinate<Float> nochEineKoordinate = new Koordinate<Float>(4.4f, 5.5f);

 Beispiel einer einfachen generischen Klasse

Die Klasse Koordinate hat ein Hauptprogramm main() welches zwei Instanzen mit unterschiedlichen Ausprägungen erzeugt und bei der Ausgabe der Werte die Methode toString() implizit aufruft:

package Kurs2.Generics;

public class Koordinate<T> {
private T x;
private T y;

public T getX() {return x;}
public void setX(T x) {this.x = x;}
public T getx() {return x;}
public void setY(T y) { this.y = y;}
public T getY() {return y;}

public Koordinate(T xp, T yp ) {
x = xp;
y = yp;
}

public String toString() {return "x: " + x + "; y: " + y;}

public static void main (String[] args) {
Koordinate<Double> k1 = new Koordinate<Double>(2.2d, 3.3d);
System.out.println(k1);

Koordinate<Integer> k2 = new Koordinate<Integer>(2, 3);
System.out.println(k2);
}
}

Bei der Ausführung ergibt sich die folgende Konsolenausgabe:

x: 2.2; y: 3.3
x: 2; y: 3

 

Generics zur Übersetzungs- und Laufzeit

Der javac Übersetzer erzeugt aus der gegebenen generischen Klasse in der Datei Koordinate.java nur genau eine Datei Koordinate.class mit Bytecode für alle möglichen Instanzierungen.

package Kurs2.Generics;
public class Koordinate<T> {
private T x;
private T y;

public T getX() {return x;}
public void setX(T x) {this.x = x;}
public T getY() {return y;}
public void setY(T y) { this.y = y;}

public Koordinate(T xp, T yp ) {
x = xp;
y = yp;
}

public String toString() {return "x: " + x + "; y: " + y;}

public static void main (String[] args) {
Koordinate<Double> k1 = new Koordinate<Double>(2.2d, 3.3d);
System.out.println(k1);
Koordinate<Integer> k2 = new Koordinate<Integer>(2, 3);
System.out.println(k2);
Koordinate<Number> k2 = new Koordinate<Number>(4.4f, 5.5f);
System.out.println(k2);
}

}

Der erzeugte Bytecode enthält nicht mehr den Formalparameter <T>. Er enthält den Referenztyp Object. Aus diesem Grund kann der Bytecode mit allen Instanzierungen arbeiten solange sie aus der Klasse Object abgeleitet sind. Direkte Basistypen wie int oder long können daher nicht direkt in generischen Klassen verwendet werden. Die Stellvertreterklassen Integer und Long in Verbindung mit dem "Autoboxing" sind der Ersatz für die direkte Verwendung.

Zur Laufzeit kann dann der Bytecode verwendet werden um den aktuell parametrisierten Parametern zu arbeiten.

Das Ersetzen der Parametrisierung <T> durch die Klasse Object nennt man Type-Erasure (engl. Auslöschen).

Wichtig

Polymorphismus und Vererbung von generischen Klassen

Aufgrund der Möglichkeit die Klassen mit unterschiedlichen aktuellen Parametern zu benutzen sind generische Klassen polymorph.

Die aktuell parametrisierten Objekte stehen jedoch auf der gleichen Vererbungsebene. Sie haben keine Vererbungsbeziehung!

Generics, Autoboxing, Subtyping

Die im vorhergehenden Beispiel benutzte Klasse kann aber auch als aktuellen Parameter eine abstrakte Klasse wie Number verwenden:

Mit Hilfe des Java Autoboxing kann man die Variablen k3 und k4 auch wie folgt belegen:

Koordinate<Number> k3 = new Koordinate<Number>(2l, 3l);
System.out.println(k3);

k3 = new Koordinate<Number>(4.4f, 5.5f);
System.out.println(k3);

Die Variable k3 hat den formalen Parametertyp Number.  Die aktuellen Parameter 21 und 31 sind int Typen. Sie werden automatisch in Instanzen von Integer umgewandelt und sind daher Spezialisierungen der Klasse Number. Die Variable k3 zeigt hier zuerst auf eine Koordinate die aus ganzen Zahlen besteht und anschließend auf eine Koordinate die aus Fließkommanzahlen bestehen

Ohne Autoboxing würde man die Variable k3 so belegen:

Koordinate<Number> k3 = new Koordinate<Number>(new Integer(2l), new Integer(3l));

Subtyping

Die parametrisierte Klasse Koordinate<Number> kann zwar wahlweise auf verschiedene Varianten von Objekten zugreifen die parametrisiert mit Koordinate<Number> erzeugt wurden. Sie kann aber nicht auf Objekte gleichen Inhalts aus Koordinate<Integer> zugreifen. Das folgende Implementierungsbeispiel erlaubt nicht die letzte Zuweisung von k2 auf k3:

Koordinate<Double> k1 = new Koordinate<Double>(2.2d, 3.3d);
Koordinate<Integer> k2 = new Koordinate<Integer>(2, 3);
Koordinate<Number>  k3 = new Koordinate<Number>(2l, 3l);
                    k3 = new Koordinate<Number>(4.4f, 5.5f);
                    k3 = k2; // Fehler

Die Vererbungsbeziehung besteht nicht zwischen den generischen Klassen selbst. Der Javaübersetzer erzeugt den folgenden Fehler:

found   : Kurs2.Generics.Koordinate<java.lang.Integer>
required: Kurs2.Generics.Koordinate<java.lang.Number>
k3 = k2;

In diesem Beispiel wird das "Liskov Substitution Principle" verletzt! Der Übersetzer javac erkennt diese Fehler und übersetzt diesen Quellcode nicht.

 

Vererbung und generische Klassen

Die Vererbung zwischen generischen Klassen untereinander und mit nicht generischen Klassen ergibt eine zweidimensionale Tabelle von Möglichkeiten:

  Oberklasse generisch Oberklasse nicht generisch
Unterklasse generisch
Unterklasse nicht generisch
  • "Klassische" Vererbung

Eine generische Klasse erweitert eine generische Klasse

Will man eine generische Klasse aus einer anderen generischen Klasse ableiten so gibt es zwei Möglichkeiten:

  • der formale Typ-Parameter der Oberklasse wird weitervererbt
  • der formale Typ-Parameter der Oberklasse wird durch einen aktuellen Parameter ersetzt.

Formaler Parameter der Oberklasse ersetzt formalen Parameter der Unterklasse

 Bei dieser Form der Vererbung hat die Klassendeklaration der Unterklasse die folgende Noatation:

public class Unterklasse<T> extends Oberklasse<T>

Ein Beispiel ist die Erweiterung der zweidimensionalen Klasse Koordinate zu einer drei dimensionalen Koordinate in  der Klasse Koordinate3DGen

package Kurs2.Generics;

public class Koordinate3DGen<T> extends Koordinate<T> {
private T z;

public T getZ() {return z;}
public void setZ(T z) {this.z = z;}

public Koordinate3DGen (T x, T y, T z) {
super (x,y);
this.z = z;
}

public String toString() {return super.toString()+", z: "+ z;}

public static void main (String[] args) {
Koordinate3DGen<Double> k1 = new Koordinate3DGen<Double>(1.1d, 2.2d, 3.3d);
System.out.println(k1);

Koordinate3DGen<Integer> k2 = new Koordinate3DGen<Integer>(1,2,3);
System.out.println(k2);
}

}

Die z Dimension kann in dieser Klasse immer nur mit dem aktuellen Parameter der beiden anderen Dimensionen instanziiert werden.

Formaler Typ-Parameter der Oberklasse wird durch aktuellen Parameter ersetzt

Eine andere Möglichkeit besteht darin den formalen Typparameter der Oberklasse durch einen aktuellen zu ersetzen und gleichzeitig einen neuen formalen Typparameter einzuführen. Hier haben die Klassendeklarationen die folgende Form:

public class Unterklasse<T> extends Oberklasse<konkrete-Klasse>

Ein Beispiel hierfür sei eine zweidimensionale Koordinate die über ein generisches Gewichtsattribut verfügt. Als aktueller Typparameter wird hier der Typ Double ausgewählt:

package Kurs2.Generics;

public class Koordinate2DGewicht<T> extends Koordinate<Double> {
private T gewicht;

public T getGewicht() {return gewicht;}
public void setGewicht(T g) {gewicht = g;}

public Koordinate2DGewicht (Double x, Double y, T g) {
super (x,y);
gewicht = g;
}

public String toString() {return super.toString()+", Gewicht: "+ gewicht;}

public static void main (String[] args) {
Koordinate2DGewicht<Double> k1 = new Koordinate2DGewicht<Double>(1.1d, 2.2d, 9.9d);
double dx = k1.getX(); // Die Generizität ist nicht mehr erkennbar!
System.out.println(k1);
Koordinate2DGewicht<Integer> k2 = new Koordinate2DGewicht<Integer>(1.1d,2.2d,9);
System.out.println(k2);
}
}

Achtung: Der formale Typparamter T der Klasses Koordinate2Gewicht wurde an dieser Stelle neu eingeführt. Er ist ein anderer als der formale Typparameter T der Oberklasse Koordinate!

Generische Unterklasse leitet aus nicht generischer Oberklasse ab

Eine generische Klasse auch eine nicht generische Klasse erweitern. Hier wird der Typ-Parmater neu in die Klassenhierarchie eingeführt. Die Klassendeklarationen genügen dann der folgenden Form:

public class Unterklasse<T> extends Oberklasse

Ein Beispiel hierfür ist eine nicht generische Oberklasse Koordinate2D aus der eine generische Unterklasse Koordinate2DGewichtGen<T> abgeleitet wird:

package Kurs2.Generics;

public class Koordinate2D {
private Double x;
private Double y;

public Double getX() {return x;}
public void setX(Double x) {this.x = x;}
public Double getY() {return y;}
public void setY(Double y) { this.y = y;}

public Koordinate2D(Double xp, Double yp ) {
x = xp;
y = yp;
}

public String toString() {return "x: " + x + "; y: " + y;}

public static void main (String[] args) {
Koordinate2D k1 = new Koordinate2D(2.2d, 3.3d);
System.out.println(k1);
}

public class Koordinate2DGewichtGen<T> extends Koordinate2D {
private T gewicht;

public T getGewicht() {return gewicht;}
public void setGewicht(T g) {gewicht = g;}

public Koordinate2DGewichtGen (Double x, Double y, T g) {
super (x,y);
gewicht = g;
}

public String toString() {return super.toString()+", Gewicht: "+ gewicht;}

public static void main (String[] args) {
Koordinate2DGewichtGen<Double> k1 = new Koordinate2DGewichtGen<Double>(1.1d, 2.2d, 9.9d);
double dx = k1.getX();
System.out.println(k1);
Koordinate2DGewichtGen<Integer> k2 = new Koordinate2DGewichtGen<Integer>(1.1d,2.2d,9);
System.out.println(k2);
}
}

Nicht generische Unterklasse leitet aus generischer Oberklasse ab

Die letzte Variante besteht aus nicht generischen Klassen die aus einer generischen Oberklasse ableiten in dem sie einen aktuellen Typ-Parameter für die Unterklasse aus der Oberklasse wählen. Die Klassendeklarationen dieser Klassen haben die folgende Schreibweise:

public class Unterklasse extends Oberklasse<konkrete-Klasse>

Ein Beispiel hierfür sei die Klasse Koordinate3DDouble die nicht generisch ist und für die x und y Koordinate die Klasse Koordinate mit dem aktuellen Typparameter Double verwendet:

package Kurs2.Generics;

public class Koordinate3DDouble extends Koordinate<Double> {
private Double z;

public Double getZ() {return z;}
public void setZ(Double z) {this.z = z;}

public String toString() {return super.toString()+", z: "+ z;}

public Koordinate3DDouble (Double x, Double y, Double z) {
super (x,y);
this.z = z;
}

public static void main (String[] args) {
Koordinate3DDouble k1 = new Koordinate3DDouble(1.1d, 2.2d, 3.3d);
double d1 = k1.getX(); //Generezität nicht mehr sichtbar
System.out.println(k1);
}
}

 

 

 

Wildcards

 "Wildcards" sind ein Begriff aus dem Englischen und beziehen sich auf die Joker im Pokerspiel, die vielfältig eingesetzt werden können. Der Begriff wird im Computerumfeld verwendet wenn es um Platzhalter für andere Zeichen geht.

Unbound Wildcards

Die Wildcards werden benötigt um mit Referenzen auf generische Objekte zu zeigen und deren Ausprägung mehr oder weniger allgemein zu definieren. Eine Referenz auf ein Objekt der verwendeten generischen Klasse Koordinate<T> kann man mit einer Wildcard so beschreiben:

Koordinate<?> zeiger;

Ein Fragezeichen ? ist eine "unbound Wildcard". Sie erlaubt es auf jeden beliebigen Typ der Klasse Koordinate<T> zu zeigen.

Beispiel:

package Kurs2.Generics;

public class Koordinate<T> {
private T x;
private T y;

public T getX() {return x;}
public void setX(T x) {this.x = x;}
public T getY() {return y;}
public void setY(T y) { this.y = y;}

public Koordinate(T xp, T yp ) {
x = xp;
y = yp;
}

public String toString() {return "x: " + x + "; y: " + y;}

public static void main (String[] args) {
Koordinate<Double> k1 = new Koordinate<Double>(2.2d, 3.3d);
System.out.println(k1);

Koordinate<Integer> k2 = new Koordinate<Integer>(2, 3);
System.out.println(k2);

Koordinate<Number> k3 = new Koordinate<Number>(2l, 3l);
System.out.println(k3);

k3 = new Koordinate<Number>(4.4f, 5.5f);
System.out.println(k3);

Koordinate<?> zeiger;
zeiger = k1;
zeiger = k2;
zeiger = k3;
}

}

Die Referenzvariable zeiger kann im gezeigten Beispiel auf beliebig parametrisierte Objekte der Klasse Koordinate<T> zeigen.

Wichtig

Die Wildcard kann nicht in der Sektion des Typ-Parameter einer Klasse, Methode oder Schnittstelle stehen!

Sie kann nur im Kontext von Referenzvariablen verwendet werden!

Umgangssprachlich: Die Wildcard findet man immer nur links des Zuweisungsoperators oder in einer Variablendeklaration.

Die Upper Bound Wildcard

Die bisher verwendeten Typparameter erlauben die Instanziierung einer Klasse mit jeder beliebigen Klasse (die aus der Klasse Object abgeleitet wird). Dies ist oft zu allgemein und kann kontraproduktiv sein.

Bei der Klasse Koordinate macht es keinen Sinn sie mit dem Typ-Parameter Boolean zu parametrisieren:

Koordinate<Boolean> k = new Koordinate<Boolean>(true, true);

Das oben gezeigte Beispiel ist eine korrekte Zuweisung die man übersetzen und ausführen kann. Man kann eine solche Verwendung unterbinden wenn die Typenstruktur Klassenhierachie von Java für Zahlen nutzt:

 

Man kann die Klasse Number als Oberklasse für alle erlaubten Parametrisierungen wählen und den generischen Typ der Klasse Koordinate<T> einschränken:

public class Koordinate<T extends Number> 

Durch diese Notation wird der formale Typparameter auf die Klasse Number oder Spezialisierungen daraus beschränkt. Man spricht von einer "Upper Bound Wildcard" weil die Verwendung von Klassen nach oben (zur Wurzel der Klasse) hin beschränkt ist.

Die Lower Bound Wildcard

Neben der "Upper Bound Wildcard" gibt es auch eine "Lower Bound Wild Card". Sieht wird trotz des ähnlichen Namens, sehr unterschiedlich verwendet.

Sie wird ausschließlich mit der "Unbound Wildcard" in der Schreibweise "? super Lowerbound" bei der Typfestlegung von Referenzen verwendet.

Die generische Klasse selbst ist hier beliebig. Sie sei:

public class MyGenericClass<T> { 
   // Inhalt der Klasse ist nicht wichtig
}

Weiterhin sei eine Klassenhierarchie mit Klassen von A bis F gegeben:

 

mit Hilfe der "Lower Bound Wildcard" kann die Verwendung einer Referenz so eingeschränkt werden, dass nur Instanzen der Klasse C oder Klassen von denen C abgeleitet wurde, verwendet werden darf.

public class TestClass {
...
   public void testCreate(MyGenericClass<? super C> zeiger) {
      ....
   }
   public void test () {
      TestClass.testCreate(new MyGenericClass<C>());      // Korrekt
      TestClass.testCreate(new MyGenericClass<A>());      // Korrekt
      TestClass.testCreate(new MyGenericClass<Object>()); // Korrekt
      TestClass.testCreate(new MyGenericClass<F>());      // Fehler!!
      TestClass.testCreate(new MyGenericClass<D>());      // Fehler!!
   }
}

Die Typ-Parameter der Klassen E, F und B können nicht übergeben werden, da sie nicht in der Typhierarchie der Klasse C vorkommen.

Im Java API werden auch "Lower Bound Wildcards" verwendet. Ein Beispiel ist die drainTo() Methode der Schnittstelle BlockingQueue:

int drainTo(Collection<? super E> c, int maxElements)

Der "Diamondoperator" in Java

Bei der Verwendung generischer Datentypen ensteht zuätzlicher Schreibaufwand, da man den Typ inklusive seines generischen Typs an vielen Stellen nennen muss. Ein Bespiel hierfür ist:

Koordinate<Double> meinPunkt = new Koordinate<Double>(1.1D,2.2D);

Man muss den generischen Typ Double in der Deklaration der Referenzvariable, wie auch im Konstruktoraufruf beim Anlegen des Objekts nennen.

Seit Java 7 ist es erlaubt eine verkürzte Schreibweise zu verwenden. Man nennt diese verkürzte Schreibweise den "Diamondoperator" weil das Kleiner- und GrößerZeichen an einen Kristall erinnern. Das obige Beispiel lässt sich auch verkürzt so programmieren:

Koordinate<Double> meinPunkt = new Koordinate<>(1.1D,2.2D);

Der Übsetzer leitet sich die notwendige Typinformation beim Konstruktoraufruf aus dem generischen Typ der Referenzvariable ab.

Bemerkung: Dies ist eine stark verkürzte Erklärung.

Die automatische Bestimmung des Typs durch den Übersetzer kann recht kompliziert sein, da die Konzepte der Vererbung, Interfaces (Schnittstellen), Casts und Autoboxing beachtet werden müssen.

Weiterführende Ressourcen

 

Übungen, Fragen (Generics)

1. Frage: Instanziierungen

Gegeben seien die folgenden Klassen:

public class Kaefig<T> {
private T einTier;
public void setTier(T x) {
einTier = x;
}
public T getTier() {
return einTier;
} }

public class Tier{ }
public class Hund extends Tier { }
public class Vogel extends Tier { }

Beschreiben Sie was mit dem folgenden Code geschieht. Die Möglichkeiten sind

  • er übersetzt nicht
  • er übersetzt mit Warnungen
  • er erzeugt Fehler während der Ausführung
  • er übersetzt und läuft ohne Probleme

1.1

Kaefig<Tier> zwinger = new Kaefig<Hund>();

1.2

Kaefig<Vogel> voliere = new Kaefig<Tier>();

1.3

Kaefig<?> voliere = new Kaefig<Vogel>();
voliere.setTier(new Vogel());

1.4

Kaefig voliere = new Kaefig();
voliere.setTier(new Vogel());

2. Umwandeln einer nicht generischen Implementierung in eine generische Implementierung

In diesem Beispiel werden Flaschen mit verschiedenen Getränken gefüllt und entleert. Das Befüllen und Entleeren erfolgt in der main() Methode der Klasse Flasche.

Die vorliegende Implementierung erlaubt das Befüllen der Flaschen f1 und f2 mit beliebigen Getränken.

Aufgabe:

Ändern Sie die Implementierung der Klasse Flasche in eine generische Klasse die für unterschiedliche Getränke parametrisiert werden kann.

  • passen Sie alle Methoden der Klasse Flasche so an das sie mit einem parametrisierten Getränk befüllt werden können (Bier oder Wein)
  • Ändern Sie die main() Methode derart, dass f1 nur mit Bier befüllt werden kann und f2 nur mit Wein.

2.1 Klasse Flasche

Die Klasse dient als Hauptprogramm. In dieser Übung muss ausschließlich die Methoden dieser Klasse und das Hauptprogramm angepasst werden.

package Kurs2.Generics;

public class Flasche {

Getraenk inhalt = null;

public boolean istLeer() {
return (inhalt == null);
}

public void fuellen(Getraenk g) {
inhalt = g;
}

public Getraenk leeren() {
Getraenk result = inhalt;
inhalt = null;
return result;
}

public static void main(String[] varargs) {
// in generischer Implementierung soll
// f1 nur für Bier dienen
Flasche f1 = new Flasche();
f1.fuellen(new Bier("DHBW-Bräu"));
System.out.println("f1 geleert mit " + f1.leeren());
f1 = new Flasche();
f1.fuellen(new Bier("DHBW-Export"));
System.out.println("f1 geleert mit " + f1.leeren());

// In der generischen Implementierung soll f2 nur für
// Weinflaschen dienen
Flasche f2;
f2 = new Flasche();
f2.fuellen(new Weisswein("Pfalz"));
System.out.println("f2 geleert mit " + f2.leeren());

f2 = new Flasche();
f2.fuellen(new Rotwein("Bordeaux"));
System.out.println("f2 geleert mit " + f2.leeren());
}
}

2.2 Klasse Getraenk

package Kurs2.Generics;

public abstract class Getraenk {

}

2.3 Klasse Bier

package Kurs2.Generics;

public class Bier extends Getraenk {
private String brauerei;

public Bier(String b) { brauerei = b;}
public String getBrauererei() {
return brauerei;
}
public String toString() {return "Bier von " + brauerei;}
}

2.4 Klasse Wein

package Kurs2.Generics;

public class Wein extends Getraenk {
private String herkunft;

public String getHerkunft() {
return herkunft;
}

public String toString(){ return ("Wein aus " + herkunft);}

public Wein (String origin) {
herkunft = origin;
}

}

2.5 Klasse Weisswein

package Kurs2.Generics;

public class Weisswein extends Wein {
public Weisswein(String h) {super(h);}

}

2.6 Klasse Rotwein

package Kurs2.Generics;

public class Rotwein extends Wein {
public Rotwein(String h) {super(h);}

}

3. Typprüfungen

Welche Zeilen in der main() Methode werden vom Übersetzer nicht übersetzt? 

Markieren Sie die Zeilen und nennen Sie den Fehler:

package Kurs2.Generics;

public class KoordinateTest<T extends Number> {

public T x;
public T y;

public KoordinateTest(T xp, T yp) {
x = xp;
y = yp;
}

public static void main(String[] args) {
KoordinateTest<Double> k11, k12;
KoordinateTest<Integer> k21, k22;
Koordinate<String> k31;
KoordinateTest<Number> k41, k42;

k11 = new KoordinateTest<Float>(2.2d, 3.3d);
k12 = new KoordinateTest<Double>(2.2d, 3.3d);
k21 = new KoordinateTest<Integer>(2, 3);
k31 = new Koordinate<String>("11","22");
k41 = new KoordinateTest<Number>(2l, 3l);

k41 = new KoordinateTest<Number>(4.4d, 5.5f);
k11 = new Koordinate<Double>(3.3f,9.9d);

KoordinateTest<? super Double> k99;
k99 = k11;
k99 = k41;
k99 = k31;

k11 = k12;
k12 = k21;
KoordinateTest k55 = new KoordinateTest<Number>(7.7f, 8.8f);
KoordinateTest k66 = new KoordinateTest(7.7f, 8.8f);
}
}

 

Lösungen, Antworten

1. Antwort: Instanziierungen

Gegeben seien die folgenden Klassen:

public class Kaefig<T> {
private T einTier;
public void setTier(T x) {
einTier = x;
}
public T getTier() {
return einTier;
}

public class Tier{ }
public class Hund extends Tier { }
public class Vogel extends Tier { } }

Beschreiben Sie was mit dem folgenden Code geschieht. Die Möglichkeiten sind

  • er übersetzt nicht
  • er übersetzt mit Warnungen
  • er erzeugt Fehler während der Ausführung
  • er übersetzt und läuft ohne Probleme

1.1

Kaefig<Tier> zwinger = new Kaefig<Hund>();

Übersetzungsfehler: Hund ist zwar eine Unterklasse von Tier. Kaefig<Tier> und Kaefig<Hund> sind keine kompatiblen Typen.

1.2

Kaefig<Vogel> voliere = new Kaefig<Tier>();

Übersetzungsfehler: Vogel ist zwar eine Unterklasse von Tier. Kaefig<Tier> und Kaefig<Vogel> sind keine kompatiblen Typen.

1.3

Kaefig<?> voliere = new Kaefig<Vogel>();
voliere.setTier(new Vogel());

Übersetzungsfehler in der zweiten Zeile. Die erste Zeile ist in Ordnung. Man kann einen Kaefig eines unbekannten aktuellen Parametertyps erzeugen. Die zweite Zeile kann nicht übersetzen, da der Übersetzer nicht wissen kann welche Tiere in voliere verwaltet werden sollen. Der folgende Code würde übersetzen:

Kaefig<?> voliere = new Kaefig<Vogel>();
Kaefig<Vogel> k= new Kaefig<Vogel>(); k.setTier(new Vogel()); voliere=k;

Anmerkung: Man darf nur Referenzen auf Zeiger mit Wildcards (hier voliere) zuweisen. Man darf keine Objektmethoden aufrufen weil hierfür der Typ zur Übersetzungszeit nicht bekannt ist.

1.4

Kaefig voliere = new Kaefig();
voliere.setTier(new Vogel());

Der Übersetzer übersetzt den Quellcode mit einer Warnung. Er kann nicht wissen welchen Typ er benutzt. Er erzeugt deshalb eine Warnung, weil es beim Zuweisen eines Vogels zu Problemen kommen kann. Man verliert die Vorteile der Typprüfung der generischen Klassen. Dieser Codierstil sollte deshalb vermieden werden.

2. Umwandeln einer nicht generischen Implementierung in eine generische Implementierung

package Kurs2.Generics;

public class FlascheGeneric<T extends Getraenk> {

T inhalt = null;

public boolean istLeer() {
return (inhalt == null);
}

public void fuellen(T g) {
inhalt = g;
}

public T leeren() {
T result = inhalt;
inhalt = null;
return result;
}

public static void main(String[] varargs) {
// in generischer Implementierung soll
// f1 nur für Bier dienen
FlascheGeneric<Bier> f1 = new FlascheGeneric<Bier>();
f1.fuellen(new Bier("DHBW-Bräu"));
System.out.println("f1 geleert mit " + f1.leeren());

f1 = new FlascheGeneric<Bier>();
f1.fuellen(new Bier("DHBW-Export"));
System.out.println("f1 geleert mit " + f1.leeren());
// In der generischen Implementierung soll f2 nur für
// Weinflaschen dienen
FlascheGeneric<Wein> f2;
f2 = new FlascheGeneric<Wein>();
f2.fuellen(new Weisswein("Pfalz"));
System.out.println("f2 geleert mit " + f2.leeren());

f2 = new FlascheGeneric<Wein>();
f2.fuellen(new Rotwein("Bordeaux"));
System.out.println("f2 geleert mit " + f2.leeren());
}
}

3. Typprüfungen

Welche Zeilen in der main() Methoder werden vom Übersetzer nicht übersetzt? 

Markieren Sie die Zeilen und nennen Sie den Fehler:

ppackage Kurs2.Generics;

public class KoordinateTest<T extends Number> {

public T x;
public T y;

public KoordinateTest(T xp, T yp) {
x = xp;
y = yp;
}

public static void main(String[] args) {
KoordinateTest<Double> k11, k12;
KoordinateTest<Integer> k21, k22;
//Koordinate<String> k31; Die Klasse String ist nicht aus Number abgeleitet. Siehe extends Klausel
KoordinateTest<Number> k41, k42;
//k11 = new KoordinateTest<Float>(2.2d, 3.3d); Die Eingabeparameter sind vom Typ double und nicht vom Typ Float
k12 = new KoordinateTest<Double>(2.2d, 3.3d);
k21 = new KoordinateTest<Integer>(2, 3);
//k31 = new Koordinate<String>("11","22"); Nicht erlaubt, da der Typ weiter oben nicht für die Variable erlaubt war
k41 = new KoordinateTest<Number>(2l, 3l);
k41 = new KoordinateTest<Number>(4.4d, 5.5f);
//k11 = new Koordinate<Double>(3.3f,9.9d); Der erste Parameter ist ein Float und nicht ein Double wie gefordert

KoordinateTest<? super Double> k99;
//k99 = k11; Nicht erlaubt, da der Typ weiter oben nicht für die Variable erlaubt war
k99 = k41;
//k99 = k31; Nicht erlaubt, da der Typ weiter oben nicht für die Variable erlaubt war

k11 = k12;
//k12 = k21; k21 ist vom Typ KoordinateTest<Integer>. k12 muss aber vom Typ KoordinateTest<Double> sein
KoordinateTest k55 = new KoordinateTest<Number>(7.7f, 8.8f);
KoordinateTest k66 = new KoordinateTest(7.7f, 8.8f);
}
}

 

 

Beispiel: Von einer nicht generischen Klasse zu einer generischen Klasse

 Die Klassen MerkerX implementieren eine Warteschlange der Länge 2. Sie ist in der Lage sich den letzten und den vorletzten Wert zu merken.

Im Folgenden erhalten die Klassen neue Namen (Merker2, Merker3 etc.). Dies erlaubt die Klassen gleichzeitig im Paket zu verwalten

Von Basistypen zu Objekten

Die Klasse Merker1 ist in der Lage Basistypen vom Typ int zu verwalten. Die Werte werden als Teil der Instanzen  der Klasse Merker1 verwaltet.

Die Klasse Merker2 verwaltet Objekte vom Typ Integer. Verschiedene Instanzen von Merker2 könne also auf die gleichen Instanzen der Klasse Integer zeigen.

Klasse Merker1 Klasse Merker2
package Kurs2.Generics;

public class Merker1 {
private int letzter;
private int vorletzter;

public Merker1() {
letzter = 0;
vorletzter = 0;
}

public int getLetzter() {
return letzter;
}

public int getVorLetzter() {
return vorletzter;
}

public void merke(int wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "[" + letzter + ";" + vorletzter + "]";
}

public static void main(String[] args) {
// Teil 1: int Verwalten
Merker1 f1 = new Merker1();
int i10 = 10;
f1.merke(i10);
int i11 = 11; // Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12);//Erz. mit Autoboxing
f1.merke(i12);
System.out.println(f1);

// Teil 2: int verwalten
Merker1 f2 = new Merker1();
int i20 = 100;
f2.merke(i20);
int i21 = 101;
f2.merke(i21);
int i22 = 102;
f2.merke(i22);
System.out.println(f2);

// Teil 3: int verwalten
Merker1 f3 = new Merker1();
int i30 = 200;
f3.merke(i30);
int i31 = 201;
f3.merke(i31);
int i32 = 202;
f3.merke(i32);
System.out.println(f3);

// Warum sind diese Zuweisungen erlaubt ?
f1 = f2;
f2 = f3;
f1 = f3;
}
}

package Kurs2.Generics;

public class Merker2 {
private Integer letzter;
private Integer vorletzter;

public Merker2() {
letzter = null;
vorletzter = null;
}

public Integer getLetzter() {
return letzter;
}

public Integer getVorLetzter() {
return vorletzter;
}

public void merke(Integer wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "[" + letzter + ";" + vorletzter + "]";
}

public static void main(String[] args) {
// Teil 1: Integer Verwalten
Merker2 f1 = new Merker2();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11);// Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12);// Reguläre Erzeugung
f1.merke(i12);
System.out.println(f1);

// Teil 2: Integer verwalten
Merker2 f2 = new Merker2();
int i20 = 100;
f2.merke(i20);
int i21 = 101;
f2.merke(i21);
int i22 = 102;
f2.merke(i22);
System.out.println(f2);

// Teil 3: Integer verwalten
Merker2 f3 = new Merker2();
int i30 = 200;
f3.merke(i30);
int i31 = 201;
f3.merke(i31);
int i32 = 202;
f3.merke(i32);
System.out.println(f3);

// Warum sind diese Zuweisungen erlaubt ?
f1 = f2;
f2 = f3;
f1 = f3;
}
}

Von Integer-Objekten zur Verwaltung beliebiger Objekte

Die Klasse Merker3 kann nun nicht nur Instanzen der Klasse Integer verwalten. Sie kann beliebige Objekte verwalten.

Klasse Merker2 Klasse Merker3
package Kurs2.Generics;

public class Merker2 {
private Integer letzter;
private Integer vorletzter;

public Merker2() {
letzter = null;
vorletzter = null;
}

public Integer getLetzter() {
return letzter;
}

public Integer getVorLetzter() {
return vorletzter;
}

public void merke(Integer wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "[" + letzter + ";" + vorletzter + "]";
}

public static void main(String[] args) {
// Teil 1: Integer Verwalten
Merker2 f1 = new Merker2();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11);// Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12);// Reguläre Erzeugung
f1.merke(i12);
System.out.println(f1);

// Teil 2: Integer verwalten
Merker2 f2 = new Merker2();
int i20 = 100;
f2.merke(i20);
int i21 = 101;
f2.merke(i21);
int i22 = 102;
f2.merke(i22);
System.out.println(f2);

// Teil 3: Integer verwalten
Merker2 f3 = new Merker2();
int i30 = 200;
f3.merke(i30);
int i31 = 201;
f3.merke(i31);
int i32 = 202;
f3.merke(i32);
System.out.println(f3);

// Warum sind diese Zuweisungen erlaubt ?
f1 = f2;
f2 = f3;
f1 = f3;
}
}

package Kurs2.Generics;

public class Merker3 {
private Object letzter;
private Object vorletzter;

public Merker3 () {
letzter = null;
vorletzter = null;
}

public Object getLetzter() {
return letzter;
}

public Object getVorLetzter() {
return vorletzter;
}

public void merke(Object wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "["+ letzter +";" + vorletzter + "]";
}

public static void main (String[] args) {
// Teil 1: Integer Verwalten
Merker3 f1 = new Merker3();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11);// Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12);// Reguläre Erzeugung
f1.merke(i12);
System.out.println (f1);

// Teil 2: Float verwalten
Merker3 f2 = new Merker3();
Float i20 = 100.1f;
f2.merke(i20);
Float i21 = 101.2f;
f2.merke(i21);
Float i22 = 102.3f;
f2.merke(i22);
System.out.println (f2);

// Teil 3: Alles Verwalten
Merker3 f3 = new Merker3();
Integer i30 = 200;
f3.merke(i30);
Float i31 = 201.f;
f3.merke(i31);
String i32 = "Zeichenkette";
f3.merke(i32);
System.out.println (f3);

// Warum sind diese Zuweisungen erlaubt ?
f1 = f2;
f2 = f3;
f1 = f3;
}
}

Von der unsicheren Verwaltung beliebiger Objekte zu einer generischen Klasse

Die Klasse Merker4 ist jetzt generisch. Sie kann typsicher Objekte verwalten. In der main() Methode der Klassse Merker4 wird jedoch diese neue gewonnen Fähigkeit ignoriert. Die Klasse Merker4 wird benutzt wie eine Klasse die beliebige Typen verwalten kann.

Klasse Merker3 Klasse Merker4
package Kurs2.Generics;
public class Merker3 {
private Object letzter;
private Object vorletzter;

public Merker3 () {
letzter = null;
vorletzter = null;
}

public Object getLetzter() {
return letzter;
}

public Object getVorLetzter() {
return vorletzter;
}

public void merke(Object wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "["+ letzter +";" + vorletzter + "]";
}

public static void main (String[] args) {
// Teil 1: Integer Verwalten
Merker3 f1 = new Merker3();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11);// Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12);// Reguläre Erzeugung
f1.merke(i12);
System.out.println (f1);

// Teil 2: Float verwalten
Merker3 f2 = new Merker3();
Float i20 = 100.1f;
f2.merke(i20);
Float i21 = 101.2f;
f2.merke(i21);
Float i22 = 102.3f;
f2.merke(i22);
System.out.println (f2);

// Teil 3: Alles Verwalten
Merker3 f3 = new Merker3();
Integer i30 = 200;
f3.merke(i30);
Float i31 = 201.f;
f3.merke(i31);
String i32 = "Zeichenkette";
f3.merke(i32);
System.out.println (f3);

// Warum sind diese Zuweisungen erlaubt ?
f1 = f2;
f2 = f3;
f1 = f3;
}
}

package Kurs2.Generics;
public class Merker4<T> {
private T letzter;
private T vorletzter;

public Merker4 () {
letzter = null;
vorletzter = null;
}

public T getLetzter() {
return letzter;
}

public T getVorLetzter() {
return vorletzter;
}

public void merke(T wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "["+ letzter +";" + vorletzter + "]";
}

public static void main (String[] args) {
// Teil 1: Integer Verwalten
Merker4 f1 = new Merker4();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11); //Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12); //Reguläre Erzeugung
f1.merke(i12);
System.out.println (f1);

// Teil 2: Float verwalten
Merker4 f2 = new Merker4();
Float i20 = 100.1f;
f2.merke(i20);
Float i21 = 101.2f;
f2.merke(i21);
Float i22 = 102.3f;
f2.merke(i22);
System.out.println (f2);

// Teil 3: Alles Verwalten
Merker4 f3 = new Merker4();
Integer i30 = 200;
f3.merke(i30);
Float i31 = 201.f;
f3.merke(i31);
String i32 = "Zeichenkette";
f3.merke(i32);
System.out.println (f3);

// Warum sind diese Zuweisungen erlaubt ?
f1 = f2;
f2 = f3;
f1 = f3;

}
}

Typsichere Nutzung der generischen Klasse

Die Klasse Merker5 ist mit Ausnahme der main() Methode identisch zur Klasse Merker4. In der main() Methode der Klassse Merker4 werden jetzt Objekte f1, f2, f3 angelegt die nur die Verwaltung von bestimmten Typen in der Klasse Merker5 erlauben. Da die aktuellen Typen von f1, f2, f3 unterschiedlich sind kann man Sie nicht mehr beliebig aufeinander zuweisen.

Klasse Merker4 Klasse Merker5
package Kurs2.Generics;
public class Merker4<T> {
private T letzter;
private T vorletzter;

public Merker4 () {
letzter = null;
vorletzter = null;
}

public T getLetzter() {
return letzter;
}

public T getVorLetzter() {
return vorletzter;
}

public void merke(T wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "["+ letzter +";" + vorletzter + "]";
}

public static void main (String[] args) {
// Teil 1: Integer Verwalten
Merker4 f1 = new Merker4();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11); //Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12); //Reguläre Erzeugung
f1.merke(i12);
System.out.println (f1);

// Teil 2: Float verwalten
Merker4 f2 = new Merker4();
Float i20 = 100.1f;
f2.merke(i20);
Float i21 = 101.2f;
f2.merke(i21);
Float i22 = 102.3f;
f2.merke(i22);
System.out.println (f2);

// Teil 3: Alles Verwalten
Merker4 f3 = new Merker4();
Integer i30 = 200;
f3.merke(i30);
Float i31 = 201.f;
f3.merke(i31);
String i32 = "Zeichenkette";
f3.merke(i32);
System.out.println (f3);

// Warum sind diese Zuweisungen erlaubt ?
f1 = f2;
f2 = f3;
f1 = f3;
}
}

package Kurs2.Generics;
public class Merker5 <T> {
private T letzter;
private T vorletzter;

public Merker5 () {
letzter = null;
vorletzter = null;
}

public T getLetzter() {
return letzter;
}

public T getVorLetzter() {
return vorletzter;
}

public void merke(T wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "["+ letzter +";" + vorletzter + "]";
}

public static void main (String[] args) {
// Teil 1: Integer Verwalten
Merker5<Integer> f1 = new Merker5<Integer>();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11);// Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12);// Reguläre Erzeugung
f1.merke(i12);
System.out.println (f1);

// Teil 2: Float verwalten
Merker5<Float> f2 = new Merker5<Float>();
Float i20 = 100.1f;
f2.merke(i20);
Float i21 = 101.2f;
f2.merke(i21);
Float i22 = 102.3f;
f2.merke(i22);
System.out.println (f2);

// Teil 3: Alles Verwalten
Merker5<Integer> f3 = new Merker5<Integer>();
Integer i30 = 200;
f3.merke(i30);
Float i31 = 201.f;
//f3.merke(i31);
String i32 = "Zeichenkette";
//f3.merke(i32);
System.out.println (f3);

// Welche Zuweisungen sind erlaubt ?
//f1 = f2;
//f2 = f3;
//f1 = f3;

}
}

Die Klassenhierarchie der verschiedenen Varianten der generischen Klasse Merker5:

 

Einschränkung der generischen Klasse Merker6 auf bestimmte Typen

Die Klasse Merker6 verwendet das Schlüsselwort extends um die Verwendung auf zahlenartige Typen (Number) einzuschränken. Die Einschränkungen bei den Zuweisungen zwischen den Variablen f1, f2, und f3 sind zu beachten.

Anbei die Klassen Hierarchie des Java API zur Klasse Number und ihrer Unterklassen:

Klasse Merker4 Klasse Merker5
package Kurs2.Generics;
public class Merker5 <T> {
private T letzter;
private T vorletzter;

public Merker5 () {
letzter = null;
vorletzter = null;
}

public T getLetzter() {
return letzter;
}

public T getVorLetzter() {
return vorletzter;
}

public void merke(T wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "["+ letzter +";" + vorletzter + "]";
}

public static void main (String[] args) {
// Teil 1: Integer Verwalten
Merker5<Integer> f1 = new Merker5<Integer>();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11);// Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12);// Reguläre Erzeugung
f1.merke(i12);
System.out.println (f1);

// Teil 2: Float verwalten
Merker5<Float> f2 = new Merker5<Float>();
Float i20 = 100.1f;
f2.merke(i20);
Float i21 = 101.2f;
f2.merke(i21);
Float i22 = 102.3f;
f2.merke(i22);
System.out.println (f2);

// Teil 3: Alles Verwalten
Merker5<Integer> f3 = new Merker5<Integer>();
Integer i30 = 200;
f3.merke(i30);
Float i31 = 201.f;
//f3.merke(i31);
String i32 = "Zeichenkette";
//f3.merke(i32); // Warum ist dies nicht erlaubt?
System.out.println (f3);

// Welche Zuweisungen sind erlaubt ?
//f1 = f2;
//f2 = f3;
//f1 = f3;

}
}

package Kurs2.Generics;
public class Merker6<T extends Number> {
    private T letzter;
    private T vorletzter;

public Merker6() {
letzter = null;
vorletzter = null;
}

public T getLetzter() {
return letzter;
}

public T getVorLetzter() {
return vorletzter;
}

public void merke(T wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "[" + letzter + ";" + vorletzter + "]";
}

public static void main(String[] args) {
// Teil 1: Integer Verwalten
Merker6<Integer> f1 = new Merker6<Integer>();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11); // Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12); // Reguläre Erzeugung
f1.merke(i12);
System.out.println(f1);

// Teil 2: Float verwalten
Merker6<Float> f2 = new Merker6<Float>();
Float i20 = 100.1f;
f2.merke(i20);
Float i21 = 101.2f;
f2.merke(i21);
Float i22 = 102.3f;
f2.merke(i22);
System.out.println(f2);

// Teil 3: Alles Verwalte
Merker6<Number> f3 = new Merker6<Number>();
Integer i30 = 200;
f3.merke(i30);
Float i31 = 201.f;
f3.merke(i31);
String i32 = "Zeichenkette";
//f3.merke(i32); // Warum ist dies nicht erlaubt?
System.out.println(f3);

// f1 = f2; // Warum ist dies nicht erlaubt?
// f2 = f3; // Warum ist dies nicht erlaubt?
//
}
}

Wild Cards

Die Klasse Merker7 benutzt in der main() Methode eine Variable merkeAlles die mit einer Wildcard versehen ist.

Die Variable kann auf alle Varianten der Klasse Merker7 zeigen, sie kann aber nicht alle Methoden nutzen da die Methoden zur Übersetzungszeit nicht bekannt sein müssen.

package Kurs2.Generics;
public class Merker7<T extends Number> {

private T letzter;
private T vorletzter;

public Merker7() {
letzter = null;
vorletzter = null;
}

public T getLetzter() {
return letzter;
}

public T getVorLetzter() {
return vorletzter;
}

public void merke(T wert) {
vorletzter = letzter;
letzter = wert;
}

public String toString() {
return "[" + letzter + ";" + vorletzter + "]";
}

public static void main(String[] args) {
// Teil 1: Integer Verwalten
Merker7<Integer> f1 = new Merker7<Integer>();
Integer i10 = 10; // Erzeugung mit Autoboxing
f1.merke(i10);
Integer i11 = new Integer(11); // Reguläre Erzeugung
f1.merke(i11);
Integer i12 = new Integer(12); // Reguläre Erzeugung
f1.merke(i12);
System.out.println(f1);

// Teil 2: Float verwalten
Merker7<Float> f2 = new Merker7<Float>();
Float i20 = 100.1f;
f2.merke(i20);
Float i21 = 101.2f;
f2.merke(i21);
Float i22 = 102.3f;
f2.merke(i22);
System.out.println(f2);

// Teil 3: Alles Verwalten
Merker7<Number> f3 = new Merker7<Number>();
Integer i30 = 200;
f3.merke(i30);
Float i31 = 201.f;
f3.merke(i31);
String i32 = "Zeichenkette";
//f3.merke(i32); // Warum ist dies nicht erlaubt?
System.out.println(f3);

// Teil 4: Alles Verwalten
Merker7 f4 = new Merker7();
Integer i40 = 400;
f4.merke(i40);
Float i41 = 401.f;
f4.merke(i41);
String i42 = "Zeichenkette";
//f4.merke(i42); // Warum ist dies nicht erlaubt?
System.out.println(f4);
f2 = f4;

Merker7<?> merkeAlles;

merkeAlles = f1;

// Hier wird implizit die Methode toString() aufgerufen
System.out.println(merkeAlles);
int m;
// Warum ist diese Zuweisung nicht erlaubt?
//m = merkeAlles.letzter;
// Warum ist diese Zuweisung erlaubt?
m = 3 * f1.letzter;

System.out.println(merkeAlles.letzter);
merkeAlles = f2;
System.out.println(merkeAlles);
merkeAlles = f3;
System.out.println(merkeAlles);
}
}

Lernziele (Generics)

Am Ende dieses Blocks können Sie:

  • ... die Syntax zur Deklaration generischer Typen anwenden.
  • ... die erhöhte Typsicherheit die durch die generischen Javatypen eingeführt werden erklären.
  • ... erklären warum generische Datentypen eine Form des Polymorphismus snd.
  • ... den Zusammenhang zwischen Subtypen (Vererbung), Autoboxing und generischen Typen erklären und bei Zuweisungen die Typsicherheit erkennen.
  • ... können die verschiedenen Kombinationen von generischen Typen und Spezialisierung (bei der Vererbung) anwenden.
  • ... mit "bound wildcards" und "unbound wildcards" umgehen und nicht Probleme mit Typkonversionen bei Zuweisungen erkennen und lösen.

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: Übungsfragen zu generischen Typen

Java Collections Framework

Der Begriff "Collection" wird in Java für alle Klassen verwendet, mit denen man Objekte zusammenfassen und verwalten kann. Die Java-Collections sind Großbehälter die genau gesehen, Referenzen auf Objekte verwalten.

Felder (Arrays) sind ein Teil der Javasprache und erlauben die extrem effiziente Verwaltung von statischen Daten über ihren Feldindex. Felder erlauben nur die Verwaltung von Daten eines genauen Typs mit einer vorher festgelegten Größe. Das Java Collections Framework hat nicht diese Einschränkungen und erlaubt die Verwaltung von Objekten vieler Arten. Das Java Collections Framework ist daher sehr interessant, wenn man vorab nicht weiß wieviele Objekte man verwalten, oder die Objekte nach ganz bestimmten Kriterien verwaltet werden müssen.

Die Klassen im Java Collection Framework implementieren alle gängigen abstrakten Datenstrukturen wie sie in der Vorlesung vorgestellt wurden. 

Die Klassen des Java Collection Framework's sind im Paket java.util zu finden. Sie bestehen aus den drei Gruppen:

  • Algorithmen: effiziente Algorithmen zur Verwaltung von Objekten
  • Implementierungen: Klassenhierarchie mit abstrakten Klassen zur Implementierung eigener Klassen und konkrete Implementierungen zur direkten Benutzung
  • Schnittstellen (Interfaces): Hierarchie von Schnittstellen zur einheitlichen Behandlung von Objekten über die Collectionklassen hinweg. Die Interfaces des Collection Frameworks werden hier wie abstrakte Datentypen verwendet.

Generizität

Alle Klassen des Java Collection Framework sind seit JDK 5.0 generische Klassen. Hierdurch können die Klassen sehr viel allgemeiner verwendet werden. Es gibt weniger Situationen in denen man die Klassen selbst spezialisieren muss um Typsicherheit zu gewährleisten.

Die Benutzung generischer Typen hat die folgenden Vorteile:

  • Rückgabewerte werden typsicher
  • Hierdurch geht die Typinformation der verwalteten Objekte nicht verloren
  • Es werden keine expliziten Typkonversionen (Casts) mehr benöigt um beim Auslesen von Objekten diese zuzuweisen.

 

Überblick Java Collections

Die Klassen des Java Collection Framework in java.util stellen vier Familien von abstrakten Datentypen und Funktionen zur Verfügung:

  • Listen (abstrakte Klasse List): geordnete Datenstrukturen auf die man wie auf Felder mit einem numerischen Index zugreifen kann. Im Unterschied zu Feldern (Array) erlauben sie das listentypische Einfügen an einer beliebigen Stelle
  • Mengen (abstrakte Klasse Set): Eine Implementierung mathematischer Mengen. Objekte können nur einmal in einer Menge vorkommen. Man kann prüfen ob bestimmte Objekte in einer Menge enthalten sind. Eine Reihenfolge oder Ordnung in der Menge ist nicht relevant.
  • Verzeichnisse (abstrakte Klasse Map): Verzeichnisse können als verallgemeinerte Felder angesehen werden. Felder erlauben einen direkten zugriff mit einem numerischen Index. Verzeichnisse erlauben die Wahl einer beliebigen Zugriffskriteriums. Man kann zum Beispiel Personen nach ihrem Nachnamen verwalten und eine Person mit Hilfe eines gegebenen Nachnamens aurufen.
  • Warteschlangen (abstrakte Klasse Queue): Warteschlangen sind Listen die nach dem FIFO Prinzip (First In , First Out) aufgebaut sind. Sie verfügen über keinen wahlfreien Zugriff.

Hinzu kommt die Hilfsklasse Arrays zum Verwalten von Feldern. Die Klasse fasst viele nützliche statische Methoden zum Suchen und Sortieren von Feldern zusammen. Die Klasse Arrays gehört zu diesem Paket, da Felder logisch zu den abstrakten Datentypen gehören. Da Felder wegen der benötigten Effizienz  ein Hybrid zwischen Basistypen und komplexen Typen sind, passen Sie nicht sauber in die hier vorgestellte Vererbunghierarchie.

Die vier Familien werden durch die folgende Hierarchie von Schnittstellen in Java implementiert:

Jede dieser Familien wird durch eine abstrakte Klasse und eine Schnittstelle implementiert. Die Aufteilung in diese beiden Aspekte findet nach dem folgenden Prinzip statt:

  • Schnittstelle: Sie definiert welche Operationen für eine bestimmte Familie erlaubt sind (funktionale Eigenschaft). Diese funktionalen Eigenschaften sind:
    • sortiert oder unsortiert
    • geordnet oder ungeordnet. Bleibt die Einfügereihenfolge erhalten?
    • sequenziell oder wahlfreier Zugriff
  • abstrakte Basisklasse: Die abstrakte Klasse implementiert die Schnittstelle und bestimmt damit den Algorithmus, der zu einer bestimmten Effizienz führt.

Zur Auswahl der optimalen Klasse kann man sich an der folgenden Tabelle von Klassen und Eigenschaften orientieren:

Klassen des Java Collection Frameworks
Klasse Famlie Zugriff Duplikate erlaubt Ordnung Kommentar
ArrayList Liste(List) wahlfrei über Index Ja geordnet effizientes Lesen
LinkedList Liste(List) wahlfrei über Index Ja geordnet  effizientes Einfügen
Vector Liste(List) wahlfrei über Index Ja geordnet  synchronisiert(!) 
Stack Liste(List) sequentiell (letztes Element) Ja geordnet  LIFO: Last In First Out
HashSet   Menge(Set) ungeordnet; Test auf Existenz Nein ungeordnet schnelles Lesen
TreeSet   Menge(Set) sortiert; Test auf Existenz Nein sortiert   
LinkedList Warteschlange(Queue)  sequentiell, Nachfolger Ja  geordnet  effizientes Einfügen (am Rand) 
PriorityQueue  Warteschlange(Queue)  sequentiell, Nachfolger  Ja  sortiert   
HashMap  Verzeichnis(Map)  wahlfrei über Schlüssel  Keine Schlüsselduplikate, Werteduplikate erlaubt  ungeordnet  effizientes Lesen 
TreeMap  Verzeichnis(Map)  wahlfrei über Schlüssel  Keine Schlüsselduplikate, Werteduplikate erlaubt  sortiert  

 In Bezug auf die Spalte Ordnung:

  • geordnet: Elemente tauchen beim Iterieren in der gleichen Reihenfolge auf
  • sortiert: Elemente tauchen beim Iterieren entsprechend ihre Kardinalität auf.

Iteratoren

Neben dem spezifischen Zugriff auf diese abstrakten Datentypen bietet das  Java Collection Framework Iteratoren die es Erlauben, nacheinander alle Objekte einer Collection auszulesen. Hierzu dient die Schnittstelle Iterator. Iteratoren werden hier im Detail im Skript vorgestellt.

Beispiel

Im folgenden Beispiel werden Studenten in einer verzeigerten Liste verwaltet. Die Implementierung ist eine nicht generische Verwendung.

Klasse LinkedListDemo Klasse Student
package Kurs2.Collection;
import java.util.LinkedList;
import java.util.List;
public class LinkedListDemo {
   public static void main(String[] args) {
      List ll = new LinkedList();
     Student s;
     s = new Student("Müller", 17);
     ll.add(s);
     s = new Student("Meyer", 18);
     ll.add(s);
     s = new Student("Schneider", 19);
     ll.add(s);
     s = new Student("Schmidt", 20);
     ll.add(s);
   
     System.out.println("Element auf Index 1:" + ll.get(1));
     System.out.println("Liste:" + ll);
    }
}
package Kurs2.Collection;
public class Student {
int matrikelnr;
String name;

public Student (String n, int nr) {
name = n;
matrikelnr = nr;
}

public String toString() {
return "("+matrikelnr+","+name+")";
}
}






 

 

Konsolenausgabe:

Element auf Index 1:(18,Meyer)
Liste:[(17,Müller), (18,Meyer), (19,Schneider), (20,Schmidt)]

In diesem Beispiel wurde eine gängige Technik angewendet:

  • Die Variable ll ist vom Schnittstellentyp List.
  • Die erzeugte Instanz ist vom Typ LinkedList. LinkedList ist eine spezielle Implementierung die man bei Bedarf gegen eine andere Implementierung austauschen kann ohne die anschließende Verwendung der Variable ll zu modifizieren. Dies würde hier durch die Auswahl der Klasse ArrayList geschehen. Man muss hierzu in der Methode main() nur eine Zeile ändern um das Laufzeitverhalten zu ändern:
List ll = new ArrayList();

 

Schnittstellen (Interfaces) des Collection Framework

Ziel des Java Collections Frameworks ist es alle wichtigen abstrakten Datentypen als Schnittstellen für den Entwickler anzubieten.

Der Entwickler soll möglichst nur gegen die Methoden der Schnittstelle implementieren. Dies erlaubt es bei der Objekterzeugung eine optimale Implementierung des entsprechenden Datentyps zu wählen. Aus diesem Grund werden in der Vorlesung die zu benutzenden Schnittstellen immer gemeinsam mit den unterschiedlichen Implementierungen beschrieben. Anbei die Hierachie der Schnittstellen, die aus der gemeinsamen Schnittstelle java.util.Collection abgeleitet werden:

Maps sind sehr komplexe Klassen. Sie werden Aufgrund ihres sehr speziellen Zugriffs mit freigewählten Schlüsseln nicht aus der Klasse java.util.Collection spezialisiert.

Schnittstellen und Implementierungen

Entwickler sollten wann immer möglich gegen die Schnittstellen (z.Bsp. Interface List) implementieren und nicht gegen die Implementierungen ( z.Bsp. Klasse ArrayList, LinkedList). Hierdurch können Sie Implementierung ihres Codes leichter an geänderte Anforderungen anpassen. Sie müssen dann nur bei der Objekterzeugung einen anderen Typ verwenden.

Guter Stil

List meineListe = new ArrayList<String>();
// Später im Code:
meineListe.add(0,"DHBW");
meineListe.add(0,"Mannheim");
List neueReferenz = meineListe;

Häufiges Einfügen am Anfang der Liste ist bei einer ArrayList sehr aufwendig. Für den Wechsel der Implementierung muß nur die Erzeugung des Objekts in der ersten Zeile geändert werden:

List meineListe = new LinkedList<String>();

Schlechter Stil

ArrayList meineListe = new ArrayList<String>();
// Später im Code:
meineListe.add(0,"DHBW");
meineListe.add(0,"Mannheim");
ArrayList neueReferenz = meineListe;

Wählen Sie als Datentyp List für meineListe, können Sie in der Regel nicht absehen wie diese Referenz später verwendet wird. Es können die folgenden Probleme beim Wechsel der Implementierung auftreten:

  1. Zuweisungen auf andere Variablen haben Typprobleme (siehe Beispiel)
  2. Es wurden eventuell Methoden verwendet die es nur in der Implementierung der Klasse ArrayList gab und nicht in der Klasse LinkedList.

Listen (Collections)

Das Java Collections Framework bietet eine Schnittstelle List<E> mit der man uniform auf verschiedene Implementierungen von Listen zugreifen kann.

Listen sind geordnete Folgen die man in der Informatik als Sequenz (engl. sequence) bezeichnet.

Die Schnittstelle List<E> definiert die Methoden mit denen man auf Listen zugreifen kann:

Die Schnittstelle List<E> besitzt noch mehr Methoden als die hier aufgeführten. Sie sind in der API Dokumentation zu finden.

Das Java Collections Framework bietet zwei empfohlene Implementierungen und die Klasse Vector für diese Schnittstelle an, die nach Bedarf gewählt werden können:

Klasse ArrayList

Fie Klasse ArrayList ist im Normalfall die bessere Wahl. Sie verhält sich wie ein Feld (Array). Im Unterschied zu einem Feld kann man die Größe jedoch dynamisch verändern. Die Operationen zum dynamischen Ändern der Größe sind nur in der Klasse ArrayList zu finden. Benutzt man die Schnittstelle List<E> hat man keinen Zugriff auf die zusätzlichen Methoden der Klasse ArrayList .

Die Klasse ArrayList verhält sie weitgehend wie die Klasse Vector. Die Klasse ArrayList ist jedoch nicht synchronisiert.

Eigenschaften:

  • Methoden mit einem konstanten Aufwand bei der Ausführung: size(), isEmpty(), get(), set(), iterator(), listIterator()
  • Die Methode add() wird im Durchschnitt mit einer konstanten Zeit ausgeführt
  • Alle anderen Methoden haben einen linearen Aufwand mit einem niedrigen konstanten Faktor im Vergleich zur Klasse LinkedList.

Klasse Vector

Die Klasse Vector verhält sich wie die Klasse ArrayList . Die Klasse Vector ist historisch älter als das Java Collections Framework. Sie wurde in JDK 1.2 eingeführt und dann später überarbeitet um mit den Schnittstellen des Java Collections Framework konform zu sein. Die Klasse ist daher aus Kompatibilitätsgründen synchronisiert. Hierdurch ist sie zum einen sicher bei der Verwendung bei Anwendungen mit mehreren Threads. Sie ist hierdurch aber auch deutlich langsamer. Die Klasse Vector sollte in neuen Implementierungen zugunsten der Klasse ArrayList vermieden werden.

Klasse LinkedList

Die Klasse LinkedList ist eine Implementierung einer doppelt verzeigerten Liste.

Sie ist sehr gut geeignet für Anwendungen in denen sehr oft Elemente eingefügt oder entnommen werden müssen. Beim indexierten, wahlfreien Zugriff auf ein Objekt muss jedoch die Liste traversiert werden.

Die Klasse LinkedList ist wie die anderen Klassen des Java Collections Frameworks nicht synchronisiert.

Mengen (Collection)

Das Java Collections Framework stellt mit der Schnittstelle Set<E> einen Behälter zur Verfügung der keine Elemente mehrfach enthält. Die Schnittstelle Set<E> stellt die folgenden Methoden für die unterschiedlichen Implementierungen zur Verfügung:

public interface Set<E> extends Collection<E> {
// Grundlegende Operationen
int size();
boolean isEmpty();
boolean contains(Object element);
boolean add(E element);
boolean remove(Object element);
Iterator<E> iterator(); // Massenoperationen boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean removeAll(Collection<?> c); boolean retainAll(Collection<?> c); void clear(); // Feld Operationen Object[] toArray(); <T> T[] toArray(T[] a); }

Neben der Schnittstelle Set<E> existiert auch eine Schnittstelle SortedSet<E> in der die Mengenelemente nach ihrer natürlichen oder einer definierten Ordnung sortiert sind.

public interface SortedSet<E> extends Set<E> {
// Range-view
SortedSet<E> subSet(E fromElement, E toElement);
SortedSet<E> headSet(E toElement);
SortedSet<E> tailSet(E fromElement); // Endpoints E first(); E last(); // Comparator access Comparator<? super E> comparator(); } 

Als konkrete Implementierungen können die folgenden Klassen benutzt werden:

Klasse TreeSet

Die Klasse TreeSet verwaltet alle Elemente in ihrer natürlichen Ordnung oder in der Ordnung die mit der Schnittstelle Comparator definiert wurde.

Die Kosten für die Zugriffsmethoden add(), remove(), contains() liegen bei O(log(n)).

Die Klasse TreeSet ist nicht synchronisiert.

Anwendungsfälle:

  • Man muß über die Menge mit einem bestimmten Schlüsselkriterium iterieren können
  • Die erhöhten Kosten für Einfügen und Entfernen von Objekten sind nicht relevant.

Klasse EnumSet

Die Klasse EnumSet ist eine Spezialimplementierung für Aufzählungen (enum). Der Aufzählungstyp enum ist extrem effizient mit Hilfe von Bitvektoren implementiert. Die Klasse EnumSet unterstützt extrem effiziente Mengenoperationen auf einem gegegebenen Aufzählungstyp.

Die Iteratoren aller anderen Collections sind "fail safe". Das heißt sie werfen eine  ConcurrentModificationException Ausnahme falls die Collection an mehreren Stellen gleichzeitig modifiziert wird. Dies ist nicht der Fall bei den Iteratoren der Klasse EnumSet. Ihre Iteratoren sind nur "weakly consistent" (siehe Dokumentation im API).

Klasse HashSet

Die Klasse HashSet verwendet intern eine Implementierung einer HashMap. Die Klasse HashSet hat keine Ordnung der Elemente. Das bedeutet, dass beim Iterieren über die Menge die Elemente der Menge nicht in einer sortierten Reihenfolge präsentiert werden. Es gibt nicht einmal eine Garantie, dass bei mehrfachem Iterieren über die Menge die Reihenfolge konstant ist.

Der Zugriff der Methoden add(), remove(), contains(), size() haben einen konstanten Aufwand. Diese Operationen sind bei großen Mengen schneller als die Methoden der Klasse TreeSet.

Anwendungsfälle:

  • Einfüge-, Entfernungs- und Leseoperationen sollen sehr effizient sein
  • Die Ordnung der Menge nach einem Schlüsselkriterium spielt keine Rolle

Verzeichnisse-Maps (Collections)

Die Schnittstelle Map im Java Collections Framework erlaubt die Benutzung von Verzeichnissen (engl. Dictionary). Verzeichnisse sind Datenstrukturen in denen man Objekte organisiert nach einem beliebigen Schlüssel verwalten kann. Maps sind Modelle mathematischer Funktionen die einen gegebenen Wert einer Schlüsselmenge auf eine Zielmenge abbilden.

Maps funktionieren wie ein Wörterbuch. Man kann man sie als Tabellen mit zwei Spalten verstehen. In der ersten Spalte steht der Zugriffsschlüssel mit dem man nach einem Objekt sucht, in der zweiten Spalte steht das Objekt welches zu einem gegebenen Schlüssel gehört. Hierdurch werden zwei Objekte in eine Beziehung gesetzt.

Anwendungsbeispiel

In einer Java-Map kann man zum Beispiel Kraftfahrzeuge und deren Halter verwalten. Da ein Kraftfahrzeug genau einen Halter hat, verwendet man das Kraftfahrzeug als Schlüsselobjekt. Die Person die das Fahrzeug besitzt wird als Wert verwaltet. Fügt man dieses Tupel von Schlüsselobjekt und Zielobjekt in die Datenstruktur ein, kann man zu jedem gegebenen Fahrzeug den Halter ausgegeben.

Beispiel einer Java Map

In Java-Maps müssen die Schlüssel eindeutig sein. Jedes Kfz kann daher nur einmal vorkommen. Die verwalteten Werte müssen nicht eindeutig sein. Im Beispiel oben besitzt die Person Müller zwei Kfz.

Abgrenzung zu Objektattributen

Die oben aufgeführten Beispiel benutzte Beziehung zwischen einem Kraftfahrzeug und einem Halter kann man natürlich ebenso gut direkt mit Hilfe von Referenzen in den zwei Klassen modellieren.

Der Vorteil einer Java Map besteht darin, dass man diese Beziehung dynamisch in einem Javaobjekt modelliert und nicht permanent als Objekteigenschaft. Das Verwalten dieser Beziehung in einem Objekt hat die folgenden Vorteile:

  • Man kann die Beziehung modellieren ohne die Klassen zu modifizieren
  • Man kann jederzeit neue Beziehungen modellieren (z.Bsp Kraftfahrzeug<->Fahrer, Kraftfahrzeug<->Betreuer etc.)
  • Man kann zusätzliche Operationen des Java Collection Frameworks ausnutzen um die Daten zu bearbeiten. Man kann z.Bsp.
    • über alle Schlüssel iterieren (was ist die Menge der Kraftfahrzeuge?)
    • über alle Objekte iterieren (was ist die Menge der Halter? Es kann mehrere Krfatfahrzeuge für einen gegebenen Halter existieren!)

Klassenstruktur

Die Map-Klassen des Java Collections Framework werden nicht wie die anderen Klassen des Frameworks aus der Klasse Collection abgeleitet. Man kann jedoch mit Hilfe der Methoden keySet() und values() über die die Schlüssel oder über die eigentlichen Objekte iterieren.

 

Klasse HashMap

HashMap<K,V> benutzt wie die Klasse HashSet einen Hash-Code für die Schlüssel mit deren Hilfe die Objekte verwaltet werden. Dadurch hat sie auch die gleichen Leistungseigenschafen wie die Klasse HashSet (in Bezug auf die Schlüssel).

Die Klasse HashMap erlaubt es beliebige Klassen als Schlüssel zu verwenden. Die einzige Bedingung ist, dass die Methoden equals() und hashCode() für diese Klassen implementiert sind (triviale Implementierungen existieren bereits in der Klasse Object).

Klasse TreeMap

Die Klasse TreeMap implementiert die Schnittstelle SortedMap. Hierdurch wird sicher gestellt, das Objekte beim Einfügen direkt sortiert eingefügt werden. Die Schlüssel sind also bereits sortiert und erlauben eine effiziente Iteration in der Sortierreihenfolge.

Durch die sortierten Schlüssel ist es zum Beispiel möglich mit Hilfe der Schnittstellenmethoden von SortedMap einen Teilbereich auszuwählen. Ein Verzeichnis welches Zeichketten (Strings) als Schlüssel verwendet kann man z.Bsp. auf die folgende Weise einen Bereich erzeugen der alle Zeichenketten zwischen den Stringvariablen low und high enthält:

SortedMap<String, V> sub = m.subMap(low, high+"\0");

Weitere Klassen

Die Klasse EnumMap erlaubt das Verwalten von Aufzählungstypen (wie auch die Klasse EnumSet).

Die Klasse WeakHashMap erlaubt die Verwaltung schwacher Referenzen. Schwach referenzierte Objekte können in Java bei Bedarf vom Garbage Collector gelöscht werden. Sie benötigen daher eine gewisse Sonderbehandlung.

Beide Klassen werden im Rahmen der Vorlesung nicht weiter behandelt.

Ausgewählte Methoden für Maps

Die Schnittstelle Map hat sehr viele Methoden, die man am besten in der Online API Dokumentation nachliest. Die wichtigsten Methoden werden hier kurz vorgestellt:

Konstruktor

Die Konstruktoren sind nicht Teil der Schnittstelle. Sie hängen von den individuellen Ausprägungen ab. Anbei zwei Beispiele zum Erzeugen von Map-objekten wie sie im Beispiel oben gezeigt wurden:

import java.util.HashMap;
import java.util.TreeMap; import Person; import Kfz; Map<Kfz,Person> halterMapsortiert = new TreeMap<Kfz,Person>();
Map<Kfz,Person> halterMapunsortiert = new HashMap<Kfz,Person>();

Hinzufügen: put(K key, V value)

Tupel von Schlüsseln und Werten können mit der put(K key,V value) Methode hinzugefügt werden. Zwei Tupel für das obige Beispiel werden mit der folgenden Syntax eingefügt:

Person p = new Person("Müller");
Kfz wagen1 = new Kfz("D-U-3",3456789);
Kfz wagen2 = new Kfz("M-V-4",4567890);
halterMapsortiert.put(wagen1,p);
halterMapsortiert.put(wagen2,p);

Methode get(Object key): Auslesen eines Wertes zu einem gegebenen Schlüssel

Die "klassische" Verzeichnisoperation ist das Auslesen eines Wertes zu einem gegebenen Schlüssel. Hierzu dient die get(Object key) Methode. Im obigen Beispiel kann man die Person "Müller" finden wenn man eines der Fahrzeuge dieser Person kennt. Die geschieht mit der folgenden Syntax:

Person p = halterMapsortiert.get(wagen2);

Methode keySet(): Auslesen alle Schlüsselelemente

Java-Maps erlauben auch das Auslesen aller Schlüssel mit der Methode keySet().

Man kann sich im obigen Beispiel die Menge (Javatyp Set) aller Kfz-objekte dem folgenden Kommando auslesen:

Set<Kfz> KfzSet = halterMapsortiert.keySet();

Die neu erzeugte Menge zeigt auf die gleichen Schlüsselobjekte wie die Map:

Zum Auslesen der Mengenelemente muss man dann einen Mengeniterator anwenden.

Menge von Kfz

Methode values(): Auslesen aller Werte

Die Werte der Map werden mit der Methode values() ausgelesen. Im Beispiel von oben würde man die Werte der Map mit dem folgenden Befehl auslesen:

Person p;
Collection<Person> personColl = halterMapsortiert.values();
Iterator<Person> iterPerson = personColl.iterator();
while (iterPerson.hasNext()) {
p = iterPerson.next();
System.out.println("Person: " + p);
}

Die zurückgebene Datenstruktur hat den Typ Collection. Der genaue Typ der Collection hängt von der gewählten Mapimplementierung ab.

values einer Collection

Weiterführende Quellen

Warteschlangen (Collections)

 Das Java Collections Framework besitzt Klassen zur Implementierung von Warteschlangen (engl. Queue).

Die Warteschlangen implementieren das FIFO Prinzip (First In, First Out). Warteschlangen werden oft zur Kommunikation zwischen Threads verwendet. Ein weitere typischer Anwendungsfall ist das Verwalten von Alarmnachrichten.

Da Alarmnachrichten eventuell mit unterschiedlichen Prioritäten verwaltet werden müssen erlauben es die Klassen des Java Collections Frameworks Objekten unterschiedliche Prioritäten zuzuweisen. Innerhalb einer Priorität gilt dann wieder das FIFO Prinzip. 

Einfache Warteschlangen

Einfache Warteschlangen implementieren die Schnittstelle Queue:

public interface Queue<E> extends Collection<E> {
E element();
boolean offer(E e);
E peek();
E poll();
E remove();
}
  • offer(): Erlaubt das Einfügen in eine Warteschlange. Sie gibt den Wert false zurück wenn die Warteschlange ihre maximale Kapazität erreicht hat
  • peek(): Liefert das nächste Element es wird jedoch nicht entfernt 
  • element(): Identisch zur Methode peek()
  • poll(): Entfernt das nächste Element der Warteschlange und gibt es zurück
  • remove(): Identisch zur Methode poll(). wirft jedoch eine Ausnahme NoSuchElementException falls die Warteschlange leer ist.

Hinweis: Die Klasse LinkedList implementiert die Queue Schnittstelle. Sie ist somit in der Lage sich wie eine Warteschlange zu verhalten.

Priorisierte Warteschlangen: Klasse PriorityQueue

Die Klasse PriorityQueue benutzt zur  Priorisierung von Objekten deren Sortierkriterium die entweder durch die Schnittstelle Comparable<E> oder durch ein spezielles Vergleichsobjekt der Klasse Comparator<E> implementiert wird.

Blockierende Warteschlangen: Schnittstelle BlockingQueue<E>

Es gibt Fälle in denen der Produzent und der Konsument unabhängig voneinander arbeiten und der Produzent nicht in der Lage ist, die Warteschlange immer mit Objekten gefüllt zu halten. Das Gegenteil ist genauso möglich. Ein Konsument ist nicht in der Lage die Warteschlange schnell genug auszulesen. In den beiden Fällen einer leeren, sowie er vollen Warteschlange erlaubt die Schnittstelle BlockingQueue dem Produzenten oder dem Konsumenten eine bestimmte Zeit zu warten. Die Schnittstelle fordert die Implementierung der Einfüge-, Lese- und Entfernmethoden mit Angabe eines Zeitintervalls für das der Thread warten sollen:

Die Klasse LinkedBlockingQueue implementiert diese Schnittstelle.

Priorisierte und blockierende Warteschlangen: Klasse PriorityBlockingQueue

Die Klasse PriorityBlockingQueue vereint die beiden Eigenschaften einer priorisierten und blockierenden Warteschlange.

Weitere Informationen

Iteratoren

Iteratoren sind ein universelles Konzept des Java Collections Framework um alle Objekte einer Collection auszulesen. Sie erlauben es Objekte genau einmal aus einer Collection auszulesen und festzustellen ob es noch weitere Elemente gibt.

Iterator ist eine Schnittstelle die drei Methoden für alle Collections anbietet:

  • hasNext(): Die Collection hat noch weitere Objekte die ausgelesen werden können
  • next(): Liefert das nächste Objekt der Collection
  • remove(): entfernt das letzte Objekt welches aus einer Collection gelesen wurde.

Jedes Collectionobjekt ist in der Lage ein Iteratorobjekt  mit Hilfe der iterator() Methode anzubieten. Dies kann wie im folgenden Bespiel geschen:

List<Student> ll = new LinkedList<Student>();
... Iterator<Student> iter = ll.iterator();

Beispiel

Der Iterator iter erlaubt über die generische Liste mit Studenten zu iterieren und alle Objekte der Liste auszulesen:

package Kurs2.Collection;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class LinkedListIterDemo {

public static void main(String[] args) {
List<Student> ll = new LinkedList<Student>();
Student s;
s = new Student("Müller", 17);
ll.add(s);
s = new Student("Meyer", 18);
ll.add(s);
s = new Student("Schneider", 19);
ll.add(s);
s = new Student("Schmidt", 20);
ll.add(s);

Iterator<Student> iter = ll.iterator();
System.out.println("Inhalt der Liste:");
while (iter.hasNext()){ // Prüfen ob noch Elemente vorhanden sind
s = iter.next(); // Holen des nächsten Listenelements
System.out.println("Student: " +s);
}
}
}

Iterieren mit der erweiterten for-Schleife

Seit JDK 5 kann man auch mit der Syntax der erweiterten for-Schleife über alle Objekte einer Collection iterien.

Der im vorherigen Beispiel vorgestellte Iterator mit der while-Schleife kann sehr elegant mit der erweiterten for-Schleife implementiert werden:

        System.out.println("Inhalt der Liste:");
for (Student st : ll)
System.out.println("Student: " +st);

Der Iterator ist hier in die for-Schleife integriert.

Vorsicht: Stabilität von Iteratoren und Collections

Beim Bearbeiten einer Collection mit einem Iterator darf die Collection selbst nicht verändert werden. Operationen mit dem aktuellen Operator sind jedoch als Ausnahme erlaubt. Wird eine Collection beim iterieren verändert so wird eine ConcurrentModifikationException geworfen.

 

Übungen (Collections)

Fragen

Was sind günstige, bzw. ungünstige Anwendungsszenarien für die folgenden Klassen des Java Collection Frameworks?

  • LinkedList
  • ArrayList
  • TreeSet
  • HashSet

Klasse Student

Die Übungen dieser Seite verwenden die Klasse Student. Studenten haben eine Matrikelnummer, einen Vornamen, einen Nachnamen und eine aktuelles Semester. Für die Klasse wurden alle Methoden zum Vergleichen überladen. Zwei Studenten sind identisch wenn sie die gleiche Matrikelnummer besitzen.

package Kurs2.Collection;

public class Student implements Comparable {

int matrikelnr;
String name;
String vorname;
int semester;

public Student(String n, String vn, int nr, int sem) {
name = n;
vorname = vn;
matrikelnr = nr;
semester = sem;
}

public String toString() {
return "(" + matrikelnr + "," + name + " " + vorname
+ ", " + semester + ")";
}

/**
* Studenten werden anhand Ihre Matrikelnr verglichen
* @param o
* @return
*/
public int compareTo(Object o) {
int diff = 0;
if (o.getClass() == getClass()) {
diff = matrikelnr - ((Student) o).matrikelnr;
}
if (diff > 0) {
diff = 1;
}
if (diff < 0) {
diff = -1;
}
return diff;
}

public boolean equals(Object o) {
return (compareTo(o) == 0);
}
public int hashCode() {
return matrikelnr;
}
}

Mengen von Studenten

Das Programm soll eine Menge von Studenten verwalten. Vervollständigen Sie das das folgende Programm:

  • main() Methode
    • Setzen Sie die Objekterzeugung für die Variablen ll und ls ein
    • Fügen Sie alle Studenten in beide Mengen ein indem Sie die Platzhalter-Konsolenausgaben ersetzen
  • ausgaben() Methode: Iterator über eine Menge implementieren
    • Deklarieren und initialisieren Sie ein Iteratorobjekt für das Listenobjekt welches als Parameter übergeben wurde
    • Implementieren Sie korrekte Abbruchbedingung für die while-Schleife
    • Implementieren die while-Schleife. Weisen Sie der Variablen s einen jeweils neuen Student der Menge zu

Beobachtungen:

  • Welche der beiden Mengen ist sortiert?
  • Welche Mengenimplementierungen nutzen Sie für welche Aufgabenstellung?
  • Was geschieht mit Caroline Herschel?
package Kurs2.Collection;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

public class SetIterDemo {

public static void main(String[] args) {
Set<Student> mengeUnsortiert = null; // Hier HashSetobjekt einfügen
Set<Student> mengeSortiert = null; // Hier TreeSetobjekt einfügen
Student s;
s = new Student("Curie", "Marie", 19, 1);
System.out.println("Hier Student " + s + "in beide Mengen einfügen");
s = new Student("Merian", "Maria-Sybilla", 17, 3);
System.out.println("Hier Student " + s + "in beide Mengen einfügen");
s = new Student("Noether", "Emmi", 16, 1);
System.out.println("Hier Student " + s + "in beide Mengen einfügen");
s = new Student("Liese", "Meitner", 15, 2);
System.out.println("Hier Student " + s + "in beide Mengen einfügen");
s = new Student("Herschel", "Caroline", 15, 2);
System.out.println("Hier Student " + s + "in beide Mengen einfügen");
ausgaben(mengeUnsortiert);
ausgaben(mengeSortiert);
}

public static void ausgaben(Set<Student> menge) {
Student s;
System.out.println("Hier Iterator deklarieren");
System.out.println("Inhalt der Menge ("
+ menge.getClass() + "):"); // Nicht ersetzen
while (true) { // Abbruchbedingung ersetzen. Die Schleife terminiert so nicht!
System.out.println("Hier Student aus Iterator in Schleife auslesen");
s = null;
System.out.println("Student: " + s); // Nicht ersetzen
}
}

}

Maps von Studenten

Das folgende Programm soll Studenten mit Hilfe zweier Maps verwalten. Studenten sollen nach Matrikelnummer und nach Nachnamen verwaltet werden.  Vervollständigen Sie das das folgende Programm:
  • main() Methode
    • Setzen die die Objekterzeugung für die Variablen matrikelMap und nachName ein (In den beiden Kommentarzeilen)
      • matrikelMap soll als Schlüssel die Matrikelnummer des Studenten verwenden, der Inhalt soll aus Studenten bestehen
      • nachnameMap soll als Schlüssel den Nachnamen des Studenten verwenden, der Inhalt soll aus Studenten bestehen
      • Welche Typen müssen für die Platzhalter xxx? bei der Variablen und dem Map-Parameter eingesetzt werden?
    • Fügen Sie alle Studenten in beide Mengen ein indem Sie die Platzhalter-Konsolenausgaben ersetzen
  • ausgabenMatrikelnr() Methode: 
    • Iterieren Sie über die Matrikelnummern. Die Matrikelnummern sind die Schlüssel der Map mp
      • Legen Sie einen Iterator an;
      • Definieren Sie die Abbruchbedingung für die while Schleife
      • Lesen Sie die nächste Matrikelnummer in der while Schleife aus (Variable s)
    • Finden Sie die Studenten zur Matrikelnummern 15 und 16.
    • Iterieren Sie über die Studenten. Die Studenten sind die eigentlichen Objekte der Map mp
      • Legen Sie einen Iterator an;
      • Definieren Sie die Abbruchbedingung für die while Schleife
      • Lesen Sie die nächste Matrikelnummer in der while Schleife aus (Variable st)
  • ausgabenNamen() Methode: 
    • Iterieren Sie über die Nachnamen. Die Nachnamen sind die Schlüssel der Map mp
      • Legen Sie einen Iterator an;
      • Definieren Sie die Abbruchbedingung für die while Schleife
      • Lesen Sie den nächsten Nachnamen in der while Schleife aus (Variable str)
    • Finden Sie die Studenten mit den Namen Herschel und Merian.
    • Iterieren Sie über die Studenten. Die Studenten sind die eigentlichen Objekte der Map mp
      • Legen Sie einen Iterator an;
      • Definieren Sie die Abbruchbedingung für die while Schleife
      • Lesen Sie die nächste Matrikelnummer in der while Schleife aus (Variable st)
Beobachtungen:
  • Welche der beiden Maps ist sortiert?
  • Sind die Schlüssel sortiert?
package Kurs2.Collection;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class MapIterDemo {

public static void main(String[] args) {
Map<xxx?,Student> matrikelMap = new TreeMap<xxx?,Student>(); // Ersetzen Sie xxx?
Map<xxx?, Student> nachnameMap = new HashMap<xxx?,Student>();// Ersetzen Sie xxx?
Student s;
s = new Student("Curie", "Marie", 19, 1);
System.out.println("Einsetzen: Studenten "+ s + " in die beiden Maps eintragen. Schlüssel beachten!");
s = new Student("Merian", "Maria-Sybilla", 17, 3);
System.out.println("Einsetzen: Studenten "+ s + " in die beiden Maps eintragen. Schlüssel beachten!");
s = new Student("Noether", "Emmi", 16, 1);
System.out.println("Einsetzen: Studenten "+ s + " in die beiden Maps eintragen. Schlüssel beachten!");
s = new Student("Meitner", "Lise", 15, 2);
System.out.println("Einsetzen: Studenten "+ s + " in die beiden Maps eintragen. Schlüssel beachten!");
s = new Student("Herschel", "Caroline", 20, 2);
System.out.println("Einsetzen: Studenten "+ s + " in die beiden Maps eintragen. Schlüssel beachten!");
ausgabenMatrikelnr(matrikelMap);
ausgabenNamen(nachnameMap);
}

public static void ausgabenMatrikelnr(Map<Integer,Student> mp) {
int s;
Student st;
System.out.println("Einsetzen: Vorbereitungen zum Auslesen der Matrikelnr");

Iterator<Integer> iterMatrikel = null; // Einsetzen: Zuweisen des Iterators
System.out.println("Name ("
+ mp.getClass() + "):");
while (null) { // Einsetzen: Iteratorbedingung einfügen
s = null // Einsetzen: Auslesen des Iterators
System.out.println("Matrikelnummer: " + s);
}
int mnr = 15;
System.out.println("Student mit Matrikelnummer " + mnr +
" ist:" + null); // Einsetzen: Student mit Matrikelnr mnr
mnr = 16;
System.out.println("Student mit Matrikelnummer " + mnr +
" ist:" + null ); // Einsetzen: Student mit Matrikelnr mnr
System.out.println("Alle Werte der MatrikelMap:");
Collection<Student> l = null; // Einsetzen: Collection mit den Studenten
Iterator<Student> iterStudi = l.iterator();
System.out.println("Name ("
+ mp.getClass() + "):");
while (null) { // Einsetzen: Schleifenbedingung des Iterators
// Einsetzen: Auslesen des nächsten Studenten
System.out.println("Student: " + st);
}
}

public static void ausgabenNamen(Map<String,Student> mp) {
String str;
Student st;
System.out.println("Einsetzen: Vorbereitungen zum Auslesen der Nachnamen");
System.out.println("Namen ("
+ mp.getClass() + "):");
while (null) { // Einsetzen: Iteratorbedingung einfügen
str = null// Einsetzen: Auslesen des Iterators
System.out.println("Nachname: " + str);
}
String nme = "Merian";
System.out.println("Student mit Name " + nme +
" ist:" + null); // Einsetzen der Operation zum Auslesen des Student mit Namen nme
nme = "Herschel";
System.out.println("Student mit Name " + nme +
" ist:" + + null); // Einsetzen der Operation zum Auslesen des Student mit Namen nme
System.out.println("Alle Werte der NamenMap:");
Collection<Student> l = null; // Einsetzen: Auslesen der gesamten Collection
Iterator<Student> iterStudi = l.iterator();
System.out.println("Name ("
+ mp.getClass() + "):");
while (null) { // Einsetzen: Iteratorbedingung einfügen
st = null// Einsetzen: Auslesen des Iterators
System.out.println("Student: " + st);
}
}
}

 

 

Lösungen (Collections)

Antworten zu den Fragen

Klasse Günstig Ungünstig
LinkedList
  • Häufiges Einfügen an beliebigen Positionen
  • Häufiges Entfernen an beliebigen Positionen
  • Überwiegend sequentieller Zugriff
  • Einmaliges Füllen mit anschließenden, ausschließlichen Leseoperationen
ArrayList
  • Einmaliges Füllen mit anschließenden ausschließlichen Leseoperationen
  • Zugriffsweise (sequentiell oder wahlfrei) ist nicht synchronisiert und sehr schnell
  • Häufiges Einfügen an beliebigen Positionen
  • Häufiges Entfernen an beliebigen Positionen
TreeSet
  • Randbedingung: Ordnung in der Menge ist wichtig (relevant)
  • konstante und kurze Zugriffszeiten bei sehr großen Mengen
  • Ordnung der Menge ist unwichtig
HashSet
  • Randbedingung: Ordnung in der Menge nicht relevant
  • Extrem schneller Lesezugriff mit konstanten Zugriffszeiten
  • Einfügeoperationen mit konstanter Geschwindigkeit (bei großen Mengen)
  • Ausschluß: Ordnung ist relevant 

Mengen von Studenten

package Kurs2.Collection;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

public class SetIterDemo {

public static void main(String[] args) {
Set<Student> mengeUnsortiert = new HashSet<Student>();
Set<Student> mengeSortiert = new TreeSet<Student>();
Student s;
s = new Student("Curie", "Marie", 19, 1);
mengeUnsortiert.add(s);
mengeSortiert.add(s);
s = new Student("Merian", "Maria-Sybilla", 17, 3);
mengeUnsortiert.add(s);
mengeSortiert.add(s);
s = new Student("Noether", "Emmi", 16, 1);
mengeUnsortiert.add(s);
mengeSortiert.add(s);
s = new Student("Meitner","Liese", 15, 2);
mengeUnsortiert.add(s);
mengeSortiert.add(s);
s = new Student("Herschel", "Caroline", 15, 2);
mengeUnsortiert.add(s);
mengeSortiert.add(s);
ausgaben(mengeUnsortiert);
ausgaben(mengeSortiert);
}

public static void ausgaben(Set<Student> menge) {
Student s;
Iterator<Student> iter = menge.iterator();
System.out.println("Inhalt der Menge ("
+ menge.getClass() + "):");
while (iter.hasNext()) {
s = iter.next();
System.out.println("Student: " + s);
}
}

}

Konsolenausgaben

Inhalt der Menge (class java.util.HashSet):
Student: (17,Merian Maria-Sybilla, 3)
Student: (16,Noether Emmi, 1)
Student: (19,Curie Marie, 1)
Student: (15,Meitner Lise, 2)
Inhalt der Menge (class java.util.TreeSet):
Student: (15,Meitner Lise, 2)
Student: (16,Noether Emmi, 1)
Student: (17,Merian Maria-Sybilla, 3)
Student: (19,Curie Marie, 1)

Maps von Studenten

package Kurs2.Collection;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class MapIterDemo {

public static void main(String[] args) {
Map<Integer,Student> matrikelMap = new TreeMap<Integer,Student>();
Map<String, Student> nachnameMap = new HashMap<String,Student>();
Student s;
s = new Student("Curie", "Marie", 19, 1);
matrikelMap.put(s.matrikelnr,s);
nachnameMap.put(s.name,s);
s = new Student("Merian", "Maria-Sybilla", 17, 3);
matrikelMap.put(s.matrikelnr,s);
nachnameMap.put(s.name,s);
s = new Student("Noether", "Emmi", 16, 1);
matrikelMap.put(s.matrikelnr,s);
nachnameMap.put(s.name,s);
s = new Student("Meitner", "Lise", 15, 2);
matrikelMap.put(s.matrikelnr,s);
nachnameMap.put(s.name,s);

s = new Student("Herschel", "Caroline", 20, 2);
matrikelMap.put(s.matrikelnr,s);
nachnameMap.put(s.name,s);
ausgabenMatrikelnr(matrikelMap);
ausgabenNamen(nachnameMap);
}

public static void ausgabenMatrikelnr(Map<Integer,Student> mp) {
int s;
Student st;
Set<Integer> matrikelSet = mp.keySet();
Iterator<Integer> iterMatrikel = matrikelSet.iterator();
System.out.println("Name ("
+ mp.getClass() + "):");
while (iterMatrikel.hasNext()) {
s = iterMatrikel.next();
System.out.println("Matrikelnummer: " + s);
}
int mnr = 15;
System.out.println("Student mit Matrikelnummer " + mnr +
" ist:" +mp.get(mnr));
mnr = 16;
System.out.println("Student mit Matrikelnummer " + mnr +
" ist:" +mp.get(mnr));
System.out.println("Alle Werte der MatrikelMap:");
Collection<Student> l = mp.values();
Iterator<Student> iterStudi = l.iterator();
System.out.println("Name ("
+ mp.getClass() + "):");
while (iterStudi.hasNext()) {
st = iterStudi.next();
System.out.println("Student: " + st);
}
}

public static void ausgabenNamen(Map<String,Student> mp) {
String str;
Student st;
Set<String> nameSet = mp.keySet();
Iterator<String> iterName = nameSet.iterator();
System.out.println("Namen ("
+ mp.getClass() + "):");
while (iterName.hasNext()) {
str = iterName.next();
System.out.println("Familienname: " + str);
}
String nme = "Merian";
System.out.println("Student mit Name " + nme +
" ist:" +mp.get(nme));
nme = "Herschel";
System.out.println("Student mit Name " + nme +
" ist:" +mp.get(nme));
System.out.println("Alle Werte der NamenMap:");
Collection<Student> l = mp.values();
Iterator<Student> iterStudi = l.iterator();
System.out.println("Student: ("
+ mp.getClass() + "):");
while (iterStudi.hasNext()) {
st = iterStudi.next();
System.out.println("Student: " + st);
}
}
}

Konsolenausgabe

Name (class java.util.TreeMap):
Matrikelnummer: 15
Matrikelnummer: 16
Matrikelnummer: 17
Matrikelnummer: 19
Matrikelnummer: 20
Student mit Matrikelnummer 15 ist:(15,Meitner Lise, 2)
Student mit Matrikelnummer 16 ist:(16,Noether Emmi, 1)
Alle Werte der MatrikelMap:
Name (class java.util.TreeMap):
Student: (15,Meitner Lise, 2)
Student: (16,Noether Emmi, 1)
Student: (17,Merian Maria-Sybilla, 3)
Student: (19,Curie Marie, 1)
Student: (20,Herschel Caroline, 2)
Namen (class java.util.HashMap):
Matrikelnummer: Meitner
Matrikelnummer: Curie
Matrikelnummer: Herschel
Matrikelnummer: Noether
Matrikelnummer: Merian
Student mit Name Merian ist:(17,Merian Maria-Sybilla, 3)
Student mit Name Herschel ist:(20,Herschel Caroline, 2)
Alle Werte der NamenMap:
Name (class java.util.HashMap):
Student: (15,Meitner Lise, 2)
Student: (19,Curie Marie, 1)
Student: (20,Herschel Caroline, 2)
Student: (16,Noether Emmi, 1)
Student: (17,Merian Maria-Sybilla, 3)

 

Lernziele (Java Collections)

Am Ende dieses Blocks können Sie:

  • ...abhängig von den Anforderungen Klassen zur Verwaltung von Listen, Mengen, Verzeichnissen (Maps) und Warteschlangen nennen
  • ... Iteratoren anwenden um an alle Elemente einer Collection-klasse auszulesen und zu verwenden
  • ... das Zusammenspiel von Schnittstellen und Implementierungen der verschiedenen Klassen der Java-Collections erklären und anwenden
  • ... können die Vor- und Nachteile der unterschiedlichen Imlementierungen für die gegebenen Schnittstellen nennen
  • ... generische Typen im Zusammenhang mit den Java-Collection-klassen anwenden.

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: Fragen zu Collections

Threads (Nebenläufigkeit)

Duke in Threads

Einleitung

Java bietet als Programmiersprache von Beginn an eine aktive Unterstützung für das Programmieren mit Threads (engl der Faden, das Fädchen). Hierdurch ist es in Java relativ einfach nebenläufige Programme zu implementieren.
Das Threadkonzept erlaubt es in der gleichen Javaanwendungen Dinge parallel abzuarbeiten und trotzdem auf die gleichen Daten zuzugreifen.

 

Hiermit ist die Implementierung von Javaanwendungen mit den folgenden Vorteilen möglich:

  • man kann sehr viel mehr Aufgaben abarbeiten, da man es gleichzeitig tun kann.
  • man kann Aufgaben schneller zu erledigen, da man es parallel tun kann.
  • man kann Aufgaben asynchron, parallel im Hintergrund abarbeiten ohne andere Aufgaben warten zu lassen.

Prozesse und Threads im Betriebssystem

Mit Java-Threads kann man nebenläufige Programme programmieren dies es erlauben mehrere Dinge gleichzeitig zu tun. Ein sehr einfaches Beispiel ist eine Programm mit einer graphische Benutzeroberfläche. Dieses Programm lädt typischerweise in einem Thread eine Datei aus dem Internet während gleichzeitig ein anderer Thread auf dem Bildschirm einen Fortschrittsbalken vergrößert.

Betriebssysteme verwalten die Ressourcen eines Rechners. Beim Programmieren mit nebenläufigen Java-Threads ist es wichtig zu verstehen, wie die beiden wichtigen Resourcen Hauptspeicher und Prozessoren vom Javalaufzeitsystem und dem Betriebsystem verwaltet werden. Bei der Betrachtung des Speichers spielt nur der virtuelle Speicher des Betriebsystems eine Rolle, da der Javaentwickler in der Regel keinen direkten Einfluss auf dem physischen Speicher nehmen kann. Moderne Betriebsysteme sind in der Lage Programme gleichzeitig bzw. nebenläufig auszuführen.

Definition
Prozess(Informatik)
Ein Prozess bezeichnet in der Informatik ein im Ablauf befindliches Computerprogramm (siehe Wikipedia). Zum Prozess gehört das Programm samt seiner Daten und dem Prozesskontext (siehe Wikipedia).

 

Das Javalaufzeitsystem ist aus der Sicht des Betriebssystems während seiner Ausführung ein Prozess.

Prozesse besitzen während ihrer Ausführung typischerweise:

  • ein Programm welches sie ausführen. Mehrere Prozesse können durchaus das gleiche Programm ausführen
  • einen eigenen Speicherbereich zur Verwaltung der Daten. Das Betriebsystem verwaltet diesen Speicher und sorgt dafür das alle Prozesse ihren eigenen Speicher benutzen können ohne sich gegenseitig zu beinflussen. Bei Java ist der Heap der bekannteste gemeinsame Speicherbereich.
  • Zumindest einen Programmstack (Stapel) zur Verwaltung der Daten von Methoden und einen dazugehörige Befehlszähler/zeiger
  • Zugriff auf Betriebssystemressourcen (Bildschirm, Tastatur, Massenspeicher, Netzwerk etc.)

Threads (engl. der Faden, das Fädchen) sind leichtgewichtige Ausführungseinheiten eines Prozesses:

Definition
Thread (Informatik)
Ein Thread (auch: Aktivitätsträger oder leichtgewichtiger Prozess) bezeichnet in der Informatik einen Ausführungsstrang oder eine Ausführungsreihenfolge in der Abarbeitung eines Programms. Ein Thread ist Teil eines Prozesses. (siehe Wikipedia)

Das Javalaufzeitsystem ist typischerweise ein Prozess des Betriebssystems. Die Java-Threads werden normalerweise auf Betriebssystem-Threads abgebildet. Dies war in frühen Javaimplementierungen (1.1) nicht der Fall. Hier wurden die Threads von Java selbst verwaltet (siehe Green Threads) .

Ein Thread besitzt typischerweise

  • keinen eigenen Speicher (Heap). Er und alle anderen Threads des Prozesses haben Zugriff auf den gemeinsamen Hauptspeicher seines Prozesses. Durch das gemeinsame, gleichzeitige Arbeiten auf den gemeinsamen Daten ist der Datenaustausch zwischen Threads sehr einfach. Die Konsistenz der Daten muss jedoch aktiv verwaltet werden.
  • einen eigenen Programmstack. Er dient der Verwaltung der aktuell aufgerufenen Methodenvariablen.
  • einen eigenen Befehlszähler

Prozesse bestehen aus mindestens einem Thread (dem Haupt-Thread, Main-Thread) und eventuell zusätzlichen Threads die eigene Programmstacks zur parallelen Ausführung besitzen. Die Lebensdauer von Threads ist durch die Lebensdauer des dazugehörigen Prozesses beschränkt. Sie enden mit dem Beenden des Pozesses.

 

Nebenläufige Ausführung im Betriebssystem

Betriebsysteme weisen den Prozessen die Prozessoren zur Ausführung zu. Hierfür verwendet man die englischen Begriffe des "scheduling" oder "dispatching". Ziel des Betriebssystems ist es die Prozessoren möglichst gut auszunutzen und eine faire, vorteilhafte Abarbeitung aller Programme in Prozessen zu gewährleisten.

Die (historisch) einfachste Art der Verwaltung von Prozessen durch das Betriebsystem ist der Batchbetrieb (Stapelbetrieb). Ein Rechner hat typischerweise nur einen Prozessor. Das Betriebssystem kann nur ein Programm gleichzeitig als Prozess ausführen. Es weist dem Prozessor eine Programm A zu dieses läuft bis es beendet wird. Anschließend wird das nächste Programm ausgeführt: 

Multi tasking: Um interaktive Benutzer zu bedienen ist es geschickter laufende Prozesse zu unterbrechen und andere Prozesse teilweise abzuarbeiten. Aufgrund der hohen Prozessorgeschwindigkeit hat der menschliche Betrachter den Eindruck, die Prozesse laufen gleichzeitig. Alle moderne Betriebsysteme arbeiten nach diesem Prinzip. Das Unterbrechen der Prozesse ist oft problemlos möglich, da sie sich oft selbst blockieren. Sie müssen da si relativ lange auf Daten von Benutzern, Festplatten, dem Netzwerk warten müssen. In diesen vielen Zwangpausen kann das Betriebssyteme andere, lauffähige Prozesse abarbeiten. 

Die Prozesse laufen jetzt verschränkt und sie werden in vielen einzelnen Blöcken abgearbeitet. Sie werden quasi-parallel abgearbeitet.

Multi tasking-Multiprozessor: Da alle modernen Prozessoren mehrere Ausführungseinheiten besitzen können die Betriebsysteme Prozesse parallel abarbeiten. Die Prozesse müssen nicht zwangsweise auf dem gleichen Prozessor ausgeführt werden (siehe Beispiel). Die Gesamtausführungszeiten können bei mehren Prozessoren entsprecht verkürzen.

Multithreaded-Multiprozessor: Da Threads leichtgewichtige Prozesse mit einem Programmstack und einem eigenen Programmablauf sind, werden sie bei der Prozessorvergabe wie Prozesse behandelt.  Ein Prozess kann jetzt mehrere Prozessoren gleichzeitig verwenden wenn er nur über mehrere Theads verfügt. Anwendungen können jetzt innerhalb eines Prozesses skalieren. Dies bedeutet, sie können (threoretisch) beliebig viele Prozessoren benutzen und damit beliebig viele Aufgaben in einer bestimmten Zeit abarbeiten. Der Durchsatz eines Prozesses, bzw. die Abarbeitungsgeschwindigkeit ist nicht mehr direkt an die Geschwindigkeit eines einzelnen Prozessors gebunden.

 

 

Thread Zustandsübergänge

Threads und Prozesse haben unterschiedliche Zustände, die von den verfügbaren Betriebsmitteln abhängen:

  • blocked: ein Thread der auf Daten von einem Gerät wie Festplatte, Tastatur, Maus oder Netzwerk wartet ist blockiert. Das Betriebssystem wird ihm keinen Prozessor zuweisen da er ihn nicht nutzen kann solange ihm die notwendigen Daten fehlen.
  • ready-to-run: Der Thread hat alle Betriebsmittel, mit Ausnahme des Prozessors, die er zum Laufen benötigt. Er wartet bis der Dispatcher einen Prozessor zuweist.
  • running: Der Thread hat alle nötigen Betriebsmittel inklusive eines Prozessors. Er führt sein Programm aus bis er eine nicht vorhandene Ressource benötigt oder bis er vom Dispatcher den Prozessor entzogen bekommt.

Die Übergänge zwischen den vereinfachten Zuständen eines Threads sind im folgenden Diagramm dargestellt:

Threads haben ähnlich wie Prozesse eine Reihe von Zuständen. Sie besitzen jedoch mehr Zustände, da ihre Lebensdauer kürzer als die des Prozesses ist und sie sich miteinandere synchronisieren müssen. Threads haben die folgenden fünf Zustände:

  • new: Der Thread wurde mit dem new Operator erzeugt. Er befindet sich im Anfangszustand. Auf seine Daten kann man zugreifen. Er ist noch nicht ablauffähig.
  • ready-to-run: Der Thread ist lauffähig und wartet auf eine Prozessorzuweisung
  • running: Der Thread hat einen Prozessor und führt das Programm aus
  • blocked: Der Thread wartet auf Ressourcen
  • dead: Der Thread kann nicht wieder gestartet werden

Eine Reihe dieser Zustände kann durch Methodenaufrufe vom Entwickler beeinflusst werden:

  • start(): Ein Thread wechselt vom Zustand "new" zu "ready-to-run" 
  • sleep(): Ein laufender Thread wird für eine bestimmte Zeit blockiert
  • join(): Ein Thread blockiert sich selbst bis der Thread dessen join() Methode aufgerufen wurde sich beendet hat
  • yield(): Ein Thread gibt freiwillig den Prozessor auf und erlaubt der Ablaufsteuerung den Prozessor einem anderen Thread zuzuweisen
  • interrupt(): Erlaubt es Threads die wegen eines sleep() oder join() blockiert sind wieder in den Zustand "ready-to-run" zu versetzen

Programmieren mit Threads

Java erlaubt das Erzeugen und Verwalten von Threads mit Hilfe der Systemklasse Thread. Beim Starten einer Javaanwendung bekommt die Methode main() automatisch einen Thread erzeugt und zugewiesen der sie ausführt. Mit Hilfe der Klasse Thread kann man selbst zusätzliche Threads erzeugen und starten.

Erzeugen und Start mit der Klasse Thread

Man kann einen neuen Thread starten indem man ein Objekt von Thread erzeugt. Hiermit wird parallel im Hintergrund ein Javathread erzeugt. Das Aufrufen der Methode start() startet dann den neuen Thread. Dies geschieht wie im folgenden Beispiel gezeigt:

public static void main(String[] args {
   ...
   Thread t1= new Thread(...);
   t1.start();
   ...
}

Im Laufzeitsystem wird hierdurch zuerst ein neuer Thread erzeugt und dann gestartet:

 

Es verbleibt die Frage welchen Programmcode der neue Thread ausführt.

Der ausgeführte Programmcode steht in einer Methode mit den Namen run() und muss von einer Klasse nach den Vorgaben der Schnittstelle Runnable implementiert werden.

Hierfür muss beim Erzeugen des Thread-objekts eine Referenz auf ein Objekt mitgegeben werden welches diese Schnittstelle implementiert. Geschieht dies nicht wird die Methode run() des Tread-objekts aufgerufen. Hierdurch ergeben sich zwei Möglichkeiten einen eigenen Thread mit einem bestimmten Programm zu starten:

Starten eines Threads durch Erweitern der Klasse Thread

Die erste Möglichkeit besteht im Erweitern der Klasse Thread und im überschreiben der Methode run().
Die Klasse Thread implementiert schon selbst die Schnittstelle Runnable. Ruft man die Methode start() ohne einen Parameter auf, so wird bei einer angeleiteten Klasse die überschriebene Methode run() in einem eigenen neuen Thread aufgerufen.
Im unten aufgeführten Beispiel wurde die Klasse myThread aus der Klasse Thread abgeleitet:

Die Klasse myThread verwaltet die Threads in ihrer main() Methode. Das Erzeugen und Starten der Threads der Klasse myThread könnte auch aus jeder anderen beliebigen Klasse erfolgen.

package Kurs2.Thread;

public class myThread extends Thread {
public void run() {
System.out.println("Hurra ich bin myThread in einem Thread mit der Id: "
+ Thread.currentThread().getId());
}

public static void main(String[] args) {
System.out.println("Start myThread.main() Methode im Thread mit der Id: "
+ Thread.currentThread().getId());
myThread t1 = new myThread();
t1.start();
System.out.println("Ende myThread.main() Methode im Thread mit der Id: "
+ Thread.currentThread().getId());
myThread t2 = new myThread();
// t2 ist zwar ein Threadobjekt und repräsentiert einen Thread
// da das Objekt nicht mit start() aufgerufen läuft es im gleichen
// Thread wie die main() Routine!
t2.run();
}
}

Das Programm erzeugt die folgende Konsolenausgabe:

Start myThread.main() Methode im Thread mit der Id: 1
Hurra ich bin myThread in einem Thread mit der Id: 10
Ende myThread.main() Methode im Thread mit der Id: 1
Hurra ich bin myThread in einem Thread mit der Id: 1

Im Beispielprogramm wird für die Referenz t1 ein neues Threadobjekt erzeugt. Anschliesend wird es durch seine start() Methode in einem eigenen Thread gestartet. Die run() Methode wird nach dem Starten im eigenen Thread ausgeführt.

Das Objekt mit der Referenz t2 ist zwar auch ein Thread. Das es aber direkt mir der run() Methode aufgerufen wird, läuft es im gleichen Thread wie die main() Methode.

Im UML Sequenzdiagramm ergibt sich der folgende Ablauf:

Starten eines Threads durch Implementieren der Schnittstelle Runnable

Das Erweitern einer Klasse aus der Klasse Thread ist nicht immer möglich. Man kann einen Thread auch starten indem man ein Thread-objekt erzeugt und ihm die Referenz auf eine Instanz der Schnittstelle Runnable mitgibt. Die Programmierabfolge ist dann:

  • Erzeugen eine Threadobjekts mit Referenz auf eine Instanz von Runnable.
  • Aufrufen der start() Methode des Threadobjekts
    • die Methode run() der Runnable-objekts wird automatisch aufgerufen

Die Klasse myRunnable:

package Kurs2.Thread;

public class myRunnable implements Runnable {

public void run() {
System.out.println("Hurra ich bin myRunnable in einem Thread mit der Id: "
+ Thread.currentThread().getId());
}

}

Die Klasse ThreadStarter die als Hauptprogramm dient:

package Kurs2.Thread;

public class ThreadStarter{
public static void main(String[] args) {
System.out.println("Start ThreadStarter.main() Methode im Thread mit der Id: "
+ Thread.currentThread().getId());
myRunnable r1 = new myRunnable();
myRunnable r2 = new myRunnable();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
System.out.println("Ende ThreadStarter.main() Methode im Thread mit der Id: "
+ Thread.currentThread().getId());
// r2 ist zwar ein Runnableobjekt , da das Objekt aber nicht von einem
// Threadobjekt indirekt aufgerufen wirdläuft es im gleichen
// Thread wie die main() Routine!
r2.run();
}
}

Das Programm erzeugt die gleichen Ausgaben wie das vorherige Programm:

Start ThreadStarter.main() Methode im Thread mit der Id: 1
Hurra ich bin myRunnable in einem Thread mit der Id: 10
Ende ThreadStarter.main() Methode im Thread mit der Id: 1
Hurra ich bin myRunnable in einem Thread mit der Id: 1

Das Aufrufen von r2.run() startet keinen eigenen Thread. Der Vorteil der Benutzung der Schnittstelle Runnable liegt darin, dass man die Methode run() in jeder beliebigen Klasse implementieren kann.

Die wichtigsten Methoden der Klasse Thread

  • Konstruktor: Thread(Runnable target) : Erzeugt einen neuen Thread und übergibt ein Objekt dessen run() Methode beim Starten des Threads aufgerufen (anstatt die eigene run() Methode aufzurufen.
  • static Thread currentThread(); liefert den aktuellen Thread der ein Codestück gerade ausführt
  • long getId(): Liefert die interne Nummer des Threads
  • join(): Hält den aktuellen Thread an bis der referenzierte Thread beendet ist.
  • static void sleep(long millis): Lässt den aktuellen Thread eine Anzahl von Millisekunden schlafen.
  • start(): Lässt die VM den referenzierten Thread starten. Dieser ruft dann die run() Methode auf.

Synchronisation

Da Threads auf die gleichen Objekte auf im Heap zugreifen, können sie so sehr effizient Daten austauschen. Es besteht jedoch das Risiko der Datenkorruption, da man oft mehrere Daten gleichzeitig verändern muss um sie von einem konsistenten Zustand in den nächsten konsistenten Zustand zu überführen. 

Die Sitzplatzreservierung in einem Flugzeug ist hierfür ein typisches Beispiel:

Mehrere Reisebüros prüfen ein Flugzeug auf die Verfügbarkeit von 10 Plätzen für eine Reisegruppe. Ergibt das Lesen der Belegungsvariable 20 freie Plätze, so fährt Reisebüro 1 fort und liest weitere Daten um die Buchung vorzubereiten. Verzögert sich die endgültige Buchung so kann es vorkommen, dass ein zweites Reisebüro die Verfügbarkeit von 15 Plätzen abfragt und die 15 Plätze bucht. Das zweite Reisebüro erhöht den Belegungszähler also um 15 Plätze.

Kommt das erste Reisebüro nun endlich mit seiner Buchung vorran und erhöht die ursprünglich ausgelesene Variable um 10 ergibt sich ein inkonsistenter Zustand.

Man muss also in Systemen mit parallelen Zugriff auf Daten die Möglichkeit schaffen nur einen Thread über eine gewisse Zeit auf einem Datensatz (Objekt) arbeiten zu lassen um wieder einen konsistenten Zustand herzuführen.

Darf nur ein Thread gleichzeitig auf einer Variablen arbeiten, so nennt man diese eine kritische Variable. Die Zeit die ein Thread mit der Bearbeitung einer solchen Varriablen verbringt nennt man den kritischen Abschnitt oder auch den kritischen Pfad.

Das oben geschilderte Problem beim gleichzeitigen Zugriff nennt man auch "Reader/Writer" Problem, da das Lesen und Schreiben auf dem Datum atomar erfolgen muss. Da in in nebenläufigen Systemen diese Datenkorruption ausschlieslich von der Geschwindigkeit und dem zufälligen paralleln Zugriff abhängt nennt man ein solches Problem auch eine "Race Condition". Die Datenkorruption tritt zufällig und abhängig von der Ausführungsgeschwindigkeit auf.

Beispiel: Nichtatomares Inkrement in Java

Der ++ Operator ist Java ist nicht atomar. Dies bedeutet, dass zwei Threads einen bestimmten Wert auslesen können und der erste Thread schreibt den um 1 erhöhten Wert zurück während eventuell der zweite Thread noch etwas Zeit benötigt. Schreibt der zweite Thread dann den gleichen inkrementierten Wert zurück wurde die Zahl nur einmal inkrementiert. Es liegt eine Datenkorruption vor.

Im Programm ParaIncrement wird eine gemeinsame Variable zaehler von zwei Threads gleichzeitig inkrementiert. Der Wert der Vriablen sollte immer doppelt so groß wie die Anzahl der Durchläufe (Konstante MAX) eines einzelnen Threads sein.Die Korruption im Programm ParaIncrement findet relativ selten selten statt. Man muss die Konstante K für die Anzahl der Durchläufe eventuell abhängig vom Rechner und der Java virtuellen Maschine anpassen:

package Kurs2.Thread;

public class ParaIncrement extends Thread {
public static int zaehler=0;
public static final int MAX= Integer.MAX_VALUE/10;

public static void increment() {
zaehler++;
}

/**
* Starten des Threads
*/
public void run() {
for (int i=0; i < MAX; i++) {
increment();
}
}

public static void main(String[] args) {
ParaIncrement thread1 = new ParaIncrement();
ParaIncrement thread2 = new ParaIncrement();
long time = System.nanoTime();
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
}
time = (System.nanoTime() -time)/1000000L; // time in milliseconds
if ((2* ParaIncrement.MAX) == ParaIncrement.zaehler)
System.out.println("Korrekte Ausführung: " +
+ ParaIncrement.zaehler + " (" + time + "ms)");
else
System.out.println("Fehler! Soll: " + (2* ParaIncrement.MAX) +
"; Ist: " +ParaIncrement.zaehler + " (" + time + "ms)");
}
}

Wechselseitiger Ausschluss

Zur Vermeidung der oben genannten Korruption ist es wichtig sicher zustellen, dass nur ein Thread gleichzeitig Zugriff auf diese Daten hat.

Definition
Kritischer Abschnitt/ Kritischer Pfad
Ein kritischer Abschnitt ist eine Folge von Befehlen, die ein Thread nacheinander vollständig abarbeiten muss, auch wenn er vorübergehend die CPU and einen anderen Thread abgibt. Kein anderer Thread darf einen kritischen Abschnitt betreten, der auf die gleichen Variablen zugreift, solange der erstgenannte Thread mit der Abarbeitung der Befehlsfolge noch nicht fertig ist. (siehe: Goll, Seite 744)

Einfachste Lösung: Verwendung von Typen die den kritischen Pfad selbst schützen

Das Java Concurrency Paket bietet reichhaltige Möglichkeiten und Klassen. Das oben gezeigte Beispiel kann mit Hilfe der Klasse AtomicInteger sicher implementiert werden. Die Klasse AtomicInteger erlaubt immer nur einem Thread Zugriff auf das Datum. Die entsprechende Implementierung ist:

package Kurs2.Thread;
import java.util.concurrent.atomic.AtomicInteger;
public class ParaIncrementAtomicInt extends Thread {
public static AtomicInteger zaehler;
public static final int MAX= Integer.MAX_VALUE/100;
public static void increment() {
zaehler.getAndIncrement();
}
/**
* Starten des Threads
*/
public void run() {
for (int i=0; i < MAX; i++) {
increment();
}
}
public static void main(String[] args) {
zaehler = new AtomicInteger(0);
ParaIncrementAtomicInt thread1 = new ParaIncrementAtomicInt();
ParaIncrementAtomicInt thread2 = new ParaIncrementAtomicInt(); long time = System.nanoTime();
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
} time = (System.nanoTime() -time)/1000000L; // time in milliseconds
if ((2* ParaIncrementAtomicInt.MAX) == zaehler.get())
System.out.println("Korrekte Ausführung: " +
+ ParaIncrementAtomicInt.zaehler.get() + " (" + time + "ms)");
else
System.out.println("Fehler! Soll: " + (2* ParaIncrementAtomicInt.MAX) +
"; Ist: " +ParaIncrementAtomicInt.zaehler.get() + " (" + time + "ms)");
}
}

Vergleichen Sie die Laufzeiten beider Anwendungen! Die Anwendung bei der immer nur ein Thread auf das Objekt zugreifen kann ist erheblich langsamer, jedoch korrekt.

Sperren durch Monitore mit dem Schlüsselwort synchronized

Um die oben gezeigten Möglichkeiten von Korruptionen zu vermeiden verfügen Javaobjekte über Sperren die Monitore genannt werden. Jedes Javaobjekt besitzt einen Monitor der gesetzt ist oder nicht gesetzt ist.

  • Ein Monitor wird gesetzt wenn eine Instanzmethode des Objekts aufgerufen wird, die mit dem Schlüsselwort synchronized versehen ist.
  • Kein anderer Thread kann eine synchronisierte Instanzmethode des gleichen Objekts aufrufen, solange der Thread der den Monitor erworben hat noch in der synchronisierten Methode arbeitet.
    • Alle anderen Threads werden beim Aufruf einer synchronisierten Instanzmethode des gleichen Objekts blockiert und müssen warten bis der erste Thread die synchronisierte Methode verlassen hat.
  • Nach dem der erste Thread die synchronisierten Methode verlassen hat, wird der Monitor wieder freigegeben.
  • Der nächste Thread, der eventuell schon wartet kann den Monitor erwerben.

Es ist wichtig zu verstehen, dass in Java immer die individuellen Objekte mit einem Monitor geschützt sind. Sind zum Beispiel die Sitzplätze eines Flugzeuges durch Java-Objekte implementiert, so kann man mit der gleichen synchronisierten Methode auf untrschiedlichen Objekte parallel arbeiten.

Am Beispiel der Klasse Sitzplatz kann man sehen wie man den Monitor für einen bestimmten Sitzplatz setzen kann:

package Kurs2.Thread;

public class Sitzplatz {
private boolean frei = true;
private String reisender;

boolean istFrei() {return frei;}

/**
* Buche einen Sitzplatz für einen Kunden falls er frei ist
* @param kunde Name des Reisenden
* @return erfolg der Buchung
*/
synchronized boolean belegeWennFrei(String kunde) {
boolean erfolg = frei; // Kein Erfolg wenn nicht frei
if (frei) {
reisender = kunde;
frei = false;
}
return erfolg;
}

}

Die Methode belegeWennFrei() kann jetzt nur noch von einem Thread auf einem Objekt gleichzeitig aufgerufen werden. Die Methode istFrei() ist nicht synchronisiert und in einer parallelen Umgebung nicht sehr relevant. Man kann sich nicht darauf verlassen, dass bei der nächsten Operation der freie Zustand noch gilt.

Wichtig
Monitore und Schutz von Daten

Monitore schützen nur die synchronsierten Methoden eines Objekts. Dies bedeutet

  • Nicht synchronisierte Methoden der Klasse können weiterhin parallel aufgerufen werden
  • Die Attribute einer Objektinstanz sind nicht selbst geschützt. Man schützt Sie indirekt mit dem Schlüsselwort private. Hierdurch ist der Zugriff auf die Attribute auf die eigenen Methoden beschränkt. Die Methoden der Klasse können wiederum mit synchronized geschützt werden.

 

Statische synchronisierte Methoden

Das Schlüsselwort synchronized kann auch verwendet werden um einen Monitor für die Klasse zu setzen. Dieser Monitor hat aber keinen Einfluss auf den Zugriff auf die Objekte einer Klasse! Er schützt nur statische Methoden der Klasse.

Beispiel: Man kann das Korruptionsproblem in der Klasse ParaIncrement beheben in dem man die statische  Methode increment() synchronsiert:

public static synchronized void increment() {
        zaehler++;
    }

Die Variable zaehler ist in diesem Beispiel eine statische Variable. Sie gehört nicht zu einem der beiden erzeugten Objekten.

Synchronisierte Blöcke

Man muss nicht notwendigerweise eine gesamt Methode synchroniseren. Java bietet auch die Möglichkeit einzelne Blöcke zu synchronisieren. Das Synchronsieren eines Blocks erfolgt ebenfalls mit Hilfe des Schlüsselworts synchronized. Hier muss man jedoch das Objekt angeben für welches man einen Monitor erwerben will.

Man kann die Sitzplatzreservierung auch mit Hilfe eines synchronisierten Blocks implementieren:

boolean belegeWennFrei(String kunde) {
        boolean erfolg;
        synchronized (this) {
            erfolg =  frei; // Kein Erfolg wenn nicht frei
            if (frei) {
                reisender = kunde;
                frei = false;
            }
        }
        return erfolg;
    }

In dieser Implementierung kann die Methode belegeWennFrei() parallel aufgerufen werden. Beim Schlüsselwort synchronized muss jedoch für das aktuelle Objekt der Monitor erworben werden bevor der Thread fortfahren kann. 

Referenzen

  • Goll,Heinisch, Müller-Hoffman: Java als erste Programiersprache, Teubner Verlag
  • IBM Developerworks: Going Atomic: Gute Erklärung zu synchronisierten Zählern (AtomicInteger)

 

Aufgaben (Threads)

Programmieraufgabe JoinAndSleep

Ziel der Aufgabe ist es drei Threads zu programmieren die auf das Beenden des anderen Warten und dann eine Zeit schlafen:

  1. Sie drucken jeden neuen Zustand auf der Konsole aus
  2. Als erstes nach ihrem Start warten sie bis ein anderer Thread auf den sie zeigen sich beendet hat. Zeigen sie auf keinen anderen Thread so gehen sie sofort über zum nächstens Schritt.
  3. Die Threads schlafen für eine vorgegebene Zeit in ms
  4. Die Threads beenden sich

Die geforderte Aufgabe soll in einer Klasse implementiert werden

  1. Erweitern Sie die Klasse JoinAndSleep aus der Klasse Thread
  2. Attribute: Die Klasse hat ein Ganzzahlattribut sleep zur Verwaltung der Schlafzeit
    1. Die Klasse hat ein Ganzzahlattribut sleep zur Verwaltung der Schlafzeit
    2. Die Klasse hat eine Referenz auf ein Objekt der Klasse JoinAndSleep
  3. Konstruktor
    1. Der Konstruktor der Klasse erlaubt es die Schlafzeit zu übergeben und eine Referenz auf einen anderen Thread. Dies ist der Thread auf den gewartet werden soll.
  4. run() Methode: Diese Methode implementiert die oben genannte Semantik zum Warten und Schlafen
    1. Falls ein Thread gegeben ist soll auf sein Ende gewartet werden
    2. Anschliesend soll eine bestimmte Zeit geschlafen werden
    3. Fügen Sie zwischen allen Schritten Konsolenausgaben ein um den Fortschritt zu kontrollieren. Geben Sie hier immer auch den aktuellen Thread aus!
  5. main() Methode
    1. Erzeuge Thread 3: Er soll auf keinen Thread warten und dann 4000ms schlafen
    2. Erzeuge Thread 2: Er soll auf Thread 3 warten und dann 3000ms schlafen
    3. Erzeuge Thread 1: Er soll auf Thread 2 warten und dann 2000ms schlafen
    4. Starten Sie Thread 1
    5. Starten Sie Thread 2
    6. Starten Sie Thread 3

Hinweise:

Ich welchen Thread bin ich gerade?

  • Die statische Methode Thread.currentThread() liefert einen Zeiger auf den aktuellen Thread
  • Diesen kann man direkt ausdrucken

Wie warte ich auf einen anderen Thread?

Die Methode Thread.join() erlaubt auf das Beenden eines anderen Threads zu warten. Man muss auf eine InterruptedException vorbereitet sein, da man aufgeweckt werden kann:

Thread aThread;
...
try {
    aThread.join();
   } catch (InterruptedException e) {}

Wie lasse ich einen Thread schlafen?

Die Methode Thread.sleep() ist eine statische Methode. Man muss seinen eigenen Thread nicht kennen um ihn ruhen zulassen! Auch diese Methode kann eine InterruptedException werfen und muss mit einer Ausnahmebehandlung versehen werden:

try {
    Thread.sleep(schlafen);
   } catch (InterruptedException e) {}

Verständnisfragen

  • Wer wartet hier auf wen?
  • Ist dies an den Konsolenaufgaben zu erkennen?
  • Woran erkenne ich bei den Konsolenausgaben, das der Code in einem eigenen Thread läuft?

 

 

 

Lösungen (Threads)

Programmieraufgabe JoinAndSleep

package Kurs2.Thread;
public class JoinAndSleep  extends Thread{
    Thread joinIt;
    int schlafen;

    public JoinAndSleep(int sleeptime, Thread toJoin) {
        joinIt = toJoin;
        schlafen = sleeptime;
        System.out.println("Thread: " + this + " erzeugt");
    }

    public void run() {
        System.out.println("Thread: " +Thread.currentThread() + " gestartet");
        try {
            if (joinIt!=null) {
                joinIt.join();
                System.out.println("Thread: " +Thread.currentThread()
                        + " join auf " + joinIt + " fertig");
            }
        } catch (InterruptedException e) {}
        System.out.println("Thread: " +Thread.currentThread() 
                + " schlaeft jetzt fuer " + schlafen + "ms");
        try {
            Thread.sleep(schlafen);
        } catch (InterruptedException e) {}
        System.out.println("Thread: " +Thread.currentThread() + " endet");
    }

    public static void main (String[] args) {
        JoinAndSleep s3= new JoinAndSleep(2003, null);
        JoinAndSleep s2= new JoinAndSleep(2002, s3);
        JoinAndSleep s1= new JoinAndSleep(2001, s2);
        s1.start();
        s2.start();
        s3.start();
    }
}

 

Beispiel: Kritischer Pfad

Das hier benutzte Beispielprogramm visualiert 15 Threads die sich auf einem synchronisierten Objekt serialisieren.

Threads in grüner Farbe befinden sich nicht im kritischen Pfad. Threads in roter Farbe befinden sich im kritischen Pfad.

Der kritische Pfad in in der Klasse EinMonitor in der Methode buchen() implementiert.

Die einzige Instanz von EinMonitor verfügt über zwei Variablen a und b die Konten darstellen sollen.

Die Methode buchen() "verschiebt" mehrfach einen Betrag zwischen den beiden Konten. Die Summe der beiden Variablen sollte am Ende der Methode stets gleich sein.

Die Methode buchen() enthält etwas Overhead zur Visualierung des Konzepts:

  • am Anfang und am Ende  muss das GUI Subsystemen über den Eintritt und das Verlassen des kritischen Pfads informiert werden
  • zwischen allen Buchungen werden kleine Schlafpausen eingelegt um die Zeit im kritischen Pfad künstlich zu verlängern

Die Methode buchen() enthält eine Konsistenzprüfung am Ende der Methode die bei einem Fehler in der Buchführung eine Konsolenmeldung ausdruckt. Sie kommt in der auf dieser Seite gezeigten Variante nicht zum Zuge!

Die Klasse MainTest dient zum Starten des Programms. Sie erzeugt und startet die 15 Threads. Jeder Thread führt nur eine gewisse Anzahl von Buchungen durch und beendet sich dann.

Aufgaben

  • Übersetzen Sie die Klassen und starten Sie das Hauptprogramm
  • Der Schieberegler erlaubt das Einstellen der Schlafpausen im kritischen Pfad. Was geschieht wenn der kritische Pfad verkürzt wird?
  • Entfernen die das Schlüsselwort synchronized in der Methode buchen(). Was geschieht?
  • Was würde geschehen geschehen wenn die künstlichen sleep Aufrufe entfernt werden?
    • Hinweis: Sie müssen dann auch die Anzahl der Durchläufe pro Thread stark erhöhen. Da die Zeit im kritischen Pfad sehr kurz wird.
  • Was geschieht wenn man den yield() Aufruf für in der run() Methode von MainTest entfernt

Kommentar

Da die aktuelle Ausführungsgeschwindigkeit 4-5 Zehnerpotenzen jenseits der menschlichen Wahrnehmungsfähigkeit liegt ist es sehr schwer die echten Abläufe im Zeitlupentempo zu visualisieren. Ein künstlicher sleep() Aufruf blockiert den Prozess und gibt den Prozessor an das Betriebssystem zurück. Der Scheduler des Betriebssystems trifft bei dieser künstlichen Verlangsamung eventuell andere Entscheidungen in Bezug auf den Thread den er ausführt. Das gleiche Problem besteht beim Debuggen von Javaprogrammen. Durch das Bremsen bestimmter Threads können existierende Fehler nicht mehr reproduzierbar sein oder bisher nicht aufgetretene Fehler in der Synchronsiation können sichtbar werden.

Starten des Programms

Die benötigten Klassen sing in Threading.jar zusammen gefaßt.

Man kann diese jar Datei mit

java -jar Treading.jar

von der Kommandozeile nach dem runterladen starten. Vielleicht reicht auch ein Doppelklick auf die Datei im Download-Ordner...

Nach dem Übersetzen der Dateien oder nach dem Starten der jar-Datei erscheint ein GUI wie es im folgenden Bild zu sehen ist:

Klasse MainTest

Hauptprogramm der Anwendung.

package Kurs2.Thread;

public class MainTest extends Thread {

public static final int INCRITICALPATH = 0;
public static final int NOTINCRITICALPATH = 1;
public static final int ENDED = 2;
public static int anzahlThreads = 15;
public static MainTest[] mt;
public int threadStatus = NOTINCRITICALPATH;
private static EinMonitor myMonitor;
public static int sleepPeriod = 500;
public int meineID;
public static ThreadingPanel tp;
public static ThreadFenster tg;
public boolean stop = false;
public boolean synchron = true;

public MainTest(int id) {
meineID = id;
}

public void run() {
long anfangszeit = System.nanoTime();
System.out.println("Thread [" + meineID + "] gestartet");
//GUIupdate(NOTINCRITICALPATH);
for (long i = 0; i < 200; i++) {
Thread t = Thread.currentThread();
// Erlaube anderen Threads die CPU zu holen
t.yield();
if (tg.synchron)
myMonitor.buchen(10);
else
myMonitor.parallelbuchen(10);
}
threadStatus = ENDED;
System.out.println("Thread [" + meineID + "] beendet...");
}

public static void main(String[] args) {
// Anlegen des Monitorobjekts
myMonitor = new EinMonitor(1000000L);
mt = new MainTest[anzahlThreads];
tg = new ThreadFenster();
tp = tg.tp;
// Erzeuge die Threads
for (int i = 0; i < anzahlThreads; i++) {
mt[i] = new MainTest(i);
}
// Starte die Threads
for (int i = 0; i < anzahlThreads; i++) {
mt[i].start();
}
}
}

Klasse EinMonitor

package Kurs2.Thread;

public class EinMonitor {

long invariante;
long a;
long b;

public EinMonitor(long para) {
invariante = para;
a = para;
b = 0L;
}

synchronized public void buchen(long wert) {
GUIupdate(MainTest.INCRITICALPATH);
sleepABit(MainTest.sleepPeriod/5);
this.a = this.a - wert;
sleepABit(MainTest.sleepPeriod/5);
this.b = this.b + wert;
sleepABit(MainTest.sleepPeriod/5);
this.a = this.a + wert;
sleepABit(MainTest.sleepPeriod/5);
this.b = this.b - wert;
sleepABit(MainTest.sleepPeriod/5);
GUIupdate(MainTest.NOTINCRITICALPATH);

if ((a+b) != invariante)
System.out.println("Inkonsistenter Zustand");
}

public void parallelbuchen(long wert) {
GUIupdate(MainTest.INCRITICALPATH);
sleepABit(MainTest.sleepPeriod/5);
this.a = this.a - wert;
sleepABit(MainTest.sleepPeriod/5);
this.b = this.b + wert;
sleepABit(MainTest.sleepPeriod/5);
this.a = this.a + wert;
sleepABit(MainTest.sleepPeriod/5);
this.b = this.b - wert;
sleepABit(MainTest.sleepPeriod/5);
GUIupdate(MainTest.NOTINCRITICALPATH);

if ((a+b) != invariante)
System.out.println("Inkonsistenter Zustand");
}

private void sleepABit(int sleep) {
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {}
}

private void GUIupdate(int status) {
MainTest t = (MainTest) Thread.currentThread();
t.threadStatus = status;
t.tp.repaint();
}
}

Klasse ThreadFenster

package Kurs2.Thread;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class ThreadFenster {

private JFrame hf;
private JButton okButton;
private JButton exitButton;
JTextField threadDisplay;
private final static int SLEEPMIN = 1;
private final static int SLEEPMAX = 2000;
private final static int SLEEPINIT = 500;
private final int threadCurrent = 10;
public ThreadingPanel tp;
public boolean synchron = true;
JRadioButton syncButton;
JRadioButton nosyncButton;

public class exitActionListener implements ActionListener {

public void actionPerformed(ActionEvent e) {
System.exit(0);
}
}

/**
* Aufbau des Fensters zur Ausnahmebehandlung
*
*/
public ThreadFenster() {
JPanel buttonPanel;
// Erzeugen einer neuen Instanz eines Swingfensters
hf = new JFrame("Thread Monitor");

//Nicht Beenden bei Schliesen des Fenster
hf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

// Anlegen der Button
exitButton = new JButton("Beenden");

JLabel threadsLabel = new JLabel("sleep(ms):");
JSlider threadSlider = new JSlider
(JSlider.HORIZONTAL, SLEEPMIN, SLEEPMAX, SLEEPINIT);
threadDisplay = new JTextField();
threadDisplay.setText(Integer.toString(threadCurrent));
threadDisplay.setColumns(4);
threadDisplay.setEditable(false);

threadSlider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
JSlider source = (JSlider) e.getSource();
if (!source.getValueIsAdjusting()) {
MainTest.sleepPeriod = source.getValue();
threadDisplay.setText(Integer.toString(MainTest.sleepPeriod));
}
}
});

exitButton.addActionListener(new exitActionListener());

syncButton = new JRadioButton("Synchronisiert");
syncButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
synchron= true;
System.out.println("Synchronisiert");
}
} );
syncButton.setSelected(true);
nosyncButton = new JRadioButton(" Nicht Sync.");
nosyncButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
synchron= false;
System.out.println("Nicht synchronisiert");
}
} );
ButtonGroup group = new ButtonGroup();
group.add(syncButton);
group.add(nosyncButton);
JPanel syncPanel = new JPanel();
BoxLayout bl = new BoxLayout(syncPanel, BoxLayout.Y_AXIS);
syncPanel.setLayout(bl);
syncPanel.add(syncButton);
syncPanel.add(nosyncButton);

//Aufbau des Panels
//buttonPanel = new JPanel(new GridLayout(1, 0));
buttonPanel = new JPanel();
buttonPanel.add(threadsLabel);
buttonPanel.add(threadSlider);
buttonPanel.add(threadDisplay);
//buttonPanel.add(okButton);
buttonPanel.add(syncPanel);
buttonPanel.add(exitButton);
tp = new ThreadingPanel();
// Aubau des ContentPanes
Container myPane = hf.getContentPane();
myPane.add(buttonPanel, BorderLayout.SOUTH);
myPane.add(tp, BorderLayout.CENTER);

JMenuBar jmb = new JMenuBar();
JMenu jm = new JMenu("Ablage");
jmb.add(jm);
JMenuItem jmi = new JMenuItem("Beenden");
jmi.addActionListener(new exitActionListener());
jmi.setEnabled(true);
jm.add(jmi);
hf.setJMenuBar(jmb);

//Das JFrame sichtbar machen
hf.pack();
hf.setVisible(true);
hf.setAlwaysOnTop(true);
}
}

Klasse ThreadingPanel

package Kurs2.Thread;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JPanel;

/**
*
* @author sschneid
*/
public class ThreadingPanel extends JPanel {

private int ziffernBreite = 10; // Breite einer Ziffer in Pixel
private int ziffernHoehe = 20; // Hoehe einer Ziffer in Pixel

public ThreadingPanel() {
setPreferredSize(new Dimension(200, 100));
setDoubleBuffered(true);
}

/**
* Methode die das Panel überlädt mit der Implementierung
* der Treads
* @param g
*/
public void paintComponent(Graphics g) {
super.paintComponent(g);
int maxWidth = getWidth();
int maxHeight = getHeight();
g.setColor(Color.black);
g.drawString("Anzahl threads: " + MainTest.anzahlThreads, 10, 20);
for (int i = 0; i < MainTest.anzahlThreads; i++) {

paintThread(g, i, 20 + 25 * i, 30);
}
}

/**
* Malen eines Threads und seines Zustands
* @param g Graphicshandle
* @param k zu malender Thread
* @param x X Koordinate des Thread
* @param y Y Koordinate des Thread
*/
public void paintThread(Graphics g, int id, int x, int y) {
int xOffset = 1; // offset Box zu Text
int yOffset = 7; // offset Box zu Text
//String wertThread = k.toString(); // Wert als Text

if (MainTest.mt[id] != null) {
if (MainTest.mt[id].threadStatus == MainTest.ENDED) {
g.setColor(Color.LIGHT_GRAY);
}
if (MainTest.mt[id].threadStatus == MainTest.NOTINCRITICALPATH) {
g.setColor(Color.GREEN);
}
if (MainTest.mt[id].threadStatus == MainTest.INCRITICALPATH) {
g.setColor(Color.RED);
}
}
int breite = 2 * ziffernBreite;
int xNextNodeOffset = 20;
int yNextNodeOffset = ziffernHoehe * 6 / 5; // Vertikaler Offset zur naechsten Kn.ebene
//g.setColor(Color.); // Farbe des Rechtecks im Hintergrund
g.fillRoundRect(x - xOffset, y - yOffset, breite, ziffernHoehe, 3, 3);
g.setColor(Color.black); // Schriftfarbe
g.drawString(Integer.toString(id), x + xOffset, y + yOffset);
}
}


 

Lernziele (Threading)

Am Ende dieses Blocks können Sie:

  • ... zwischen einem Thread und einem Prozess unterscheiden
  • ... die Zustände eines Threads nennen und wissen wie man Threads von einem Zustand in einen anderen Zustand überführt
  • ... mit dem Schlüsselwort synchronized Objekte in kritischen Abschnitten sperren um Threads zu synchronisieren
  • die wichtigsten Methoden der Javaklasse Thread anwenden. Hierzu gehören
    • start()
    • run()
    • sleep()
    • join()
    • getId()
    • currentThread()
    • Konstruktor

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: Fragen zur Nebenläufigkeit ( Multithreading)

Algorithmen

Zeitkomplexität von Algorithmen

Ziel ist immer die effiziente Ausführung von Algorithmen in einer minimalen Zeit, mit einem minimalen Aufwand (Ausführungsaufwand).

Der Ausführungsaufwand ist zu unterscheiden vom Aufwand den Algorithmus zu entwickeln, dem Entwicklungsaufwand. Langsame Anwendungen sind leider oft das Resultat von Implementierungen bei denen die Kostengründe für den Entwicklungsaufwand zu einem höheren Ausführungsaufwand führen...

Ausführungsaufwand versus Entwicklunggaufwand

Der Ausführungsaufwand hängt von vielen Faktoren ab. Viele Faktoren beruhen auf den unbekannten Komponenten des Ausführungssystems:

Die verlässlichste Art und Weise zur Beurteilung der Zeiteffizienz von Algorithmen ist die Abschätzung der Anzahl der Verarbeitungsschritte in Abhängigkeit von der Anzahl der Daten die zu verarbeiten sind.

Definition
Ordnung "O" eines Algorithmus
Die funktionale Abhängigkeit (bzw. Proportionalität) zwischen der Zahl der Ausführungsprogrammschritte und der Zahl (n) der zu verarbeitenden Datenelemente

Beispiele

Hierdurch ergeben sich verschiedene Kategorien von Problemen:

Praktisch ausführbare Algorithmen

Algorithmen die auch bei einer sehr viel größeren Zahl von Elementen in einer akzeptablen Zeit ausgeführt werden können.

Dies sind im Idealfall: O(n), O(log n), O (n*log n)

Man zählt hierzu aber auch Algorithmen mit einer polynomialen Ordnung wie O(n2) oder O(n3).

Praktisch nicht mehr ausführbare Algorithmen

Praktisch nicht mehr ausführbare Algorithmen lassen sich nicht in akzeptabler Zeit bei größer werdenden Datenbeständen ausführen. Dies sind Algorithmen mit einer exponentiellen Ordnung. O(xn). Hierzu zählt auch das Problem des Handlungsreisenden mit einer Ordnung von O(n!).

Rechenregeln für die Aufwandsbestimmung

Die Rechenregeln für die Aufwände von Algorithmen erlauben dem Entwickler die Komplexitätsklasse eine Algorithmus abzuschätzen.

Hierzu geht der Entwickler in zwei Schritten vor:

  1. Man bestimmt die Anzahl der auszuführenden Befehle eine Algorithmus in Abhängigkeit von den zu verarbeitenden Datenelementen n
  2. Man vereinfacht den Term mit dem Gesamtaufwand derart, dass der Term und jeder Teilterm in der gleichen Komplexitätsklasse bleibt

Am Ende bleibt dann ein einfacher Ausdruck für die Komplexitätsklasse übrig.

Klassen von Komplexität

  • konstant: O(1)
  • logarithmisch: O(log(n))
  • linear: O(n)
  • jenseits von linear: O(n*log(n))
  • polynomial: O(n2), O(n3), O(n4) etc.
  • exponentiell: O(kn)

Rechenregeln für einfache Betrachtungen

Gegeben sei eine Funktion f(n) mit dem Aufwand O(n). n sei die vriable Anzahl der zu verarbeitenden Datenelemente

  • Konstanten: Konstanten haben keinen Einfluss:
    • O(f(n)+k) = O(f(n))+k = O(f(n)) für k konstant
    • O(f(n)*k)=O(f(n))*k = O(f(n)) für k konstant
    • O(k) = O(1)
    • konstante Aufwände werden nicht einberechnet. Dies gilt für Multiplikation, sowie für die Addition.
  • Nacheinander ausgeführte Funktion f und g
    • Der Aufwand O(f(n)) sei größer als der Aufwand von O(g(n))
    • O(f(n)+g(n))=O(f(n))+O(g(n)) = O(f(n))
    • ... der größere Aufwand gewinnt.
  • Ineinander geschachtelte Funktionen oder Programmteile wie Schleifen f(g(n))
    • O(f(g(n))) = O(f(n))*O(g(n))
    • der Aufwand multipliziert sich.
    • Einschränkung: Hier wird davon ausgegangen, dass beide Schleifen von der gleichen Variablen abhängen!

Weitere Informationen

Suchalgorithmen

Motivationsbild zum Suchen

Such- und Sortiervorgänge sind sehr häufige Aufgaben die in Anwendungen gelöst werden müssen.

Suchen ist ein Vorgang der komplementär zum Sortieren ist. Datenbestände die vollständig oder teilweise sortiert sind, lassen sich wesentlich einfacher durchsuchen. Ungeordnete Datenbestände lassen sich lassen sich im schlechtesten Fall nur linear durchsuchen.

Suchvorgänge lassen wie folgt kategorisieren:

  • Suchkriterium: Das gesuchte Objekt (Datum) unterscheidet sich durch eine eindeutiges Merkmal von allen anderen Daten. Das Merkmal kann aus einem einzelnen Attribut oder mehreren Attribute bestehen. Das Merkmal wird auch als Suchschlüssel bezeichnet.
  • Suchauftrag: Die Suche eines bestimmten Datums in einer Menge von Daten.
  • Suchziel: Der Zugriff auf das gesuchte Datum (Objekt).

Internes und externes Suchen

Man unterscheidet zwischen Suchvorgängen bei denen man den gesamten Datenbestand im Hauptspeicher verwalten kann (internes Suchen) und Suchvorgängen bei denen der Datenbestand nur teilweise in den Hauptspeicher geladen werden kann(externes Suchen).

Beim externen Suchen muss man also einen schnellen, aber begrenzten Hauptspeicher geschickt einsetzen um einen langsamen, aber kostengünstigeren Massenspeicher zu durchsuchen.

Die Unterscheidung zwischen den beiden Suchverfahren ist sehr wichtig für produktiv eingesetzte Anwendungen, in der Berechung der Zeitaufwände ist sie nicht hilfreich!

Ein Festplattenspeicher mag etwa 1000 mal langsamer als der Hauptspeicher eines Rechners sein. Dies macht bei einer echten Anwendung einen großen Unterschied. Bei der Berechnung der Zeitkomplexität wird dieser Faktor jedoch als "unbedeutend" weggekürzt.

Suchverfahren

Sequentielle Suche (lineare Suche)

 Die sequentielle Suche beruht auf dem naiven Ansatz einen Datenbestand vollständig zu durchsuchen bis das passende Element gefunden wird. Dieses Verfahren nennt man auch erschöpfende Suche oder im englischen das Greedy-Schema (engl. für gefräßig). Der Algorithmus für das Durchsuchen eines Felds ist der folgende:

  1. Spezifiziere Suchschlüssel
  2. Vergleiche jedes Element mit dem Suchschlüssel
    1. Erfolgsfall: Liefere Indexposition beim ersten Auffinden des Objekts und beende das Suchen
    2. Kein Erfolg: Gehe zum nächsten Objekt und vergleiche es.
  3. Gib eine Meldung über das erfolglose Suchen zurück wenn das Ende des Datenbestands erreicht wurde ohne das, das Objekt gefunden wird. Dies kann eine java-Ausnahme sein oder ein spezieller Rückgabewert (z.Bsp .1)

Der Aufwand für diesen Algorithmus ist linear, also O(n). Die Anzahl der nötigen Vergleichsoperationen hängt direkt von der Anzahl der Elemente im Datenbestand ab. Im statischen Mittel sind dies n/2 Vergleiche.

Sequentielle Suche in einem unsortierten Datenbestand

In diesem Fall muss bei jeder Suche der gesamte Datenbestand durchlaufen werden bis ein Element gefunden wurde

Beispiel einer Javaimplementierung

Die Methode sequentielleSuche() durchsucht ein Feld von 32 Bit Ganzzahlen nach einem vorgegebenen Schlüssel schluessel und gibt den Wert -1 zurück wenn kein Ergebnis gefunden wird.

public static int sequentielleSuche() (int[] feld, int schluessel) {
   int suchIndex = 0;
      while (suchIndex< feld.length && feld[suchIndex] != schluessel) suchIndex++;
      if (suchIndex == feld.length) suchindex = -1; // Nichts gefunden
   return suchindex;
}

Sequentielle Suche in einem sortierten Datenbestand

Bei einem sortierten Datenbestand kann man die Suche optimieren in dem man sie abbricht so bald man weiß, das die folgenden Elemente alle größer bzw. kleiner als das gesucht Element sind. Man muss also nicht das Feld vollständig durchlaufen. Hierdurch lässt sich Zeit sparen. Der durchschnittliche Aufwand von n/2 bleibt jedoch ein linearer Aufwand.

Beispiel einer Javaimplementierung

Die Methode sequentielleSuche() durchsucht ein Feld von 32 Bit Ganzzahlen nach einem vorgegebenen Schlüssel schluessel und gibt den Wert  -1 zurück wenn kein Ergebnis gefunden wird.

public static int sequentielleSuche() (int[] feld, int schluessel)
   int suchIndex = 0;
   while (suchIndex< feld.length && feld[suchIndex] < schluessel) suchIndex++;
      if ((suchindex < feld.length) && (feld[suchIndex] == feld[schluessel])) 
           return suchindex; // Erfolg
      else return -1; //kein Erfolg
}

Binäre Suche

Die binäre Suche erfolgt nach dem "Teile und Herrsche" Prinzip (divide and impera) durch Teilen der zu durchsuchenden Liste.

Voraussetzung: Die Folge muss steigend oder fallend sortiert sein!

Der Algorithmus lässt sich sehr gut rekursiv beschreiben:

Suche in einer sortierten Liste L nach einem Schlüssel k:

  • Beende die Suche erfolglos wenn die Liste leer ist
  • Nimm das Element auf der mittleren Position m der Liste und Vergleiche es mit dem Schlüssel
    • Falls der Schlüssel k kleiner als dasElement L[m] is (k<L[m]) durchsuche die linke Teilliste bis zum Element L[m]
    • Falls der Schlüssel k größer als dasElement L[m] is (k>L[m]) durchsuche die rechte Teilliste vom Element L[m] bis zum Ende
    • Beende die Suche wenn der Schlüssel k gleich L[m] ist (k=L[m])

Das binäre Suchen ist ein Standardverfahren der Informatik da es sehr effizient ist. Der Aufwand beträgt selbst im ungünstigsten Fall O(N)=log2(N).

Im günstigsten Fall ist der Aufwand O(N)=1 da eventuell der gesuchte Schlüssel sofort gefunden wird.

Beispiel einer binären Suche

Das folgende Feld hat 12 Elemente zwischen 1 und 23. Es wird ein Element mit dem Wert 15 gesucht. Zu Beginn ist das Suchintervall das gesamte Feld von Position 0 (links) bis 11 (rechts). Der Vergleichswert (mitte) wird aus dem arithmetischen Mittel der Intervallgrenzen berechnet.

Beispielimplementierung in Java

Die Methode binaerSuche() sucht einen Kandidaten in einem aufsteigend sortierten Feld von Ganzzahlen. Das Hauptprogramm erzeugt ein Feld mit der Größe 200 und aufsteigenden Werten

public class Binaersuche {
int[] feld; /** * * @param feld: Das zu durchsuchende Feld * @param links: linker Index des Intervalls * @param rechts: rechter Index des Intervalls * @param kandidat: der zu suchende Wert */

static void binaerSuche(int[] feld, int links,int rechts, int kandidat) {
int mitte;
do{
System.out.println("Intervall [" + links +
"," + rechts + "]");

mitte = (rechts + links) / 2;
if(feld[mitte] < kandidat){
links = mitte + 1;
} else {
rechts = mitte - 1;
}
} while(feld[mitte] != kandidat && links <= rechts);
if(feld[mitte]== kandidat){
System.out.println("Position: " + mitte);
} else {
System.out.println("Wert nicht vorhanden!");
}
}


public static void main(String[] args) {
int groesse=200;
int[] feld = new int[groesse];
for (int i=0; i<feld.length;i++)
feld[i] = 2*i; //Feld besteht aus geraden Zahlen
System.out.println("Suche feld["+ 66 + "]=" + feld[66]);
binaerSuche(feld, 0,(feld.length-1), feld[66]);
}
}

Programmausgabe auf Konsole:

Suche feld[66]=132
Intervall [0,199]
Intervall [0,98]
Intervall [50,98]
Intervall [50,73]
Intervall [62,73]
Intervall [62,66]
Intervall [65,66]
Intervall [66,66]
Position: 66

Weiterführe Quellen und Beispiele

Beispielanwendung zum Suchen

 Das folgende Programm zeigt eine sequentielle und eine binäre Suche in einem Feld mit zufälligen Werten.

Die Variable groesse erlaubt es die Größe des Felds zu verändern. Mit dem Nanotimer werden die benötigten Zeiten gemessen. Vergleichen Sie die Zeitaufwände für das sequentielle und das binäre Suchen mit dem Zeitaufwand für das Sortieren mit der Hilfklasse Arrays!

package Kurs2.Sort;
import java.util.Arrays;
/**
*
* @author sschneid
* @version 1.0
*/
public class Suchen {
public static int[] feld;
public static int groesse = 10000000;
/**
* Hauptprogramm
* @param args
*/
public static void main(String[] args) {
erzeugeFeld();
int suchwert =feld[groesse-2];
System.out.println("Suche feld["+ (groesse-2) +"] = " + suchwert);
sucheSequentiell(suchwert);
sortiere();
binaereSucheSelbst(suchwert);
binaereSuche(suchwert);

}
/**
* Erzeuge ein Feld mit Zufallszahlen
*/
public static void erzeugeFeld() {
feld = new int[groesse];
for (int i=0; i<feld.length;i++) {
feld[i]=(int)(Math.random() * (double)Integer.MAX_VALUE);
}
System.out.println("Feld mit "+ groesse + " Elementen erzeugt.");
}
/**
* Suche sequentiell einen Wert in einem unsortierten Feld
* @param suchwert der gesuchte Wert
*/
public static void sucheSequentiell(int suchwert) {
System.out.println("Sequentielle Suche nach Schlüssel");
long t = System.nanoTime();
int i=0;
while ( (i<groesse) && (suchwert != feld[i])) i++;
t = (System.nanoTime() -t)/1000000;
if (i== groesse) {
System.out.println(" Der Wert: " + suchwert +
" wurde nicht gefunden");
}
else {
System.out.println(" Der Suchwert wurde auf Position " + i +
" gefunden");
}
System.out.println(" Dauer sequentielle Suche: " + t +"ms");
}
/**
* Sortiere ein Feld mit der Klasse Arrays und messe die Zeit
* @param suchwert der gesuchte Wert
*/
public static void sortiere() {
System.out.println("Sortieren mit Arrays.sort()");
long t = System.nanoTime();
Arrays.sort(feld);
t = (System.nanoTime() -t)/1000000;
System.out.println(" Feld sortiert in " + t +" ms");
}
/**
* Suche binär einen Wert in einem sortierten Feld
* @param suchwert der gesuchte Wert
*/
public static void binaereSucheSelbst(int suchwert) {
System.out.println("Selbstimplementierte binäre Suche");
long t = System.nanoTime();
int intervall = (feld.length+1)/2;
int pos = intervall;
while ((intervall > 1) && (feld[pos] != suchwert)) {
intervall =(intervall+1)/2;
if ((feld[pos] > suchwert)) {pos -= intervall;}
else {pos += intervall;}
}
t = (System.nanoTime() -t);
if (feld[pos]== suchwert) {
System.out.println(" Der Suchwert wurde auf Position " +
pos +" gefunden");
}
else {
System.out.println(" Der Wert: " + suchwert +
" wurde nicht gefunden");
}
System.out.println(" Dauer binäre Suche " + (t/1000000) +"ms" +
" (" + t + " ns)");
}
/**
* Suche binär einen Wert in einem sortierten Feld
* Nutze die binäre Suchmethode der Klasse Arrays
* @param suchwert der gesuchte Wert
*/
public static void binaereSuche(int suchwert) {
System.out.println("Binäre Suche mit Arrays.binarySearch()");
long t = System.nanoTime();
int pos = Arrays.binarySearch(feld, suchwert);
t = (System.nanoTime() -t);
System.out.println(" Der Suchwert wurde auf Position " +
pos +" gefunden");
System.out.println(" Dauer binäre Suche " + (t/1000000) +
"ms" + " (" + t + " ns)");
}

}

Typische Konsolenausgabe:

Feld mit 10000000 Elementen erzeugt.
Suche feld[9999998] = 1719676120
Sequentielle Suche nach Schlüssel
Der Suchwert wurde auf Position 9999998 gefunden
Dauer sequentielle Suche: 28ms
Sortieren mit Arrays.sort()
Feld sortiert in 1296 ms
Selbstimplementierte binäre Suche
Der Suchwert wurde auf Position 8008593 gefunden
Dauer binäre Suche 0ms (6000 ns)
Binäre Suche mit Arrays.binarySearch()
Der Suchwert wurde auf Position 8008593 gefunden
Dauer binäre Suche 0ms (11000 ns)

 

Sortieralgorithmen

In diesem Abschnitt gehen wir davon aus, dass die zu sortierenden Datensätze in einem Feld f der Größe N in aufsteigender Reihenfolge sortiert werden. Die Feldelemente sollen in aufsteigender Reihenfolge sortiert werden.

Das Feld f dient als Eingabe für die Folge des Sortierverfahrens sowie als Ausgabedatenstruktur. Die Elemente müssen also innerhalb des Folge umsortiert werden. Nach dem Sortieren gilt für das Feld f:

f[1].wert <= f[2].wert ...<=f[N].wert

Beispiel

Unsortierte Folge mit N=7 Elementen:

Sortierte Folge mit N=7 Elementen

Bewertungskriterien

Bei der Bewertung der der Sortieralgorithmen wird jeweils in Abhängigkeit der Größe des zu sortierenden Feldes

  • die Anzahl der Schlüsselvergleiche C (englisch comparisons) und
  • die Anzahl der Bewegungen M (englisch: movements) der Elemente

gemessen.

Bei diesen zwei Parametern interessieren

  • der beste Fall (die wenigsten Operationen)
  • der schlechteste Fall (die meisten Operationen)
  • der durchschnittliche Fall

Schlüsselvergleiche sowie Bewegungen tragen beide zur Gesamtlaufzeit bei. Man wählt den größeren der beiden Werte um eine Abschätzung für die Gesamtlaufzeit zu erlangen.

Stabilität von Sortierverfahren

Ein weiteres Kriterium für die Bewertung von Sortiervorgängen ist die Stabilität.

Definition: Stabiles Sortierverfahren
Ein Sortierverfahren ist stabil wenn nach dem Sortieren die relative Ordnung von Datensätzen mit dem gleichen Sortierschlüssel erhalten bleibt.

Beispiel: Eine Folge von Personen die ursprünglich nach der Mitarbeiternummer (id) sortiert. Diese Folge soll mit dem Nachnamen als Sortierschlüssel sortiert werden.

Im folgenden Beispiel wurde ein stabiles Sortierverfahren angewendet. Die relative Ordnung der beiden Personen mit dem Namen "Schmidt" bleibt erhalten. Der Mitarbeiter mit der Nummer 18 bleibt vor dem Mitarbeiter mit der Nummer 21. 

Bei nicht stabilen Sortierverfahren bleibt diese relative Ordnung nicht unbedingt erhalten. Hier könnte eine sortierte Folge so aussehen:

nicht stabiles Sortierverfahren

Stabile Sortierverfahren erlauben das Sortieren nach mehreren Prioritäten da eine Vorsortierung erhalten wird.

Im oben gezeigten Beispiel kann man eine Folge

  • primär nach Nachnamen sortierten und
  • sekundär nach der Personalnummer (id).

Man sortiert die Personen zuerst nach dem Kriterium mit der niedrigen Priorität. Dies ist hier die Personalnummer. Diese Vorsortierung war schon gegeben. Anschließend sortiert man nach dem wichtigeren Kriterium, dem Nachnamen. Die so entstandene Folge ist primär nach Nachnamen sortieret, sekundär nach Personalnummer.

Beispielprogramme

 Die folgenden Beispielprogramme erlauben das Testen von unterschiedlichen Sortieralgorithmen.

Hinweis: Die Implementierungen der Sortieralgorithmen selbst sind in den Abschnitten der Algorithmen zu finden. Im Hauptprogramm MainSort kann man durch Instanziieren einen Sortieralgorithmus wählen.

Arbeitsanweisung

Kopieren, Bauen und Starten der Referenzimplementierung

  1. Legen Sie in Ihrer Entwicklungsumgebung das Paket Kurs2.Sort an
  2. Kopieren Sie die Quellen der unten aufgeführten Klassen (inklusive Trivialsort) in Ihre Entwicklungsumgebung. Achten Sie darauf, daß alle Klassen im Paket Kurs2.Sort angelegt werden.
  3. Übersetzen Sie alle Klassen
  4. Starten Sie Anwendung durch Aufruf der Klasse Kurs2.Sort.MainSort

Auf der Kommandozeile geschieht das mit dem Befehl

$ java Kurs2.Sort.MainSort

Die Konsolenausgabe dieser Anwendung ist:

Phase 1: Einfacher Test mit 6 Elementen
Algorithmus: Sortiere drei Werte
Unsortiert:
feld[0]=6
feld[1]=2
feld[2]=4
feld[3]=3
feld[4]=5
feld[5]=7
Zeit(ms): 0 Vergleiche: 2 Vertauschungen: 2. Fehler in Sortierung
Sortiert:
feld[0]=2
feld[1]=4
feld[2]=6
feld[3]=3
feld[4]=5
feld[5]=7
Keine Phase 2 (Stresstest) aufgrund von Fehlern...

Der Trivialsort hat leider nur die Schlüssel auf der Position 0 und 1 sortiert.

Effizienz des Algorithmus messen

Das Hauptprogramm MainSort wird bei einer korrekten Sortierung eines Testfelds mit 6 Werten automatisch in die zweite Phase eintreten.

Hier wird es ein Feld mit 5 Werten und Zufallsbelegungen Generieren und Sortieren.

Die Feldgröße wird dann automatisch verdoppelt und es wird eine neue Sortierung mit neuen Zufallswerten durchgeführt. Dies wird solange wiederholt bis ein Sortiervorgang eine voreingestellte Maximalzeit (3 Sekunden) überschritten hat.

Mit Hilfe dieser Variante kann man die benötigte Zeit pro Anzahl sortierter Elemente beobachten und die Effizienz des gewählten Algorithmus abschätzen.

Beim Sortieren durch Auswahl ergibt die die folgende Konsolenausgabe:

Phase 1: Einfacher Test mit 6 Elementen
Algorithmus: Sortieren durch Auswahl
Unsortiert:
feld[0]=6
feld[1]=2
feld[2]=4
feld[3]=3
feld[4]=5
feld[5]=7
Zeit(ms): 0 Vergleiche: 15 Vertauschungen: 5. Korrekt sortiert
Sortiert:
feld[0]=2
feld[1]=3
feld[2]=4
feld[3]=5
feld[4]=6
feld[5]=7
Phase 2: Stresstest
Der Stresstest wird beendet nachdem mehr als 3 Sekunden benötigt werden.
Maximalzeit(s): 3
Sortieren durch Auswahl. Feldgroesse: 10 Zeit(ms): 0 Vergleiche: 45 Vertauschungen: 9. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 20 Zeit(ms): 0 Vergleiche: 190 Vertauschungen: 19. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 40 Zeit(ms): 0 Vergleiche: 780 Vertauschungen: 39. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 80 Zeit(ms): 1 Vergleiche: 3160 Vertauschungen: 79. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 160 Zeit(ms): 5 Vergleiche: 12720 Vertauschungen: 159. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 320 Zeit(ms): 14 Vergleiche: 51040 Vertauschungen: 319. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 640 Zeit(ms): 14 Vergleiche: 204480 Vertauschungen: 639. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 1280 Zeit(ms): 19 Vergleiche: 818560 Vertauschungen: 1279. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 2560 Zeit(ms): 26 Vergleiche: 3275520 Vertauschungen: 2559. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 5120 Zeit(ms): 84 Vergleiche: 13104640 Vertauschungen: 5119. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 10240 Zeit(ms): 575 Vergleiche: 52423680 Vertauschungen: 10239. Korrekt sortiert
Sortieren durch Auswahl. Feldgroesse: 20480 Zeit(ms): 1814 Vergleiche: 209704960 Vertauschungen: 20479. Korrekt sortiert

Warnung

  • Diese zweite Phase funktioniert nicht mit derm Trivialsort. Hier werden nur zwei Vergleiche und zwei Vertauschnungen durchgeführtImplementieren der eigenen Algorithmen

 Nutzen Sie die vorgebenen Klassen zum Testender  eigener Algorithmen

  • Erzeugen Sie eine eigene Klasse für den Sortieralgorithmus
    • Sie können die Klasse TrivialSort übernehmen.
      • Ändern Sie den Klassennamen
      • Ändern Sie in der Methode algorithmus() den Namen des Algorithmus
      • Implementieren Sie die Methode sortieren(int von, int bis) die ein bestimmtes Intervall der Folge sortiert
  • Ändern Sie in der Klasse MainSort in der Methode algorithmusWahl() den benutzten Algorithmus.
  • Tipps: Nutzen Sie die Infrastruktur der Oberklasse Sortierer beim Testen
    • istKleiner(int a, int b), istKleinerGleich(int a, int b) : Vergleichen Sie die Werte des Feldes indem Sie bei diesen beiden Methoden den jeweiligen Index angeben. Es wird ein Zähler gepflegt der die Anzahl der gesamten Vergleiche beim Sortiervorgang zählt!
    • tausche(int i, int j): tauscht die Werte auf Index i mit dem Wert von Index j. Es wird ein Zähler gepflegt der die Anzahl der Vertauschungen zählt
    • tZaehler(): Inkrementiert die Anzahl der Vertauschungen falls nicht die Methode tausche() verwendet wurde. Dieser Zähler ist multi-threading (mt) save!
    • vglZaehler(): Inkrementiert die Anzahl der Vergleiche falls nicht die Methoden istKleiner() oder istKleinerGleich() verwendet werden. Dieser Zähler ist mt-save!
    • druckenKonsole() : druckt das Feld in seiner aktuellen Sortierung aus.
    • Tracing: Ihre Klasse mit dem Sortieralgorithmus erbt eine statische boolsche Variable geschwaetzig. Sie können diese Variable jederzeit setzem mit geschwaetzig=true. Es werden alle Vertauschungen und Vergleiche auf der Konsole ausgegeben:

Beim TrivialSort führt das Setzen dieser Variable zur folgenden Konsolenausgabe:

Phase 1: Einfacher Test mit 6 Elementen
Algorithmus: Sortiere drei Werte
Unsortiert:
feld[0]=6
feld[1]=2
feld[2]=4
feld[3]=3
feld[4]=5
feld[5]=7
Vergleich:feld[1]<feld[0] bzw. 2<6
Getauscht:feld[0<->1];f[0]=2;feld[1]=6
Vergleich:feld[2]<feld[1] bzw. 4<6
Getauscht:feld[1<->2];f[1]=4;feld[2]=6

Zeit(ms): 0 Vergleiche: 2 Vertauschungen: 2. Fehler in Sortierung
Sortiert:
feld[0]=2
feld[1]=4
feld[2]=6
feld[3]=3
feld[4]=5
feld[5]=7
Keine Phase 2 (Stresstest) aufgrund von Fehlern...

 

MainSort.java: Ein Testprogramm

Klasse MainSort.java

package Kurs2.Sort;
/**
*
* @author sschneid
* @version 2.0
*/
public class MainSort {
/**
*
* Das Hauptprogramm sortiert sechs Zahlen in Phase 1
* Phase 2 wird nur aufgerufen wenn der Sortieralgorithmus
* für die erste Phase korrekt war
* @param args
*/
public static void main(String[] args) {
int zeit = 3;
System.out.println("Phase 1: Einfacher Test mit 6 Elementen");
boolean erfolg = phase1();
if (erfolg) {
System.out.println ("Phase 2: Stresstest");
System.out.print("Der Stresstest wird beendet nachdem mehr ");
System.out.println("als " + zeit + " Sekunden benötigt werden.");
phase2(zeit);
}
else {System.out.println("Keine Phase 2 (Stresstest) " +
"aufgrund von Fehlern...");}
}
/**
* Auswahl des Sortieralgorithmus
* @ param das zu sortierende Feld
* @ return der Sortieralgorithmus
*/
public static Sortierer algorithmusWahl(int[] feld) {
Sortierer sort;
// Wähle ein Sortieralgorithmus abhängig von der
// gewünschten Implementierung
sort= new TrivialSort(feld);
//sort = new SelectionSort(feld);
//sort = new InsertionSort(feld);
//sort = new BubbleSort(feld);
//sort = new QuickSort(feld);
//sort = new QuickSortParallel(feld);
//sort = new HeapSort(feld);
//sort.geschwaetzig = true;
return sort;
}
/**
* Sortiere 6 Zahlen
* @return wurde die Folge korrekt sortiert?
*/
public static boolean phase1() {
long anfangszeit = 0;
long t = 0;
int[] gz = new int[6];
gz[0] = 6;
gz[1] = 2;
gz[2] = 4;
gz[3] = 3;
gz[4] = 5;
gz[5] = 7;
Sortierer sort = algorithmusWahl(gz);
System.out.println("Algorithmus: " + sort.algorithmus());
System.out.println("Unsortiert:");
sort.druckenKonsole();
anfangszeit = System.nanoTime();
sort.sortieren(0, gz.length - 1);
t = System.nanoTime() - anfangszeit;
System.out.print(
" Zeit(ms): " + t / 1000000
+ " Vergleiche: " + sort.anzahlVergleiche()
+ " Vertauschungen: " + sort.anzahlVertauschungen());
boolean erfolg = sort.validierung();
if (erfolg) {System.out.println(". Korrekt sortiert");}
else {System.out.println(". Fehler in Sortierung");}
System.out.println("Sortiert:");
sort.druckenKonsole();
return erfolg;
}
/**
* Sortieren von zufallsgenerierten Feldern bis eine maximale
* Zeit pro Sortiervorgang in Sekunden erreicht ist
* @param maxTime
*/
public static void phase2(int maxTime) {
// Maximale Laufzeit in Nanosekunden
long maxTimeNano = (long) maxTime * 1000000000L;
long t = 0;
// Steigerungsfaktor für Anzahl der zu sortierenden Elemente
double steigerung = 2.0; // Faktor um dem das Feld vergrößert wird
int anzahl = 5; // Größe des initialen Felds
long anfangszeit;
int[] gz;
Sortierer sort;
System.out.println("Maximalzeit(s): " + maxTime);
while (t < maxTimeNano) {
// Sortiere bis das Zeitlimit erreicht ist
anzahl = (int) (anzahl * steigerung);
// Erzeugen eines neuen Feldes
gz = new int[anzahl];
for (int i = 0; i < gz.length; i++) {
gz[i] = 1;
}
sort = algorithmusWahl(gz);
sort.generiereZufallsbelegung();
sort.zaehlerRuecksetzen();
anfangszeit = System.nanoTime();
sort.sortieren(0, gz.length - 1);
t = System.nanoTime() - anfangszeit;
System.out.print(
sort.algorithmus() +
". Feldgroesse: " + anzahl + " Zeit(ms): " + t / 1000000 +
" Vergleiche: " + sort.anzahlVergleiche() +
" Vertauschungen: " + sort.anzahlVertauschungen());
if (sort.validierung()) {System.out.println(". Korrekt sortiert");}
else {System.out.println(". Fehler in Sortierung");}
sort.zaehlerRuecksetzen();
}
}
}

Sortierer.java

 Die Klasse Sortierer ist eine abstrakte Klasse die die Infrastruktur zum Sortieren zur Verfügung stellt.

  • Sie verwaltet ein Feld von ganzen Zahlen (int).
  • Sie stellt Operationen zum Vergleichen und Tauschen von Elementen zur Verfügung
  • Sie hat Zähler für durchgeführte Vertauschungen
  • Sie kann ein Feld auf korrekte Sortierung prüfen
  • Sie kann das Feld mit neuen Zufallszahlen belegen
  • Sie hat eine statische Variable geschwaetzig mit der man bei Bedarf jeden Vergleich und jede Vertauschung auf der Kommandozeile dokumentieren kann.
  • Die Klasse verwendete mt-sichere Zähler vom Typ AtomicInteger bei parallelen Algorithmen. Diese Zähler sind langsamer aber Sie garantieren ein atomares Inkrement.

Implementierung

package Kurs2.Sort;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* @author sschneid
* @version 2.1
*/
public abstract class Sortierer {
/**
* Das zu sortierende Feld
*/
protected int[] feld;
private int maxWert = Integer.MAX_VALUE;
// Zähler für serielle Sortieralgorithmen
private long tauschZaehlerSeriell;
private long vergleichszaehlerSeriell;
// Zähler für paralelle Sortieralgorithmen
private AtomicInteger tauschZaehlerParallel;
private AtomicInteger vergleichszaehlerParallel;
/**
* Der Algorithmus arbeitet parallel
*/
private final boolean parallel;
/**
* erweiterte Ausgaben beim Sortieren
*/
public static boolean geschwaetzig = false;
/**
* Initialisieren eines Sortierers mit einem
* unsortierten Eingabefeld s
* @param s ein unsortiertes Feld
* @param p der Algorithmus is parallel implementiert
*/
public Sortierer(int[] s, boolean p) {
feld = s;
parallel = p;
if (parallel) {
tauschZaehlerParallel = new AtomicInteger();
vergleichszaehlerParallel = new AtomicInteger();
}
else {
tauschZaehlerSeriell = 0;
vergleichszaehlerSeriell = 0;
}
}
/**
* die Groesse des zu sortierenden Feldes
*/
public int feldgroesse() {
return feld.length;
}
/**
* Gibt ein Feldelement auf gegebenem Index aus
* @param index
* @return
*/
public int getElement(int index) {
return feld[index];
}
/**
* sortiert das Eingabefeld
* @param s ein unsortiertes Feld
*/
abstract public void sortieren(int startIndex, int endeIndex);
/**
* Eine Referenz auf das Feld
* @return
*/
public int[] dasFeld() {
return feld;
}
/**
* kontrolliert ob ein Feld sortiert wurde
* @return
*/
public boolean validierung() {
boolean korrekt;
int i = 0;
while (((i + 1) < feld.length) &&
(feld[i]<=feld[i + 1])) {i++;}
korrekt = ((i + 1) == feld.length);
return korrekt;
}
/**
* Liefert den Namen des implementierten Sortieralgorithmus
* @return
*/
abstract public String algorithmus();
/**
* Drucken des Feldes auf System.out
*/
public void druckenKonsole() {
for (int i = 0; i < feld.length; i++) {
System.out.println("feld[" + i + "]=" + feld[i]);
}
}
/**
* Anzahl der Vertauschungen die zum Sortieren benoetigt wurden
* @return Anzahl der Vertauschungen
*/
public long anzahlVertauschungen() {
if (parallel) return tauschZaehlerParallel.get();
else return tauschZaehlerSeriell;
}
/**
* Anzahl der Vergleiche die zum Sortieren benoetigt wurden
* @return Anzahl der Vergleiche
*/
public long anzahlVergleiche() {
if (parallel) return vergleichszaehlerParallel.get();
else return vergleichszaehlerSeriell;
}
/**
* vergleicht zwei Zahlen a und b auf Größe
* @param a
* @param b
* @return wahr wenn a kleiner b ist
*/
public boolean istKleiner(int a, int b) {
vglZaehler();
if (geschwaetzig) {
System.out.println("Vergleich:feld["+a+"]<feld["+b+"] bzw. " +
feld[a] +"<"+ feld[b]);}
return (feld[a]<(feld[b]));
}
/**
* vergleicht zwei Zahlen a und b auf Größe
* @param a
* @param b
* @return wahr wenn a kleiner oder gleich b ist
*/
public boolean istKleinerGleich(int a, int b) {
vglZaehler();
if (geschwaetzig) {
System.out.println("Vergleich:feld["+a+"]<=feld["+b+"] bzw. " +
feld[a] +">"+ feld[b]);}
return (feld[a]<=(feld[b]));
}
/**
* diese Methode zaehlt alle Vergleiche. Sie ist mt-sicher
* für alle Algorithmen die das parallel Flag gesetzt haben
*/
public void vglZaehler() {
if (parallel) vergleichszaehlerParallel.getAndIncrement();
else vergleichszaehlerSeriell++;
}
/**
* Tausche den Inhalt der Position a mit dem Inhalt der Position b
* @param a
* @param b
*/
public void tausche(int a, int b) {
tZaehler();
if (geschwaetzig)
System.out.println("Getauscht:feld["+a+"<->"+b+"];f["+a
+ "]="+feld[b]+";feld["+b+"]="+feld[a]);
int s = feld[a];
feld[a] = feld[b];
feld[b] = s;
}
/**
* diese Methode zaehlt alle Vertauschungen. Sie ist mt-sicher
* für alle Algorithmen die das parallel Flag gesetzt haben
*/
public void tZaehler() {
if (parallel) tauschZaehlerParallel.getAndIncrement();
else tauschZaehlerSeriell++;
}
/**
* Belege das Feld mit Zufallswerten. Alte Belegungen werden gelöscht!
*/
public void generiereZufallsbelegung() {
// Generiere neue Belegung
maxWert = 2 * feld.length;
for (int i = 0; i < feld.length; i++) {
feld[i]=(int)(Math.random() * (double) maxWert);
}
}
/**
* Setzt Zaehler für Vertauschungen und Vergleiche zurueck auf Null
*/
public void zaehlerRuecksetzen() {
if (parallel) {
tauschZaehlerParallel.set(0);
vergleichszaehlerParallel.set(0);
}
else {
tauschZaehlerSeriell = 0;
vergleichszaehlerSeriell = 0;
}
}
}

 

TrivialSort.java

 Diese Klasse ist ein Platzhalter der nur die beiden ersten Elemente eines Intervalls sortieren kann.

Das Programm dient zum Testen des Beispiels und es zeigt den Einsatz der schon implementierten istKleiner() und tausche() Methoden die von der Oberklasse geerbt werden.

package Kurs2.Sort;
/**
*
* @author sschneid
* @version 2.0
*/
public class TrivialSort extends Sortierer{
/**
* Konstruktor: Akzeptiere ein Feld von int. Reiche
* das Feld an die Oberklasse weiter.
* Der Algorithmus ist nicht parallel (false Argument)
* @param s
*/
public TrivialSort(int[] s) { super(s,false); }
/**
* Diese Methode sortiert leider nur die beiden ersten Elemente
* auf der Position von und (von+1)
* @param von
* @param bis
*/
@Override
public void sortieren(int von, int bis) {
geschwaetzig = false;
int temp; // Zwischenspeicher
if (istKleiner(von+1,von)) {
tausche(von,von+1);
}
if (istKleiner(von+2,von+1)) {
tausche(von+1,von+2);
}
//druckenKonsole();
}
@Override
public String algorithmus() {
return "Sortiere drei Werte";
}
}

 

Sortieren durch Auswahl (Selection Sort)

Beim Suchen durch Auswahl durchsucht man das jeweilige Feld nach dem kleinsten Wert und tauscht den Wert an der gefundenen Position mit dem Wert an der ersten Stelle.

Anschließend sucht man ab der zweiten Position aufsteigend nach dem zweitkleinsten Wert und tauscht diesen Wert mit dem Wert der zweiten Position.

Man fährt entsprechend mit der dritten und allen weiteren Positionen fort bis alle Werte aufsteigend sortiert sind.

Verfahren

  • Man suche im Feld f[1] bis f[N] die Position i1 an der sich das Element mit dem kleinsten Schlüssel befindet und vertausche f[1] mit f[i1]
  • Man suche im Feld f[2] bis f[N] die Position i2 an der sich das Element mit dem kleinsten Schlüssel befindet und vertausche f[2] mit f[i2]
  • Man verfahre wie oben weiter für Position i3 bis iN-1

Beispiel

Ausgangssituation: Ein Feld mit 7 unsortierten Ganzzahlwerten (N=7)

Die Zahl 12 auf Position 5 ist die kleinste Zahl im Feld f[1] bis f[7]. Somit ist i1=5. Nach Vertauschen von f[1] mit f[5] ergibt sich:

Die Zahl 13 auf Position 4 ist die kleinste Zahl im Feld f[2] bis f[7]. Somit ist i2=4. Nach Vertauschen von f[2] mit f[4] ergibt sich:

Die Zahl 18 auf Position 5 ist die kleinste Zahl im Feld f[3] bis f[7]. Somit ist i3=5. Nach Vertauschen von f[3] mit f[5] ergibt sich:

Das Verfahren wird entsprechend weiter wiederholt bis alle Werte sortiert sind.

Aufwand

Der minimale Aufwand Mmin(N), der maximale Aufwand Mmax(N) und er mittlere Aufwand Mmit(N) ist bei diesem naiven Verfahren gleich, da immer die gleiche Anzahl von Operationen (N-1) ausgeführt wird. Der Faktor 3 ensteht dadurch, dass bei einer Vertauschung zweier Werte insgesamt drei Operationen braucht:

 

Die Anzahl der Vergleiche sind die Summe von n-1, n-1 etc. unabhängig davon ob das Feld sortiert oder unsortiert ist:

 

Selectionsort: Implementierung in Java

Die folgende Implementierung implementiert die abstrakte Klasse Sortierer die in den Beispielprogrammen zum Sortieren zu finden ist.

Implementierung: Klasse Selectionsort

package Kurs2.Sort;
/**
*
* @author sschneid
* @version 2.0
*/
public class SelectionSort extends Sortierer{
/**
* Konstruktor: Akzeptiere ein Feld von int. Reiche
* das Feld an die Oberklasse weiter.
* Der Algorithmus ist nicht parallel (false Argument)
* @param s
*/
public SelectionSort(int[] s) {super(s,false);}
/**
* sortiert ein Eingabefeld s und gibt eine Referenz auf dea Feld wieder
* zurück
* @param s ein unsortiertes Feld
* @return ein sortiertes Feld
*/
@Override
public void sortieren(int startIndex, int endeIndex){
//geschwaetzig=true;
int minimumIndex;
for (int unteresEnde=startIndex; unteresEnde<endeIndex; unteresEnde++) {
minimumIndex = unteresEnde; //Vergleichswert
// Bestimme Position des kleinsten Element im Intervall
for (int j=unteresEnde+1; j<=endeIndex; j++) {
if (istKleiner(j,minimumIndex)) {
minimumIndex=j; // neuer Kandidat
}
}
// Tausche kleinstes Element an den Anfang des Intervalls
tausche(unteresEnde,minimumIndex);
// das kleinste Element belegt jetzt das untere Ende des Intervalls
}
}
/**
* Liefert den Namen des SelectionSorts
* @return
*/
@Override
public String algorithmus() {return "Sortieren durch Auswahl";}
}

 

Sortieren durch Einfügen (Insertion Sort)

Verfahren

Das Sortieren durch Einfügen ist ein intuitives und elementare Verfahren:

  • Man teilt die zu sortierende Folge f mit N Elementen in eine bereits sortierte Teilfolge f[0] bis f[s-1] und in eine unsortierte Teilfolge f[s] bis f[n].
    • Zu Beginn ist die sortierte Teilfolge leer.
    • Der Beginn der unsortierten Folge f[s] wird die Sortiergrenze genannt.
  • Man setzt eine Sortiergrenze hinter der bereits durch Zufall sortierte Teilfolge f[1] bis f[s-1]
  • Vergleiche f[s] mit f[s-1] und vertausche die beiden Elemente falls f[s]<f[s-1]
    • Führe weitere Vergleiche und absteigende Vertauschungen durch bis man ein f[s-j] gefunden hat für das gilt f[s-j-1]<f[s-j]
  • Erhöhe die Sortiergrenze von f[i] auf f[i+1] und wiederhole die Vergleichs- und Vertauschungsschritte wie im vorhergehenden Schritt beschrieben.
  • Die Folge ist sortiert wenn die Sortiergrenze das Ende des Folge erreicht hat.

Beispiel

Initiale teilsortierte Folge Gegeben sei eine Folge in einem Feld mit sieben Elementen. Die ersten drei Elemente sind bereits sortiert und die Sortiergrenze wird mit 4 belegt. Die unsortierte Folge beginnt auf dem Feldindex vier.
Initiale teilsortierte Folge  Als erstes wird der Wert f[4]=20 mit f[3]=35 verglichen. Da f[4] größer als f[3] ist, werden die beiden Werte getauscht.
Initiale teilsortierte Folge Anschließend folgt der Vergleich von f[3]=20 mit f[2]=23. Der Wert von f[3] ist größer als f[2]. Die beiden Werte werden getauscht. 
Initiale teilsortierte Folge Der Vergleich von f[2] mit f[1] zeigt, dass f[2] größer als f[1] ist. f[2]=20 hat also seine korrekte Position in der sortierten Teilfolge gefunden.
Initiale teilsortierte Folge Der aktuelle Durchlauf ist hiermit beendet. Die Sortiergrenze wird um eins erhöht und erhält den Wert 5.
Initiale teilsortierte Folge Die sortierte Teilfolge ist um eins größer geworden. Es wird wieder f[5]=17 mit f[4]=35 verglichen. Da f[5] größer als f[4] ist, werden die beiden Werte getauscht. Jeder Durchlauf endet, wenn der Wert nicht mehr getauscht werden muss. Der Algorithmus endet wenn die Sortiergrenze die gesamt Folge umfasst.

 

Aufwand

Das Sortieren durch Einfügen braucht im günstigsten Fall nur N-1 Vergleiche für eine bereits sortierte Folge. Im ungünstigsten Fall (fallend sortiert) ist aber ein quadratisches Aufwand nötig.

Das gleiche gilt für die Vertauschungen. Hier müssen im ungünstigsten Fall Vertauschungen in einer quadratischen Größenordnung durchgeführt werden.

Insertionsort: Implementierung in Java

 Die folgende Implementierung implementiert die abstrakte Klasse Sortierer die in den Beispielprogrammen zum Sortieren zu finden sind.

Implementierung: Klasse InsertionSort

package Kurs2.Sort;
/**
*
* @author sschneid
* @version 2.0
*/
public class InsertionSort extends Sortierer {
/**
* Konstruktor: Akzeptiere ein Feld von int. Reiche
* das Feld an die Oberklasse weiter.
* Der Algorithmus ist nicht parallel (false Argument)
* @param s
*/
public InsertionSort(int[] s) {
super(s,false);
}
/**
* sortiert ein Feld im Bereich startIndex bis endeIndex
* @param startIndex
* @param endeIndex
*/
public void sortieren(int startIndex, int endeIndex) {
for (int sortierGrenze = startIndex;sortierGrenze < endeIndex;
sortierGrenze++) {
int probe = sortierGrenze + 1;
int j = startIndex;
while (istKleiner(j, probe)) {
j++;
}
// Verschiebe alles nach rechts.
// Tausche den Probenwert gegen den unteren Nachbarn
// bis man bei der Position j angekommen ist
for (int k = probe - 1; k >= j; k--) {
tausche(k, k + 1);
}
}
}
/**
* Liefert den Namen des Insertion Sorts
* @return
*/
public String algorithmus() {
return "Sortieren durch Einfuegen";
}
}

Bubblesort

Der Bubblesort hat seinen Namen von dem Prinzip erhalten, dass der größte Wert eine Folge durch Vertauschungen in der Folge austeigt wie eine Luftblase im Wasser.

Verfahren

  • Vergleiche in aufsteigender Folge die Werte einer unsortierten Folge und vertausche die Werte wenn f(i) > f(i+1) ist
  • Wiederhole die Vergleiche und eventuell nötige Vertauschungen bis keine Vetauschung bei einem Prüfdurchlauf vorgenommen werden muss.

Beispiel

Es werden die Werte aufsteigen auf ihre korrekte Reihenfolge verglichen. Im ersten "Durchlauf" müssen die Werte 19 und 15 (in grau) getauscht werden. Anschließend werden die Werte 28 und 23 getauscht und im nächsten Schritt die Werte 28 und 17. Die letzte Vertauschung findet zwischen 28 auf Position 5 und 21 auf Position 6 statt. Der  erste Durchlauf ist beendet und der zweite Durchlauf beginnt mit den Vergleichen ab Index 1.
Im zweiten Durchlauf sind die Werte auf Index 1 und 2 in der richtigen Reihenfolge. Die Werte 23 und 17 sind die ersten die getauscht werden müssen. Der Wert 23 steigt weiter auf und wird noch mit dem Wert 21 getauscht. Die letzten beiden Werte auf Index 6 und 7 sind in der richtigen Reihenfolge und müssen nicht getauscht werden. Der zweite Durchlauf ist beendet.
Im dritten Durchlauf muß die 19 auf Position 2 mit der 17 auf Position 3 getauscht werden. Auf den höheren Positionen sind keine Veratuschungen mehr nötig.

Der vierte Durchlauf ist der letzte Durchlauf. In ihm finden keine weiteren Vertauschungen statt. Alle Elemente sind aufsteigend sortiert. Der Algorithmus bricht nach dem vierten Durchlauf ab, da keine Vertauschungen nötig waren. Alle Elemente sind sortiert.

Aufwand

Der günstigste Fall für den Bubblesort ist eine sortierte Folge. In diesem Fall müssen nur N-1 Vergleiche und keine Vertauschungen vorgenommen werden.

Ungünstige Fälle sind fallende Folgen. Findet man das kleinste Element auf dem höchsten Index sind N-1 Durchläufe nötigt bei denen das kleinste Element nur um eine Position nach vorne rückt. 

In ungünstigen Fällen ist der Aufwand N2. Man kann zeigen, dass der mittlere Aufwand bei N2 liegt:

Effizienzsteigerungen

Es gibt eine Reihe von Verbesserungen die darauf beruhen, dass man nicht bei jedem Durchlauf in der ganzen Folge die Nachbarn vergleichen muss. Nach dem ersten Durchlauf ist das größte Element auf dem Index N angekommen. Man muss im nächsten Durchlauf also nur noch alle Nachbarn bis zur Position N-1 kontrollieren.

BubbleSort: Implementierung in Java

 Die folgende Implementierung implementiert die abstrakte Klasse Sortierer die in den Beispielprogrammen zum Sortieren zu finden sind.

Implementierung: Klasse BubbleSort

package Kurs2.Sort;
/**
*
* @author sschneid
* @version 2.0
*/
public class BubbleSort extends Sortierer {
/**
* Konstruktor: Akzeptiere ein Feld von int. Reiche
* das Feld an die Oberklasse weiter.
* Der Algorithmus ist nicht parallel (false Argument)
* @param s
*/
public BubbleSort(int[] s) {super(s, false); }
/**
* sortiert ein Eingabefeld s und gibt eine Referenz auf dea Feld wieder
* zurück
* @param s ein unsortiertes Feld
* @return ein sortiertes Feld
*/
public void sortieren(int startIndex, int endeIndex) {
boolean vertauscht;
do {
vertauscht = false;
for (int i = startIndex; i+1 <= endeIndex; i++) {
if (istKleiner(i + 1,i)) {
tausche(i,i+1);
vertauscht = true;
}
}
} while (vertauscht);
}
/**
* Liefert den Namen des Bubble Sorts
* @return
*/
public String algorithmus() {
return "Bubble Sort";
}
}

 

Quicksort: Sortieren durch rekursives Teilen

Der Quicksort ist ein Verfahren welches auf dem Grundprinzip des rekursiven Teilens basiert:

  • Man nimmt ein beliebiges Pivotelement als Referenz (Pivotpunkt: Drehpunkt, Angelpunkt)
  • Man teilt die unsortierte Folge in drei Unterfolgen
    • Alle Elemente die kleiner oder gleich dem Pivotelement sind
    • Das Pivotelement
    • Alle Element die größer oder gleich dem Pivotelement sind

Die Unterfolgen die größer bzw. kleiner als das Pivotelement sind, sind selbst noch unsortiert. Das Pivotelement steht bereits an der richtigen Position zwischen den zwei Teilfolgen.

Durch rekursives Sortieren werden auch sie sortiert.

Der Quicksort wurde 1962 von C.A.R. Hoare (Computer Journal) veröffentlicht.

Verfahren

  • Folgen die aus keinem oder einem Element bestehen bleiben unverändert.
  • Wähle ein Pivotelement p der Folge F und teile die Folge in Teilfolgen F1 und F2 derart das gilt:
    • F1 enhält nur Elemente von F ohne das Pivotelement p die kleiner oder gleich p sind
    • F2 enhält nur Elemente von F ohne das Pivotelement p die größer oder gleich p sind
  • Die Folgen F1 und F2 werden rekursiv nach dem gleichen Prinzip sortiert
  • Die Ergebnis der Sortierung ist eine Aneinanderreihung der der Teilfolgen F1, p, F2.

Anmerkung: Der Quicksort folgt dem allgemeinen Prinzip von "Teile und herrsche" (lat.: "Divide et impera", engl. "Divide and conquer")

Beispiel

Im ersten Schritt wird das vollständige Feld vom Index 1 bis 7 sortiert, indem der Wert 23 auf dem Index 7 als  Pivotwert (Vergleichswert) gewählt.

Es wird nun vom linken Ende des Intervals (Index 1) aufsteigend das erste Element gesucht welches größer als das Pivotelement ist. Hier ist es der Wert 39 auf dem Index 2. Anschließend wird vom oberen Ende des Intervals von der Position links vom Pivotelement absteigend das erste Element gesucht welches kleiner als das Pivotelement ist. Hier ist es der Wert 19 auf dem Index 6.

Im nächsten Schritt werden die beiden gefundenen Werte getauscht und liegen nun in der "richtigen" Teilfolge.

Jetzt wird die Suche von links nach rechts weitergeführt bis das nächste Element gefunden ist welches größer als das Pivotelement ist. Hier ist es 35 auf Position 3. Die fallende Suche von Rechts endet bei dem Wert 17 auf Index 5. Die beiden Werte werden dann getauscht.

Nach der Vertauschung wird die aufsteigende Suche von Links weitergeführt und endet beim Wert 28 auf dem Index 4. Die fallende Suche von Rechts endet sofort da der Suchindex der steigenden Suche erreicht ist.

Der Pivotwert 23 auf Index 7 wird jetzt mit 28 auf dem Index  getauscht und hat seine korrekte Endposition erreicht.

Der Sortiervorgang wird jetzt rekursiv im Intervall 1 bis 3 und 5 bis 7 fortgeführt. Wie zuvor werden die Werte auf dem höchsten Index der Unterintervalle wieder als Pivotwerte gewählt.

Im linken Unterintervall endet die aufsteigende Suche schnell beim Wert 19 auf Index 2 der dann mit dem Pivotwert 17 getauscht wird. Im rechten Unterintervall muss bei der aufsteigenden suche der Wert 35 auf Index 5 mit dem Pivotwert 28 getauscht werden.

Bei den weiteren Teilungen in Unterintervalle bleiben nur noch das Intervall von Index 1 bis 2 sowie das Intervall von Index 6 bis 7 übrig. Hier werden wieder die Elemente am rechten Rand als Pivotwerte gewählt.

Im linken Unterintervall durchläuft die aufsteigende Suche das einzige Element und endet ohne Vertauschung. Im rechten Intervall muss ein letztes mal der Wert 39 mit 35 getauscht werden.

Aufwand

Der Quicksort ist in normalen Situationen mit einem Aufwand von n*log(n) sehr gut für große Datenmengen geeignet. Er ist einer der schnellsten Algorithmen. Bei sehr ungünstigen Sortierfolgen (schon sortiert) kann er aber auch mit einem quadratischen Aufwand sehr ineffizient sein.

Vorsortierte Folgen sind durchaus häufig bei realen Problemen zu finden. Der Quicksort ist bei schon sortierten Folgen sehr ineffizient ist und daher ist der Einsatz bei realen Anwendungen durchaus heikel.

 

Quicksort: Implementierung in Java

 Die folgende Implementierung implementiert die abstrakte Klasse Sortierer die in den Beispielprogrammen zum Sortieren zu finden sind.

Implementierung: Klasse Quicksort

package Kurs2.Sort;
/**
*
* @author sschneid
*/
public class QuickSort extends Sortierer{
/**
* Konstruktor: Akzeptiere ein Feld von int. Reiche
* das Feld an die Oberklasse weiter.
* Der Algorithmus ist nicht parallel (false Argument)
* @param s
*/
public QuickSort(int[] s) {
super(s,false);
}
/**
* sortiert ein Eingabefeld s und gibt eine Referenz auf dea Feld wieder
* zurück
* @param s ein unsortiertes Feld
* @return ein sortiertes Feld
*/
@Override
public void sortieren(int startIndex, int endeIndex) {
int i = startIndex;
int j = endeIndex;
int pivotWert = feld[startIndex+(endeIndex-startIndex)/2];
//System.out.println("von"+ startIndex+", bis:"+endeIndex +
// " pivot:" + pivotWert);
while (i<=j) {
// Suche vom unteren Ende des Bereichs aufsteigend einen
// Feldeintrag welcher groesser als das Pivotelement ist
while (feld[i] < pivotWert) {i++;vglZaehler();}
// Suche vom oberen Ende des Bereichs absteigend einen
// Feldeintrag der kleiner als das Pivotelement ist
while (feld[j] > pivotWert) {j--;vglZaehler();}
// Vertausche beide Werte falls die Zeiger sich nicht
// aufgrund mangelnden Erfolgs überschnitten haben
if (i<=j) {
tausche(i,j);
i++;
j--;
}
}
// Sortiere unteren Bereich
if (startIndex<j) {sortieren(startIndex,j);}
// Sortiere oberen Bereich
if (i<endeIndex) {sortieren(i,endeIndex);}
}
/**
* Liefert den Namen des Insertion Sorts
* @return
*/
@Override
public String algorithmus() {
return "QuickSort";
}
}
 

Implementierung: Parallelisierter Quicksort

Die unten aufgeführte Klasse nutzt die "Concurrency Utilities" die in Java 7 eingeführt wurden. Aus der Sammlung wird das Fork/Join Framework verwendet.

Das Aufteilen des Sortierintervalls erfolgt hier seriell in einer eigenen Methode.

Das Sortieren der Teilintervalle erfolgt parallel solange das Intervall eine bestimmte Mindestgröße (100) besitzt.

Die Fork/Join Klassen stellen dem Algorithmus einen Pool von Threads zur Verfügung die sich nach der Anzahl des Prozessoren des Rechners richten.

Hierzu dient eine Spezialisierung der Klasse RecursiveAction

  • Die Methode compute() wird in einem eigenen Thread durchgeführt wenn das dazugehörige Objekt mit der Methode invokeAll() aufgerufen wird. Diese Methode wird überschrieben.
  • Die Methode invokeAll() erlaubt es neue Tasks zu in Auftrag zu geben. Diese Methode wird erst beendet wenn alle Tasks ausgeführt sind.

In der vorliegenden Implementierung erfolgt ein paralleler Aufruf eines Task zum Sortieren der Teilintervalle und nicht ein sequentieller Methodenaufruf.

Die Implementierung des Task erfolgt in der inneren Klasse Sorttask. Diese Klasse ummantelt quasi die Sortiermethode.

package Kurs2.Sort;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
/**
*
* @author sschneid
* @version 2.0
*/
public class QuickSortParallel extends Sortierer{
static int SIZE_THRESHOLD=100; // Schwellwert für paralleles Sortieren
private static final ForkJoinPool threadPool = new ForkJoinPool();
/**
* Konstruktor: Akzeptiere ein Feld von int. Reiche
* das Feld an die Oberklasse weiter.
* Der Algorithmus ist parallel (true Argument)
* @param s
*/public QuickSortParallel(int[] s) {
super(s, true);
}
/**
* Innere statische Klasse die Fork and Join aus dem Concurrency package
* implementiert. Sie macht aus Methodenaufrufen, Taskaufrufe von Threads
*/
static class SortTask extends RecursiveAction {
int lo, hi;
QuickSortParallel qsp;
/**
* Speichere alle wichtigen Parameter für den Task
* @param lo untere Intervallgrenze
* @param hi obere Intervallgrenze
* @param qsp Referenz auf das zu sortierende Objekt
*/
SortTask(int lo, int hi, QuickSortParallel qsp) {
this.lo = lo;
this.hi = hi;
this.qsp =qsp;
}
/**
* Führe Task in eigenem Thread aus und nutze Instanzvariablen
* als Parameter um Aufgabe auszuführen.
*/
@Override
protected void compute() {
//System.out.println(" Thread ID => " + Thread.currentThread().getId());
if (hi - lo < SIZE_THRESHOLD) {
// Sortiere kleine Intervalle seriell
qsp.sortierenSeriell(lo, hi);}
else { // Sortiere große Intervalle parallel
int obergrenzeLinkesIntervall;
obergrenzeLinkesIntervall = qsp.teilsortieren(lo,hi);
// der serielle rekursive Aufruf wird ersetzt durch
// den parallelen Aufruf zweier Threads aus dem Threadpool
//System.out.println("Parallel: "+
// lo+","+obergrenzeLinkesIntervall+","+hi);
invokeAll(new SortTask(lo,obergrenzeLinkesIntervall,qsp),
new SortTask(obergrenzeLinkesIntervall+1,hi,qsp));
}
}
}
/**
* sortiert ein Eingabefeld s und gibt eine Referenz auf dea Feld wieder
* zurück
* @param s ein unsortiertes Feld
* @return ein sortiertes Feld
*/
@Override
public void sortieren(int startIndex, int endeIndex) {
threadPool.invoke(new SortTask(startIndex,endeIndex,this));
}
/**
* sortiert ein Eingabefeld s und gibt eine Referenz auf dea Feld wieder
* zurück
* @param s ein unsortiertes Feld
* @return ein sortiertes Feld
*/
public void sortierenSeriell(int startIndex, int endeIndex) {
if (endeIndex > startIndex) {
int obergrenzeLinkesIntervall= teilsortieren(startIndex, endeIndex);
//System.out.println("Seriell: "+
// startIndex+","+obergrenzeLinkesIntervall+","+endeIndex);
// Sortiere unteren Bereich
if (startIndex<obergrenzeLinkesIntervall) {
sortierenSeriell(startIndex,obergrenzeLinkesIntervall);}
// Sortiere oberen Bereich
if (obergrenzeLinkesIntervall+1<endeIndex) {
sortierenSeriell(obergrenzeLinkesIntervall+1,endeIndex);}
}
}
public int teilsortieren(int startIndex, int endeIndex) {
int i = startIndex;
int j = endeIndex;
int pivotWert = feld[startIndex+(endeIndex-startIndex)/2];
//druckenKonsole();
while (i<=j) {
// Suche vom unteren Ende des Bereichs aufsteigend einen
// Feldeintrag welcher groesser als das Pivotelement ist
while (feld[i] < pivotWert) {i++;vglZaehler();}
// Suche vom oberen Ende des Bereichs absteigend einen
// Feldeintrag der kleiner als das Pivotelement ist
while (feld[j]> pivotWert) {j--;vglZaehler();}
// Vertausche beide Werte falls die Zeiger sich nicht
// aufgrund mangelnden Erfolgs überschnitten haben
if (i<=j) {
tausche(i,j);
i++;
j--;
}
}
//System.out.println("von"+ startIndex+", bis:"+endeIndex +
// " pivot:" + pivotWert + " , return: " +i);
return i-1;
}
/**
* Liefert den Namen des Insertion Sorts
* @return
*/
public String algorithmus() {
return "QuickSort mit Fork and Join";
}
}
 

Hinweis: Dieser Algorithmus ist parallel. Er teilt dies der Oberklasse im Konstruktor als Parameter mit. Hierdurch werden die Zähler für die Vergleiche von Vertauschungen und Vergleichen vor einem parallelen Inkrement geschützt. Die Synchronisierung dieser Zählvariablen ist leider so aufwendig, dass der Algorithmus nicht mehr schneller als der seriell Algorithmus ist...

Man muss hier leider eine Entscheidung treffen:

  • setzen des Flag auf true: Die Vertauschungen werden korrekt gezählt. Der Algorithmus ist korrekt und langsam.
  • setzen des flag auf false: Die Veretauschungen und Vergleiche werden nicht korrekt gezählt. Der Algorithmus ist korrekt und schnell. Das bedeutet, er skaliert auf Mehrprozessorsystemen. 

 

Übung (Quicksort)

Führen Sie einen Durchlauf des Quicksorts manuell durch. Teilen Sie ein gegebenes Sortierintervall (Aufgabe: Vorher) nach den Regeln des Quicksorts in zwei Unterintervalle die noch sortiert werden müssen.

  • Sortieren Sie aufsteigend
  • Wählen Sie das Pivotelement ganz rechts im Intervall.
    • Markieren Sie das Pivotelement in der Aufgabe mit einem Pfeil von unten (siehe Beispiel).
  • Wenden Sie die Regeln des Quicksorts an. Zeichnen Sie zweiseitige Pfeile im "Vorher" Diagramm ein, um die notwendigen Vertauschungen zu markieren.
  • Zeichnen Sie mit einem zweiseitigen Pfeil die nötige Vertauschung des Pivotelements im "Vorher" Diagramm ein.
  • Tragen Sie alle neuen Werte im "Nachher" Diagramm ein.
  • Zeichnen sie die beiden verbliebenen zu sortierenden Zahlenintevalle mit eckigen Linien (siehe "Vorher" Diagramm) ein.

Lösung (Quicksort)

 

Heapsort

Der Heapsort (Sortieren mit einer Halde) ist eine Variante des Sortierens durch Auswahl. Er wurde in den 1960er Jahren von Robert W. Floyd und J. W. J. Williams entwickelt (siehe: Artikel in Wikipedia).

Der Heapsort hat den Vorteil, dass er auch im ungünstigsten Fall in der Lage ist mit einem Aufwand von O(N*log(N)) zu sortieren.

Hierfür wird eine Datenstruktur (der Heap) verwendet mit der die Bestimmung des Maximums einer Folge in einem Schritt möglich ist.

Heap und Feldrepräsentation

Beim Heapsort werden die zu sortierenden Schlüssel einer Folge in zwei Repräsentation betrachet:

  • Feldrepräsentation: Die Folge der unsortierten Schlüssel in einem Feld die sortiert werden soll. Am Ende des Heapsorts soll diese Folge als Feld aufsteigend sortiert sein.
  • Heaprepräsentation: Eine interne Repräsentation auf der die Regeln des Heapsorts angewendet werden. Diese Heaprepräsentation in der Form eines Binärbaums dient ausschließlich dem Verständnis des Betrachters

Beide Repräsentationen sind Sichten auf die gleichen Daten. Beim Heapsort werden nicht die Schlüssel in einen Binärbaum umkopiert. Die Heaprepräsentation dient nur dem Betrachter zur Verdeutlichung der Beziehung gewisser Feldpositionen.

Die Grundidee des Heapsorts besteht darin, daß an der Wurzel des Binärbaums immer der größte Schlüssel stehen muss. Diesen größten Schlüssel kann man dann dem Binärbaum entnehmen und als größtes Element der Folge benutzen. Die verbliebene unsortierte Teilfolge muss dann wieder so als ein Baum organisiert werden in dem das größte Element an der Wurzel steht. Durch sukzessives Entnehmen der verbliebenen gößten Elemente kann man die gesamte Folge sortieren.

Beim Heapsort muss nicht nur an der Wurzel des Baums der größte Schlüssel stehen. Jeder Knoten im Binärbaum muss größer als die Unterknoten sein. Diese Bedingung nennt man die Heapbedingung.

Verfahren

Beim Heapsort wird auf eine Feld mit N unsortierten Schlüsseln das folgende Verfahren wiederholt angewendet:

  • Herstellen der Heapbedingung  durch das Bootom-Up für ein Feld mit N Schlüsseln
  • Tauschen des ersten und größten Schlüssels auf der Position 1 mit dem letzten unsortierten Schlüssel auf Position N.
    • Wiederherstellen des Heapbedingung durch Versickern (Top-Down Verfahren)
  • Wiederholen des Verfahrens für die Positionen 1 bis N-1. Ab der Position N ist das Feld schon sortiert.

Beispiel

Grafische Visualisierung Beschreibung
Beispiel Heapkriterium

Das Beispiel links zeigt nicht eine vollständige Sortierung der gesamten Folge.

Es zeigt nur das Einsortieren eines Elements für eine Feldstruktur die schon der Heapbedingung genügt.

Das Feld mit den N=9 Elementen wurde so vorsortiert, dass es der Heapdebingung genügt. Das Herstellen der Heapbedingung wird später erläutert.

Beispiel Heapkriterium

Das Element mit dem Schlüssel 9 wird von der Spitze des Baums f[1] entfernt...

Beispiel Heapkriterium

... und mit dem Wert 2 der Position N=9 getauscht. Der größte Schlüssel der Folge befindet sich jetzt auf der Position 9.

Alle Elemente ab f[9]= F[N] aufwärts sind jetzt aufsteigend sortiert.

Alle Elemente von f[1] bis f[N-1]=f[8] sind unsortiert und müssen noch sortiert werden.

Die Elemente von f[1] bis f[8] genügen auch nicht mehr der Heapbedingung. Der Wert f[1]=2 ist nicht größer als f[2]=8 oder f[3]=7


Die Heapbedingung

Ein Feld f[1] bis f[N] mit Schlüsseln bildet einen Heap wenn die folgende Bedingung gilt:

Heapbedingung

f[i]<=f[i/2] für 2<= i <= N]
oder
f[i]>=f[2*i] und f[i]>=f[2*i+1] für 2*i<=N und 2*i+1<= N

Die Heapbedingung lässt sich leichter grafisch in einer Baumdarstellung verdeutlichen:

Beispiel Heapkriterium

Das Feld f mit der Feldgröße N=9

  • f[1] = 9
  • f[2] = 8
  • f[3] = 7
  • f[4] = 3
  • f[5] = 6
  • etc.

genügt der Heapbedingung da jeder obere Knoten größer als die beiden Unterknoten ist.

Die Position im Feld wird hier in rot dargestellt. Der Schlüssel (Wert) des Feldelements hat einen gelben Hintergrund

Herstellen der Heapbedingung für einen unsortierten Heap (Bottom-Up Methode)

Mit dem Bottom-Up Verfahren kann man die Heapbedingung für vollständig unsortierte Folgen von Schlüsseln herstellen.

Das Verfahren basiert auf der Idee, dass beginnend mit dem Schlüssel auf der höchsten Feldposition f[N] der Wert versickert wird um anschließend den nächst niedrigeren Wert F[N-1] zu versickern bis man den größten Wert f[1] versickert hat.

Beispiel

Grafische Visualisierung Beschreibung
Schritt 1

Der gegebene Baum sei vollständig mit Zufallsschlüsseln belegt. Alle Werte sind zufällig.

Die Schlüssel f[9] bis f[5] können nicht versickert werden, da sie keine Blattknoten zum Versickern haben.

Bottom Up Schritt 2 Das Bottom-Up Verfahren beginnt mit dem ersten Knoten i der über Unterknoten verfügt. Dieser Knoten ist immer die Position N/2 abgerundet. Bei N=9 ist die 9/2= 4.5. Der abgerundete Wert ist i=4. Hier muß f[8]=9 mit f[4]=3 getauscht werden um den Schlüssel zu versickern.
Bottom Up Schritt 3 Bei i=3 muss f[3]=4 mit f[7]=5 getauscht werden und ist damit auch versickert.
Bottom Up Schritt 4 Bei i=2 muss f[4] mit f[2] getauscht werden. Das rekursives Versickern bei f[4]=9 is nicht notwendig. f[4]=9 ist größer als f[8]=3 und f[9]=2 
Bottom Up Schritt 4

Bei i=1 muss f[1] mit  f[2]=9 getauscht werden.

 

Bottom Up Schritt 4 Nach dem Neubesetzen der Position f[2]=7 zeigt sich aber, dass die Heapbedingung für den entsprechenden Teilbaum von f[2] nicht gilt da f[2|<f[2*i+1] ist. f[2] muss also rekursiv weiter versickert werden. 
Bottom Up Schritt 4

Nach dem Tauschen von f[2]=7 mit f[5]=8 ist das rekursive Versickern beendet. f[5] hat keine Unterknoten da 5 > N/2 ist.

Der Baum genügt jetzt der Heapbedingung

 

Herstellen der Heapbedingung durch Versickern (Top-Down Methode)

Herstellen der Heapbedingung

Top-Down-Verfahren

Dieses Verfahren dient der Herstellen der Heapbedingung, wenn schon für die Unterheaps für die Heapbedingung gilt.

Man wendet es an, nachdem man den größten Schlüssel entnommen hat und ihn durch den Wert mit dem größten Index im unsortierten Baum ersetzt hat.

Das Versickern dieses neuen Schlüssels an der Wurzel des Baums erfolgt mit den folgenden Schritten:

  • Beginne mit der Position i=1 im Baum
  • Der Schlüssel f[i] hat keine Unterknoten da 2*i>N ist,
    • Der Schlüssel kann nicht mehr versickert werden. Das Verfahren ist beendet. 
  • Der Schlüssel f[i] hat noch genau einen Unterknoten f[i*2]
    • Der Schlüssel f[i] wird mit dem Schlüssel f[i*2] getauscht falls f[i]<f[i*2]. Das Verfahren ist nach diesem Schritt beendet.
  • Der Schlüssel f[i] hat noch Unterknoten f[2*i] und f[2*i+1]
    • Der Schlüssel f[i] ist der größte der drei Schlüssel f[i], f[i*2],f[i*2+1]
      • Das Verfahren ist beendet die Heapbedingung ist gegeben
    • Der Schlüssel f[i*2] ist der größte der drei Schlüssel f[i], f[i*2],f[i*2+1]
      • Tausche die Schlüssel f[i] mit f[2*i]
      • Versickere den Schlüssel f[i*2] nach dem Top-Down Verfahren
    • Der Schlüssel f[i*2+1] ist der größte der drei Schlüssel f[i], f[i*2],f[i*2+1]
      • Tausche die Schlüssel f[i] mit f[2*i+1]
      • Versickere den Schlüssel f[i*2+1] nach dem Top-Down Verfahren

Diese rekursive Verfahren wird hier an einem Beispiel gezeigt:

Grafische Visualisierung Beschreibung
Beispiel Heapkriterium

Durch das Entfernen des größten Schlüssels f[1] entstehen zwei Unterbäume für die die Heapbedingung nach wie vor gilt.

Beispiel Heapkriterium

Durch das Einsetzen eines Schlüssel von der Position N ist nicht mehr garantiert, dass die Heapbedingung für den gesamten Restbaum f[1] bis f[N-1]=f[8] gegeben ist.

Der Schlüssel f[1]=2 ist eventuell größer als die Schlüssel f[2*1] und f[2*1+1]. In diesem Fall ist die Heapbedingung für f[1] bis f[8] schon gegeben. Es muss nichts mehr getan werden.

Im vorliegenden Fall ist f[1]=2 aber nicht der größte Schlüssel. Er muss rekursiv versickert werden.

Dies geschieht durch das Vertauschen mit f[2] = 8. f[3] = 7 ist nicht der richtige Kandidat. 7 ist nicht der größte der drei Schlüssel.

Beispiel Heapkriterium

Nach dem Vertauschen von f[1] mit f[2] gilt die Heapbedingung zwar für den obersten Knoten. Sie gilt jedoch nicht für den Teilbaum unter f[2]= 2. Der Schlüssel 2 muss weiter versickert werden.

In diesem Fall ist f[5]=6 der größte Schlüssel mit dem f[2]= 2 vertauscht weden muss.

Beispiel Heapkriterium

Nach dem Vertauschen von f[2] mit f[5] gilt die Heapbedingung wieder für den gesamten Baum.

 

 

Heapsort: Implementierung in Java

package Kurs2.Sort;
/**
*
* @author sschneid
* @version 2.0
*/
public class HeapSort extends Sortierer{
/**
* Konstruktor: Akzeptiere ein Feld von int. Reiche
* das Feld an die Oberklasse weiter.
* Der Algorithmus ist nicht parallel (false Argument)
* @param s
*/
public HeapSort(int[] s) {super(s,false);}
/**
* sortiert ein Eingabefeld s
* @param s ein unsortiertes Feld
* @return ein sortiertes Feld
*/
public void sortieren(int startIndex, int endeIndex){
// Erzeugen der Heapbedingung, Bottom up...
for (int i=(endeIndex-1)/2; i>=0; i--) versickere (i,endeIndex);
//System.out.println("Heapbedingung hergestellt");
for (int i=endeIndex; i>0; i--) {
// Tausche das jeweils letzte Element mit dem Groeßten an der
// Wurzel. Versickere die neue Wurzel und verkürze
// das zu sortierende Intervall von hinten nach vorne
tausche(0,i); // Groesstes Element von der Wurzel an das Ende
// des Intervals getauscht
versickere(0,i-1); // versickere die neue Wurzel um Heapbedingung
// herzustellen
}
}
/**
* Berechne Index des linken Sohns für gegebenen Index
* liefere -1 zurück falls keine linker Sohn existiert
* @param index für den Sohn berechnet wird
* @param endeIndex letzter belegter Indexplatz
* @return
*/
private int linkerSohn(int index, int endeIndex) {
int ls = index*2+1;
if (ls > endeIndex)
return -1;
else return ls;
}
/**
* Berechne Index des linken Sohns für gegebenen Index
* liefere -1 zurück falls keine linker Sohn existiert
* @param index für den Sohn berechnet wird
* @param endeIndex letzter belegter Indexplatz
* @return
*/
private int rechterSohn(int index, int endeIndex) {
int rs = (index+1)*2;
if (rs > endeIndex)
return -1;
else return rs;
}
/**
* Versickere ein Element auf der Position "vers"
* @param vers Index des zu versickernden Elements
* @param endeIndex hoechste Indexposition die zum Verisckern zur Verfügung
* steht. Sie wird bei der Berechnung des rechts Sohns
* benötigt
*/
private void versickere (int vers, int endeIndex) {
int ls = linkerSohn(vers,endeIndex);
int rs = rechterSohn(vers,endeIndex);
int groessererSohn;
while (ls != -1) { // Es gibt einen linken Sohn
// Versickere bis Heapbedingung erfüllt ist oder keine
// Söhne mehr vorhanden sind
groessererSohn =ls; // linker Sohn als groesseren Sohn nominieren
if ((rs != -1) && (istKleiner(ls,rs))) groessererSohn = rs;
// der rechte Sohn existiert und war groesser...
if (istKleiner(vers,groessererSohn)) {
tausche(vers,groessererSohn); // beide Felder wurden getauscht
vers = groessererSohn;
ls = linkerSohn(vers,endeIndex);
rs = rechterSohn(vers,endeIndex);
}
else ls=-1; // Abbruchbedingung für while Schleife
}
}
/**
* Liefert den Namen des Heap Sorts
* @return
*/
public String algorithmus() {return "Heap Sort";}
}

 

Zusammenfassung

Aufwände der vorgestellten SortieralgorithmenO(N2)

  Cmin Caver Cmax Mmin Maver Mmax
Selectionsort O(N2) O(N2) O(N2) O(N) O(N) O(N)
Insertionsort O(N) O(N2) O(N2) O(N) O(N2) O(N2)
Bubblesort O(N) O(N2) O(N2) O(0) O(N2) O(N2)
Quicksort O(NlogN) O(NlogN) O(N2) O(0) O(NlogN) O(NlogN)
Heapsort* O(NlogN) O(NlogN) O(NlogN) O(NlogN) O(NlogN) O(NlogN)

* Der Heapsort ist ein nicht stabiles Sortierverfahren!

Bei der Betrachtung der Gesamtlaufzeit muss auch der Speicherplatzverbrauch berücksichtigt werden:

Gesamtlaufzeit
  Min Average Max Speicher
Selectionsort O(N2) O(N2) O(N2) O(1)
Insertionsort O(N) O(N2) O(N2) O(1)
Bubblesort O(N) O(N2) O(N2) O(1)
Quicksort O(NlogN) O(NlogN) O(N2) O(logN)
Heapsort O(NlogN) O(NlogN) O(NlogN) O(1)

Der Quicksort benötigt Speicherplatz der dem Logarithmus der zu sortierenden Anzahl der Werte abhängt. Die rekursiven Aufrufe benötigen zusätzlichen Platz zum Verwalten der Intervallgrenzen.

Übungen (Sortieren)

Teamübung: Analyse und Implementierung eines Sortieralgorithmus

Wählen Sie einen Algorithmus

  • recherchieren Sie seine Funktion
  • implementieren Sie den Algorithmus

1. Konfiguration der Laufzeitumgebung

Benutzen Sie die Beispielprogramme zum Sortieren:

  • Kopieren Sie sich die Klassen Sortierer, MainSort auf ihr System
  • Kopieren Sie sich die Klasse SelectionSort auf Ihr System
  • "Aktivieren" Sie sich den gewünschten Sortieralgorithmus in der Klasse MainSort, Methode algorithmusAuswahl(). Alle Sortieralgorithmen sind als Kommentarzeilen vorhanden. Das Entfernen genau eines Kommentar's aktiviert den gewünschten Algorithmus. Beginnen Sie zum Testen mit der Referenzimplementierung der Klasse SelectionSort.

Testen Sie die Gesamtanwendung durch das Aufrufen des Hauptprogramms MainSort.

In diesem Fall beginnt das Programm ein kleines Feld zu sortieren und gibt die Anzahl der Vergleiche, Vertauschungen, Größe des Feldes und die benötigte Zeit in Millisekunden aus. Dies ist die Phase 1. Wurde das kleine Feld erfolgreich sortiert beginnt Phase 2. Hier wird die Feldgröße jeweils verdoppelt. Das Programm bricht ab nachdem zum ersten Mal eine Zeitgrenze (3 Sekunden) beim Sortieren überschritten wurde. Hiermit lässt sich die Effizienz des Algorithmus testen und die Komplexitätsabschätzungen kontrollieren.

2. Implementieren Sie den neuen Sortieralgorithmus

Orientieren Sie sich am Beispiel der Klasse SelectionSort

  • Erzeugen Sie eine neue Klasse, die aus der Klasse Sortierer abgeleitet wird
  • Implementieren sie einen einfachen Konstruktor der das übergebene Feld an die Oberklasse Sortierer weiterreicht:
    • public XYSort(Sortierbar[] s) {super(s);}
  • Implementieren Sie eine Methode mit dem Namen algorithmus() die den Namen des Algorithmus ausgibt
    • public String algorithmus() {return "XYsort";}
  • Implementieren Sie den eigentlichen Sortieralgorithmus in der Methode public void sortieren(anfang, ende)
    • Diese Methode soll als Ergebnis alle Elemente von der Position anfang im Feld bis zum Feldelement ende aufsteigend sortiert haben.

 2.1 Infrastruktur für die Methode sortieren(anfang, ende)

Die Oberklasse Sortierer stellt alles zur Verfügung was man zum Sortieren benötigt

  • tausche(x,y) erlaubt das Tauschen zweier Feldelemente auf der Position x mit dem Element auf der Position y
  • boolean istKleiner(x,y) gibt den Wert true (wahr) zurück wenn der Inhalt der Feldposition x kleiner als der Inhalt der Feldposition y ist

Diese beiden Hilfsmethoden sind die einzigen Methoden die zum Implementieren benötigt werden. Der Typ des zu sortierenden Feldes bleibt hinter dem Interface Sortierbar verborgen. Im vorliegenden Fall sind es ganze Zahlen.

Um die Entwicklung zu Vereinfachen bietet die abstrakte Klasse Sortieren eine Reihe von Hilfsmethoden:

  • druckenKonsole(): druckt das gesamt Feld mit seiner Belegung auf der Konsole aus
  • generiereZufallsbelegung(): Belegt ein existierendes Feld mit neuen Testdaten 
  • long anzahlVertauschungen(): Gibt die Zahl der Vertauschungen an. Sie werden durch Aufrufe von tausche() gezählt
  • long anzahl Vergleiche(): Gibt die Zahl der Vergleiche aus. Sie werden durch Aufrufe von istKleiner() gezählt
  • boolean validierung(): Erlaubt die Prüfung eines sortierten Felds. Der Wert ist wahr wenn das Feld korrekt sortiert ist.

3. Anpassen des Hauptprogramms MainSort

Das Hauptprogramm MainSort erzeugt an einer Stelle eine Instanz der Klasse SelectionSort. Ändern Sie diese Stelle im Programm derart, dass eine Instanz Ihrer neuen Sortierklasse aufgerufen wird.

4. Vorbereitende Tests für die Präsentation

Ändern Sie die zu sortierenden Testdaten im Programm MainSort derart ab, dass Sie

  • den ungünstigsten Fall für den Sortieralgorithmus wählen (Vertauschungen, Prüfungen)
  • den günstigsten Fall wählen (Vertauschungen, Prüfungen)

Tragen Sie die abgelesen Werte in eine Tabellenkalkulation ein und prüfen Sie ob die Aufwände mit den Vorhersagen des Lehrbuchs übereinstimmen (Grafiken!)

Bereiten Sie eine 15-20 minütige Vorstellung/Präsentation des Algorithmus vor:

  • Erklären des Algorithmus (mit Beispiel)
    • Tafel oder Folie genügen
  • Vorführen einer Implementierung
  • Komplexitätsüberlegungen und Bewertung
    • Was ist der durchschnittliche Aufwand?
    • Was ist der minimale Aufwand (Beispiel)?
      • Wählen Sie die Anzahl der Vertauschungen oder der Vergleiche
    • Was ist der Aufwand im ungünstigsten Fall (Beispiel)?
      • Wählen Sie die Anzahl der Vertauschungen oder der Vergleiche
  • Wichtige Punkte die abhängig vom Algorithmus beachtet werden sollen
    • Sortieren durch Auswahl
      • Präsentation
        • Erklären Sie die "Sortiergrenze"
    • Bubblesort
      • Präsentation
        • Welche Elemente werden immer vertauscht?
        • Warum ist diese Strategie intuitiv aber nicht effizient?
    • Quicksort
      • Nutzen Sie Bleistifte oder Streichhölzer zum Visualieren des Verfahrens.
      • Legen Sie das Pivotelement in Ihrer Implementierung der Einfachheit halber an eine feste Intervallgrenze!
      • Algorithmus:
        • Erklären Sie das grundlegende Konzept des Quicksorts (3 Sätze reichen hier!)
        • Erklären Sie die Rekursion (Teile und Herrsche) und das Ende der Rekursion (Abbruch)
      • Implementierung
        • Erklären Sie den Begriff des "Pivotelement"
        • Die Implementierung ist durchaus kompliziert. Skizieren Sie sie nur.
    • Heapsort
      • Diskutieren Sie den Dualismus der beiden Zugriffstrukturen
        • Sie arbeiten auf einem Feld (Array)
        • Sie betrachten auf der abstrakten Ebene einen Baum
        • Wie ist der Zusammenhang zwischen Blattknoten und Feldpositionen?
      • Implementierung
        • Schreiben Sie Methoden zur Bestimmung der linken und rechten Blattknoten
        • Schreiben Sie eine Methode zum Versickern
      • Präsentation
        • Erläutern Sie den Dualismus
        • Erläutern Sie die Bestimmung der Blattknoten
        • Erläutern Sie die "Heapbedingung"
          • Wie wird sie hergestellt
        • Erläutern Sie das Konzept des "Versickerns" 

 

Lernziele

Am Ende dieses Blocks können Sie:

  • ... den Aufwand für einfache Algorithmen mit einem Parameter bestimmen
    • geschachtelte Schleifen mit dem gleichen Zähler
    • nacheinander ausgeführte Blöcke
  • ... Blöcke mit konstantem Aufwand erkennen
  • ... die vorgestellten Sortierverfahren erklären und auf kleine Folgen anwenden
  • ... den Aufwand für die vorgestellten Sortierverfahren abschätzen
  • .... stabile von nicht stabilen Sortierverfahren unterscheiden
  • ... Vor- und Nachteile der vorgestellten Sortierverfahren erläutern
  • ... eine Menge von Schlüsseln effizient sortieren.
  • ... für unterschiedliche Sortierprobleme die optimalen Suchalgorithmen auswählen und deren Vorteile erklären
  • ... die wichtigsten Kriterien zum Sortien, von zum Beispiel, 15 Bleistiften zu nennen.
  • ... für die vorgestellten Grundalgorithmen passende Beispiele nennen.

Referenzen

T.Ottmann / P.Widmayer: Algorithmen und Datenstrukturen: Kapitel Sortieren

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: Fragen zu Algorithmen

Datenstrukturen

Einführung

Das Zusammenfassen von Objekten, Datenstrukturen zu einer Menge von Objekten ist eine typische Anforderung in der Informatik. Zur Lösung dieses Anforderung müssen die folgenden Aspekte betrachtet werden:

Beispiele aus dem echten Leben

Welche der oben geforderten Kriterien gilt hier?

Felder

Die bisher verwendete Datenstruktur zum Verwalten von Objekten war das Feld (Array). Ein Feld hat die folgenden Vorteile:

Diesen Vorteilen stehen die folgenden Nachteile gegenüber:

Die Referenz ist die zweite Datenstruktur mit der Objekte verwaltet werden können. Sie erlaubt jedoch nur den direkten Zugriff auf ein einzelnes Objekt.

Dynamische Datenstrukturen

Zur Verwaltung von beliebig vielen Objekten verwendet man dynamische Datenstrukturen in Java die man durch Verkettung von Objekten mit Objektreferenzen erhält.

Die Datenstrukturen sind dynamisch, da während der Laufzeit beliebig viele Objekte in sie eingefügt oder aus ihnen entfernt werden können. Die Anzahl ihrer Objekte (Knoten) ist nicht vorab festgelegt.

Definition
Knoten
Ein Knoten ist ein Bestandteil einer verkettenden, dynamischen Datenstruktur. In Java werden Knoten durch Objekte und Referenzen, die auf die Knoten zeigen, realisiert

Verketteten Datenstrukturen können linear (nicht verzweigt) oder auch verzweigt sein.

 

Vertreter dynamischer Datenstrukturen 

Listen

Listen sind verkettete Datenstrukturen, die im Gegensatz zu Feldern dynamisch wachsen können um beliebig viele Elemente (Knoten) zu verwalten. Sie werden anstatt Felder verwendet wenn

  • die maximale Größe nicht vorab bekannt ist
  • kein wahlfreier Zugriff auf beliebige Listenelemente (bzw. Knoten) benötigt wird.
  • die Reihenfolge der Knoten wichtig ist.

Typische Operationen die für Listen zur Verfügung stehen sind:

  • Einfügen von Knoten in die Liste
  • Entfernen von Knoten aus der Liste
  • Veränderung der Knotenreihenfolge
  • Bestimmung der Länge der Liste
  • Abfrage ob ein Knoten in der Liste enthalten ist

Mit Hilfe von Listen können auch spezialisiertere Datenstrukturen wie Warteschlangen (engl. queues) oder Stapel (engl. stack) implementiert werden.

Listen in Java

Eine verkettete Datenstruktur kann mit jeder Javaklasse aufgebaut werden die in der Lage ist auf Objekte der gleichen Klasse zu zeigen.

Eine solche Verkettung kann man zum Beispiel mit jeder Klasse implementieren, die die folgende Schnittstelle implementiert:

package Kurs2.Listen;
public interface EinfacherListenknoten {
   public void setNachFolger(EinfacherListenknoten n);
   public EinfacherListenknoten getNachfolger();
}

Dies geschieht im folge für die Klasse Ganzzahl, die neben der Listeneigenschaft auch in der Lage ist eine Ganzzahl zu verwalten:

package Kurs2.Listen;
 
public class Ganzzahl implements EinfacherListenknoten {
 
    public int wert;
    private EinfacherListenknoten naechsterKnoten;
 
    public Ganzzahl(int i) {
        wert = i;
        naechsterKnoten = null;
    }
 
    public void setNachFolger(EinfacherListenknoten n) {
        naechsterKnoten = n;
    }
 
    public EinfacherListenknoten getNachfolger() {
        return naechsterKnoten;
    }
 
    /**
     * Hauptprogramm zum Testen
     * @param args
     */
    public static void main(String[] args) {
        Ganzzahl z1 = new Ganzzahl(11);
        Ganzzahl z2 = new Ganzzahl(22);
Ganzzahl z3 = new Ganzzahl(33)
        z1.setNachFolger(z2);
z2.setNachFolger(z3);
    }
}

In UML:

 

Hiermit kann eine verkettete Struktur aufgebaut werden, die die Verwaltung von beliebig vielen Objekten erlaubt. In der main() Methode wird eine verkettete Liste von drei Objekten aufgebaut:

Zum Zugriff auf diese Liste reicht jetzt die Referenzvariable z1. Alle anderen Knoten können mit Hilfe der Methode getNachfolger() erreicht werden. Es existiert jedoch kein wahlfreier Zugriff auf die Knoten der Liste.

Werden die beiden Variablen z2 und z3 dereferenziert, können die einzelnen Knoten nach wie vor erreicht werden:

z2 = null;
z3 = null;

Wird die Variable z1 dereferenziert,

 z1 = null;

sind alle drei Objekte nicht mehr erreichbar und können vom Garbagekollektor gelöscht werden.

Unsortierte Listen

Im einfachsten Fall kann eine Liste als einfach verzeigerte Liste implementiert die nur auf den Kopfknoten zeigt.

  • Das Einfügen und Löschen am Kopfende hat einen konstanten Aufwand
  • Das Suchen bzw. Löschen von Knoten hat einen Aufwand von O(n)
  • Relativer Zugriff von einem bekannten Knoten ist nur in Richtung der Nachfolger möglich.

Bei Listen kann man auch das Ende der Liste im Listenobjekt mit pflegen. Einfüge- und Entnahmeoperationen werden hierdurch aufwendiger zu implementieren. Der Vorteil liegt jedoch darin, dass auf das Ende der Liste wahlfrei zugegriffen werden kann.

Vorteile einfach verzeigerter Listen mit Referenz auf das Ende der Liste

  • Zugriff auf das und erste und letzte Element der Liste mit konstantem Aufwand
  • Iterieren über die Menge aller Listenelemente ist möglich
  • Löschen des letzten Elements ist mit konstantem Aufwand möglich (Hierdurch kann eine Warteschlange implementiert werden)

Nachteile

  • Kein wahlfreier Zugriff auf alle Elemente
  • Man kann nur auf ein Nachbarelement (den Nachfolger) mit konstantem Aufwand zugriefen

Zweiseitig verzeigerte Listen haben den Vorteil, dass sie einen direkten Zugriff auf den Vorgängerknoten sowie auf den Nachfolgerknoten haben:

Vorteile (über die einfach verzeigerte Liste hinaus):

  • Iteririeren ist in zwei Richtungen möglich
  • Der Vorgängerknoten eines Listenknoten kann auch mit konstantem Aufwand errreicht werden

 

 

Übungen (Listen)

Doppelt verzeigerte Liste mit Referenz auf das Ende der Liste

Implementieren Sie eine doppelt verzeigerte Liste zur Verwaltung ganzer Zahlen. Der Listenkopf referenziert auf das Kopfelement sowie auf das letzte Element der Liste

Eine Liste mit drei Knoten soll wie folgt implementiert sein:

Beachten Sie die die beiden Sonderfälle einer Liste mit einem Element und den Sonderfall einer leeren Liste.

Zur beschleunigten Implementierung sind alle Klassen schon gegeben

  • ListenKnoten: Eine einfache Klasse zur Verwaltung ganzer Zahlen mit Referenzen auf Vorgänger- und Nachfolgerknoten
  • MainTestListe: Ein Hauptprogramm mit diversen Testroutinen zum Testen der Liste
  • EineListe: Eine Rahmenklasse die vervollständigt werden soll. Implementieren Sie die folgenden Methoden:
    • einfuegeAnKopf(einfuegeKnoten): Einfügen eines Knotens am Kopf der Liste
    • laenge(): Berechnung der Länger der Liste
    • loesche(Knoten): Löschen eines Knotens einer Liste
    • enthaelt(Knoten): Prüfung ob ein Knoten in der Liste enthalten ist. Die Prüfung erfolgt auf Objektidentität, nicht auf Wertgleichheit
    • einfuegeNach(nachKnoten,einfuegeKnoten): Einfügen eines neuen Knotens einfuegeKnoten hinter dem Knoten nachKnoten

UML Diagramm der drei Klassen:

Klasse Listenknoten

package Kurs2.Listen;

public class Listenknoten {
private Listenknoten vorgaenger;
private Listenknoten nachfolger;
private int wert;

public Listenknoten(int einWert) { wert=einWert;}
public Listenknoten getNachfolger() { return nachfolger;}
public void setNachfolger(Listenknoten nachfolger) {
this.nachfolger = nachfolger;
}
public Listenknoten getVorgaenger() {
return vorgaenger;
}
public void setVorgaenger(Listenknoten vorgaenger) {
this.vorgaenger = vorgaenger;
}
public int getWert() {
return wert;
}
public void setWert(int wert) {
this.wert = wert;
}
/**
* Erlaubt den Zahlenwert als Text auszudrucken
* @return
*/
@Override
public String toString() { return Integer.toString(wert);}
}

Klasse EineListe

Vervollständigen Sie die folgende Klasse

package Kurs2.Listen;
public class EineListe {
    protected Listenknoten kopf;
    protected Listenknoten ende;
    /**
     * Konstrukor. Erzeuge eine leere Liste
     */
    public EineListe() {
        kopf = null;
        ende = null;
    }
    /**
    * Berechne die Länge der Liste
    * @return Länge der Liste
    */
    public int laenge() {
        int laenge = 0;
        System.out.println("1. Implementieren Sie EineListe.laenge()");
        return laenge;
    }
    /**
     * Liefert den Kopf der Liste oder einen Null Zeiger
     * @return
     */
    public Listenknoten getKopf() {
        return kopf;
    }
    /**
     * Liefert das Ende der Liste oder einen Null Zeiger
     * @return
     */
    public Listenknoten getEnde() {
        return ende;
    }
    /**
     * Drucke eine Liste alle Objekte auf der Konsole
     */
    public void drucken() {
        System.out.println("Länge Liste: " + laenge());
        Listenknoten k = kopf;
        while (k != null) {
            System.out.println("Knoten: " + k);
            k = k.getNachfolger();
        }
    }
    /**
     * Füge hinter dem Listenknoten "nach" den Knoten "k"ein.
     * Füge nichts ein wenn der Listenknoten "nach" nicht in der Liste
     * vorhanden ist
     * @param nach
     * @param k
     */
    public void einfuegeNach(Listenknoten nach, Listenknoten k) {
        System.out.println("5. Implementieren Sie EineListe.einfuegeNach()");
    }
    /**
     * Füge einen Listenknoten am Kopf der Liste ein
     * @param k neuer Listenknoten
     */
    public void einfuegeAnKopf(Listenknoten k) {
        System.out.println("2. Implementieren Sie EineListe.einfuegeAnKopf()");
    }
    /**
     * Lösche einen Knoten aus der Liste. Mache nichts falls es nichts zu
     * gibt
     * @param k zu löschender Listenknoten
     */
    public void loesche(Listenknoten k) {
        System.out.println("3. Implementieren Sie EineListe.loesche()");
    } // Ende Methode loesche()
    /**
     * Ist wahr wenn ein Knoten mit dem gleichen Wert existiert
     * @param k
     * @return
     */
    public boolean enthaelt(Listenknoten k) {
        boolean result = false;
        System.out.println("4. Implementieren Sie EineListe.enthaelt()");
        return result;
    } // Ende Methode enhaelt()
} // Ende der Klasse EineListe

Klasse MainTestListe

package Kurs2.Listen;
   public class MainTestListe {
     /**
     * Testroutinen für einfache Listen
     * @param args
     */
    public static void main(String[] args) {
        EineListe testListe = new EineListe();
        einfuegeTest(testListe, 4, 6);
        System.out.println("Kontrolle: Drei Knoten von 4 bis 6");
        einfuegeTest(testListe, 1, 3);
        System.out.println("Kontrolle: Sechs Knoten von 1 bis 6");
        einfuegeTest(testListe, 21, 27);
        System.out.println("Kontrolle: 13 Knoten von 21 bis 27, 1 bis 6");
        loeschenEndeTest(testListe, 3);
        System.out.println("Kontrolle: 10 Knoten von 21 bis 27, 1 bis 3");
        loeschenKopfTest(testListe, 8);
        System.out.println("Kontrolle: 2 Knoten von 2 bis 3");
        loeschenKopfTest(testListe, 3);
        System.out.println("Kontrolle: 0 Knoten. Versuch löschen in leerer Liste");
        enthaeltTest(testListe, 10);
        System.out.println("Kontrolle: Einfügen in der Liste");
        testListe = new EineListe();
        einfuegeNachTest(testListe, 10);
    }
    /**
     * Einfügen einer Reihe von Zahlen in eine gegebene Liste
     * @param l Liste in die eingefügt wird
* @param min kleinster Startknoten der eingefügt wird
* @param max größter Knoten der eingefügt werden
     */
    public static void einfuegeTest(EineListe l, int min, int max) {
        System.out.println("Test: Einfügen am Kopf [" + min + " bis " + max + "]");
        for (int i = max; i >= min; i--) {
            l.einfuegeAnKopf(new Listenknoten(i));
            System.out.println("Länge der Liste: " + l.laenge());
        }
        l.drucken();
    }
    /**
     * Lösche eine bestimmte Anzahl von Knoten am Ende der Liste
     * @param l Liste aus der Knoten gelöscht werden
     * @param anzahl der zu loeschenden Knoten
     */
    public static void loeschenEndeTest(EineListe l, int anzahl) {
        System.out.println("Test: Löschen am Ende der Liste " + anzahl + "fach:");
        for (int i = 0; i < anzahl; i++) {
            l.loesche(l.ende);
            System.out.println("Länge der Liste: " + l.laenge());
        }
        l.drucken();
    }
    public static void loeschenKopfTest(EineListe l, int anzahl) {
        System.out.println("Test: Löschen am Kopf der Liste " + anzahl + "fach:");
        for (int i = 0; i < anzahl; i++) {
            l.loesche(l.getKopf());
            System.out.println("Länge der Liste: " + l.laenge());
        }
        l.drucken();
    }
    public static void enthaeltTest(EineListe l, int groesse) {
        System.out.println("Test: enhaelt()  " + groesse + " Elemente");
        Listenknoten[] feld = new Listenknoten[groesse];
        for (int i = 0; i < groesse; i++) {
            feld[i] = new Listenknoten(i * 10);
            l.einfuegeAnKopf(feld[i]);
        }
        l.drucken();
        for (int i = 0; i < groesse; i++) {
            if (l.enthaelt(feld[0])) {
                System.out.println("Element " + feld[i] + " gefunden");
            }
        }
        System.out.println("Erfolg wenn alle Elemente gefunden wurden");
        Listenknoten waise = new Listenknoten(4711);
        if (l.enthaelt(waise)) {
            System.out.println("Fehler: Element gefunden welches nicht zur Liste gehört!");
        } else {
            System.out.println("Erfolg: Element nicht gefunden welches nicht zur Liste gehört");
        }
    }
    public static void einfuegeNachTest(EineListe l, int max) {
        System.out.println("Test: einfügen nach " + max + "fach");
        Listenknoten[] feld = new Listenknoten[max];
        for (int i = 0; i < max; i++) {
            feld[i] = new Listenknoten(i*10);
            l.einfuegeAnKopf(feld[i]);
        }
        System.out.println("Länge der Liste: " + l.laenge());
        for (int i = 0; i < max; i++) {
            l.einfuegeNach(feld[i], new Listenknoten(feld[i].getWert()+1));
            System.out.println("Länge der Liste: " + l.laenge());
        }
        l.einfuegeNach(l.ende, new Listenknoten(111));
        l.drucken();
    }
}

 

Lösungen

Doppelt verzeigerte Liste mit Referenz auf das Ende der Liste

Klasse EineListe

package Kurs2.Listen;
public class EineListe {
    protected Listenknoten kopf;
    protected Listenknoten ende;
    public EineListe() {
        kopf = null;
        ende = null;
    }
    /**
     * Berechnet die Anzahl der Listenelemente
     * @return Anzahl der Listenelemente
     */
    public int laenge() {
        int laenge = 0;
        Listenknoten zeiger = kopf;
        while (zeiger != null) {
            laenge++;
            zeiger = zeiger.getNachfolger();
        }
        return laenge;
    }
    /**
     * Liefert den Kopf der Liste oder einen Null Zeiger
     * @return Zeiger auf Kopf der Liste
     */
    public Listenknoten getKopf() {
        return kopf;
    }
    /**
     * Liefert das Ende der Liste oder einen Null Zeiger
     * @return Zeiger auf das Ende der Lister
     */
    public Listenknoten getEnde() {
        return ende;
    }
    /**
     * Drucke eine Liste alle Objekte auf der Konsole
     */
    public void drucken() {
        System.out.println("Länge Liste: " + laenge());
        Listenknoten k = kopf;
        while (k != null) {
            System.out.println("Knoten: " + k);
            k = k.getNachfolger();
        }
    }
    /**
     * Füge hinter dem Listenknoten "nach" den Knoten "k"ein.
     * Füge nichts ein wenn der Listenknoten "nach" nicht in der Liste
     * vorhanden ist
     * @param nach
     * @param k
     */
    public void einfuegeNach(Listenknoten nach, Listenknoten k) {
        Listenknoten t;
        t = kopf; // Diese Variable wird über die Listenknoten laufen
        while ((t != null) && (t != nach)) {
            // Iteriere über die Liste solange Knoten existieren
            // und kein passender gefunden hier
            t = t.getNachfolger();
        }
        if (t == nach) { // Der Knoten wurde gefunden
            // Merken des Nachfolger hinter der Einfuegestelle
            Listenknoten derNaechste = nach.getNachfolger();
            if (derNaechste != null) {
                // Setzen des neuen Vorgängers falls es einen Nachfolger gab
                derNaechste.setVorgaenger(k);
            } else { // Wir haben am Ende eingefuegt. Ende-Zeiger korrigieren
                ende = k;
            }
            // Der neu eingefügte Knoten soll auf den Nachfolger zeigen
            k.setNachfolger(derNaechste);
            // Der gesuchte Knoten ist der Vorgaenger. Der neue Knoten
            // soll auf ihn zeigen
            k.setVorgaenger(t);
            // Der Vorgaenger soll auf den neu einfeügten Knoten zeigen
            t.setNachfolger(k);
        }
    }
    /**
     * Füge einen Knoten am Anfang der Liste ein
     * @param k 
     */
    public void einfuegeAnKopf(Listenknoten k) {
        k.setVorgaenger(null);
        if (kopf == null) { // Die Liste ist leer
            ende = k; // Das Ende ist auch der Kopf
            k.setNachfolger(null); // Sicherstellen das kein Nachfolger benutzt wird
        } else { // Liste ist nicht leer
            k.setNachfolger(kopf); // Neuer Nachfolger ist alter Kopf
            kopf.setVorgaenger(k); // Alter Kopf bekommt Vorgaenger
 
        }
        kopf = k; // Es gibt einen neuen Kopf. Heureka
    }
 
    public void loesche(Listenknoten k) {
Listenknoten t = kopf; // Diese Variable wird über die Listenknoten laufen
        if (kopf == null) {
            System.out.println("Löschversuch auf leerer Liste");
        } else {
            //Pflege der Kopf- bzw Endezeiger
            if (kopf == k) {
                // Das Kopfelement wir geloescht
                kopf = k.getNachfolger(); //Kann auch Null sein...
            }
            if (ende == k) {
                // Das zu loeschende Objekt ist das Letzte
                ende = k.getVorgaenger(); // Kann auch Null sein...
            }
            while ((t != null) && (t != k)) {
                // Iteriere über Liste solange Nachfolger da sind
                // und nichts passendes gefunden wird
                t = t.getNachfolger();
            }
            if (t == k) { // Der Knoten wurde gefunden
                Listenknoten nachf = k.getNachfolger();
                Listenknoten vorg = k.getVorgaenger();
                if (nachf != null) {
                    // Es gibt einen Nachfolger. Pflege dessen Vorgaenger
                    nachf.setVorgaenger(vorg);
                }
                if (vorg != null) {
                    // Es gibt einen Vorgaenger. Pflege dessen Nachfolger
                    vorg.setNachfolger(nachf);
                }
                // Entfernen aller Referenzen des geloeschten Objekts
                t.setNachfolger(null);
                t.setVorgaenger(null);
            } // Ende der Knoten wurde gefunden
        } // Ende Loeschen aus einer nicht leeren Liste
    } // Ende Methode loesche
 
    /**
     * Ist wahr wenn ein Knoten mit dem gleichen Wert existiert
     * @param k
     * @return
     */
    public boolean enthaelt(Listenknoten k) {
        Listenknoten t = kopf;
        boolean result = false;
        while ((result == false) && (t != null)) {
            result = (t == k);
            if (!result) {
                t = t.getNachfolger();
            }
        }
        return result;
    } // Ende Methode enhaelt()

} // Ende der Klasse EineListe

 

Stapel (Stack)

Der Stapel (engl. Stack) ist eine der wichtigsten Grundstrukturen der Informatik. Er funktioniert nach dem Prinzip, dass man bildlich gesprochen Elemente stapelt und immer nur auf das oberste Element des Stapels zugreifen kann um es zu lesen oder zu entfernen. Stapel werden daher im Englischen auch mit LIFO abgekürzt:

LIFO: Last In First Out

 

Hierdurch ergeben sich die typischen Operationen die für einen Stapel (in englisch) definiert sind:

  • peek: Lesen der obersten Datenstruktur ohne sie zu entfernen
  • pop: Lesen der obersten Datenstruktur und gleichzeitiges Entfernen
  • push: Einfügen einer neuen Datenstruktur als oberste Datenstruktur
  • search: Suche nach einer Datenstruktur im Stapel und Rückgabe der Position (von oben gezählt)

Java verfügt bereits über eine Implementierung von Stapeln in der Klasse java.util.Stack in der die oben genannten Operationen als Methoden für beliebige Instanzen der Klasse Object zur Verfügung stehen.

Stapel sind im realen Leben nicht ganz so häufig anzutreffen wie in der Informatik. Ein sehr gutes Beispiel aus dem realen Leben sind

  • Teller die typischerweise aufeinander gestapelt werden. Der letzte Teller den man auf einen Stapel von Tellern legt ist auch typischerweise der erste Teller den man wieder von einem Stapel nimmt.
  • Passagiere die eine Flugzeug besteigen und es dann in umgekehrter Reihenfolge verlassen. (Die strenge Implementierung eines Stapel/Stacks ohne Ausnahmen ist hier weder für die Fluggesellsschaft noch für die Passagiere akzeptabel) 
  • Eine Einzelgarage mit einem Fahrzeugabstellplatz vor der Garage. Dies ist ein Stapel/Stack der Tiefe zwei
  • Ein Abstellgleis mit Prellbock.

In der Informatik sind Stapel sehr nützlich, wann immer man einen Zustand speichern muss um sich etwas Anderem zu widmen, und dann wieder auf den alten Zustand zurückzugreifen. Beispiele hierfür sind

  • Verwalten von Variablen bei Methodenaufrufen in Java: Beim jedem Aufruf einer Methode werden die Variablen der aktuellen Methode auf von den Variablen der aufgerufenen Methode verdeckt(Push). Die Variablen der aufgerufenen Methode sind eine Datenstruktur die neu auf den Stapel gelegt wurden und die Variablen der alten Methode verdecken. Nach dem Abarbeiten der neuen Methode werden deren Variablen wieder gelöscht(Pop) und die Variablen der vorhergehenden Methode stehen wieder zur Verfügung
  • Parsen (Analysieren) von Programiersprachen, Ausdrücken und Termen wie zum Beispiel ein mathematischer Term: 5 + 4*3. Hier wird bei der Analyse  von 5 und "+" festgestellt, dass der zweite Operand (4*3) selbst wieder ein Term ist. Das Parsen der Addition wird angehalten. Die nötigen Datenstrukturen zum Parsen der Multiplikation werden angelegt und verdecken die Addition(Pop). Der Wert von 4*3 wird berechnet. Alle Datenstrukturen die zur Berechnung der Multiplikation benötigt wurden können jetzt wieder gelöscht werden (POP) um mit der ursprünglichen Multiplikation fortzufahren. 

Beispiel: Die Auswertung des Terms 5+4*3

 

 

 

Warteschlangen (Queues)

 Warteschlangen (engl. queues) sind Datenstrukturen die auf dem FIFO Prinzip basieren:

FIFO
FIFO: First In, First Out

Die Objekte die zuerst eingefügt werden, werden auch als erstes wieder ausgelesen. Warteschlangen erhalten die Reihenfolge der Ankunft indem Sie immer nur Objekte am Kopf entnehmen und immer nur Objekte am Ende der Warteschlange einfügen. Typische Operationen auf einer Warteschlange sind:

  • peek: Lesen der Datenstruktur am Kopf der Warteschlange ohne sie zu entfernen
  • read: Lesen der vordersten Datenstruktur  am Kopf und gleichzeitiges Entfernen
  • write: Einfügen einer neuen Datenstruktur am Ende der Warteschlange
  • search: Suche nach einer Datenstruktur in der Warteschlange und Rückgabe der Position
  • empty: testen ob die Warteschlange leer ist.

Wichtig: Warteschlangen bieten keinen wahlfreien Zugriff auf alle ihre Elemente

Warteschlangen sind im realen Leben häufig anzutreffen wenn es sich um Prozesse handelt die alle Beteiligten demokratisch, transparent (fair?) behandeln soll:

  • Fließband
  • Rolltreppe
  • Warten bei vielen Dienstleistern wird als FIFO organisiert

In der Informatik sind Warteschlangen immer dann anzutreffen wenn die Erhaltung einer Ankunftsreihenfolge eine Rolle spielt:

  • Datenbanksperren
  • Eingehende Webserveranforderungen
  • Transaktionales Arbeiten (Sitzplatzreservierung)

Implementierung

Warteschlange sind Spezialfälle von Listen. Im Unterschied zu einer Liste erlauben sie nur das Einfügen von Objekten am Ende und das Entfernen am Kopf der Warteschlange:

Ringpuffer

Warteschlangen die auf einfach oder zweifach verketteten Knoten beruhen sind sehr flexibel. Sie sind jedoch auch ineffizient da bei jedem Aufruf neue Knotenobjekte mit dem aufwendigen new() Operator erzeugt werden müssen.

Dies kann durch die Verwendung von ausreichend großen Ringpuffern vermieden werden, da hier die Knotenobjekte nur einmalig angelegt werden müssen. Diese Ringpuffer werden durch ein Feld R mit einer festen Größe n implementiert. Beim Erreichen des Endes des Feldes wird dieses durch ein Fortsetzen am Beginn des Feldes zum Ring "zusammengebogen".

Hierzu benötigt man:

  • einen Zeiger auf das Ende der Warteschlange (im Beispiel R[j])
  • einen Zeiger auf den Kopf der Warteschlange (Im Beispiel R[i])

Beim Einfügen und Entfernen von Elementen werden beim Verrücken der Zeiger der Restwert nach der Division durch die Größe des Felds verwendet um nicht einen Feldüberlauf zu Erzeugen. Beim Einfügen eines neuen Elements in den Ringpuffer wird das neue Ende in Java wie folgt berechnet:

ende = (ende-1)%n;

Der Kopf wird nach dem Entfernen eines Elements wie folgt berechnet:

kopf = (kopf-1)%n;

Wichtig: Ringpuffer haben eine feste Größe. Es muss explizit eine Ausnahmebehandlung implementiert werden, wenn die Warteschlange über die Größe n des Feldes wächst.

 

Bäume

Bäume sind hierarchische Datenstrukturen die aus einer verzweigten Datenstruktur bestehen. Bäume bestehen aus Knoten und deren Nachfolgern.

Jeder Knoten in einem Baum hat genau einen Vorgängerknoten. Der einzige Knoten für den das nicht gilt, ist der Wurzelknoten (engl. root) er hat keinen Vorgänger. Der Wurzelknoten stellt den obersten Knoten dar. Von dieser Wurzel aus kann man von Knoten zu Knoten zu jedem Element des Baums gelangen.

Rekursive Definition eines Baums
Ein Baum besteht aus Unterbäumen und diese setzen sich bis zu den Blattknoten wieder aus Unterbäumen zusammen.

Die Baumstruktur wird in Ebenen unterteilt.

 

Die Tiefe eines Baumes ergibt sich aus der maximalen Anzahl der Ebenen.

Vollständige Bäume

Vollständige Bäume
Ein Baum ist vollständig wenn alle Ebenen ausser der letzten Ebene vollständig mit Knoten gefüllt sind.

Im folgenden Beispiel wird ein Baum gezeigt, der maximal drei Nachfolgerknoten pro Konten besitzen kann:

 Vollständiger Baum

Binärbäume

Binärbäume bestehen aus Knoten mit maximal 2 Nachfolgerknoten. Die maximale Anzahl Knoten pro Ebene ist 2(k-1) Knoten in der Ebene k.

Streng sortierte Binärbäume

Streng sortierte Binärbäume

Für jeden Baumknoten gilt:

  • Alle Knoten im linken Unterbaum haben kleinere Werte als der Vorgängerknoten
  • Alle Knoten im rechten Unterbaum haben größere Werte als der Vorgängerknoten

Beispiel eines streng sortierten Binärbaums:

 

Suchen

Sortierte Binärbäume eignen sich sehr gut zum effektiven Suchen, da mit jedem Knoten die Auswahl der Kandidaten in der Regel halbiert wird.

Das Suchen in einem sortierten Baum geschieht nach dem folgenden, rekursiven Prinzip:

  • Beginne mit dem Wurzelknoten
  • Vergleiche den gesuchten Wert mit dem Wert des Knoten und beende die Suche wenn der Wert übereinstimmt.
  • Suche im linken Teilbaum weiter wenn der gesuchte Wert kleiner als der aktuelle Knoten ist.
  • Suche im rechten Teilbaum weiter wenn der gesuchte Wert größer als der Wert des aktuellen Knoten ist.

Bemerkung: Effektives Suchen ist nur in gutartigen (balancierten) Bäumen möglich. Gutartige Bäume haben möglichst wenig Ebenen. Schlechte Bäume zum Suchen sind degenerierte Bäume die im schlimmsten Fall pro Ebene nur einen Knoten besitzen. Solche degenerierten Bäume verhalten sich wie Listen.

Gutartige Bäume haben daher eine Tiefe T= ld(n+a) was auch dem Aufwand zum Suchen O (ld(n)) entspricht. Schlechte Bäume haben eine Tiefe T = n was zu einem Suchaufwand wie bei Listen, also O(n) führt.

Beispielprogramm

Das folgende Programm erlaubt es manuell einen Binärbaum aus Ganzzahlen aufzubauen.

Download Applet im Browser
Das Programm steht als jar Datei zur Verfügung und kann nach dem Download wie folgt gestartet werden:
java -jar BaumGUI.jar

Es erlaubt das Einfügen und Entfernen von Blattknoten:

 

AVL Bäume (Ausgeglichene Bäume)

Binärbäume werden benutzt da sie ein sehr effizientes Suchen mit einem Aufwand von O(logN) erlauben solange sie balanciert sind. In extremen Fällen kann die Suche nach Knoten jedoch zu einem Aufwand von O(N) führen falls der Baum degeneriert ist.

Die im vorhergehenden Abschnitt vorgestellten Bäume verwenden in ihren Implementierungen der Einfüge- und Entferneoperation naive Verfahren. Diese naive Verfahren können dazu führen, dass ein Baum sehr unbalanciert werden kann.

Erste Vorschläge zur Vermeidung dieses Problems gehen auf die 1962 gemachten Vorschläge von Adelson-Velskij und Landis zurück. Sie schlugen höhenbalancierte AVL Bäume vor.

AVL Bäume

Definition
AVL Baum

Ein binärer Baum ist höhenbalanciert bzw AVL ausgeglichen wenn für jeden Knoten im Baum gilt:

  • Die Höhe des linken Unterbaums ist maximal 1 größer oder kleiner als die Höhe des rechten Unterbaums

 

 Beispiele von AVL Bäumen die der obigen Definition genügen sind im folgenden Diagramm zu sehen

 Der nachfolgende Baum ist kein AVL Baum. Der rechte Knoten in der zweiten Ebene von oben hat einen linken Unterbaum der Höhe 1 und einen rechten Unterbaum der Höhe 3.

Dieser Baum müsste umorganisiert werden um ein AVL Baum zu werden.

Zum Verwalten von AVL Bäumen berechet man jedoch nicht immer wieder die Höhe der Teilbäume.

Es ist einfacher einen Balanchefaktor zu jedem inneren Knoten mitzuführen der die Differenz der Höhe vom linken und rechtem Teilbaum verwaltet.

Definition
Balancefaktor bal(p) eines AVL Baums

bal(p) = (Höhe rechter Teilbaum von p) - (Höhe linker Teilbaum von p)

bal(p)∈{-1,0,1}


Im folgenden Beispiel sind die Balancefaktoren für die inneren Knoten eingetragen:

 

 AVL Bäume müssen beim Einfügen und Entfernen von Knoten unter Umständen neu balanciert werden. Der Vorteil besteht jedoch in dem sehr vorhersagbaren Verhalten mit dem Aufwand von O(logN) bei Operationen auf dem Baum

Bruderbäume

Bruderbäume kann man als expandierte AVL Bäume verstehen.

Bruderbäume bekommen durch gezieltes Einfügen unärer Knoten eine konstante Tiefe für alle Blätter. Sie sind höhenbalancierte Bäume. Mit ihnen lässt sich garantieren, dass man mit dem gleichen Suchaufwand auf alle Blätter des Baums zugreifen kann.

Bruderbäume unterscheiden sich von den Binärbäumen dadurch, dass die inneren Knoten mindesten einen Sohn haben.

Bruderbäume haben ihren Namen von dem Fakt, dass für die Söhne eines Knoten untereinander (die Brüder) bestimmte Regeln gelten.

Definition
Bruder
Zwei Knoten heißen Brüder wenn sie denselben Vater (Vorgängerknoten) haben.

Ein binärer Baum ist ein Bruderbaum wenn das folgende gilt:

Definition
Bruderbaum

Ein Baum heißt Bruderbaum wenn die folgenden Bedingungen gelten:

  • Jeder innere Knoten hat einen oder zwei Söhne
  • Jeder unäre Knoten hat einen binären Bruderknoten
  • Alle Söhne haben die gleiche Tiefe

Beispiele

Im folgenden Biagramm ist der rechte Baum ist kein Bruderbaum da es auf der zweiten Ebene von oben zwei unäre Bruder gibt.

Im Diagramm unten ist der rechte Baum kein Bruderbaum weil die Blätter eine unterschiedliche Tiefe besitzen

Bemerkung

Bei Bruderbäumen müssen bei Bedarf innerer Knoten eingefügt werden um die Bruderbedingungen zu erfüllen. Bruderbäume sind daher Bäume bei denen die zu verwaltenden Datenstrukturen nicht notwendigerweise in den inneren Knoten verwaltet werden können.

Übungen (Bäume)

Balancierte und unbalancierte Bäume

Das folgende Programm erlaubt es manuell einen streng geordneten Binärbaum aus Ganzzahlen aufzubauen. Es gibt zwei Möglichkeiten das Programm zu starten:

Applet (im Browser) Download und Start als Hauptprogramm

Das Programm steht als jar Datei zur Verfügung und kann nach dem Download wie folgt gestartet werden:

java -jar BaumGUI.jar

Balancierter Baum

Benutzen Sie das Beispielprogramm und erzeugen sie einen balancierten Baum mit 15 Knoten und der Höhe 4 wie zum Bsp.:

In welcher Reihenfolge müssen die Werte eingegeben werden?

Degenerierter Baum

Erzeugen Sie einen degenerierten Baum mit 5 Knoten und der Höhe 5:

 

Welche Eingabefolgen von Zahlen erzeugen eine Liste degenerierten Teilbäumen?

Implementierung einer Binärbaums

Implementieren Sie einen streng geordneten Binärbaum in dem man ganze Zahlen Einfügen und Löschen kann.

Es ist nicht notwendig einen balancierten Baum oder AVL Baum zu implementieren.

Vervollständigen Sie die 3 drei fehlenden Methoden:

  • Kurs2.Baum.Baumknoten
    • Methode hoehe() : Implementieren Sie eine rekursive Methode zum bestimmen der Höhe des Baums (1.ter Schritt)
  • Kurs2.Baum.Binaerbaum
    • Methode einfuegen(teilBaum, Knoten) (2.ter Schritt)
      • Tipps
        • Betrachten Sie zuerst Sonderfälle (fehlen von Söhnen)
        • Welchen Teilbaum müssen Sie modifizieren wenn der neue Knoten kleiner als die aktuelle Wurzel ist?
        • Welchen Teilbaum müssen Sie modifizieren wenn der neue Knoten größer als die aktuelle Wurzel ist?
        • Fügen Sie keinen Knoten mit einem Wert ein, der schon existiert!
    • Methode loeschen(teilBaum, Knoten) (3.ter Schritt)
      • Tipps
        • Was müssen Sie tun wen der aktuelle Knoten gelöscht werden muss?
        • Was müssen Sie tun wenn der zu löschende Knoten die Wurzel des gesamten Baums ist?

Übersetzen Sie alle Klassen. Starten Sie die Anwendung mit

$ java Kurs2.Gui.BaumGUI

Testen Sie die Anwendung mit der automatisierten Generierung mit dem Befehl

$ java Kurs2.Gui.BaumGUI magic

Hinweis: Die Implementierung des Algorithmus zum Entfernen von Knoten ist sehr viel aufwendiger da viele Randbedingungen geprüft werden müssen. Implementieren Sie zu Beginn nur das Einfügen von Knoten und Testen Sie zuerst das Einfügen. Die aktuelle Trivialimplementierung zum Entfernen erlaubt das Übersetzen und Ausführen der Anwendung ohne das Knoten entfernt werden.

UML Diagramm der beteiligten Klassen:

UML Diagramm Baumuebung

Notwendige Vorarbeiten

Erzeugen Sie die Infrastruktur für die beiden folgenden Pakete:

  • Kurs2.Baum
  • Kurs2.Gui

Klasse Kurs2.Baum.Baumknoten

package Kurs2.Baum;

/**
*
* @author sschneid
*/
public class Baumknoten {
private int wert;
//private final int maxWert= Integer.MAX_VALUE;
private final int maxWert= 99;

public int getWert() {
return wert;
}

public void setWert(int wert) {
this.wert = wert;
}
/**
* Verwalte linken Knoten
*/
private Baumknoten l;
/**
* verwalte rechten Knoten
*/
private Baumknoten r;

public Baumknoten(int i) {wert=i;}
/**
* Liefere linken Knoten zurück
* @return linker Knoten
*/
public Baumknoten getLinkerK() {
return l;
}
/**
* Setze linken Knoten
* @param k Referenz auf linken Knoten
*/
public void setLinkerK(Baumknoten k) {
l = k;
}
/**
* Liefere rechten Knoten zurück
* @return rechter Knoten
*/
public Baumknoten getRechterK() {
return r;
}
/**
* Setze rechten Knoten
* @param k rechter Knoten
*/
public void setRechterK(Baumknoten k) {
r = k;
}
/**
* Drucken einen Unterbaum und rücke entsprechend bei Unterbäumen ein
* @param einruecken
*/
public void druckeUnterbaum(int einruecken) {
if (l != null) {
l.druckeUnterbaum(einruecken + 1);
}
for (int i = 0; i < einruecken; i++) {
System.out.print(".");
}
System.out.println(toString());
if (r != null) {
r.druckeUnterbaum(einruecken + 1);
}
}
/**
* Berechne Höhe des Baums durch rekursive Tiefensuche
* @return
*/
public int hoehe() {
System.out.println("Implementieren Sie Baumknoten.hoehe() als rekursive Methode");
return -1;
}
/**
* Generiere eine Zufallsbelegung für den gegebenen Knoten
* Die Funktion darf nicht mehr nach EInfügen in den Baum
* aufgerufen werden, da der neue Wert zu einer inkorrekten Ordnung führt
*/
public void generiereZufallswert() {
wert= (int)(Math.random()*(double)maxWert);
}
/**
* Erlaubt den Zahlenwert als Text auszudrucken
* @return
*/
public String toString() { return Integer.toString(wert);}
}

Klasse Kurs2.Baum.Binaerbaum

package Kurs2.Baum;

/**
*
* @author sschneid
*/
public class Binaerbaum {
private Baumknoten wurzelKnoten;

public Baumknoten getWurzelknoten() {return wurzelKnoten;}
/**
* Füge einen neuen Baumknoten in einen Baum ein
* @param s
*/
public void einfuegen(Baumknoten s) {
if (wurzelKnoten == null) {
// Der Baum ist leer. Füge Wurzelkonoten ein.
wurzelKnoten = s;
}
else // Der Baum ist nicht leer. Normales Vorgehen
einfuegen(wurzelKnoten,s);
}
/**
* Füge einen gegebenen Knoten s in einen Teilbaum ein.
* Diese Methode ist eine rekursive private Methode
* Da der neue Knoten die Wurzel des neuen Teilbaums bilden kann,
* wird eventuell ein Zeiger auf einen neuen Teilbaum zurückgeliefert
* Randbedingung:
* * Es wird kein Knoten mit einem Wert eingefügt der schon existiertz
* @param teilbaum
* @param s
*/
private void einfuegen(Baumknoten teilbaum, Baumknoten s) {
System.out.println("Implementieren Sie die Methode Binaerbaum:einfuegen()");
}
/**
* Öffentliche Methoden zum Entfernen eines Baumknotens
* @param s
*/
public void entfernen(Baumknoten s) {
wurzelKnoten = entfernen(wurzelKnoten,s);}
/**
* Private, rekursive Methode zum Entfernen eines Knotens aus einem
* Teilbaum. Es kann ein neuer Teilbaum enstehen wennn der Wurzelknoten
* selbst entfernt werden muss. Der neue Teilbaum wird daher wieder mit
* ausgegeben
* @param teilbaum Teilbaum aus dem ein Knoten entfernt werden soll
* @param s der zu entfernende Knoten
* @return Der verbleibende Restbaum. Es kann auch Null für einen leeren Baum ausgegeben werden
*/
private Baumknoten entfernen(Baumknoten teilbaum, Baumknoten s) {
System.out.println("Implementieren Sie die Methode Binaerbaum:entfernen()");
return teilbaum;
}
/**
* Berechnung der Hoehe des Baums
* @return Hoehe des Baums
*/
public int hoehe() {
if (wurzelKnoten == null) return 0;
else return wurzelKnoten.hoehe();
}
/**
* Rückgabe des Namens
* @return
*/
public String algorithmus() {return "Binaerbaum";}

public void druckenBaum() {
System.out.println("Tiefe:" + hoehe());
if (wurzelKnoten != null) wurzelKnoten.druckeUnterbaum(0);
System.out.println("A-----------A");
}

}

Klasse Kurs2.Gui.BaumGUI

Hinweis: Diese Klasse befindet sich in einem anderen Package. Erzeugen Sie sich die Infrastruktur für dieses Paket bevor Sie den Code kopieren

package Kurs2.Gui;

import Kurs2.Baum.Binaerbaum;
import Kurs2.Baum.Baumknoten;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JTextField;

/**
*
* @author sschneid
*/
public class BaumGUI implements ActionListener {

private JFrame hf;
private JButton einfButton;
private JButton entfButton;
private JTextField eingabeText;
private JMenuBar jmb;
private JMenu jm;
private JMenuItem exitItem;
private Exception myException;
private BaumPanel myBaum;
private Binaerbaum b;

public BaumGUI(Binaerbaum bb) {
b = bb;
JLabel logo;
//ButtonGroup buttonGroup1;
JPanel buttonPanel;
// Erzeugen einer neuen Instanz eines Swingfensters
hf = new JFrame("BaumGUI");
// Gewünschte Größe setzen
// 1. Parameter: horizontale Größe in Pixel
// 2. Parameter: vertikale Größe
hf.setSize(220, 230);

// Beenden bei Schliesen des Fenster
hf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

// Erzeugen der Buttons und Texteingabefeld
eingabeText = new JTextField("17");
einfButton = new JButton();
einfButton.setText("Einfügen");
entfButton = new JButton();
entfButton.setText("Entfernen");

// Registriere die eigene Instanz
// zum Reagieren auf eine Aktion der beiden Buttons
einfButton.addActionListener(this);
entfButton.addActionListener(this);

// Einfügen der drei Komponenten in ein Panel
// Das Gridlayout führt zum Strecken der drei Komponenten
buttonPanel = new JPanel(new GridLayout(1, 1));
buttonPanel.add(eingabeText);
buttonPanel.add(entfButton);
buttonPanel.add(einfButton);

// Erzeugen des Panels zum Malen des Baums
myBaum = new BaumPanel(b);

// setze Titel des Frame
hf.setTitle(b.algorithmus());

// Erzeuge ein Menueeintrag zum Beenden des Programms
jmb = new JMenuBar();
jm = new JMenu("Datei");
exitItem = new JMenuItem("Beenden");
exitItem.addActionListener(this);
jm.add(exitItem);
jmb.add(jm);
hf.setJMenuBar(jmb);

Container myPane = hf.getContentPane();
myPane.add(myBaum, BorderLayout.CENTER);
myPane.add(buttonPanel, BorderLayout.SOUTH);

hf.pack();
hf.setVisible(true);
hf.setAlwaysOnTop(true);
}

/**
* Diese Methode wird bei allen Aktionen der Menüleiste oder
* der Buttons aufgerufen
* @param e
*/
public void actionPerformed(ActionEvent e) {
Object source = e.getSource();
int wert = 0;
try {
if (source == entfButton) { //Entfernen aufgerufen
wert = Integer.parseInt(eingabeText.getText());
b.entfernen(new Baumknoten(wert));
myBaum.fehlerText("");
myBaum.repaint();
eingabeText.setText("");
}
if (source == einfButton) { // Einfügen aufgerufen
wert = Integer.parseInt(eingabeText.getText());
b.einfuegen(new Baumknoten(wert));
myBaum.fehlerText("");
myBaum.repaint();
eingabeText.setText("");
}
if (source == exitItem) { // Beenden aufgerufen
System.exit(0);
}
} catch (java.lang.NumberFormatException ex) {
// Fehlerbehandlung bei fehlerhafter Eingabe
myBaum.fehlerText("Eingabe '" + eingabeText.getText() + "' ist keine Ganzzahl");
myBaum.repaint();
eingabeText.setText("");
}
}

public static void main(String[] args) {
BaumGUI sg = new BaumGUI(new Binaerbaum());
if ((args.length > 0) && (args[0].equalsIgnoreCase("magic"))) {
sg.magicMode(15);
}

}

public void magicMode(int anzahl) {
Baumknoten[] gz = new Baumknoten[anzahl];
for (int i = 0; i < gz.length; i++) {
gz[i] = new Baumknoten(0);
gz[i].generiereZufallswert();
}
try {
for (int i = gz.length - 1; i >= 0; i--) {
eingabeText.setText(gz[i].toString());
b.einfuegen(gz[i]);
Thread.sleep(800);
myBaum.repaint();
}
for (int i = 0; i < gz.length; i++) {
eingabeText.setText(gz[i].toString());
b.entfernen(gz[i]);
Thread.sleep(800);
myBaum.repaint();
}
} catch (InterruptedException e) {
}
}

}

Klasse Kurs2.Gui.BaumPanel

package Kurs2.Gui;

import Kurs2.Baum.Binaerbaum;
import Kurs2.Baum.Baumknoten;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JPanel;

/**
*
* @author sschneid
*/
public class BaumPanel extends JPanel{
private Binaerbaum b;
private int ziffernBreite = 10 ; // Breite einer Ziffer in Pixel
private int ziffernHoehe = 20; // Hoehe einer Ziffer in Pixel
private String fehlerString = "";

public BaumPanel(Binaerbaum derBaum) {
b = derBaum;
setPreferredSize(new Dimension(600,200));
setDoubleBuffered(true);
}

/**
* Setzen des Fehlertexts des Fehlertexts
* @param s String mit Fehlertext
*/
public void fehlerText(String s) {fehlerString = s;}

/**
* Methode die das Panel überlädt mit der Implementierung
* der Baumgraphik
* @param g
*/
public void paintComponent(Graphics g) {
super.paintComponent(g);
int maxWidth = getWidth();
int maxHeight = getHeight();
Baumknoten k = b.getWurzelknoten();
if (k != null) { // Male Wurzelknoten falls existierend
g.setColor(Color.black);
g.drawString("Hoehe: " + b.hoehe(), 10, 20);
g.drawString(fehlerString, 10, 40);
paintKnoten(g,k,getWidth()/2, 20);
}
else {
g.setColor(Color.RED);
g.drawString("Der Baum ist leer. Bitte Wurzelknoten einfügen.",10,20);
}
}

/**
* Malen eines Knotens und seines Unterbaums
* @param g Graphicshandle
* @param k zu malender Knoten
* @param x X Koordinate des Knotens
* @param y Y Koordinate des Knotens
*/
public void paintKnoten(Graphics g,Baumknoten k, int x, int y) {
int xOffset = 1; // offset Box zu Text
int yOffset = 7; // offset Box zu Text
String wertString = k.toString(); // Wert als Text
int breite = wertString.length() * ziffernBreite;
int xNextNodeOffset = ((int)java.lang.Math.pow(2,k.hoehe()-1))*10;
int yNextNodeOffset = ziffernHoehe*6/5; // Vertikaler Offset zur naechsten Kn.ebene
if (k.getLinkerK() != null) {
// Male linken Unterbaum
g.setColor(Color.black); // Schriftfarbe
g.drawLine(x, y, x-xNextNodeOffset, y+yNextNodeOffset);
paintKnoten(g,k.getLinkerK(),x-xNextNodeOffset,y+yNextNodeOffset);
}
if (k.getRechterK() != null) {
// Male rechten Unterbaum
g.setColor(Color.black); // Schriftfarbe
g.drawLine(x, y, x+xNextNodeOffset, y+yNextNodeOffset);
paintKnoten(g,k.getRechterK(),x+xNextNodeOffset,y+yNextNodeOffset );
}
// Unterbäume sind gemalt. Male Knoten
g.setColor(Color.LIGHT_GRAY); // Farbe des Rechtecks im Hintergrund
g.fillRoundRect(x-xOffset, y-yOffset, breite, ziffernHoehe, 3, 3);
g.setColor(Color.black); // Schriftfarbe
g.drawString(wertString, x+xOffset, y+yOffset);
}

}

Baumschule...

Fragen zu Binärbäumen

Welche der beiden Bäume sind korrekte, streng sortierte Binärbäume?

Welche sind keine streng sortierten Binärbäume und warum?

Streng sortierter Binärbaum 1

Streng sortierter Binärbaum 2

AVL Bäume

Welche der beiden AVL-Bäume sind korrekte AVL-Bäume?

Welche Bäume sind keine korrekten AVL Bäume und warum?

AVL-Baum 1

 

AVL Baum 2

Bruder-Baum

 Welche der beiden Bäume sind keine korrekten Bruder-Bäume?

Warum sind sie keine Bruderbäume?

Bruder-Baum 1

Bruder-Baum 2

 

Bruder-Baum 3

Lösungen (Bäume)

Balancierte und unbalancierte Bäume

Balancierter Baum

Eingabe: 8, 4, 12, 2, 6, 10, 14, 1, 3, 5, 7, 9, 11, 13, 15

Es sind auch andere Lösungen möglich.

Degenerierte Baum

Eingabemöglichkeiten

  • 5, 4, 3, 2, 1
  • 1, 2, 3, 4, 5

Sortierte Folgen, gleich ob sie fallend oder steigend sortiert sind, führen bei einfachen binären Bäumen zu degenerierten Bäumen mit großer Höhe.

Implementierung eines Binärbaums

Klasse Kurs2.Baum.Baumknoten

 

Informelle Beschreibung der Algorithmen

Höhe eines Baums

  • Bestimme Höhe des linken Unterbaums
    • Gibt es einen linkenUnterbaum?
      • Nein: Höhe links =0
      • Ja: Höhe links= Höhe linker Unterbaum
  • Bestimme Höhe des rechten Unterbaum
    • Gibt es einen rechten Unterbaum?
      • Nein: Höhe rechts = 0
      • Ja: Höhe rechts = Höhe rechter Unterbaum
  • Bestimme den höchsten der beiden Unterbäume
  • Höhe des Baumes= 1 + Höhe höchster Unterbaum

Quellcode

 

package Kurs2.Baum;

/**
*
* @author sschneid
*/
public class Baumknoten {
private int wert;
//private final int maxWert= Integer.MAX_VALUE;
private final int maxWert= 99;
public int getWert() {
return wert;
}
public void setWert(int wert) {
this.wert = wert;
}
/**
* Verwalte linken Knoten
*/
private Baumknoten l;
/**
* verwalte rechten Knoten
*/
private Baumknoten r;
public Baumknoten(int i) {wert=i;}
/**
* Liefere linken Knoten zurück
* @return linker Knoten
*/
public Baumknoten getLinkerK() {
return l;
}
/**
* Setze linken Knoten
* @param k Referenz auf linken Knoten
*/
public void setLinkerK(Baumknoten k) {
l = k;
}
/**
* Liefere rechten Knoten zurück
* @return rechter Knoten
*/
public Baumknoten getRechterK() {
return r;
}
/**
* Setze rechten Knoten
* @param k rechter Knoten
*/
public void setRechterK(Baumknoten k) {
r = k;
}
/**
* Drucken einen Unterbaum und rücke entsprechend bei Unterbäumen ein
* @param einruecken
*/
public void druckeUnterbaum(int einruecken) {
if (l != null) {
l.druckeUnterbaum(einruecken + 1);
}
for (int i = 0; i < einruecken; i++) {
System.out.print(".");
}
System.out.println(toString());
if (r != null) {
r.druckeUnterbaum(einruecken + 1);
}
}
/**
* Berechne Höhe des Baums durch rekursive Tiefensuche
* @return
*/
public int hoehe() {
int lmax = 1;
int rmax = 1;
int max = 0;
if (l != null) lmax = 1 + l.hoehe();
if (r != null) rmax = 1 + r.hoehe();
if (rmax > lmax) return rmax;
else return lmax;

}
/**
* Generiere eine Zufallsbelegung für den gegebenen Knoten
* Die Funktion darf nicht mehr nach EInfügen in den Baum
* aufgerufen werden, da der neue Wert zu einer inkorrekten Ordnung führt
*/
public void generiereZufallswert() {
wert= (int)(Math.random()*(double)maxWert);
}
/**
* Erlaubt den Zahlenwert als Text auszudrucken
* @return
*/
public String toString() { return Integer.toString(wert);}

}

Klasse Kurs2.Baum.Binaerbaum

Informelle Beschreibung der Algorithmen

Einfügen von Knoten

  • Ist der Wert des neuen Knoten gleich der Wurzel meines Baums?
    • Ja: Fertig. Es wird nichts eingefügt
    • Nein:
      • Ist der Wert kleiner als mein Wurzelknoten?
        • Ja: Der neue Knoten muss links eingefügt werden
          • Gibt es einen linken Knoten?
            • Nein: Füge Knoten als linken Knoten ein
            • Ja: Rufe Einfügemethode rejursiv auf...
        • Nein: Der neue Knoten muss rechts eingefügt werden
          • Gibt es einen rechten Knoten?
            • Nein: Füge Knoten als rechten Knoten ein
            • Ja: Rufe Einfügemethode rekursiv auf...

Quellcode

package Kurs2.Baum;
/**
*
* @author sschneid
*/
public class BinaerbaumLoesung {
private Baumknoten wurzelKnoten;
public Baumknoten getWurzelknoten() {return wurzelKnoten;}
/**
* Füge einen neuen Baumknoten in einen Baum ein
* @param s
*/
public void einfuegen(Baumknoten s) {
if (wurzelKnoten == null) {
// Der Baum ist leer. Füge Wurzelkonoten ein.
wurzelKnoten = s;
}
else // Der Baum ist nicht leer. Normales Vorgehen
einfuegen(wurzelKnoten,s);
}
/**
* Füge einen gegebenen Knoten s in einen Teilbaum ein.
* Diese Methode ist eine rekursive private Methode
* Da der neue Knoten die Wurzel des neuen Teilbaums bilden kann,
* wird eventuell ein Zeiger auf einen neuen Teilbaum zurückgeliefert
* Randbedingung:
* * Es wird kein Knoten mit einem Wert eingefügt der schon existiertz
* @param teilbaum
* @param s
*/
private void einfuegen(Baumknoten teilbaum, Baumknoten s) {
if (!(s.getWert()==teilbaum.getWert())) {
// Nicht einfuegen wenn Knoten den gleichen Wert hat
if (s.getWert()<teilbaum.getWert()) {
// Im linken Teilbaum einfuegen
if (teilbaum.getLinkerK() != null)
einfuegen(teilbaum.getLinkerK(),s);
else teilbaum.setLinkerK(s);
}
else // Im rechten Teilbaum einfuegen
if (teilbaum.getRechterK() != null)
einfuegen(teilbaum.getRechterK(),s);
else teilbaum.setRechterK(s);
}
}

/**
* Öffentliche Methoden zum Entfernen eines Baumknotens
* @param s
*/
public void entfernen(Baumknoten s) {
wurzelKnoten = entfernen(wurzelKnoten,s);}
/**
* Private, rekursive Methode zum Entfernen eines Knotens aus einem
* Teilbaum. Es kann ein neuer Teilbaum enstehen wennn der Wurzelknoten
* selbst entfernt werden muss. Der neue Teilbaum wird daher wieder mit
* ausgegeben
* @param teilbaum Teilbaum aus dem ein Knoten entfernt werden soll
* @param s der zu entfernende Knoten
* @return Der verbleibende Restbaum. Es kann auch Null für einen leeren Baum ausgegeben werden
*/
private Baumknoten entfernen(Baumknoten teilbaum, Baumknoten s) {
Baumknoten result = teilbaum;
if (teilbaum != null) {
if (teilbaum.getWert()==s.getWert()) {
// der aktuelle Knoten muss entfernt werden
Baumknoten altRechts = teilbaum.getRechterK();
Baumknoten altLinks = teilbaum.getLinkerK();
if (altRechts != null) {
result = altRechts;
if (altLinks != null) einfuegen(result, altLinks);
}
else
if (altLinks!=null) result = altLinks;
else result = null;
}
else if (teilbaum.getWert()<s.getWert()) {
Baumknoten k = teilbaum.getRechterK();
teilbaum.setRechterK(entfernen(k,s));
}
else {
Baumknoten k = teilbaum.getLinkerK();
teilbaum.setLinkerK(entfernen(k,s));
}
}
return result;
}

/**
* Berechnung der Hoehe des Baums
* @return Hoehe des Baums
*/
public int hoehe() {
if (wurzelKnoten == null) return 0;
else return wurzelKnoten.hoehe();
}
/**
* Rückgabe des Namens
* @return
*/
public String algorithmus() {return "Binaerbaum";}

public void druckenBaum() {
System.out.println("Tiefe:" + hoehe());
if (wurzelKnoten != null) wurzelKnoten.druckeUnterbaum(0);
System.out.println("A-----------A");
}
}

Baumschule...

Fragen zu Binärbäumen

Welche der beiden Bäume sind korrekte, streng sortierte Binärbäume?

Welche sind keine streng sortierten Binärbäume und warum?

Streng sortierter Binärbaum 1

Der Baum ist kein streng sortierter Binärbaum. Knoten 3 müsste rechter Sohn von Knoten 2 sein. Knoten 4 müsste rechter Sohn von Knoten 3 sein.

Streng sortierter Binärbaum 2

Der Baum ist ein streng sortierter Binärbaum.

AVL Bäume

Welche der beiden AVL-Bäume sind korrekte AVL-Bäume?

Welche Bäume sind keine korrekten AVL Bäume und warum?

AVL-Baum 1

Der Baum ist ein AVL-Baum. Alle Blätter sind auf nur zwei unterschiedlichen Ebenen angeordnet.

 

AVL Baum 2

Der Baum ist kein AVL Baum. Das Fehlen eines zweiten Sohns von Knoten B führt zu einem Unterschied von zwei Ebenen im Baum.

Bruder-Baum

 Welche der beiden Bäume sind keine korrekten Bruder-Bäume?

Warum sind sie keine Bruderbäume?

Bruder-Baum 1

Der Baum ist ein Bruder-Baum. Alle Blätter sind auf der untersten Ebene. Der einzige unäre Knoten B hat einen binären Bruderknoten.

Bruder-Baum 2

Der Baum ist kein Bruder-Baum. Der Blattknoten E ist nicht auf der untersten Ebene.

 

Bruder-Baum 3

Der Baum ist kein Bruder-Baum. Knoten B und C sind unäre Brüder. Einer von ihnen müsste binär sein.

Knoten B und C sind überflüssig. Nach ihrem Entfernen entsteht wieder ein Bruder-Baum.

Lernziele (Datenstrukturen)

Am Ende dieses Blocks können Sie:

  • ... Listen, Warteschlangen, Stapel und Bäume anwenden umd große Mengen von Daten abhängig von den benötigten Zugriffsoperationen effizient verwalten.
  • ... mit hierarchischen Datenstrukturen die aus Knoten und Blättern existieren umgehen.
  • ... den Aufwand zum Suchen und Einfügen in hierarchische Datenstruktruen abschätzen.
  • ... die folgenden Bäumtypen erkennen und anwenden
    • Binärbäume
    • AVL-Bäume
    • Bruderbäume
    • vollständige Bäume
    • höhenbalancierte Bäume

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: Fragen zu Datenstrukturen