Montag, 21. September 2009

Generics: Generics auf Klassenebene

Zuletzt war das Thema, wozu man Generics zum Beispiel brauchen kann. In dem Beispiel gab es eine Methode, die einen generischen Typen verwendet hat, damit der Parameter mit dem tatsächlichen Typen zurückgegeben werden kann.
Methoden sind aber nur ein Ort, wo man Generics verwenden kann. Der zweite sind ganze Klassen. Dazu mache ich kein eigenes Beispiel, sondern verwende ein bestehendes: ArrayList. Beim Lernen von Collections tauchen vielleicht solche Sachen auf:

ArrayList l = new ArrayList();
l.add("a");
l.add("bb");
Iterator it = l.iterator();
while(it.hasNext()) {
System.out.println(((String) it.next()).length());
}

Die Liste enthält nur Strings, und soll auch nur Strings enthalten, aber der Compiler weiß es nicht. Das riecht schwer nach dem Einsatzgebiet für Generics!

ArrayList<String> l = new ArrayList<String>();
l.add("a");
l.add("bb");
Iterator<String> it = l.iterator();
while(it.hasNext()) {
System.out.println(it.next().length());
}

Der Klassenkopf von ArrayList sieht so aus:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

Der Klassenkopf enthält die Typenvariable E dreimal: einmal hinter dem Klassenname, zweimal im extends/implements. Der Wert der Typenvariable wird beim Konstruktoraufruf festgelegt: new ArrayList<String>(); Dieser generische Typ verleiht dem ganzen eine gewisse Typensicherheit:

ArrayList<String> l;
l = new ArrayList<Date>(); //funktioniert nicht
l = new ArrayList<String>(); //funktioniert

//Der rückgabetyp von ArrayList<E>.iterator() ist vom Typ Iterator<E> (also hier Iterator<String>)
Iterator<Date> it = l.iterator(); //funktioniert nicht
Iterator<String> it = l.iterator(); //funktioniert

while(it.hasNext()) {
//Ebenso liefert Iterator<E>.next() ein E (hier einen String) zurück
Date d = it.next(); //funktioniert nicht
String s = it.next(); //funktioniert
}

ArrayList<Object> l2 = l; //funktioniert auch nicht!
List<String> l2 = l; //funktioniert schon

Alle diese Fehler werden schon vom Compiler erkannt! Im letzten Punkt steckt eine Rafinesse. Warum ist ArrayList mit List, aber nicht String mit Object kompatibel? Die Antwort gibts nächstes mal!

Sonntag, 20. September 2009

Generics: Erste Schritte

Wenn man Gerüchte über Generics hört, dann heißt es meistens, dass es eine ganz komplizierte Angelegenheit ist. Ich weiß gar nicht, wer auf die Idee gekommen ist.
Bei Generics geht es darum, Einen Klassennamen durch eine Variable zu ersetzen. Wer sich darunter schon etwas vorstellen kann, gut. Ansonsten macht es auch nichts.
Stell dir folgende Situation vor: Du willst die Objekte speichern, die im Laufe deines Programms erzeugt werden:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public void addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

//size++ wird hier als Ausdruck und nicht als Anweisung benutzt.
//Der Wert des Ausdrucks ist der alte Wert
//Auf ähnliche Weise ist ++size der Wert nach der Erhöhung
objects[size++] = o;
}

//hier gleich ein Kleiner Test
//Die Objekte werden danach noch verwendet, deswegen werden sie in Variablen gespeichert
public static void main(String[] args) {
ObjectList l = new ObjectList();

String s = "hallo";
l.addObject(s);
System.out.println(s.length());

Date d = new Date();
l.addObject(d);
System.out.println(d.getTime());
}
}

Vielleicht findest du die Main-Methode genau so mühsam wie ich. Netto will ich ja von beiden Objekten nur eine Sache: einmal die Länge, und einmal die Millisekunden. Dazu würde auch "hallo".length() oder new Date().getTime() genügen. Eine lokale Variable wäre unnötig.
Und was kann man dafür tun? Zum Beispiel den Parameter in der gleichen Methode wieder zurückgeben:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public Object addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(((String) l.addObject("hallo")).length());
System.out.println(((Date) l.addObject(new Date())).getTime());
}
}

