Programmieren

Programmieren

 Dieser Abschnitt behandelt fortgeschrittene Javakonzepte:

Stefan Schneider Wed, 01/19/2011 - 20:14

Oberflächenprogrammierung mit Swing

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)
Stefan Schneider Wed, 01/19/2011 - 20:25

Entwicklungsgeschichte der grafischen Benutzerschnittstellen in Java (JRE)

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 Browser 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.

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.

"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.

Stefan Schneider Fri, 02/25/2011 - 10:04

Implementieren von einfachen Swingkomponenten

Implementieren von einfachen Swingkomponenten

Swing-Komponenten

Eine sehr anschauliche Übersicht über die existierenden Swing-Komponenten kann man auf der Seite des MIT finden. Hier gibt es auch Beispielcode. Bei Oracle kann man auch ein Tutorial mit Beispielcode finden.

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
  • 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 s2.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:

 

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 JFrame

package s2.swing;

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

public class JFrameButtonTest {
   public static void main(String[] args) {
      JFrame myFrame = new JFrame("Einfaches Fenster");
      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

package s2.swing;
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/Hauptprogramm

 

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 s2.swing;

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

/**
*
* @author s@scalingbits.com
*/
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;
/**
 * 
 * @author s@scalingbits.com
 */
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

Quellcode
package s2.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;

/**
*
* @author s@scalingbits.com
*/
public class JPanelJApplet extends JApplet implements ActionListener {
final 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);
}

@Override
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());
}
}

 

Stefan Schneider Wed, 04/13/2011 - 16:25

Anonymous (not verified)

Mon, 04/18/2016 - 19:11

Hallo,
das Beispiel: "JTextfield in JFrame unter JPanel" ("Editier mich. Ich bin ein JTextfield") hat mit JPanel eigentlich nichts zu tun oder?
Ist es möglicherweise aus dem Abschnitt JTextfield kopiert und vergessen worden, es abzuändern?

Stefan Schneider

Mon, 04/18/2016 - 20:34

In reply to by Anonymous (not verified)

eigentlich schon.
Das Beispiel zeigt ein Panel welches drei Komponenten enthält. JPanels kann man halt nicht sehen :-(

Anonymous (not verified)

Sat, 05/18/2019 - 13:43

Hallo Herr Dr. Schneider,

der Link zur API Dokumentation Ihres Skriptes im Satz "Einige weitere wichtige Methoden sind in der API Dokumentation des Skripts beschrieben." funktioniert nicht.

Schönes Wochende :-D

Stefan Schneider

Sun, 05/19/2019 - 16:13

In reply to by Anonymous (not verified)

Danke,

gut beobachtet. Ich habe den Satz gelöscht. Er machte keinen Sinn.

Anonymous (not verified)

Sat, 06/08/2019 - 14:48

Moin Moin,

in ihrem Skript steht etwas von "3 Top-Level Container", aufgezählt sind aber 4.
Also wie viele sind es?

Anmerkung: Hinter "JApplet" steht ein Semikolon und kein Doppelpunkt. Und es fehlt ein "p" bei pain, bei "JFrame" ganz am Ende im letzten Absatz.

Gruß

Applets werden von keinen gängigen Browsern aus Sicherheitsgründen mehr unterstützt. Da man mit Applet nichts mehr machen kann bleiben noch drei. Habe das Skript angepasst.

Layoutmanager

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 s2.swing;

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

/**
 * Zeigt einen sehr einfachen FlowlayoutManager mit 5 Buttons
 * @author s@scalingbits.com
 */
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 s2.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 im Norden 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 s2.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 dargestellt
  • 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 s2.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:

 

Anonymous (not verified) Tue, 03/01/2011 - 11:04

Anonymous (not verified)

Mon, 04/18/2016 - 19:22

Hallo,

"Das folgende Beispiel zeigt ein vertikales Boxlayout welches nach 2 Sekunden automatisch die Orientierung zu vertikal und zurück triggert:"
-> Müsste sicher "die Orientierung zu HORIZONTAL und zurück triggert heißen ?

und

"Ein horizontales Layout wird mit der Konstanten BoxLayout.Y_AXIS beim Konfigurieren des Layoutmanagers erzeugt:"
-> horizontal müsste BoxLayout.X_AXIS sein, richtig ?

Anonymous (not verified)

Tue, 06/26/2018 - 12:05

Hallo,

Der BorderLayoutManager teilt Norden und Süden die gewünschte Breite und nicht die gewünschte Höhe zu, oder sehe ich das falsch?

Vgl. Westen / Osten.

lg

Schauen Sie sich mal das Bild mit den Buttons an:

  • Norden und Süden sind gerade groß genug. Sie sind aber breiter als minimal notwendig.
  • Osten, Westen, Mitte sind ursprünglich so breit wie sie minimal sein müssen. Zieht man das GUI in die Länge werden sie höher als notwendig.

Sikerim (not verified)

Sat, 05/28/2022 - 21:29

Jede Komponente wird in ihrer Wunschgröße gargestellt???
Eigenschaften von BoxLayout

Danke schön. Sie haben Augen wie ein Adler. Wurde verbessert.

Ereignisse und deren Behandlung

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 s2.swing;

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

/**
*
* @author s@scalingbits.com
*/
public class ActionListenerBeispiel implements ActionListener {

   @Override
   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);
   } // Ende main()
} // Ende der Klasse

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 s2.swing;

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

/**
*
* @author s@scalingbits.com
*/
public class ActionListenerBlockiert implements ActionListener {
   @Override
   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

Nachdenklicher Duke
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.

 

 

Stefan Schneider Tue, 03/01/2011 - 11:22

Anonymous (not verified)

Mon, 06/24/2013 - 16:31

Sollte der zweite Absatz unter der Überschrift eigentlich so formuliert sein?
"Der Entwickler kann dann nach der Registrierung eines bestimmten Ereignisses die Auswertung des Ereignisses vornehmen und auf das Ereignis mit der gewünschten Aktion reagieren."

Und beim ersten Beispiel steht: "Nach klicken des Button "feuert" ..."
Dort ist der Button "Hier klicken" gemeint, oder?

Anonymous (not verified)

Thu, 06/27/2013 - 22:55

In dem Beispiel:

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

müsste der Listener der auf jb registriert wird, "ActionListenerBeispiel" heißen oder?

Ich bin mir nicht ganz sicher, dass wir über das gleiche sprechen.
Kommunikation über diese Kommentare haben ihre Grenzen.

behandeln ist ein Zeiger auf ein Objekt ActionListenerBeispiel.
Dieses Objekt wird beim dem Objekt jb mit addActionListener registriert.

Das Programm übersetzt und funktioniert auch.

Event-Interface und Event-Adapter

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 nach dem Entwurfsmuster Adapter Klassen 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 s2.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");
  }
} // Ende der Klasse

