Threads, Images

Inhalt

 

 

Threads

 

Synchronisation

 

Images

 

Images Laden

 

Images Erzeugen

 

Übungsaufgaben

Threads

Threads sind scheinbar parallel arbeitende Teile eines Programs, die auf denselben Datenbestand zugreifen. Bei zwei parallel arbeitenden getrennten Programmen spricht man von Prozessen. Unsere bisherigen Grafikprogramme arbeiteten schon parallel, ohne dass wir das ausgenutzt haben. Der eine Thread war nämlich das Programm main, und der andere war die Ereignisbehandlung. Über den Thread von main hat unser Programm allerdings keine Kontrolle, da es ihn nicht besitzt. So kann man diesen Thread insbesondere nicht unterbrechen oder warten lassen. Der main-Thread endet natürlich, wenn das Unterprogramm main beendet wird. Das Programm selbst kann trotzdem weiter laufen. Erst wenn das letzte Fenster geschlossen wird, endet auch die Ereignisbehandlung, und damit das Programm. Alternativ kann das Programm jederzeit komplett mit System.exit(0) abgebrochen werden.

Eine typische Anwendung für Multi-Threading sind animierte Applets. Wir wollen zuerst ein Beispiel angeben, wo die aktuelle Zeit in einem Applet tickt. Dazu startet man einen Thread, der im Sekundentakt ein repaint des Applets aufruft. Der Code für die Darstellung der Zeit stammt aus der Lösung einer Übungsaufgabe.

Der Sekundentakt wird dadurch erzeugt, dass die Methode sleep() von Thread aufgerufen wird. Diese Methode erzeugt eventuell eine Exception (wenn das Unterbrechen nicht möglich war), die abgefangen werden muss.

import java.awt.*;
import java.applet.*;
import java.util.*; // wegen Date

class RedrawThread extends Thread
// Der Thread, der alle 1000 ms sein Werk verrichten soll
{   Applet A;
    public RedrawThread (Applet a)
    {   A=a;
        start();
    }
    public void run ()
    {   while (true)
        {   try // Notwendig!
            {   sleep(1000); // Warte 1 Sekunde
            }
            catch (Exception ex) {}
            A.repaint();
        }
    }
}

public class ClockApplet extends Applet
{   public void init ()
    {   new RedrawThread(this); 
        // Erzeuge Thread. Der startet sich selber.
    }
    public void paint (Graphics g)
    {   g.drawString(new Date().toString(),50,50);
            // 50,50 ist ueber den Daumen gepeilt
    }
}

Wie man sieht, ist nichts weiter zu tun, als eine Instanz von Thread zu schaffen, und diesen Thread zu starten, indem man seine start()-Methode aufruft. Dies startet einen Hintergrund-Thread mit der Methode run(). Der Thread wird beendet, wenn die Methode run() beendet wird. In unserem Fall hat diese Methode eine Dauerschleife.

Das Programm funktioniert so schon. Aber man sollte den Thread unterbrechen, wenn die Seite nicht mehr zu sehen ist, d.h. wenn die stop()-Methode des Applets aufgerufen wurde. In Java 1.0.2. wurde dies einfach dadurch erreicht, dass man die stop()-Methode von Thread aufruft. Aus Sicherheitsgründen ist dies in Java 1.1 nicht die korrekte Vorgehensweise. Man erreicht hier die Unterbrechung, indem man die Dauerschleife von einer Variablen abhängig macht, die man gleich false setzt, wenn der Thread abbrechen soll.

import java.awt.*;
import java.applet.*;
import java.util.*;

class RedrawThread extends Thread
{   Applet A;
    public boolean Stop; // Stopt den Thread.
    public RedrawThread (Applet a)
    {   A=a;
        Stop=false;
    }
    public void run ()
    {   while (!Stop) // Damit laesst der Thread sich sauber beenden.
        {   ...
        }
    }
}

public class ClockApplet extends Applet
{   RedrawThread T;
    public void start ()
    {   T=new RedrawThread(this); // Merke Thread
    }
    ...
    public void stop ()
    {   T.Stop=true;
    }
}

Nun wird der Thread auch neu gestartet, wenn die Seite angezeigt wird. Beachten Sie, dass dann ein neuer Thread erzeugt wird. Es ist unklar, ob man einen abgelaufenen Thread in jedem Betriebssystem neu starten kann.