Das ist ehrlich gesagt auch nicht meines. Ich weiß ja eigentlich ganz genau, was raus kommt, nur der Compiler weiß es nicht.
Wie bringt man es dem Compiler bei? Da gibt es natürlich den hässlichen Weg: Die Methode für die relevanten Typen überladen...


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public Object addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public String addObject(String s) {
return (String) addObject((Object) s);
}

public Date addObject(Date d) {
return (Date) addObject((Object) d);
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(l.addObject("hallo").length());
System.out.println(l.addObject(new Date()).getTime());
}
}

Was ein relevanter Typ ist, kann sich ja ändern. Und deshalb gibt es, angefangen mit Java 5, dieses schöne neue Feature Namens Generics.
Wenn man Generics benutzt, wird der Typ sozusagen nicht in der Methodendeklaration sondern beim Aufruf deklariert. (Dabei meine ich allerdings nicht, während das Programm läuft, sondern wenn der Aufruf kompiliert wird. Die Feinheiten gibts vielleicht in einem späteren Post!)
Hier erst einmal der schöne Code:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public <T> T addObject(T o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(l.addObject("hallo").length());
System.out.println(l.addObject(new Date()).getTime());
}
}

T ist hierbei eine Typenvariable und wird in spitzen Klammern deklariert. Der "Wert" der Variable kommt beim Aufruf zustande, indem der Typ des Parameters genommen wird. In weiterer Folge hat T in der ganzen Methode den gleichen Wert und kann ohne Casts verwendet werden.
Das heißt, wenn addObject mit einem String als Parameter aufgerufen wird, liefert es auch einen String zurück.

Donnerstag, 30. Juli 2009

File-I/O: RandomAccessFile

Ein Einfacher Weg, um ein File zu öffnen, um darin Werte zu lesen oder zu schreiben, ist ein RandomAccessFile.
die Klasse ist eigentlich selbsterklärend. Zusammengefasst tut sie folgendes:

Je nach dem, in welchem Modus eine Datei geöffnet wurde, kann man entweder Lese-, Schreibmethoden oder beides aufrufen. Für diese Operationen wird ein "filePointer" benutzt, der sozusagen als Schreib-/Lesekopf fungiert: Jede Operation führt den filePointer vorwärts an den bytes der Datei vorbei, und entweder werden die passierten bytes gelesen oder überschrieben.

Für die normalen Datentypen von Java ist das eine einfache Sache. Z.B. ist ein int eine 32-bit Zahl. readInt() und writeInt() bewegen also den filePointer jeweils um 4 bytes weiter. Bei Strings, und das ist der einzige andere Typ, den man mit RandomAccessFile bearbeiten kann, ist die Sache komplizierter. Strings haben keine fixe Länge, und für sie gibt es auch mehr Methoden:
readUTF()
writeUTF(String)
Diese Methoden sind ein echtes Paar: etwas, das man mit writeUTF geschrieben hat, kann man mit readUTF wieder eins zu eins auslesen. Aber:
readLine()
writeBytes(String)
writeChars(String)
Hier wird es ein Bisschen unübersichtlich. writeChars passt eigentlich gar nicht rein, weil Zeichen damit mit 2 bytes gespeichert werden, writeBytes (klarerweise) und auch readLine benutzen ein byte.
Die beiden Schreibmethoden können dabei beliebig viele Zeilen schreiben, weil ein String ja Zeilenumbrüche enthalten kann. außerdem wird das Geschriebene nicht mit einem Zeilenumbruch aufhören, wenn man nicht dafür sorgt. readLine dagegen wird auf leden Fall genau eine ganze Zeile lesen.
Insgesamt gesehen ist writeChars eigentlich fast nicht zu gebrauchen. writeBytes und readLine sollte man nur verwenden, wenn die Datei nur Text enthält. Ansonsten macht man sich damit nur Probleme.