Das Programm registriert sich selbst gegen den Button und ist jetzt in der Lage, die Information des dEventobjekts 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 s2.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 s2.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");
    }
}
 
 
 
 
 
 
 
 

 

Stefan Schneider Sat, 03/05/2011 - 17:23

Anonyme und innere Klassen

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 umschließenden 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 s2.swing;
 
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
 
/**
 *
 * @author s@scalingbits.com
 */
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 s2.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() {
            @Override
            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();
    }
}
Duke über Blueprint

Interessant

Eine anonyme, innere Klasse kann...

  • eine Schnittstelle implementieren
  • eine abstrakte Oberklasse spezialisieren
  • eine "normale" Klasse spezialisieren

Wichtig: Es müssen alle Methoden implementiert werden, die notwendig sind um die Klasse zu instanzieren

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. 

 

Stefan Schneider Sat, 03/05/2011 - 18:17

Anonymous (not verified)

Thu, 04/25/2013 - 21:10

Für das 1. Code Beispiel "MouseAdapterInnereKlasseTest", heißt die Innere Klasse nicht "MyMouseListener" anstatt wie in der Erklärung darunter "MyMouseClicked" beschrieben?

Entschuldigen Sie falls der Fehler schon in der Vorlesung gefunden wurde, ich war heute krank.
MfG

Danke für den Hinweis. Der Text wurde korrigiert. Die beiden Programmierbeispiele wurden um eine Methode entschlackt und sind jetzt kürzer und hoffentlich besser lesbar.

Müsste es dann in der Erklärung nicht statt:

"Die Klasse MyMouseClicked implementiert das MouseAdapter"

...

"Die Klasse MyMouseListener implementiert das MouseAdapter"

... heißen ?

Anonymous (not verified)

Wed, 05/15/2019 - 17:34

Hallo Herr Schneider,

mir ist ein kleiner Rechtschreibefehler aufgefallen. Bei den besonderen Eigenschaften von Elementklassen haben sie beim ersten Punkt umschliesen anstatt umschließen geschrieben.

freundliche Grüße

Übungen (Swing)

Ü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 Bild 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 s2.swing;

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 = "https://upload.wikimedia.org/wikipedia/commons/0/01/DHBW_MA_Logo.jpg";
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 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

Stefan Schneider Mon, 03/07/2011 - 09:22

Lösungen (Swing)

Lösungen (Swing)

Ausnahmefenster

package s2.swing;

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 AusnahmeFensterLoesung 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 AusnahmeFensterLoesung(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 = "https://upload.wikimedia.org/wikipedia/commons/0/01/DHBW_MA_Logo.jpg;
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) {
AusnahmeFensterLoesung dasFenster;
try {myTestMethod();}
catch (Exception e) {
dasFenster = new AusnahmeFensterLoesung("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 s2.swing;
 
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
   /**
   *
   * @author s@scalingbits.com
   * 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 s2.swing;
 
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
 
/**
*
* @author s@scalingbits.com
*/  
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 =
 
        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();
        }
 
    }
 
    /**
     * Hauptpr0gramm 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");
    }
}
 

 

Stefan Schneider Mon, 03/07/2011 - 09:31

Anonymous (not verified)

Tue, 04/21/2015 - 22:34

Wie in der Veranstaltung: hf.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); sollte sein hf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); damit das Programm beim Schließen des Fensters beendet wird. Viele Grüße

habe den Code verbessert.
Kommentar für die nicht Eingeweihten:
Mit diesen beiden Optionen kann man steuern ob die gesamte Anwendung bei schliesen des Fensters beendet wird. In der Übung macht das Beenden der Anwendung Sinn, da man sonst nichts mehr machen kann. Bei realen Anwendungen ist es oft besser wenn man die Anwendung nicht beendet weil es oft Ikonen oder Menüleisten gibt, die es erlauben neue Fenster zu öffnen.

Lernziele (Swing, innere und anonyme Klassen)

Lernziele (Swing, innere und anonyme Klassen)

Duke shows a curve at a black board

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)

Zur Umfrage

QR Code für Umfrage

Stefan Schneider Fri, 01/04/2013 - 12:50

Generische Klassen (Generics)

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 > {//Body}

 

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 s2.generics;

/**
*
* @author s@scalingbits.com
* @param <T> Generischer Typ der Klasse Koordinate
*/
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;
   }

   @Override
   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>(4.4f, 5.5f);
      System.out.println(k3);
   } // Ende main()
} // Ende Klasse Koordinate

Bei der Ausführung ergibt sich die folgende Konsolenausgabe:

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

 

Stefan Schneider Thu, 04/14/2011 - 16:52

Generics zur Übersetzungs- und Laufzeit

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 s2.generics;

/**
*
* @author s@scalingbits.com
* @param <T> Generischer Typ der Klasse Koordinate
*/
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;
}

@Override
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>(4.4f, 5.5f);
System.out.println(k3);
}
}

 

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!

 

Stefan Schneider Fri, 04/15/2011 - 11:09

Generics, Autoboxing, Subtyping

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.

 

Stefan Schneider Fri, 04/15/2011 - 16:20

Vererbung und generische Klassen

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 s2.generics;

/**
*
* @author s@scalingbits.com
* @param <T> Generischer Typ der Klasse Koordinate
*/
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;
}

@Override
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 s2.generics;
/**
*
* @author s@scalingbits.com
* @param <T> Die Dimensionen der Koordinate
*/
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;
}

@Override
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();
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 s2.generics;
/**
*
* @author s@scalingbits.com
*/
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;
}

@Override
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;
}

@Override
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 s2.generics;
/**
*
* @author s@scalingbits.com
*/
public class Koordinate3DDouble extends Koordinate<Double> {
private Double z;

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

@Override
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);
}
}

 

 

 

 

Stefan Schneider Sat, 04/16/2011 - 15:24

Anonymous (not verified)