Man kann aber einen Thread für einen Moment anhalten. Dies geschieht mit der Methode suspend() von Thread. Man kann ihn (natürlich aus einem anderen Thread) wieder mit resume() starten. Den aktuellen Thread ermittelt man mit Thread.currentThread().

Es geht aber noch einfacher. Dazu implementiert unser Applet das Interface Runnable. Die Klasse Thread besitzt einen Konstruktor, der sich das Runnable merkt und dessen run-Methode verwendet. Das Interface Runnable hat die einzige Methode run(). Diese Methode wird vom Thread gestartet.

import java.awt.*;
import java.applet.*;
import java.util.*;

public class ClockApplet extends Applet implements Runnable
{   Thread A;
    boolean Stop;
    public void paint (Graphics g)
    {   g.drawString(new Date().toString(),50,50);
    }
    public void start ()
    {   A=new Thread(this);
        A.start();
        Stop=false;
    }
    public void stop ()
    {   Stop=true;
    }
    public void run ()
    {   while (!Stop)
        {   try
            {   A.sleep(1000); 
            }
            catch (Exception ex) {}
            repaint();
        }
    }
}

Im obigen Display wurde noch die schon vorgestellte Methode zum Zentrieren des Strings verwendet.

Synchronisation

Wir weisen an dieser Stelle darauf hin, dass Probleme auftreten können, wenn mehrere Threads auf dasselbe Objekt, oder dieselbe Variable zugreifen. Die Threads können sich nämlich nicht darauf verlassen, dass das Objekt für eine bestimmte Zeitspanne unverändert bleibt. Solchen Problemen muss man mit Synchronisation abhelfen.

Dazu kann man einen Block mit einem Objekt synchronisieren. Innerhalb dieses Blocks hat der Thread dann einen so genannten Lock auf das Objekt. Kein anderer Thread kann in den Block eintreten, bevor das Objekt nicht freigegeben ist. Solch ein Block sieht folgendermaßen aus

synchronized (Objekt)
Anweisungsblock

Alternativ kann man auch eine ganze Methode einer Klasse mit einem Lock auf die Instanz der Klasse, deren Methode aufgerufen wurde, versehen. Dazu erhält die Methode das Schlüsselwort synchronized.

Beispiel

    public synchronized int inc ()
    {   int m=n;
        n++;
        return m;
    }

Dieses Beispiel erhöht n und gibt den alten Wert zurück. Falls n Variable des Objektes A ist, so kann A.inc() nur in jeweils einem Thread aktiv sein. Dadurch wird verhindert, dass der Wert von n in A inkonsistent ist (siehe Aufgabe 4). Es ist übrigens immer gewährleistet, dass n++ nicht unterbrochen wird. Aber hier kommt es darauf an, dass der Abruf des alten Wert und die Erhöhung nicht durch Zugriffe anderer Threads unterbrochen werden.

Insgesamt ist das Schreiben von Programmen mit mehreren Threads eine Kunst, auf die wir hier nicht weiter eingehen können.

Images

In diesem Abschnitt werden wir ein Applet schreiben, bei dem ein Ball innerhalb der Fläche des Applets hüpft. Der Ansatz ist zunächst, einen separaten Thread laufen zu lassen, der die Position des Balles neu berechnet und den Ball neu zeichnet. Das Applet unten ist ein Endprodukt, dessen Entstehungsgeschichte wir langsam nachvollziehen. So werden wir uns zunächst auf einen Ball konzentrieren, der zudem als einfacher Farbkreis gezeichnet wird, und auch den Bildhintergrund einfarbig wählen.

Das folgende Programm implementiert diesen Ansatz. Die Schleife ruft hierbei einfach repaint() auf und legt eine kurze Pause ein. Da wir ohnehin das ganze Applet neu zeichnen, unterbinden wir das Löschen des Applets noch, indem wir update() überschreiben. Normalerweise sorgt diese Funktion für ein Füllen mit der Hintergrundfarbe, die dazu auf die normale Hintergrundfarbe des Applets gesetzt wird. Man kann diese Hintergrundfarbe übrigens setzen und abfragen mit

setBackground(color);
Color c=getBackground();

Diese Methoden beherrschen alle Komponenten, also auch Applets.

Es folgt der erste Versuch für den hüpfenden Ball.

import java.awt.*;
import java.applet.*;

public class BallApplet extends Applet implements Runnable
{   Thread A;
    boolean Stop;
    
    int X,Y,DX,DY, // Ballkoordinaten von links unten gesehen
        W,H; // Hoehe und Weite des Applets
    final int S=20; // Kugelgroesse als Konstante
        