Donnerstag, 16. Juli 2009

Multithreading: Selbst Threads starten

Wenn man Gerüchte über Generics hört, dann heißt es meistens, dass es eine ganz komplizierte Angelegenheit ist. Ich weiß gar nicht, wer auf die Idee gekommen ist.
Bei Generics geht es darum, Einen Klassennamen durch eine Variable zu ersetzen. Wer sich darunter schon etwas vorstellen kann, gut. Ansonsten macht es auch nichts.
Stell dir folgende Situation vor: Du willst die Objekte speichern, die im Laufe deines Programms erzeugt werden:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public void addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

//size++ wird hier als Ausdruck und nicht als Anweisung benutzt.
//Der Wert des Ausdrucks ist der alte Wert
//Auf ähnliche Weise ist ++size der Wert nach der Erhöhung
objects[size++] = o;
}

//hier gleich ein Kleiner Test
//Die Objekte werden danach noch verwendet, deswegen werden sie in Variablen gespeichert
public static void main(String[] args) {
ObjectList l = new ObjectList();

String s = "hallo";
l.addObject(s);
System.out.println(s.length());

Date d = new Date();
l.addObject(d);
System.out.println(d.getTime());
}
}

Vielleicht findest du die Main-Methode genau so mühsam wie ich. Netto will ich ja von beiden Objekten nur eine Sache: einmal die Länge, und einmal die Millisekunden. Dazu würde auch "hallo".length() oder new Date().getTime() genügen. Eine lokale Variable wäre unnötig.
Und was kann man dafür tun? Zum Beispiel den Parameter in der gleichen Methode wieder zurückgeben:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public Object addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(((String) l.addObject("hallo")).length());
System.out.println(((Date) l.addObject(new Date())).getTime());
}
}

Das ist Ehrlich gesagt auch nicht meines. Ich weiß ja eigentlich ganz genau, was raus kommt, nur der Compiler weiß es nicht.
Wie bringt man es dem Compiler bei? Da gibt es natürlich den hässlichen Weg: Die Methode für die Relevanten Typen überladen...


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public Object addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public String addObject(String s) {
return (String) addObject((Object) s);
}

public Date addObject(Date d) {
return (Date) addObject((Object) d);
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(l.addObject("hallo").length());
System.out.println(l.addObject(new Date()).getTime());
}
}

Was ein relevanter Typ ist, kann sich ja ändern. Und deshalb gibt es, angefangen mit Java 5, dieses schöne neue Feature Namens Generics.
Wenn man Generics benutzt, wird der Typ sozusagen nicht in der Methodendeklaration sondern beim Aufruf deklariert. (Dabei meine ich allerdings nicht, während das Programm läuft, sondern wenn der Aufruf kompiliert wird. Die Feinheiten gibts vielleicht in einem späteren Post!)
Hier erst einmal der schöne Code:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public T addObject(T o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(l.addObject("hallo").length());
System.out.println(l.addObject(new Date()).getTime());
}
}

T ist hierbei eine Typenvariable und wird in spitzen Klammern deklariert. Der "Wert" der Variable kommt beim Aufruf zustande, indem der Typ des Parameters genommen wird. In weiterer Folge hat T in der ganzen Methode den gleichen Wert und kann ohne Casts verwendet werden.
Das heißt, wenn addObject mit einem String als Parameter aufgerufen wird, liefert es auch einen String zurück.

File-I/O: Die File-Klasse

Wenn man ein Programm schreibt, kommt man oft in die Situation, dass man Daten oder Einstellungen speichern möchte, damit sie beim nächsten Programmstart wieder verfügbar sind. Der einfachste Weg ist, diese Daten in einer Datei zu speichern.

Dazu sollte man aber wissen, wie in Java Pfadangaben funktionieren, also testen wir das kurz (ich arbeite auf Linux, also schauen meine Ausgaben anders aus als unter Windows):

