Java Tutorial: Beispiel Stoppuhr – Programmierung in Java
Für alle, die sich mit den Anfängerbeispielen, die Paul in letzter Zeit vorgestellt hat, unterfordert fühlen, werde ich wie versprochen ein paar anspruchsvollere Programme vorstellen. Natürlich kann es sein, dass einige auch hier denken, oh man, das ist ja nicht so doll, das hätte ich auch so hinbekommen. Oder dasss ein paar den Eindruck bekommen, dass dies etwas ZU anspruchsvoll ist. Ab wann man als Fortgeschrittener zählt, was noch eher Anfängerbereich und was schon Profi-Code ist, ist eben sehr schwer festzulegen.
Ich werde es deshalb so handhaben: Die Gui-Programmierung werde ich nicht näher erklären. Das kann man mit dem Java-Editor oder einer alternativen Entwicklungsumgeben, die ein bisschen was auf sich hält (siehe auch hier) problemlos hinbekommen, auch wenn ich selbst es vorziehe, sowas von Hand zu programmieren. Deshalb sieht mein GUI-Code auch etwas anders aus als der automatisch generierte. Aber das könnt ihr handhaben wie ihr wollt. Macht es selbt, lasst es vom Editor generieren oder tippt meinen Code einfach ab. Am Ende zählt eh nur, dass es funktioniert. Aber wie gesagt: Das werde ich nicht näher erklären, interessant ist eher der Rest.
Also heute, im ersten Teil der Reihe “Java für Fortgeschrittene” soll es darum gehen, eine Stoppuhr zu programmieren. Das ist nicht unbedingt so einfach, wie man vielleicht denken mag. Man braucht dafür etwas, das man Thread-Programmierung nennt. Deshalb ist es ja auch für Fortgeschrittene. Aber dazu später mehr.
Ersteinmal ein paar grundlegende Gedanken: Was braucht man für eine Stoppuhr? Auf jeden Fall ein Fenster. Dieses muss nicht groß sein und auch nicht sonderlich schick. Wer es verzieren will, kann das gerne machen, ich verzichte mal darauf, das würde den Code nur unnötig komplex machen. Wir wollen natürlich eine Anzeige haben, wie viel Zeit denn nun eigentlich vergangen ist. Dafür reicht ein simples Label. Es ginge natürlich auch anders, beispielsweise mit einem Textfeld, aber dies ist der beste Weg. Ein Label hat den Vorteil, dass der Benutzer es nicht einfach verändern kann.
Außerdem benötigen wir zwei Knöpfe: Einen zum Starten und einen zum Stoppen. Den Reset-Knopf sparen wir uns mal, die Uhr wird einfach beim Start auf 0:0:0:0 zurückgesetzt.
Hier könnt ihr schonmal das fertige Programm als Applet ausprobieren und gucken, wie es aussehen soll:
Damit sieht der Code für die Gui-Programmierung wie folgt aus:
import java.awt.*; import java.awt.event.*; public class StoppUhr extends Frame { private Button start; private Button stop; private Label ausgabe; public StoppUhr(){ super(); setLayout(null); setTitle("Stoppuhr"); setResizable(false); start = new Button("Start"); stop = new Button("Stop"); ausgabe = new Label(); start.setBounds(100,100,100,50); stop.setBounds(220, 100, 100, 50); ausgabe.setBounds(130, 175, 250, 50); add(start); add(stop); add(ausgabe); ausgabe.setFont(new Font(Font.SERIF,Font.PLAIN, 35)); addWindowListener(new WindowLauscher()); } class WindowLauscher extends WindowAdapter{ public void windowClosing(WindowEvent e){ System.exit(0); } } public static void main(String[] args) { StoppUhr uhr = new StoppUhr(); uhr.setBounds(0, 0, 500, 300); uhr.setVisible(true); } }
Ich denke, das sollte an sich klar sein. Es werden ein Label mit Namen “ausgabe” und zwei Button, einer für “start”, einer für “stop” erzeugt und dem Fenster hinzugefügt. Das setFont setzt die Schriftart, Formatierung und Schriftgröße des Labels. Der WindowLauscher dient dazu, das Programm zu beenden, wenn jemand das Fenster schließt.
Kommen wir zum interessanten Teil:
Wer will, kann das Programm ja in der Form einmal starten. Es wird nicht viel passieren. Die Button sollten da sein, aber ohne wirklichen Nutzen. Ok, das soll natürlich nicht so bleiben. Deshalb legen wir jetzt ersteinmal die ActionListener für die beiden Button fest:
In den Konstruktor, also am besten
ausgabe.setFont(new Font(Font.SERIF,Font.PLAIN, 35)); //hierhin addWindowListener(new WindowLauscher());
kommen jetzt folgende Zeilen:
this.start.addActionListener(new ButtonListenerStart()); this.stop.addActionListener(new ButtonListenerStop());
Wenn jemand jetzt versucht, das Programm zu starten, gibt der Compiler eine Fehlermeldung aus, da er natürlich nicht weiß, was ButtonListenerStart und ButtonListenerStop sein sollen. Diese beiden Klassen erzeugen wir nun unter dem Konstruktor:
class ButtonListenerStart implements ActionListener{ public void actionPerformed(ActionEvent e){ if(!running){ running = true; UhrzeitThread uhr = new UhrzeitThread(); uhr.start(); } } } class ButtonListenerStop implements ActionListener{ public void actionPerformed(ActionEvent e){ running = false; } }
Als Erstes fällt hier natürlich das running auf, das anscheinend eine ziemlich große Rolle zu spielen scheint. Tut es auch. Es steuert dann später die Stoppuhr. Solange running auf “true” steht, läuft die Zeit. Es wird schon deutlich, von welchem Datentyp running sein muss: Korrekt, von Typ boolean. Damit es auch in den inneren Klassen, zu denen diese beiden ActionListener ja gehören, verwendet werden kann, muss es als Attribut der Klasse Stoppuhr festgelegt werden. Es wird mit dem Wert “false” versehen. (Die Zeit soll ja nicht gleich loslaufen, sobald das Programm startet)
private Label ausgabe; //bekannt private boolean running = false; //neu public StoppUhr(){ //auch bekannt
Auch in den “WindowLauscher” muss noch eine Zeile eingefügt werden, damit die Stoppuhr nicht endlos weiterläuft, wenn jemand das Programm beendet.
public void windowClosing(WindowEvent e){ running = false; //die neue Zeile System.exit(0); }
Aber noch immer ist das Programm nicht lauffähig. Der Compiler weiß zwar jetzt, was es mit ButtonListenerStart und ButtonListenerStop auf sich hat, aber nun wird er bei diesen Zeilen Fehler melden:
UhrzeitThread uhr = new UhrzeitThread(); uhr.start();
Dies ist nun das eigentliche Herz des Programms: Der Teil, der eigentlich die Zeit stoppt. Alles andere dient ja nur der Schönheit und der Benutzerfreundlichkeit. Was genau passiert hier? Es wird ein Objekt der Klasse UhrzeitThread instanziert und dann seine Methode start() aufgerufen. Der Name ist vielleicht etwas ungünstig gewählt, da der Thread nicht die Uhrzeit sondern die vergangene Zeit ermitteln soll. Aber auf das Programm an sich hat der Name keinen Einfluss.
Was aber ist ein Thread überhaupt? Ein Thread ist an sich ein Programm, das parallel zu anderen Programmen laufen kann. Der Prozessor ist ja mehr oder weniger Multitaskingfähig, das heißt, er bearbeitet mehrere Aufträge so schnell hintereinander, dass es aussieht, als würde er es gleichzeitig machen. Das Problem: Ein Programm kann immer nur genau EINEN Befehl bearbeiten lassen. Würden wir nun also unser Hauptprogramm die Zeit stoppen lassen, würde das Fenster einfrieren, heißt, wir könnten keine Tasten mehr drücken und es noch nicht einmal richtig beenden. Deshalb muss das Stoppen der Zeit ein unabhängiges Unterprogramm, ein sogenannter Thread erledigen. Nun wird vielleicht auch klar, warum das ganze Rumgehampel mit running nötig war: Die Variable “uhr” geht verloren, sobald der Thread erzeugt wurde, da dann die Methode “actionPerformed” ihr Ende erreicht. Der Thread würde dann ewig weiterlaufen, wenn wir ihn nicht über running stoppen könnten. Außerdem wird auf diese Weise verhindert, dass zwei Threads gleichzeitig laufen, was zu hässlichen Anzeigen auf dem Label führen würde (beide Threads würden abwechselnd die von ihnen gemessene Zeit ausgeben.)
So, aber was genau tut der Thread denn nun bzw. wie erzeugen wir ihn überhaupt?
class UhrzeitThread extends Thread{ private int HS = 0; private int sek = 0; private int min = 0; private int h = 0; public void run(){ while(running){ try{Thread.sleep(9);}catch(Exception e){} if(HS <= 99){ HS = HS + 1; } else { HS = 0; if(sek <= 59){ sek = sek + 1; }else { sek = 0; if(min <= 59){ min = min + 1; } else { min = 0; h = h + 1; } } } ausgabe.setText(h + " : " + min + " : " + sek + " : " + HS); } } }
Das sieht ziemlich kryptisch aus, ist aber nicht so schwer. Wir haben vier Variablen vom Typ int. Eine für die Hundertstelsekunden, eine für die Sekunden, eine für die Minuten und eine für die vergangenen Stunden. Damit könnte die Stoppuhr beliebig lange laufen, es würde immer ein vernünftiger Wert angezeigt werden (der mit der Zeit aber stark abweichen würde)
Wichtig ist hier, dass die Klasse von Thread abgeleitet ist und die Methode run(). ACHTUNG: Man startet einen Thread nie, indem man die Methode run() aufruft, sondern immer über start(). Das ist eine häufige Fehlerquelle.
Der Thread macht jetzt Folgendes: Solange running (da ist es wieder) auf “true” steht, läuft er. Und zwar macht er erstmal gar nichts. Und das genau 9 Millisekunden lang. Wir messen ja Hundertstelsekungen, also immer 10 ms. Die zehnte Millisekunde schläft der Thread nicht, die braucht er für das, was jetzt kommt. Er testet: Wenn schon 99 Hundertstelsekungen vergangen sind, setzt er HS auf 0 und erhöht die Sekundenzahl. Erreicht diese 59, setzt er sie auch auf 0 und vergrößert den Minutenzähler, sofern diese nicht auch schon bei 59 ist. Ansonsten wird eben die Stundenanzeige erhöht. Am Ende wird alles an das Label “ausgabe” übergeben, schön mit Doppelpunkten abgeteilt, wie man es von einer Stoppuhr kennt.
Ich habe mit den Programm mal ein paar Zeiten gestoppt und mit meiner richtigen Stoppuhr verglichen. Es gibt natürlich eine gewisse Abweichung, einfach weil man nicht sagen kann, wie lange der Computer für das Auswerten der Zeit braucht. Aber für ein relativ unprofessionelles Programm ist die Genauigkeit durchaus befriedigend. Ihr könnt es ja gerne einmal selbst schreiben, hier noch einmal der gesamte Code:
import java.awt.*; import java.awt.event.*; public class StoppUhr extends Frame { private Button start; private Button stop; private Label ausgabe; private boolean running = false; public StoppUhr(){ super(); setLayout(null); setTitle("Stoppuhr"); setResizable(false); start = new Button("Start"); stop = new Button("Stop"); ausgabe = new Label(); start.setBounds(100,100,100,50); stop.setBounds(220, 100, 100, 50); ausgabe.setBounds(130, 175, 250, 50); add(start); add(stop); add(ausgabe); ausgabe.setFont(new Font(Font.SERIF,Font.PLAIN, 35)); start.addActionListener(new ButtonListenerStart()); stop.addActionListener(new ButtonListenerStop()); addWindowListener(new WindowLauscher()); } class ButtonListenerStart implements ActionListener{ public void actionPerformed(ActionEvent e){ if(!running){ running = true; UhrzeitThread uhr = new UhrzeitThread(); uhr.start(); } } } class ButtonListenerStop implements ActionListener{ public void actionPerformed(ActionEvent e){ running = false; } } class WindowLauscher extends WindowAdapter{ public void windowClosing(WindowEvent e){ running = false; System.exit(0); } } class UhrzeitThread extends Thread{ private int HS = 0; private int sek = 0; private int min = 0; private int h = 0; public void run(){ while(running){ try{Thread.sleep(9);}catch(Exception e){} if(HS <= 99){ HS++; } else { HS = 0; if(sek <= 59){ sek++; }else { sek = 0; if(min <= 59){ min++; } else { min = 0; h++; } } } ausgabe.setText(h + " : " + min + " : " + sek + " : " + HS); } } } public static void main(String[] args) { StoppUhr uhr = new StoppUhr(); uhr.setBounds(0, 0, 500, 300); uhr.setVisible(true); } }
Hi,
genau nach dem hab ich gesucht. Super Java Beitrag, viel besser geht es nicht, es hat mir sehr geholfen!
Weiter so!!! :-))
Mike
Perfekt, ein wirklich gelungener Beitrag, hat mir sehr geholfen!!!
Vielen Dank und viele Grüße
Luca
Perfekte Erklärung, hat mir viel weitergeholfen!
danke erstmal für die geleistete Arbeit. Als Verbesserungsvorschlag vllt noch eine v2 schreiben und mit timestamp abgelaufene Zeit errechnen lassen?!
Mfg Jugine
Gelungenes Beispiel Michael (-;
bussi
Nachdem sich diverse meiner Studis für eine Hausarbeit hier inspirieren ließen, möchte ich anregen, darüber nachzudenken, ob die Zeit-Implementierung mit dem Zähler nach einem sleep() wirklich so gelungen ist, wie das alle hier finden..?
Kleines Einmaleins der Threads: der Scheduler garantiert nicht, dass ein Thread zu festgelegten Zeitpunkten weiterlaufen darf, außer bei einer echtzeitfähigen VM.