Tue, 06/14/2016 - 12:08

Müssten die Syntax-Regeln nicht eigentlich Oberklasse heißen, wenn ein konkreter Typ festgelegt wird um die generische, bzw nicht generische Unterklasse abzuleiten?

Ich verstehe Ihre Frage nicht so richtig.
Würden Sie vielleicht einen Vorschlag posten?

Danke.

Wildcards

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 s2.generics;
/**
 *
 * @author s@scalingbits.com
 */

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;
   }

   @Override
   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;
   }
} // Ende der Klasse Koordinate

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)

 

Stefan Schneider Sun, 04/17/2011 - 11:33

Der "Diamondoperator" in Java

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

 

Stefan Schneider Sun, 03/13/2016 - 12:17

Übungen, Fragen (Generics)

Ü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 müssen ausschließlich die Methoden dieser Klasse und das Hauptprogramm angepasst werden.

package s2.generics;
/**
*
* @author s@scalingbits.com
*/
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 s2.generics;

public abstract class Getraenk {

}

2.3 Klasse Bier

package s2.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 s2.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 s2.generics;

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

}

2.6 Klasse Rotwein

package s2.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 s2.generics;
/**
*
* @author s@scalingbits.com
* @param <T> ein Getraenk
*/
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;
KoordinateTest<Float> k81, k82;

k81 = new KoordinateTest<Float>(2.2f, 3.3f);
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 KoordinateTest<Double>(3.3d,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.7d, 3.d);
}
}

 

 

Stefan Schneider Tue, 04/19/2011 - 16:53

Fabian Kappelhoff (not verified)

Fri, 02/04/2022 - 15:49

I really like this website.

An impressed visitor

Lösungen, Antworten

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 s2.generics;

/**
*
* @author s@scalingbits.com
* @param <T> ein Getraenk
*/
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:

package s2.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);
   }
}
Stefan Schneider Tue, 04/19/2011 - 17:21

Beispiel: Von einer nicht generischen Klasse zu einer generischen Klasse

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);
}
}

Stefan Schneider Sat, 05/14/2011 - 10:28

Lernziele (Generics)

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 sind.
  • ... 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

Zur Umfrage

QR Code für Umfrage

Stefan Schneider Tue, 01/01/2013 - 14:04

Java Collections Framework

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.

 

Stefan Schneider Sat, 05/14/2011 - 14:38

Überblick Java Collections

Ü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 (implem. Schnittst.) Zugriff Duplikate erlaubt Ordnung Kommentar
ArrayList Liste(List) wahlfrei über Index Ja geordnet effizientes Lesen über Index
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, Einfügen, Löschen
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, Einfügen, Löschen
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.

Frage: Die Klasse LinkedList taucht in der Tabelle zweimal auf. Wieso eigentlich?

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 s2.collection;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
*
* @author s@scalingbits.com
*/
public class LinkedListIterDemo {

public static void main(String[] args) {
List<Student> ll = new LinkedList<>();
Student s;
s = new Student("Curie","Marie", 19,1);
ll.add(s);
s = new Student("Merian","Maria-Sybilla", 17,3);
ll.add(s);
s = new Student("Noether","Emmi", 16,1);
ll.add(s);
s = new Student("Herschel","Caroline", 15,2);
ll.add(s);

System.out.println("Element auf Index 1:" + ll.get(1));

Iterator<Student> iter = ll.iterator();
System.out.println("Inhalt der Liste:");
while (iter.hasNext()){
s = iter.next();
System.out.println("Student: " +s);
}

System.out.println("Inhalt der Liste:");
for (Student st : ll)
System.out.println("Student: " +st);
}
}

package s2.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:(17,Merian Maria-Sybilla, 3)
Inhalt der Liste:
Student: (19,Curie Marie, 1)
Student: (17,Merian Maria-Sybilla, 3)
Student: (16,Noether Emmi, 1)
Student: (15,Herschel Caroline, 2)
Inhalt der Liste:
Student: (19,Curie Marie, 1)
Student: (17,Merian Maria-Sybilla, 3)
Student: (16,Noether Emmi, 1)
Student: (15,Herschel Caroline, 2)

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();

 

Stefan Schneider Sat, 05/14/2011 - 14:40

Mikail (not verified)

Sun, 11/21/2021 - 04:48

Im Bereich wo die angebliche Konsolenausgabe abgebildet sein sollt, passt das Ergebnis also die Konsolenausgabe weder zu den Eingabe Parametern, noch zu der Klasse und damit Bedingung für die Parameterliste die für "new Student" gilt, da die Signaturen unterschiedlich sind.
In der Klasse Student haben wir eine Signatur Student(String, int)
In der LinkedListIterDemo Klasse haben wir aber eine Signatur von folgender Art Student(String, String, int, int) und in der Konsolenausgabe ist die Signatur Student(int, String, String, int).

Schnittstellen (Interfaces) des Collection Framework

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.
Stefan Schneider Sat, 05/14/2011 - 14:46

Listen (Collections)

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.

Stefan Schneider Sat, 05/14/2011 - 15:12

Mengen (Collection)

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:

package java.util;
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.

package package java.util;
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

 

Stefan Schneider Sat, 05/14/2011 - 15:36

Iteratoren

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 s2.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 s;
s = new Student("Curie","Marie", 19,1);
ll.add(s);
s = new Student("Merian","Maria-Sybilla", 17,3);
ll.add(s);
s = new Student("Noether","Emmi", 16,1);
ll.add(s);
s = new Student("Herschel","Caroline", 15,2);
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.

 

Stefan Schneider Sat, 05/14/2011 - 14:44

Verzeichnisse-Maps (Collections)

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

Stefan Schneider Sat, 05/14/2011 - 15:48

Warteschlangen (Collections)

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:

package java.util;
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

Stefan Schneider Sat, 05/14/2011 - 15:27

Übungen (Collections)

Ü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 s2.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 mengeUnsortiert und mengeSortiert 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 s2.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
     }
   }
} // Ende der Klasse

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 s2.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", "Liese", 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 der Studentin mit Namen nme
      nme = "Herschel";
      System.out.println("Student mit Name " + nme +
         " ist:" + + null); // Einsetzen der Operation zum Auslesen der Studentin 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);
     }
  }
}
Stefan Schneider Sat, 05/14/2011 - 15:57