package post07;


import java.io.File;
import java.io.IOException;


public class FileTest {
public static void main(String[] args) {
//Eine relative Pfadangabe. Unter Linux ist das eine Angabe, die nicht mit "/" beginnt.
//Dieser Pfad ist relativ zum Projektverzeichnis
File f1 = new File("relativ");
//Hier kommt "relativ" zurück, also der Pfad, den man oben angegeben hat
System.out.println(f1.getPath());
//Hier kommt der absolute Pfad zurück. In meinem Fall ist das
//"/home/clemens/workspaces/SE/ProggersDiary/relativ"
//Unter Windows würde das mit dem Laufwerksbuchstaben beginnen, und die einzelnen Ordner würden mit "\"
//getrennt.
System.out.println(f1.getAbsolutePath());

//Der erste Teil ist der Ordner, zu dem die zweite angabe relativ sein soll
//System.getProperty("user.home") liefert dabei das Home-Directory ("/home/clemens" bei mir)
File f2 = new File(System.getProperty("user.home"), "relativ");
//Der Pfad und der absolute Pfad sind in diesem Fall gleich, weil die erste Angabe absolut war
System.out.println(f2.getPath());
System.out.println(f2.getAbsolutePath());

//Hier benutze ich ".." um mich in den Ordner "/home" zu bewegen
File f3 = new File(System.getProperty("user.home"), "../relativ");
//Hier bemerkt man das aber noch nicht. beide Pfade sind "/home/clemens/../relativ"
System.out.println(f3.getPath());
System.out.println(f3.getAbsolutePath());
try {
//Im canonical path wird das dann eliminiert, außerdem werden symlinks azfgelöst. Da diese Operation
//Zugriff auf das Filesystem benötigt, braucht man hier einen try-catch-block
//das ergebnis ist "/home/relativ"
System.out.println(f3.getCanonicalPath());
} catch(IOException ex) {}
}
}
Das heißt also:
  • Relative Angaben sind normalerweise relativ zum Projektverzeichnis (Wenn das Programm in ein jar-File gepackt ist, ist das wieder nicht möglich, also ist diese Variante stark von der Laufzeitumgebung abhängig!)
  • Alternativ kann man einen Pfad Angeben, auf der die relative angabe basiert. Diese Angabe kann sowohl ein String (wie im Beispielcode) als auch ein File-Objekt sein
  • Natürlich sind auch direkt absolute Angaben möglich
Was man jetzt hier nicht direkt gesehen hat ist, dass ei File-Objekt nicht unbedingt einer Datei oder einem Ordner im Dateisystem entsprechen muss. Um das herauszufinden, benötigt man
exists()
isFile()
isDirectory()

Wenn ein File nicht existiert, kann es erzeugt werden (wobei natürlich eine Exception auftreten kann):
createNewFile()
mkdir()
mkdirs()

Eine Entsprechung von mkdirs für files gibt es nicht, aber sie kann einfach erzeugt werden:
try {
//Woher auch immer das File kommt...
File f = ...;
//Der Ordner, der die Datei enthalten soll, wird mit mkdirs erzeugt
f.getParentFile().mkdirs();
//Jetzt existiert der Ordner garantiert, also kann man createNewFile benutzen
f.createNewFile();
} catch(IOException ex) {
//Falls irgendwo was schief gegangen ist, sieht man es hier
ex.printStackTrace();
}
So, das nächste mal werden wir wirklich in Dateien schreiben ;)

Mittwoch, 8. Juli 2009

Multithreading: Threads in grafischen Oberflächen

Sehen wir uns einmal kurz ein Programm an, das du ganz am Anfang in Java geschrieben haben könntest: Mit der Konsole eine Grundrechenoperation auf zwei positive ganze Zahlen anwenden.

package post06;


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