    public void init ()
    {   W=size().width; H=size().height;
            // Bestimme Hoehe und Weite
    }
    
    public void paint (Graphics g)
    // Hintergrund und Kugel neu Zeichnen
    {   g.setColor(Color.blue.darker()); 
            // gibt ein dunkleres Blau
        g.fillRect(0,0,W,H);
        g.setColor(Color.green.darker());
        g.fillOval(X-S,H-Y-S,2*S,2*S);
    }
    
    public void update (Graphics g)
    // Hintergrund wird selbst neu gezeichnet
    // normalerweise erledigt dies update()
    {   paint(g);
    }
    
    public void start ()
    // Kugel startet in der Mitte unten
    {   X=W/2; Y=S; DX=1; DY=15;
        A=new Thread(this);
        A.start();
        Stop=false;
    }
    
    public void stop ()
    // Stoppe Thread
    {   Stop=true;
    }
    
    public void run ()
    // Die Schleife
    {   while (!Stop)
        {   if (X+DX<S) DX=-DX;
            if (X>W-S-1) DX=-DX;
            if (Y+DY<S) { DY=15; }
            X+=DX; Y+=DY; DY--;
            try { A.sleep(10); } catch (Exception e) {}
            repaint();
        }
    }
}

Allerdings zeigt die Kugel in dem Beispiel ein deutliches Flimmern. Dies liegt daran, dass zuerst gelöscht wird und dann die Kugel neu gezeichnet wird.

Es hilft da auch nichts, wenn man nur den Teil löscht, den die Kugel bedeckt. Wir wollen dennoch eine solche Technik vorstellen. Dazu muss man das Neuzeichnen der Kugel im Thread selbst in Auftrag geben. D.h. man muss sich selbst ein Graphics-Object für das Applet besorgen. Aus Effizienzgründen sollte dieses Objekt anschließend wieder freigegeben werden.

Die Methode run() nimmt damit folgende Gestalt an:

 public void run ()
    // Die Schleife
    {   while (!Stop)
        {   Graphics g=getGraphics();
            g.setColor(Color.blue.darker());
            g.fillRect(X-S,H-Y-S,2*S,2*S);
            if (X+DX<S) DX=-DX;
            if (X>W-S-1) DX=-DX;
            if (Y+DY<S) { DY=15; }
            X+=DX; Y+=DY; DY--;
            g.setColor(Color.green.darker());
            g.fillOval(X-S,H-Y-S,2*S,2*S);
            g.dispose();
            try { A.sleep(10); } catch (Exception e) {}
        }
    }

Das Flimmern ist immer noch deutlich sichtbar. Man könnte übrigens auf den Gedanken kommen, sich nur einmal ein Graphics-Objekt zu beschaffen und es in der Dauerschleife immer wieder zu verwenden. Dies funktioniert auch in allen Systemen, die mir bekannt sind. Es ist aber unklar, ob dieses Vorgehen manchmal zu Problemen führt, wenn das Fenster verschoben wird oder teilweise verdeckt ist.

Die Lösung besteht darin, in ein Hintergrundbild zu zeichnen und dieses Bild erst auf den Schirm zu kopieren, wenn es ganz fertig ist. Man nennt diese Technik Buffering.

In Java beschafft man sich einen solchen Puffer mit der createImage()-Methode, die jede Komponente besitzt. Damit diese Methode funktioniert, muss die Komponente schon dargestellt sein. Im Falle von Applets ist dies einfach. Wir erzeugen das Image in init(). Danach wird auf das Image gezeichnet, und das Image wird auf den Schirm kopiert. Dieses Kopieren kann mit der Methode drawImage() durchgeführt werden. Diese Methode hat vier Parameter, neben dem Image und den Zielkoordinaten auch einen sogenannten ImageObserver. Ein solcher Observer überwacht den Kopiervorgang. In unserem Falle geht das Kopieren so schnell, dass wir den Parameter einfach vernachlässigen. Jede Komponente kann als ImageObserver verwendet werden, und wir verwenden das gerade vorhandende Applet.

import java.awt.*;
import java.applet.*;

public class BallApplet extends Applet implements Runnable
{   Thread A;
    boolean Stop;
    
    Image I; // Puffer
    Graphics G; // Graphics fuer den Puffer
    
    int X,Y,DX,DY, // Ballkoordinaten von links unten gesehen
        W,H; // Hoehe und Weite des Applets
    final int S=20; // Kugelgroesse als Konstante
        