Lösungen (Collections)

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 im Vergleich zur Klasse Vector mit der sie im Wettbewerb steht.
  • Häufiges Einfügen an beliebigen Positionen
  • Häufiges Entfernen an beliebigen Positionen
TreeSet
  • Randbedingung: Ordnung in der Menge ist wichtig (relevant)
  • Zugriffszeit steigt mit O(log(n)) mit der Anzahl der Elemente
  • 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 
  • konstante und kurze Zugriffszeiten bei sehr großen Mengen

Mengen von Studenten

package s2.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 Liese, 2)
Inhalt der Menge (class java.util.TreeSet):
Student: (15,Meitner Liese, 2)
Student: (16,Noether Emmi, 1)
Student: (17,Merian Maria-Sybilla, 3)
Student: (19,Curie Marie, 1)

Maps von Studenten

package s2.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", "Liese", 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 Liese, 2)
Student mit Matrikelnummer 16 ist:(16,Noether Emmi, 1)
Alle Werte der MatrikelMap:
Name (class java.util.TreeMap):
Student: (15,Meitner Liese, 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 Liese, 2)
Student: (19,Curie Marie, 1)
Student: (20,Herschel Caroline, 2)
Student: (16,Noether Emmi, 1)
Student: (17,Merian Maria-Sybilla, 3)

 

Stefan Schneider Tue, 05/24/2011 - 19:22

Lernziele (Java Collections)

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

Zur Umfrage

QR Code für Umfrage

Stefan Schneider Fri, 12/28/2012 - 21:11

Ein- und Ausgabe über Streams

Ein- und Ausgabe über Streams

 

Duke surft...

Programme müssen Daten von unterschiedlichen Datenquellen lesen können und sie müssen Daten auf unterschiedliche Datenziele schreiben können. Hierbei kann es sich um die unterschiedlichsten Quellen und Ziele handeln:

  • Konsoleneingabe
  • Konsolenausgabe
  • Dateien auf dem Rechner
  • Dateien auf entfernten Rechnern (z. Bsp. dem Internet)
  • Quellen und Senken auf anderen Rechnern

 

 Alle Ein- und Ausgaben in Java laufen "stromorientiert" ab, sie folgen dem "Stream-Konzept".

Definition

Ein Stream ist die Verbindung zwischen einer Datenquelle und einem Datenziel.

Die Verbindung erfolgt immer in nur eine Richtung (von der Quelle zum Ziel).

Um Eingaben in Java zu erhalten muß man einen Strom öffnen der mit einer Datenquelle verbunden ist und anschließend die Daten sequentiel lesen.

Für Ausgaben muß man einen Datenstrom zu einem Datenziel öffnen, dann die Daten sequentiell schreiben.

Dieses Konzept ist einem Wasserschlauch ähnlich: Sie benötigen jemand der das Wasser einspeißt und einen anderen Konsumenten der das Wasser wieder entnimmt.

Kategorien von Streams

In Java werden zwei Kategorien unterschieden:

  • Character-Streams: Transportieren 16 Bit Daten. Sie arbeiten mit dem Javatyp char der Unicodezeichen darstellt
    • Ihre Basisfunktionalität wird durch die abstrakten Klassen Reader und Writer bereitgestellt
  • Byte-Stream: Transportieren 8 Bit Daten. Sie arbeiten mit dem Javatyp byte.

Wichtig: Alle Klassen die von diesen vier Basisklassen spezialisiert werden haben als Endung den Namen der abstrakten Klasse!

Bsp.: Bei der Klasse FileWriter handelt handelt es sich um einen Ausgabestrom auf Dateien.

Bsp.: Bei der Klasse FileWriter handelt handelt es sich um einen Ausgabestrom auf Dateien.

Frage: Verwaltet er Bytes oder Zeichen?

Interessant:

Man kann Streams (Datenströme) verketten! Das bedeutet, dass ein Streamsobjekt in der Regel einen anderen Stream als Eingabe oder Ausgabe akzeptiert. Hiermit kann man Datenströme elegant umkonvertieren.

Duke mit Stromstecker

 

java.nio und mehr...

Duke auf Sofa

Seit der Version JDK 1.4 gibt es in Java das Paket java.nio mit einer Reihe neuer Klassen. Diese Paket ist nicht Gegenstand der Vorlesung.

In diesem Abschnitt werden nur einige wenige, ausgewählte Methoden der Klassen vorgestellt. Bitte benutzen Sie die Hyperlinks zur Java API Dokumention und einen vollständigen Überblick zu bekommen.

Stefan Schneider Sat, 02/23/2019 - 13:46

Die Klasse File

Die Klasse File

Viele Operationen werden auf Dateien ausgeführt.  Die Klasse File im Paket java.io erlaubt das manipulieren von Dateien und Verzeichnissen.

Im Beispiel der Klasse s2.io.DateiTest (github) kann man sehen wie man Verzeichnisse anlegt und ausliest und neue Dateien anlegt. Methoden zum Manipulieren und Auslesen von Dateiattributen sind in der Java Dokumentation zur Klasse File beschrieben.

package s2.io;

/**
* @author s@scalingbits.com
*/
import java.io.File;
import java.io.IOException;

public class DateiTest {
   /**
   * Hauptprogamm
   * @param args wird nicht verwendet...
   */
   public static void main(String[] args) {
      String d ="testDir";
      String f1 = "datei1.txt";
      String f2 = "datei2.txt";
      File dir = new File(d);
      File file1 = new File(d + "/" + f1);
      File file2 = new File(d + "/" + f2);

      if (dir.exists())
         System.out.println("Verzeichnis " + d + " existiert bereits");
      else
         dir.mkdir();
         try {
            file1.createNewFile();
            System.out.println("Datei wurde angelegt in : "
               + file1.getAbsolutePath() );
            file2.createNewFile();
            System.out.println("Datei wurde angelegt in : "
               + file2.getAbsolutePath() );

            System.out.println("Dateien im Verzeichnis "
               + dir.getAbsolutePath());
            String[] alleDateien = dir.list();
            for ( String f : alleDateien)
               System.out.println("* " + f);
        } catch (IOException ex) {
            System.out.println("Probleme im IO Subsystem. Scotty beam me up!");
        }
   }
}

Übung: Erweitern Sie Anwendung so, dass die Dateien und das Verzeichnis wieder gelöscht werden!

Frage: Welche Methoden brauche ich hierfür?