public class Grundrechnung {
public static void main(String[] args) throws IOException {
System.out.print("Gib eine Rechnung ein: ");
//Liest eine einzeilige Eingabe von der Konsole
String s = new BufferedReader(new InputStreamReader(System.in)).readLine();
//Der reguläre Ausdruck [*+/-] entspricht dem einmaligen Auftauchen eines der
//in eckigen Klammern aufgelisteten Zeichen. Das Bedeutet, dass split die beiden
//Zahlen, aber nicht den Operator zurückgibt. Wegen "-" sind negative Zahlen
//nicht möglich.
String[] parts = s.split("[*+/-]");
if(parts.length != 2) {
System.err.println("Rechnung falsch");
return;
}
//Den Operator erhält man, in dem man das Zeichen direkt nach der ersten Zahl nimmt.
char operator = s.charAt(parts[0].length());

int zahl1 = Integer.parseInt(parts[0]);
int zahl2 = Integer.parseInt(parts[1]);

switch(operator) {
case '+':
System.out.println("Das Ergebnis: " + (zahl1 + zahl2));
break;
case '-':
System.out.println("Das Ergebnis: " + (zahl1 - zahl2));
break;
case '*':
System.out.println("Das Ergebnis: " + (zahl1 * zahl2));
break;
case '/':
if(zahl2 == 0) System.err.println("Division durch 0!");
else System.out.println("Das Ergebnis: " + (zahl1 / zahl2));
break;
default:
System.err.println("Unzulässiger Operator");
break;
}
}
}
Der Ablauf ist hier ganz klar, in der Reihenfolge, in der die Anweisungen im Sourcecode auftauchen. Jetzt nehmen wir das GUI-Beispiel vom letzten mal.
Übersichtlich ist es auch: Wenn man auf den Button drückt, wird das Element hinzugefügt. Allerdings ist aus dem von uns geschriebenen Code nicht ersichtlich, wie dieses wenn zustande kommt.

Sicherlich hast du auch schon eine Fehlermeldung in so einem Programm gesehen:
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
at post06.EditListener.actionPerformed(EditListener.java:25)
at javax.swing.JTextField.fireActionPerformed(JTextField.java:492)
at javax.swing.JTextField.postActionEvent(JTextField.java:705)
at javax.swing.JTextField$NotifyAction.actionPerformed(JTextField.java:820)
at javax.swing.SwingUtilities.notifyAction(SwingUtilities.java:1636)
at javax.swing.JComponent.processKeyBinding(JComponent.java:2849)
at javax.swing.JComponent.processKeyBindings(JComponent.java:2884)
at javax.swing.JComponent.processKeyEvent(JComponent.java:2812)
at java.awt.Component.processEvent(Component.java:5993)
at java.awt.Container.processEvent(Container.java:2041)
at java.awt.Component.dispatchEventImpl(Component.java:4583)
at java.awt.Container.dispatchEventImpl(Container.java:2099)
at java.awt.Component.dispatchEvent(Component.java:4413)
at java.awt.KeyboardFocusManager.redispatchEvent(KeyboardFocusManager.java:1848)
at java.awt.DefaultKeyboardFocusManager.dispatchKeyEvent(DefaultKeyboardFocusManager.java:704)
at java.awt.DefaultKeyboardFocusManager.preDispatchKeyEvent(DefaultKeyboardFocusManager.java:969)
at java.awt.DefaultKeyboardFocusManager.typeAheadAssertions(DefaultKeyboardFocusManager.java:841)
at java.awt.DefaultKeyboardFocusManager.dispatchEvent(DefaultKeyboardFocusManager.java:668)
at java.awt.Component.dispatchEventImpl(Component.java:4455)
at java.awt.Container.dispatchEventImpl(Container.java:2099)
at java.awt.Window.dispatchEventImpl(Window.java:2475)
at java.awt.Component.dispatchEvent(Component.java:4413)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:599)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:269)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:184)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:174)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:169)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:161)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)
Das ist die sogenannte "Stack Trace" der Exception: Die genaue Abfolge von Methodenaufrufen, die dazu geführt hat, dass die Exception aufgetreten ist.
Ganz oben steht in diesem Fall unsere Methode:
at post06.EditListener.actionPerformed(EditListener.java:25)
Das ist die Methode die die Exception geworfen hat. Direkt darunter steht die Methode, die unsere aufgerufen hat usw.