    public void init ()
    {   W=size().width; H=size().height;
            // Bestimme Hoehe und Weite
        I=createImage(W,H); // Erzeuge Puffer
        G=I.getGraphics();
        paintBall(G);
    }
    
    public void paintBall (Graphics g)
    // Hintergrund und Kugel neu Zeichnen
    {   g.setColor(Color.blue.darker());
        g.fillRect(0,0,W,H);
        g.setColor(Color.green.darker());
        g.fillOval(X-S,H-Y-S,2*S,2*S);
    }
    
    public void paint (Graphics g)
    // Kopiere einfach das Image auf den Schirm
    {   g.drawImage(I,0,0,this);
    }
    
    public void update (Graphics g)
    // Hintergrund wird selbst neu gezeichnet
    // normalerweise erledigt dies update()
    {   paint(g);
    }
    
    public void start ()
    // Kugel startet in der Mitte unten
    {   X=W/2; Y=S; DX=1; DY=15;
        A=new Thread(this);
        A.start();
        Stop=false;
    }
    
    public void stop ()
    // Stoppe Thread
    {   Stop=true;
    }
    
    public void run ()
    // Die Schleife
    {   while (!Stop)
        {   // Ball verschieben:
            if (X+DX<S) DX=-DX;
            if (X>W-S-1) DX=-DX;
            if (Y+DY<S) { DY=15; }
            X+=DX; Y+=DY; DY--;
            // Ball neu zeichnen und auf Schirm kopieren
            paintBall(G);
            Graphics g=getGraphics();
            paint(g);
            g.dispose();
            // Etwas verzögern
            try { A.sleep(50); } catch (Exception e) {}
        }
    }
}

Allerdings haben wir im oben sichtbaren Applet noch einige Verbesserungen vor uns, die wir im folgenden erklären.

Images Laden

Der Hintergrund im obigen Bild wurde mit einem Malprogramm (PC Paintbrush) erzeugt und auf eine Datei im GIF-Format abgespeichert (ziegel.gif). Sie können diese Datei von unserem Server herunterladen. Java kann außerdem noch mit dem JPEG-Format (Dateiendung jpg) umgehen, das eine bessere Kompression mit nur leichten Verlusten ermöglicht, wobei die Kompressionsrate, und damit die Bildqualität, einstellbar ist. Das GIF-Format komprimiert auf Wunsch ebenfalls, jedoch verlustfrei und weniger stark. Ein weiterer Unterschied ist, dass das GIF-Format mit höchstens 256 Farben arbeitet.

Das Laden eines Images von einer Datei ist in einem Applet sehr einfach. Dazu muss nur eine URL und ein dazu relativer Name angegeben werden. In unserem Fall verwenden wir einfach die URL, von der das Applet geladen wurde. Beachten Sie, dass Applets nur auf Dateien zugreifen können, die auf demselben Server wie das Applet liegen. Das Kommando zum Laden des Image ist einfach

    Image Back=getImage(getCodeBase(),"ziegel.gif");

Dieses Image kann nun immer in den Puffer gezeichnet werden, um den Puffer zu löschen. Es wird wie üblich auf den Puffer kopiert. Im obigen Programm sieht das so aus:

    g.drawImage(Back,0,0,this);

Dabei ist g ein Graphics-Objekt, das auf den Puffer zeichnen kann. Ein solches Objekt beschafft man sich, wie oben vorgeführt, mit der Methode getGraphics von Image.

Es sollte an dieser Stelle gesagt werden, dass man nicht auf geladene Images zeichnen kann. Die Methode getGraphics liefert in diesem Fall null zurück. Dies ist eine alte Einschränkung von Java, die erst in neueren Versionen behoben werden soll. Umgekehrt kann man sich von einem Image-Puffer keine Pixel besorgen, nur von geladenen Images.

Man beachte, dass das Laden des Images eine Weile dauern kann, insbesondere, wenn das Laden über das Netz geschieht. Überdies wird das Laden irgendwann gestartet, spätestens dann, wenn das Image wirklich benötigt wird und geschieht asynchron in einem separaten Prozess. Es ist daher bisweilen ratsam, das Laden zu überwachen und bis zum kompletten Bild einen einfarbigen Ersatz zu zeigen. Insbesondere ist dies dann erforderlich, wenn die Größe des Bildes benötigt wird.