Sie werden doch nicht auf diesen Hyperlink klicken: github s2.io.DateiTestLoesung.java

Stefan Schneider Sat, 02/23/2019 - 16:22

Einführung in Reader- und Writerklassen

Einführung in Reader- und Writerklassen

Die Reader und Writer Klassen in Java erlauben es Zeichen-Stöme zu verwalten.

Die spezialisierten Klassen FileReader und FileWriter haben Konstruktoren in denen man als Quelle bzw. Ziel eine Datei angeben kann:

Ihre Oberklassen InputStreamReader und OutputStreamWriter haben Konstruktoren die die Konvertierung von Streams erlauben.

Hiermit kann man einen Byte-Stream (inputStream)  in einen Zeichen-Stream (Reader) verwandeln. Dies ist nützlich wenn man eine Binärdatei (Byte-Stream) in eine Datei (Zeichen-Stream) schreiben möchte.

 

Java Reader und Writer Klassen

Im folgenden Beispiel wird gezeigt wie man einen Zeichen-Stream von der Konsole lesen kann und ihn dann in eine Datei schreibt. Die Java Klasse System bietet mit dem Attribute System.in Zugriff auf einen InputStream mit Konsoleneingaben und auf Konsolenausgaben (System.out).

/*
 * Ein Beispielprogramm welches von der Konsole liest und in eine Datei schreibt
 */
package s2.io;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
/**
*
* @author s@scalingbits.com
*/
public class SchreibInDatei {
   /**
   * Hauptprogamm
   * @param args wird nicht verwendet...
   */
   public static void main(String[] args) {
      try {
         String f = "scalingbits.txt";
         File datei = new File(f);
         Reader rein = new InputStreamReader(System.in);
         Writer raus = new FileWriter(datei);
         System.out.println("Der Text der jetzt eingegeben wird, wird in " +
            "der Datei " + f + " gespeichert");
         System.out.println("Abschliessen mit Strg-Z oder Ctrl-Z");
         System.out.println("Abschliessen Auf Unix/Linux mit Ctrl-D");
         System.out.println("Abschliessen auf Mac mit IntelliJ mit Cmd-D");
         umkopieren(rein,raus);
     } catch (IOException ex) {
         System.out.println("Probleme im IO Subsystem. Scotty beam me up");
     }
   }
   /**
   * Umkopieren zwischen zwei Streams
   * @param r Eingabestream
   * @param w Ausgabestream
   * @throws IOException
   */
   public static void umkopieren(Reader r, Writer w) throws IOException{
      int c;
      while ((c= r.read()) != -1 ) {
         w.write(c);
      }
      r.close();
      w.close();
   }
}

Übung: Erweitern Sie Anwendung so, dass sie die geschriebene Datei wieder ausliest und auf der Konsole druckt. Die Methoden umkopieren() kann man wiederverwenden...

Frage: Welche Klassen brauche ich hierfür?

Sie werden doch nicht auf diesen Hyperlink klicken: github s2.io.SchreibInDateiLoesung.java

 Gepufferte Reader- und Writer Klassen

Das Lesen bzw. Schreiben einzelner Bytes oder Zeichen vom Netzwerk oder von Dateien ist oft sehr ineffizient. Zum effizienteren Behandlung größerer Datenmengen stellt Java die Klassen BufferedReader und BufferedWriter zur Verfügung. Diese Klassen speichen die Daten zwischenzeitlich in einem Puffer und Lesen bzw. Schreiben die Daten in größeren Blöcken. Diese gepufferten Klassen müssen mit den elementaren Klassen verkettet werden. Deshalb haben sie die folgenden Konstruktoren:

Sie Verfügen über zusatzliche Methoden zum gepufferten Lesen und Schreiben

 Nutzt man diese Klassen im Beispiel, gibt es eine Reihe von Veränderungen:

  • mit BufferedReader.readLine() erhält man eine Zeichenkette als Ergebnis (oder einen Nullzeiger!)
  • man sollte beim Schreiben den Zeilenimbruch selbst einfügen

Die Klasse SchreibInDateiGepuffered

/*
 * Eins Beispielprogramm welches von der Konsole liest und in eine Datei schreibt
 */
package s2.io;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

/**
*
* @author s@scalingbits.com
*/
public class SchreibInDateiGepuffered {
   /**
   * Hauptprogamm
   * @param args wird nicht verwendet...
   */
   public static void main(String[] args) {
      try {
         String f = "scalingbits.txt";
         File datei = new File(f);
         BufferedReader rein = new BufferedReader(
            new InputStreamReader(System.in));
         BufferedWriter raus = new BufferedWriter(
         new FileWriter(datei));
         System.out.println("Der Text der jetzt eingegeben wird, wird in " +
               "der Datei " + f + " gespeichert");
         System.out.println("Abschliessen mit Strg-Z oder Ctrl-Z");
         System.out.println("Abschliessen Auf Unix/Linux mit Ctrl-D");
         System.out.println("Abschliessen auf Mac mit IntelliJ mit Cmd-D");
         umkopieren(rein,raus);

         rein = new BufferedReader(
         new FileReader(f));
         raus = new BufferedWriter(
         new OutputStreamWriter(System.out));

         System.out.println("Ausgabe des in Datei " + f
            + " gespeichertem Texts");
         umkopieren(rein,raus);
      } catch (IOException ex) {
         System.out.println("Probleme im IO Subsystem. Scotty beam me up");
      }
   }
   /**
   * Umkopieren zwischen zwei Streams
   * @param r Eingabestream
   * @param w Ausgabestream
   * @throws IOException
   */
   public static void umkopieren(BufferedReader r, BufferedWriter w) throws IOException{
      String z;
      while ((z= r.readLine()) != null) {
         w.write(z);
         w.newLine();
      }
      r.close();
      w.close();
   }
}

Die Datei SchreibInDateiGepuffered in github.

Stefan Schneider Sat, 02/23/2019 - 17:18

Behandlung von Byte-Strömen

Behandlung von Byte-Strömen

Von den Byte-Streams werden hier nur wenige ausgewählte vorgestellt

Hierarchie Byte-Ströme

Die abstrakte Basisklasse InputStream hat für die Eingabe von Byte-Streams unter anderem die folgenden Methoden:

Die abstrakte Basisklasse OutputStream besitzt ähnliche Methoden