Im Gegensatz dazu einfach eine falsche Eingabe in dem neuen Beispiel ("a+1"):
Exception in thread "main" java.lang.NumberFormatException: For input string: "a"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Integer.parseInt(Integer.java:447)
at java.lang.Integer.parseInt(Integer.java:497)
at post06.Grundrechnung.main(Grundrechnung.java:27)
Ich nehme mal an, dass ihr gesehen habt, worum es geht: Im oberen Fall ist der thread "AWT-EventQueue-0", im unteren "main". main bezieht sich einleuchtenderweise auf unsere main-Methode, die das Programm startet. Das sieht man auch in der Stack Trace: Die unterste Methode, die Methode, die alle anderen aufgerufen hat, ist die main-Methode.

alle anderen Methoden werden in main aufgerufen. Erst, wenn die Methode fertig ist, wird die nächste aufgerufen. Die Reihenfolge ist fix.

Aber was ist mit der anderen Stack Trace?
   at java.awt.EventDispatchThread.run(EventDispatchThread.java:122)
Die unterste Methode ist nicht die main-Methode! Das bedeutet, dass unsere GUI einen eigenen, unabhängigen Programmablauf hat:

Auch in unserer GUI-Anwendung gibt es eine main-Methode, aber alles, was in der GUI passiert (z.B. die actionPerformed-Methode unseres Listeners), passiert in einem separaten Thread: Diese Dinge können gleichzeitig und unabhängig zum main-Thread passieren.

Das war eine ziemlich lange Einführung für das Thema. Was ich zeigen wollte ist:
  • Mit mehreren Threads können mehrere Aufgaben gleichzeitig ablaufen
  • Auch, wenn du es nicht gewusst hast, hast du sicher schon lange mit Threads gearbeitet
  • Swing (mit AWT zusammen) verwendet einen eigenen Thread dafür, dass die Benutzereingaben zu deinem Programm gelangen
  • Das ist notwendig, weil Benutzereingaben großteils warten bedeutet: Warten, bis eine Taste oder ein Button gedrückt wurde. Und warten braucht Zeit.
  • Ohne eigenen Thread könntest du in deinem Programm keine Sachen im Hintergrund machen, während die GUI angezeigt wird.
Das gilt aber auch in die andere Richtung: wenn deine actionPerformed-Methode eine Aufgabe ausführt, die sehr lange benötigt (Suche im Dateisystem, Netzwerkarbeit, Grafikoperationen, ...), dann blockiert die Methode den Swing-Thread.
Innerhalb des Threads gibt es, genau so wie im main-Thread, eine fixe Reihenfolge der Methodenaufrufe. Solange deine actionPerformed-Methode nicht abläuft, kann Swing keine Events bearbeiten. Das sieht dann so aus, dass deine Anwendung nicht reagiert.

Für heute war das lang genug, ein paar Praxisbeispiele machen wir später ;)

Montag, 6. Juli 2009

GUIs in Java: Event Handling

Die Oberflächen, die wir bis jetzt gemacht haben, haben vielleicht nett ausgesehen, aber überhaupt nichts gemacht. Deswegen schauen wir jetzt weiter. Am Anfang haben wir diese Oberfläche entworfen.

Jetzt geben wir ihr Funktionalität: Mit einem Klick auf Add soll der ComboBox der eingegebene Text als Eintrag hinzugefügt werden.
Zum Glück funktioniert auch Swing nach dem MVC-Prinzip. Denn ansonsten müsste man eine Subklasse von JButton machen, um dort die Aktion einzuprogrammieren. Stattdessen ist JButton dafür nicht verantwortlich. Du musst JButton nur sagen, wo er bescheid sagen muss, sobald er gedrückt wurde.