Dazu verwendet man einen MediaTracker. Das Bild wird dann dem Tracker zur Überwachung des Ladevorganges anvertraut:

    MediaTracker T=new MediaTracker(this);
    T.addImage(Back,0);
    try
    {   T.waitForAll();
    }
    catch (InterruptedException e) {}

Man kann jedem Image, das der Tracker überwacht, einen Index mitgeben. Die Methode waitForAll() wartet, bis alle Images geladen sind. Alternativ kann man auch mit checkID(0) nachfragen, ob das Bild 0 schon geladen ist. Man sollte in jedem Fall mit isErrorID(0) überprüfen, ob das Bild korrekt geladen wurde.

Images Erzeugen

Der Ball im obigen Beispiel ist ein teilweise transparentes Image, dessen Bild Punkt für Punkt gerendert wurde. Dazu dient die Klasse MemoryImageSource aus dem Paket java.awt.image. Ein Objekt dieser Klasse kann an createImage übergeben werden, um das Image zu erzeugen (in diesem Fall das Image Ball). Ein MemoryImageSource wird aus einzelnen Pixeln erzeugt, die in einem int-Array gespeichert sind. Jedes Pixel besteht aus vier Bytes , die die Transparenz und die Intensität der drei Grundfarben (RGB) für das Pixel angeben (jeweils 0 bis 255, wobei 255 für "nicht transparent" steht). Man setzt das Pixel am besten mit Hilfe der Schiebeoperationen und eines bitweisen Oder (|) zusammen.

Interessant ist noch, wie die Farbanteile für die Pixel bestimmt werden. Dazu wird zunächst die zweidimensionale Aufsichtkoordinate (i,j) der Kugel in die dreidimensionale Oberflächenkoordinate (x,y,z) umgerechnet. Diese wird mit (1,1,1) skalar multipliziert. Der enstandene Wert dient, richtig skaliert, als Helligkeit an dieser Stelle. Dieser kleine mathematische Trick simuliert überzeugend einen Lichteinfall von links oben vorne.

Details kann man der Funktion createBall() im folgenden Listing entnehmen.

import java.awt.*;
import java.applet.*;
import java.awt.image.*;

public class BallApplet extends Applet implements Runnable
{   Thread A;
    boolean Stop;
    
    Image I; // Puffer
    Image Back; // Hintergrund
    Image Ball; // Ball
    Graphics G; // Graphics fuer den Puffer
    
    int X,Y,DX,DY, // Ballkoordinaten von links unten gesehen
        W,H; // Hoehe und Weite des Applets
    final int S=20; // Kugelgroesse als Konstante
        
    public void init ()
    {   W=size().width; H=size().height;
            // Bestimme Hoehe und Weite
        I=createImage(W,H); // Erzeuge Puffer
        G=I.getGraphics();
        Back=getImage(getCodeBase(),"ziegel.gif");
        MediaTracker T=new MediaTracker(this);
        T.addImage(Back,0);
        try
        {   T.waitForAll();
        }
        catch (InterruptedException e) {}
        Ball=createBall(S);
        paintBall(G);
    }
    
    Image createBall (int S)
    // Ein gerenderter Ball mit Radius S
    {   int i,j,k;
        int P[]=new int[4*(S+1)*(S+1)]; // fuer die Pixel
        k=0;
        double red,green,blue,light,x,y,z;
        
        for (i=-S; i<=S; i++)
            for (j=-S; j<=S; j++)
            {   // Berechne x,y,z-Koordinate auf der Balloberflaeche
                x=-(double)i/S;
                y=(double)j/S;
                z=1-x*x-y*y;
                
                if (z<=0) P[k]=0; 
                    // außerhalb des Balls! Transparenter Punkt.
                else
                {   z=Math.sqrt(z);
                    light=(x+y+z)/Math.sqrt(3)*0.4;
                        // Vectorprodukt mit 1,1,1
                    red=0.6*(1+light); // Rotanteil
                    green=0.2*(1+light); // Gruenanteil
                    blue=0; // Blauanteil
                    P[k]=255<<24| // nicht transparent!
                        (int)(red*255)<<16|
                        (int)(green*255)<<8|
                        (int)(blue*255);
                    // P[k] setzt sich in vier Bytes aus den
                    // Farben und der Transparenz zusammen
                }
                k++;
            }
        return createImage( // Erzeuge das Image
            new MemoryImageSource(2*S+1,2*S+1,ColorModel.getRGBdefault(),
                            P,0,2*S+1));
    }