DataInput- und DataOutputStream

Die Klassen DataInputStream und DataOutputStream erlauben das Lesen und Schreiben von Java-Basistypen. Ihre Konstruktoren arbeiten mit den Basisklassen:

Diese Klassen stellen dann typspezifische Methoden mit der folgenden Notation zur Verfügung. Zum Beispiel DataOutputStream:

  • public xxx readxx(): Lies den Typ xxx und gib ihn aus

Serialisierung und Deserialisierung von Objekten

Die Klassen ObjectInputStream und ObjectOutputStream erlauben es Objekte aus dem Hauptspeicher in einen Stream zu schreiben.

Sie wandeln den systemabhängigen Zustand eines Objects in eine neutrale Binärdarstellung in einem Datenstrom um. Diesen Vorgang nennt man Serialisierung in Java. Den umgekehrten Vorgang nennt man Deserialiserung.

Wichtig: Es könne nur Objekte serialiert werden die die Schnittstelle Serializable implementieren. Diese Schnittstelle verlangt es nicht Methoden zu implementieren.

Alle referenzierten, serialisierbaren Objekte werden auch serialisiert.

Möchte man Instanzvariablen nicht serialisieren, muss man sie mit Schlüsselwort transient kennzeichnen.

Die beiden Klassen ObjectInputStream und ObjectOutputStream haben Konstruktoren, die es erlauben aus bzw. in andere Ströme zuschreiben:

In geöffnete Ströme vom Typ ObjectInputStream kann man mit der folgenden Methoden lesen:

Ähnlich kann in ObjectOutputStream mit der folgenden Methode ein Objekt schreiben

 

Stefan Schneider Mon, 02/25/2019 - 09:19

Weitere Klassen

Weitere Klassen

 Das java.io Paket hat noch viele weiter Klassen. Hier eine kurze Liste der interessantesten Klassen

Stefan Schneider Sat, 03/02/2019 - 15:34

Übungen ( Streams)

Übungen ( Streams)

Vokalverschiebung

Ersetzen Sie alle Vokale in einer Textdatei durch bestimmte andere und erzeugen Sie eine neue Datei mit den verschobenen Vokalen.

Eine Musterlösung ist in github zu finden.

Stefan Schneider Sat, 03/02/2019 - 15:42

Lernziele

Lernziele

Am Ende dieses Blocks können Sie:

  • ... in Java aus Datein lesen und in Dateien schreiben
  • ... erkennen ob eine Klasse mit Zeichen- oder Byteströmen agiert
  • ... Ströme verketten
  • ... Zeichen, Basistypen und Objekte in Ströme lesen sowie schreiben

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: Fragen zur Streams

Zur Umfrage

QR Code für Umfrage

Stefan Schneider Sat, 03/02/2019 - 15:39

Threads (Nebenläufigkeit)

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 auf andere Aufgaben warten zu lassen.

Prozesse und Threads im Betriebssystem

Mit Java-Threads kann man nebenläufige Programme programmieren die 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.

 

 

Stefan Schneider Sat, 05/28/2011 - 10:08

Thread Zustandsübergänge

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

Stefan Schneider Sat, 05/28/2011 - 10:16

Programmieren mit Threads

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:

"Klassenhierachie Java Thread"

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 s2.thread;

/**
*
* @author s@scalingbits.com
*/
public class MyThread extends Thread {
   @Override
   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();
   }
} // Ende der Klasse

Das Programm erzeugt die folgende Konsolenausgabe:

Start MyThread.main() Methode im Thread mit der Id: 1
Ende MyThread.main() Methode im Thread mit der Id: 1
Hurra ich bin myThread in einem Thread mit der Id: 1
Hurra ich bin myThread in einem Thread mit der Id: 9

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 s2.thread;
/**
 *
 * @author s@scalingbits.com
 */
public class MyRunnable implements Runnable {
@Override
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 s2.thread;
/**
*
* @author s@scalingbits.com
*/
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
Ende ThreadStarter.main() Methode im Thread mit der Id: 1
Hurra ich bin myRunnable in einem Thread mit der Id: 1
Hurra ich bin myRunnable in einem Thread mit der Id: 9

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.

 

Stefan Schneider Sat, 05/28/2011 - 10:19

Synchronisation

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 Variablen 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 s2.thread;

/**
*
* @author s@scalingbits.com
*/

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

Quellen bei github.

Wechselseitiger Ausschluss

Zur Vermeidung der oben genannten Korruption ist es wichtig sicher zustellen, dass nur ein Thread gleichzeitig Zugriff auf diese Daten hat.

Duke thinking Das oben gezeigte Programm zeigt die Millisekunden an die es benötigt. Wieviel sind es?
Man kann das Schlüsselwort synchronized als Modifizierer in die Methode increment() einpflegen. Läuft das Programm jetzt langsamer oder schneller? Läuft es korrekt oder inkorrekt? 
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 s2.thread;
import java.util.concurrent.atomic.AtomicInteger;
/**
 *
 * @author s@scalingbits.com
 */
 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)");
    }
}

Quellen bei github.

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 s2.thread;

/**
*
* @author s@scalingbits.com
*/

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;
   }
}

Quellen bei github

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)

 

Stefan Schneider Sat, 05/28/2011 - 17:38

Beispiel: Kritischer Pfad

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 sind in Threading.jar zusammen gefaßt.

Man kann diese jar Datei mit

java -jar Threading.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 s2.thread;

/**
*
* @author s@scalingbits.com
*/
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;
   }
   @Override
   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 s2.thread;

/**
*
* @author s@scalingbits.com
*/
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 s2.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;
/**
*
* @author s@scalingbits.com
*
*/
public class ThreadFenster {
   final private JFrame hf;
   private JButton okButton;
   final 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 {
      @Override
      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 Buttons
     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 s2.thread;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JPanel;
/**
*
* @author s@scalingbits.com
*/
public class ThreadingPanel extends JPanel {

   private final int ziffernBreite = 10; // Breite einer Ziffer in Pixel
   private final 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
   */
   @Override
   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 id Identifier
   * @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) {
        switch(MainTest.mt[id].threadStatus) {
           case MainTest.ENDED:             g.setColor(Color.LIGHT_GRAY); break;
           case MainTest.NOTINCRITICALPATH: g.setColor(Color.GREEN); break;
           case MainTest.INCRITICALPATH:    g.setColor(Color.RED); break;
           default: assert(true):"Hier laeuft etwas falsch";
       }
   }
   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);
   }
}
Stefan Schneider Wed, 06/01/2011 - 08:58