Was heißt bescheid sagen? Eine bestimmte Methode aufzurufen. zu diesem Zweck gibt es das interface ActionListener. Die einzige Methode, die ActionListener definiert, ist actionPerformed, und diese wird vom Button (oder anderem Component, mit dem der Listener registriert ist) aufgerufen, wenn die Action passiert ist.
Das Herzstück unserer Änderung ist also die Aktion, die durch den ActionListener durchgeführt werden soll. Um die Aktion durchzuführen, benötigt der Listener zwei Dinge: das Textfeld und die ComboBox. Das einzige, das der Listener vom Button benötigt, ist der Methodenaufruf zur richtigen Zeit. Informationen über den Button benötigt er nicht.

package post05;


import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JComboBox;
import javax.swing.JTextField;


public class EditListener implements ActionListener {
private JTextField tf;
private JComboBox cb;

public EditListener(JTextField tf, JComboBox cb) {
this.tf = tf;
this.cb = cb;
}

//@Override zeigt dem Compiler, dass die Methode in der Superklasse oder einem Interface
//definiert ist. dadurck können Tippfehler o.Ä. vermieden werden.
@Override
public void actionPerformed(ActionEvent e) {
cb.addItem(tf.getText());
}
}
Der nächste Schritt ist, dem Button zu sagen, dass bei einem Klick die Aktion ausgeführt werden soll. dazu ist nur eine Änderung in unserer GUI-Klasse vorzunehmen.

package post05;


import java.awt.BorderLayout;
import java.awt.event.ActionListener;

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


public class Example extends JPanel {
//Die "Signatur" einer main-Methode muss immer genau so aussehen
//public static void main(String[] args)

public static void main(String[] args) {
//Der frame ist ein object vom Typ JFrame
JFrame jf = new JFrame();
//Das ist nötig, damit das Programm sauber beendet wird, wenn man das Fenster schließt.
//Der Grund ist aber für diesen Post etwas zu viel
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

//Wie bei jedem Component hat ein JFrame ein Jayout, und es können Components hinzugefügt
//werden. Das Standard-Layout für Frames ist BorderLayout. diese Zeile fügt unser panel also
//im Center Bereich ein, wo es das Gesamte Fenster ausfüllt.
jf.add(new Example());

//Standardmäßig hat ein frame die minimale Größe, die vom Betriebssystem vorausgesetzt ist.
//pack() berechnet die benötigte Größe, um den Inhalt darzustellen, und vergrößert das Fenster.
jf.pack();
jf.setVisible(true);
}

//Jetzt brauchen wir sie zwar noch nicht permanent, aber für später speichere ich die
//Komponenten in Attributen.
private JTextField tf;
private JButton b;
private JComboBox cb;

public Example() {
//super() ruft explizit den Konstruktor der Superklasse (JPanel in this case) auf.
//Ich benutze es, um das Objekt mit einem BorderLayout zu bestücken. Ich könnte aber
//auch später setLayout(new BorderLayout()); benutzen.
super(new BorderLayout());

//Der Parameter von JTextField gibt die Breite, in Textzeichen, an.
//Die Java API doc ist eine perfekte Quelle für solche Informationen.
tf = new JTextField(5);
cb = new JComboBox();
b = new JButton("Add");
ActionListener l = new EditListener(tf, cb);
b.addActionListener(l);

//Das ist das Panel, das gleich Textfeld und Button enthalten wird.
JPanel north = new JPanel(new BorderLayout());

//Hier wird das Panel im North-Bereich hinzugefügt. Der zweite Parameter
//Gibt den Ort des panels an. BorderLayout hat für alle 5 Bereiche Konstanten.
add(north, BorderLayout.NORTH);

//Ohne zweiten Parameter, benutzt BorderLayout den Center-Bereich
north.add(tf);
north.add(b, BorderLayout.EAST);
add(cb, BorderLayout.SOUTH);
}
}
Aber ein kleines Makel gibt es immer noch. Wenn man das Programm laufen lässt, fallen drei Dinge auf:
  • Das Textfeld wird nicht geleert, wenn der Text hinzugefügt wird
  • Man muss das Textfeld erst markieren, bevor man wieder etwas eingeben kann
  • Die Enter-Taste bewirkt nach der Eingabe nichts