    public void paintBall (Graphics g)
    // Hintergrund und Kugel neu Zeichnen
    {   g.drawImage(Back,0,0,this);
        g.drawImage(Ball,X-S,H-Y-S,this);
    }
    
    public void paint (Graphics g)
    // Kopiere einfach das Image auf den Schirm
    {   g.drawImage(I,0,0,this);
    }
    
    public void update (Graphics g)
    // Hintergrund wird selbst neu gezeichnet
    // normalerweise erledigt dies update()
    {   paint(g);
    }
    
    public void start ()
    // Kugel startet in der Mitte unten
    {   X=W/2; Y=S; DX=1; DY=15;
        A=new Thread(this);
        A.start();
        Stop=false;
    }
    
    public void stop ()
    // Stoppe Thread
    {   Stop=true;
    }
    
    public void run ()
    // Die Schleife
    {   while (!Stop)
        {   // Ball verschieben:
            if (X+DX<S) DX=-DX;
            if (X>W-S-1) DX=-DX;
            if (Y+DY<S) { DY=15; }
            X+=DX; Y+=DY; DY--;
            // Ball neu zeichnen und auf Schirm kopieren
            paintBall(G);
            Graphics g=getGraphics();
            paint(g);
            g.dispose();
            // Etwas verzögern
            try { A.sleep(50); } catch (Exception e) {}
        }
    }
}

Natürlich ist es auch möglich, ein Image in RGB-Werte zu zerlegen. Wie schon gesagt, funktioniert das derzeit nur mit geladenen Images, nicht mit Puffern. Der folgende Code etwa dreht ein Image um 90 Grad.

    static Image turn (Image I, Component c)
    {   int W=I.getWidth(c),H=I.getHeight(c);
        int P[]=new int [W*H];
        PixelGrabber pg=new PixelGrabber(I,0,0,W,H,P,0,W);
        try { pg.grabPixels(); } catch (Exception e) { return I; }
        int Q[]=new int[W*H];
        int i,j;
        for (i=0; i<H; i++)
            for (j=0; j<W; j++)
            {   Q[i*W+j]=P[i*W+j];                  
            }
        return Toolkit.getDefaultToolkit().createImage(
            new MemoryImageSource(H,W,ColorModel.getRGBdefault(),
                            Q,0,H));
    }

Natürlich sind auch andere Manipulationen denkbar. So könnte man Teile des Bildes transparent machen (abhängig etwa von einer Farbe), oder regelrechte Bildverarbeitung (Anti-Aliasing, Kontrastreduzierung etc.) betreiben.

Übungsaufgaben

  1. Modifizieren Sie den springenden Ball so, dass mehrere Bälle auf dem Schirm springen. Kollisionen brauchen nicht berücksichtigt zu werden.
  2. Modifizieren Sie die Uhr so, dass sie nur die Zeit anzeigt. Dies soll in der Form hh:mm:ss geschehen. Verwenden Sie einfach die Methoden getHours(), getMinutes() und getSeconds() von Date.
  3. Schreiben Sie 2 um, so dass eine einfache Uhr mit Zeigern angezeigt wird.
  4. Erzeugen Sie ein Runnable-Objekt mit einer int-Variablen n. Lassen Sie zwei Threads diese Variable hochzählen (mit Ausgabe auf dem Schirm), bis sie einen Wert überschreitet. Beobachten Sie, ob die Ausgabe chonologisch richtig ist. Gibt es vielleicht sogar doppelte Ausgaben? Sind solche doppelte Ausgaben denkbar?
  5. Schreiben Sie ein Applet, dass einen Text von links nach rechts laufen läßt. Wählen Sie einen Font mit 2/3 der Applethöhe und eine Applethöhe von ca. 50. Die Animation soll ähnlich wie bei den Bällen ablaufen und gepuffert sein.

Lösungen.

Aufgaben ohne Lösung

  1. Testen Sie in Aufgabe 4. auf Doubletten, indem Sie ein Array von boolean anlegen, dessen n-te Stelle auf wahr gesetzt wird, wenn die Zahl n getroffen wurde.
  2. Fügen Sie in die Klasse eine Methode inc() ein, die den alten Wert von n zurückgibt und n erhöht. Versehen Sie diese Methode mit dem Schlüsselwort synchronized (gleich hinter dem Schlüsselwort public). Treten bei Verwendung dieser Methode immer noch Doubletten auf?

Zurück zum Java-Kurs