Java Concurrency Paket

Java Concurrency Paket

Mit JDK 5.0 wurde das Java Concurrency Paket, welches maßgeblich von Doug Lea mitentwickelt wurde (siehe Wikipedia), Teil der Laufzeitumgebung (JRE). Das Paket java.util.concurrent fügt komplexe Klassen zur Laufzeitumgebung die das Steuern der Parallelität auf einem höheren Niveau als das grundlegenden Konstrukte von Java erlauben. 

In diesem Abschnitt werden einige, wenige Klassen vorgestellt. Schauen Sie sich dieses Paket an bevor Sie etwas Nebenläufiges implementieren. Hier gibt es sehr viele, sehr mächtige Lösungen, die die Vorlesung bei weitem übersteigen

Referenzen

Stefan Schneider Mon, 01/28/2019 - 09:10

Atomare Basistypen

Atomare Basistypen

 In java.util.concurrent.atomic werden Basistypen zur Verfügung gestellt die atomar sind.

Atomar bedeutet, dass das Lesen des Datums und das anschließende Modifizieren nicht von anderen Operationen unterbrochen werden kann. Das Lesen und Modifizieren erfolgt in einem kritischen Pfad.

Ein Beispiel: Die Klasse AtomicInteger implementiert Number und hat die zum Beispiel Methoden wie:

Anbei ein Beispiel zum nebenläufigen Addieren:

package s2.thread;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* @author s@scalingbits.com
*/

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

 

Stefan Schneider Tue, 03/05/2019 - 18:55

Das "Fork-Join-Framework"

Das "Fork-Join-Framework"

Es gibt zwei Möglichkeiten das Threading in Java zu nutzen:

  • Erledigen unterschiedlicher Aufgaben parallel
    • Bsp: Swing: Oberfläche neu zeichen, Eingaben abarbeiten, eigentliche Programmaufgaben ausführen
  • Paralleles Abarbeiten der gleichen Aufgabe
    • Bsp: Differentialgleichungen berechnen, Such & Sortieralgorithmen, Bearbeitung von Massendaten (Batch).

Das Fork & Join Framework erleichert das parallele Programmieren durch eine Optimierung der Parallelität und komfortable Klassen.

Im Rahmen der Vorlesung werden wir die Möglichkeiten einer Parallelisierung ohne und mit Rückgaben diskutieren. Die Unterschiede zum Einsatz der einfachen Threadingkonstrukte und der des Frameworks sind im Diagramm unten dargestellt:

3 Möglichkeiten der Parallelisierung

Steuerung des Grads der Parallelisieren

Java erlaubt es recht einfach viele Ausführungseinheiten (Threads) zu erzeugen. Das Betriebssystem wird diese Threads auf Hardwarethreads zuordnen. Das macht das Betriebssystem auch fast immer sehr gut. Erzeugt man aber zufiele ausführbare Threads kann der Rechner überlastet werden und ist kaum noch ansprechbar. Das bedeutet, dass Prozesse die mit dem Benutzer interagieren sollen nicht mehr unbedingt die CPU Zeit bekommen die sie benötigen.

Hat man viel, zu viele Softwarethreads und Prozesse wird das Betriebsystem auch noch ineffizient. Es muss die ganzen Zustandänderungen managen. Hier gibt es kritische Pfade und im Überlastungsfall zuwenig CPU-Zeit.

Das Fork & Join Framework erleichtert das professionelle Skalieren in dem es eine Threadpool anlegt und alle Tasks an freie Prozessoren im Threadpool vergibt. Hiermit übernimmt es eine Aufgaben des Betriebsystem und vermeidet eine Überlastung. Gibt man keinen Wert für den Pool an, wird die Anzahl aller Prozessoren im Rechner als Defaultwert verwendet.

Dies ist nur ein kleiner Ausschnitt aus dem Fork & Join Framwork. Wir betrachten hierzu die folgenden Klassen:

Fork Join Klassen

Die Klasse ForkJoinPool

Die Klasse java.util.concurrent.ForkJoinPool wurde im JDK 7 eingeführt.

In dieser Klasse werden eine Menge von Threads verwaltet und zur Ausführung von Task (Aufgaben) verwendet. Dies macht es dem Betriebsystem leichter die Threads auf die Prozessoren zu verteilen, da nicht beliebig viele Threads von Java erzeugt werden. Die Klasse ForkJoinPool verwaltet die Zuordnung der Task auf die konfigurierten (Software)threads.

Tasks können auf 3 Arten dem Framework übergeben werden:

  Aufrufe von ausserhalb Aufruf innerhalb einer Berechnung
asynchrone Ausführung ForkJoinPool.execute(ForkJoinTask) ForkJoinTask.fork()
warte auf Ergebnis ForkJoinPool.invoke(ForkJoinTask) ForkJoinTask.invoke()
asynchrone Ausführung mit Ergebnis ForkJoinPool.submit(ForkJoinTask)
ForkJoinPool.submit(Callable)
ForkJoinTask.fork(ForkJoinTasks sind Futures)

Die Implementierung wird zu einer RejectedExecutionException Ausnhame führen falls der Threadpool runtergefahren wird oder falls es keine Threads mehr in der VM gibt. 

Interessante Konstruktoren:

Paralleles Abarbeiten ohne Rückgabewerte

Ein Beispiel hierfür ist der Quicksort. Man kann parallel das linke und das rechte Teilintervall sortieren nachdem die Vorarbeit (seriell) erledigt wurde.

Hierzu nutzt man die abstrakte Klasse RecursiveAction die aus der abstrakten Klasse ForkJoinTask spezialisiert wird. Die Klasse ForkJoinPool kann Tasks die die Bedingungen der Klasse ForkJoinTask erfüllen, parallel ausführen. Hierzu gibt es in der Klasse die Methoden:

Es verbleibt das Problem, dass man einen bestimmten Code ausführen möchte. Hierzu dient die abstrakte Klasse RecursiveAction. Sie besitzt eine abstrakte Methode:

  • compute(): führt den Code aus. Diese Methode ist abstrakt!