Der dritte Punkt ist eine Art, die Aktion auszulösen, es kommt deshalb in die GUI. Die ersten beiden sind Teil der Aktion, sind also Teil des Listeners.
Der Listener:

package post05;


import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JComboBox;
import javax.swing.JTextField;


public class EditListener implements ActionListener {
private JTextField tf;
private JComboBox cb;

public EditListener(JTextField tf, JComboBox cb) {
this.tf = tf;
this.cb = cb;
}

//@Override zeigt dem Compiler, dass die Methode in der Superklasse oder einem Interface
//definiert ist. dadurck können Tippfehler o.Ä. vermieden werden.
@Override
public void actionPerformed(ActionEvent e) {
cb.addItem(tf.getText());
tf.setText("");
tf.requestFocusInWindow();
}
}
...und die GUI:

package post05;


import java.awt.BorderLayout;
import java.awt.event.ActionListener;

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

import post05.EditListener;


public class Example extends JPanel {
//Die "Signatur" einer main-Methode muss immer genau so aussehen
//public static void main(String[] args)

public static void main(String[] args) {
//Der frame ist ein object vom Typ JFrame
JFrame jf = new JFrame();
//Das ist nötig, damit das Programm sauber beendet wird, wenn man das Fenster schließt.
//Der Grund ist aber für diesen Post etwas zu viel
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

//Wie bei jedem Component hat ein JFrame ein Jayout, und es können Components hinzugefügt
//werden. Das Standard-Layout für Frames ist BorderLayout. diese Zeile fügt unser panel also
//im Center Bereich ein, wo es das Gesamte Fenster ausfüllt.
jf.add(new Example());

//Standardmäßig hat ein frame die minimale Größe, die vom Betriebssystem vorausgesetzt ist.
//pack() berechnet die benötigte Größe, um den Inhalt darzustellen, und vergrößert das Fenster.
jf.pack();
jf.setVisible(true);
}

//Jetzt brauchen wir sie zwar noch nicht permanent, aber für später speichere ich die
//Komponenten in Attributen.
private JTextField tf;
private JButton b;
private JComboBox cb;

public Example() {
//super() ruft explizit den Konstruktor der Superklasse (JPanel in this case) auf.
//Ich benutze es, um das Objekt mit einem BorderLayout zu bestücken. Ich könnte aber
//auch später setLayout(new BorderLayout()); benutzen.
super(new BorderLayout());

//Der Parameter von JTextField gibt die Breite, in Textzeichen, an.
//Die Java API doc ist eine perfekte Quelle für solche Informationen.
tf = new JTextField(5);
cb = new JComboBox();
b = new JButton("Add");
ActionListener l = new EditListener(tf, cb);
b.addActionListener(l);
tf.addActionListener(l);

//Das ist das Panel, das gleich Textfeld und Button enthalten wird.
JPanel north = new JPanel(new BorderLayout());

//Hier wird das Panel im North-Bereich hinzugefügt. Der zweite Parameter
//Gibt den Ort des panels an. BorderLayout hat für alle 5 Bereiche Konstanten.
add(north, BorderLayout.NORTH);

//Ohne zweiten Parameter, benutzt BorderLayout den Center-Bereich
north.add(tf);
north.add(b, BorderLayout.EAST);
add(cb, BorderLayout.SOUTH);
}
}
Auch ein Textfeld kann einen ActionListener haben. Hier sieht man wieder, wie die Teile unabhängig sind und trotzdem zusammenarbeiten: Bei einem Textfeld wird ein ActionEvent durch die Enter-Taste ausgelöst, bei einem Button durch einen Klick. Trotzdem können beide den gleichen Code benutzen, um die Aktion umzusetzen.