Wie implementiert man so etwas?

  1. Man implementiert den Algorithmus in einer Unterklasse von RecursiveAction.
    1. Alle Eingabeparameter werden als Attribute im Objekt gespeichert
    2. Man implementiert einen Konstruktor der die Parameter entgegennimmt und abspeichert
    3. Der Algorithmus wird in compute() implementiert
  2. Man bettet seine Klasse als innere Klasse ein
    1. Man hat dann Zugriff auf alle Attribute der äusseren Klasse!
  3. Man legt sich einen Threadpool an
    1. Wahrscheinlich als globales, statisches Objekt
  4. Man startet seinen Algorithmus aus der äusseren Klasse
    1. Anlegen eines Objekts mit den Parametern
    2. Aufruf von invoke() im Treadpool

 Hilfe! Ich muß das am Beispiel sehen...

Paralleles Abarbeiten mit Rückgabewerte

Die Aufgabe parallelisieren und als Aufgabe starten reicht oft nicht. Oft müssen die Ergebnisse entgegen genommen werden und dann rekursiv zu neuen Ergebnissen aggregiert werden.

Hierzu benötigt man einen Mechanismus der

  • die Eingabeparameter für jede Aufgabe (Task) entgegennimmt
  • die Aufgaben (Tasks) ausführt
  • und die Ergebnisse erst zurückgibt wenn die Ergebnisse vorliegen

Die Schnittstelle Callable

Die Schnittstelle java.util.concurrent.Callable muss implementiert werden um die Ausgabeparameter einer Aufgabe (Task) entgegenzunehmen. Hierfür muss man

  • einen generischen Typen wählen der das Ergebnis zur Verfügung stellt
  • einen Konstruktor implementieren der alle Eingabeparameter entgegen nimmt.
  • alle Parameter als Attribute implementiert
  • die Methode V call() die das Ergebnis zurückliefert

Ein Beispiel auf der Programmierübung Ariadnefaden

public class SearchCallable implements Callable<List<Position>> {
        Position von;
        Position nach;
        Ariadne4Parallel a;
         /**
         * 
         * @param a Instanz von Ariadne
         * @param von Start
         * @param nach Ziel
         */
        SearchCallable(Ariadne4Parallel a,Position von, Position nach) {
            this.a =a;
            this.von = von;
            this.nach = nach;
        }
        /**
         * Führe Task in eigenem Thread aus und nutze Instanzvariablen
         * als Parameter um Aufgabe auszuführen.
         * @throws java.lang.Exception
         */
        @Override
        public List<Position> call() throws Exception {
            return a.suche(von, nach);
        }
    }

In diesem Beispiel ist das Ergebnis eine Liste von Positionen die den Web aus dem Labyrinth weisen.

Die Eingabeparameter sind ein Labyrinth, die Startposition und die Position mit dem Ausgang ais dem Labyrinth.

Die Schnittstelle Future

Die Schnittstelle Future erlaubt es das Ergebnis einer Aufgabe zu analysieren. Die Schnittstelle ist generisch und erlaubt es daher nur einen bestimmten Type einer Klasse auszulesen.

Die beiden wichtigsten Methoden der Schnittstelle sind:

  • V get(): wartet falls notwendig auf das Beenden der Aufgabe und liefert das Ergebnis ab
  • V get(long timeout, TimeUnit unit): wartet eine bestimmte Zeit auf auf ein bestimmtes Ergebnis

In der Schnittstellendefinition sind noch weitere Methoden zum Kontrollieren und Beenden von Aufgaben (Tasks) vorhanden.

Abschicken von Aufgaben (Tasks)

Die Klasse  java.util.concurrent.ForkJoinPool verfügt über eine Methode

  • public <T> ForkJoinTask<T> submit(Callable<T> task)

Diese Methode nimmt die Eingaben für die Aufgabe (Task) an solange es eine Spezialierung der Klasse Callable<T> ist. Das Ergebnis ist ein Objekt vom Typ ForkJoinTask. Die Klasse ForkJoinTask implementiert die Schnittstelle Future. Das Ergebnis der Aufgabe (Task) wird hier abgeliefert sobald die Aufgabe abgearbeitet ist.

Wie implementiert man so etwas?

  1. Man implementiert den Algorithmus in einer Unterklasse von RecursiveAction.
    1. Alle Eingabeparameter werden als Attribute in einem Objekt von Callable gespeichert
    2. Man implementiert einen Konstruktor der die Parameter entgegennimmt und abspeichert
    3. Der Algorithmus wird in compute() implementiert
      1. Der Algorithmus führt rekursive Aufrufe durch und nimmt die Ergebnisse vom Typ Future an.
  2. Man bettet seine Klasse die Callable spezialisiert als innere Klasse ein
    1. Man hat dann Zugriff auf alle Attribute der äusseren Klasse!
  3. Man legt sich einen Threadpool an
    1. Wahrscheinlich als globales, statisches Objekt
  4. Man startet seinen Algorithmus aus der äusseren Klasse
    1. Anlegen eines Objekts mit den Parametern
    2. Aufruf von invoke() im Treadpool

 Hilfe! Ich muß das am Beispiel sehen...

Stefan Schneider Wed, 03/06/2019 - 18:39

Aufgaben (Threads)

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?

 

 

 

Stefan Schneider Sun, 05/29/2011 - 14:29

Lösungen (Threads)

Lösungen (Threads)

Programmieraufgabe JoinAndSleep

package s2.thread;

public class JoinAndSleep extends Thread{
/**
* Der Thread auf dessen Beendigung gewartet wird
*/
Thread joinIt;
/**
* Zeit in Millisekunden zum Schlafen
*/
int schlafen;
/**
* Der Konstruktor
* @param sleeptime Die Zeit in msdie der Thread schlafen soll
* @param toJoin der Thread auf den gewartet werden solll bis er beendet ist
*/
public JoinAndSleep(int sleeptime, Thread toJoin) {
joinIt = toJoin;
schlafen = sleeptime;
System.out.println("Thread: " + this + " erzeugt");
}
/**
* Die überschriebene Methode run() sie führt den Code aus
*/
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");
}
/**
* Das Hauptprogramm
* @param args keine Parameter benoetigt!
*/
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();
}
}

 

Stefan Schneider Sun, 05/29/2011 - 14:31

Lernziele (Threading)

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)

Feedback

Zur Umfrage

QR Code für Umfrage

Stefan Schneider Wed, 01/09/2013 - 09:25