Apleturi Java (III): Fire de executie
La Multi Ani si anul 2000 sa fie pentru toata lumea mai bun!
In episodul de luna aceasta
vom studia ceva specific limbajului Java, si anume crearea si folosirea firelor
de executie.
Apletul deseneaza niste cercuri rosii in
miscare la fiecare click pe suprafata lui. Care este legatura lui cu firele
de executie? Raspunsul este urmatorul: la fiecare click pe suprafata apletului
se genereaza un nou fir de executie. Dar haideti sa aflam mai multe despre firele
de executie:
Un fir de executie (thread) executa o serie de instructiuni. Un fir poate trai pe tot parcursul apletului sau doar cateva milisecunde. De asemenea un fir poate da nastere altui fir sau poate omori un fir existent.
Chiar si in apleturile din
episoadele trecute exista unul sau mai multe fire de executie "ascunse",
create de browser. Atunci cand are loc un eveniment de gen click de mouse, sau
cand se apeleaza o functie ca repaint(),
este executat un anumit fir ascuns. Firele ascunse sunt create si executate
automat de limbaj si sunt transparente pentru programator. Daca rulati orice
aplet in modul debugger al mediului de dezvoltare Java pe care il folositi,
veti vedea cateva fire de executie create automat.
Exista si situatii in care programatorul are nevoie sa creeze el insusi propriile
fire de executie. Trebuie stiut ca in timp ce un fir ruleaza in cadrul apletului,
respectivul fir nu poate face altceva. Daca apletul tine un fir prea mult, poate
bloca browserul. De aceea daca apletul are nevoie sa ruleze o metoda a sa pentru
mai mult timp, este necesar ca un nou fir sa fie creat. Iata care sunt situatiile
cele mai frecvent intalnite in care este nevoie de un nou fir de executie:
- Timp indelungat pentru
initializare (functia init()
a apletului). De exemplu, apletul are nevoie de ceva timp pentru
a incarca o imagine. Un fir este folosit pentru ca sistemul sa poata primi alte
evenimente.
- Task-uri repetitive sau
care trebuie sa dureze o anumita perioada de timp. De exemplu pentru animatii.
La fiecare cateva milisecunde este afisat un nou frame. Firul deseneaza frame-ul,
sta o perioada de timp, apoi reia procesul.
- Evenimente asincrone. De
exemplu un click de mouse. Daca utilizatorul apasa butonul mouse-ului, un nou
fir este creat pentru a reda un nou frame dintr-o imagine.
- Mai multe task-uri. Firele
sunt folosite pentru a face mai mult de un lucru o data. De exemplu un fir controleaza
o animatie in timp ce altul face un calcul.
Pentru a crea si controla
firele de executie se folosesc clasa Thread
si interfata Runnable din pachetul
java.lang.
Exista doua metode pentru a crea un fir:
1. Implementand
interfata Runnable. In acest
caz nu este nevoie sa creati o noua clasa. De exemplu daca vreti sa creati un
fir chiar in cadrul apletului, faceti apletul sa implementeze aceasta interfata
(amintiti-va ca o clasa nu poate mosteni mai multe clase, deci nu poate mosteni
si clasa Applet, si clasa Thread).
2. Mostenind clasa Thread.
O clasa nou creata mosteneste clasa Thread
si firul va incepe sa ruleze conform functiei run()
(veti vedea imediat in exemplu).
Nu este aproape nici o diferenta intre cele doua metode. In general metoda a doua se foloseste atunci cand doriti ca firul sa ruleze din clasa-aplet. Daca firul va rula in propria lui clasa, va fi mai convenabila prima metoda.
Dar pentru a intelege mai bine, sa vedem ce contin clasa Thread si interfata Runnable (pentru aceasta cititi si fisierele Thread.java si Runnable.java din pachetul java.lang), si pe urma voi prezenta exemple concrete.
Interfata Runnable
are o singura metoda: public abstract
void run(). Ea trebuie definita obligatoriu de clasa care implementeaza
interfata Runnable. Functia run()
va contine codul care va fi executat de firul de executie respectiv.
Clasa Thread implementeaza interfata
Runnable si contine (printre
altele) urmatoarele metode:
- mai multi constructori;
- start() - face ca firul de
executie sa inceapa sa ruleze. In acest moment Java Virtual Machine va apela
functia run();
- functia run() care suprascrie
functia run() a interfetei Runnable;
acesta functie trebuie suprascrisa cu codul ce se doreste a fi executat de firul
respectiv;
- stop() - forteaza oprirea firului
de executie.
Crearea si controlul unui
fir de executie folosind prima metoda se face parcurgand urmatorii pasi:
1. se declara clasa-aplet ca implementand interfata Runnable;
2. se declara obiectul de tip Thread;
3. se scrie functia init() in
care se construieste firul;
4. se scrie functia de pornire a firului;
5. se scrie functia de oprire a firului;
6. se suprascrie metoda run();
Exemplul urmator creeaza un fir pentru rularea unui contor care numara de la
1 la 100:
import java.awt.*;
import java.applet.*;
public class RunnableExample extends Applet
implements Runnable { //pasul
1
Thread thread; //pasul
2
int count; //variabila
contor
String displayStr; //sirul
care va fi afisat in functia paint
Font font;
public void
init() { //pasul 3
font = new Font("TimesRoman", Font.PLAIN, 72);
setFont(font);
count = 0;
displayStr = "";
thread = new Thread(this); //se
creeaza firul apeland un constructor
}
public void start() { //pasul
4
thread.start(); //se
porneste firul
}
public void stop() { //pasul
5
thread.stop();
}
public void run() { //pasul
6
while (count < 100) { //executa
firul atata vreme cat count<100
++count;
displayStr = String.valueOf(count); //transforma
variabila int in String
repaint(); //redeseneaza
try {
thread.sleep(100); //firul sta 100
milisecunde, dupa care reia corpul lui while
}
catch (InterruptedException
e) {
}
}
}
public void paint(Graphics g) {
g.drawString(displayStr,
50, 130);
}
}
(Nu va bateti capul cu secventa
try - catch de mai sus, va voi
explica mai pe larg intr-un episod in care voi trata exceptiile in Java.)
Nota. Nu este obligatoriu ca metoda stop()
(pasul 5) sau linia thread.stop()
sa existe, Java va opri automat firul atunci cand nu va mai exista nici o referinta
catre el. Totusi este recomandat sa o scrieti. Apelul thread.start()
este obligatoriu, fara el firul nu va porni,adica nu se va executa functia run().
Insa nu este obligatoriu ca el sa fie in functia public
void start().
Putea sa fie in alta parte unde se stie ca se executa inaintea apelului
celorlalte functii care apeleaza firul, de exemplu putea fi ultima linie a functiei
public void init().
Daca ati urmarit cu atentie ce am explicat pana acum, acest exemplu ar trebui
sa vi se para chiar usor. Pentru intrebari, scrieti la mirela.andronescu@er.dntis.ro.
Haideti acum sa vedem cum se face acelasi lucru folosind cea de a doua metoda.
Vom parcurge urmatorii pasi:
1. cream clasa ThreadExample
care mosteneste clasa Applet;
2. scriem functia init() care va crea un fir prin apelul constructorului clasei
MyThread;
3. cream clasa MyThread care
mosteneste clasa Thread;
4. declaram campurile clasei MyThread,
printre care obligatoriu trebuie sa fie un obiect de tipul ThreadExample
sau Applet;
5. cream constructorul clasei MyThread
care are ca parametru o clasa-aplet;
6. in acest constructor apelam functia start() pentru a porni firul de executie;
7. suprascriem functia run().
Iata mai jos codul acestui exemplu:
import java.awt.*;
import java.applet.*;
public class ThreadExample extends Applet { //pasul
1
String displayStr;
Font font;
public void init() {
font = new Font("TimesRoman", Font.PLAIN, 72);
setFont(font);
displayStr = "";
new MyThread(this); //pasul
2
}
public void paint(Graphics g) {
g.drawString(displayStr, 50, 150);
}
}
class MyThread extends Thread { //pasul
3
ThreadExample applet; //pasul
4
int count;
MyThread(ThreadExample applet) { //pasul
5
this.applet = applet;
start(); //pasul
6
}
public void run() {
//pasul 7
count = 0;
while (count < 100) {
++count;
applet.displayStr =
String.valueOf(count);
applet.repaint();
try {
sleep(100);
}
catch (InterruptedException
e) {
}
}
}
}
Nota1: Scrieti intreaga sursa de mai sus intr-un singur fisier. Puteti
crea doua fisiere, cate unul pentru fiecare clasa, dar pentru aceasta trebuie
sa mai adaugati in fisierul-aplet linia: import
MyThread;, trebuie sa declarati clasa MyThread
ca publica si trebuie sa va asigurati ca ambele fisiere sunt in directorul
CLASSES.
Nota2: Se mai obisnuieste ca in clasa ThreadExample
sa existe linia MyThread thread;
(declararea unei variabile de tip MyThread).
Astfel, apelul constructorului (pasul 2) va fi thread=new
MyThread(this);. De asemenea metodele public
void start() si public void stop()
pot exista (la fel ca la prima metoda), dar nu este obligatoriu. Va recomand
sa rulati totusi codul si intr-un fel si in altul, pentru a intelege mai bine
functionarea firelor de executie in Java.
Este de remarcat urmatorul lucru: constructorul clasei MyThread
are ca argument un obiect de tip ThreadExample,
care este apletul de unde firul de executie va fi rulat. Acest lucru este necesar
pentru ca firul sa poata comunica cu apletul.
Apoi, priviti functia run().
Clasa-fir acceseaza acum obiectul de tip aplet pentru a-i transmite noua valoare
a sirului si pentru a apela functia repaint().
In prima versiune, firul era asociat cu clasa-aplet, altfel spus facea parte
din ea, iar acum este un proces complet separat.
Acum aveti destule cunostinte
despre firele de executie pentru a putea intelege sursa apletului pe care l-ati
vazut ruland la inceputul articolului. Vom folosi a doua modalitate deoarece,
asa cum va spuneam, la fiecare click se va genera un nou fir de executie, deci
la un moment dat pot rula mai multe fire de executie, depinzand de cate click-uri
au fost date in ultimul timp. Vom parcurge urmatorii pasi:
1. cream clasa OvalThreadExample
care mosteneste clasa Applet;
2. declaram variabila Image offImage;
3. declaram variabila Graphics offGraphics;
Vom folosi aceste variabile pentru a evita licarirea ecranului in timpul animatiei.
O animatie se realizeaza prin redesenarea cadrelor. Daca acest lucru se face
direct pe ecran, se va produce o licarire a ecranului (puteti incerca!). Acest
lucru poate fi inlaturat prin tehnica descrisa in urmatorii pasi:
4. se creeaza o imagine (careia i se mai spune buffer si care la noi este variabila
offImage) care contine imaginea
curenta a apletului (sau numai o parte a suprafetei sale).
5. se ia apoi contextul grafic al acestei imagini (in variabila offGraphics),
care se adapteaza pentru fiecare cadru in parte;
6. se apeleaza in metoda paint()
functia drawImage (care este
din clasa Graphics la fel ca
si drawLine, drawOval
etc.) avand ca parametru imaginea offImage.
7. se scrie functia update(Graphics
g). Aceasta functie este apelata automat la executia functiei repaint().
(repaint() apeleaza automat update
si aceasta apeleaza paint. Pentru
mai multe detalii cititi fisierul Component.java).
Functia update va fi deci apelata
cu noul context grafic, adica offGraphics,
dupa ce acesta a fost modificat.
Pasii 3-7 sunt folositi deci pentru a inlatura licarirea imaginii.
8. se apeleaza functia public boolean
mouseDown(Event e, int x, int y). Aceasta functie face urmatorul lucru:
atunci cand se constata o apasare a butonului mouse-ului, se executa corpul
acestei functii. x si y
sunt coordonatele mouse-ului unde a avut loc evenimentul. Functia returneaza
true daca s-a inregistrat evenimentul
respectiv. Alte functii care reactioneaza la anumite evenimente mai sunt: mouseDrag,
mouseEnter, mouseExit,
mouseUp, cu aceiasi parametri.
Ele sunt metode tot ale clasei Component.
(Pentru exersarea acestor metode va propun urmatorul exercitiu: modificati apletul
din episodul trecut: Triunghiul lui Sierpinski astfel incat cand utilizatorul
da click undeva pe suprafata apletului, varful cel mai apropiat sa se deplaseze
in acel punct. Astept sa-mi trimiteti prin mail raspunsul! :))
9. in corpul metodei de la pasul 8 se scrie deci codul ce se va executa atunci
cand se da un click, si anume se creeaza un nou fir, deci se apeleaza constructorul
clasei-fir;
10. se creeaza clasa DrawTarget
care mosteneste clasa Thread
si care are acelasi rol ca si clasa MyThread
din exemplul anterior.
11. printre variabilele sale trebuie sa fie si una de tip Applet
sau clasa-aplet;
12. se scrie constructorul clasei DrawTarget,
care ca avea ca parametri clasa Applet,
coordonatele mouse-ului unde s-a dat click si contextul grafic care urmeaza
sa fie modificat;
13. in constructor se apeleaza si functia start()
pentru pornirea firului de executie;
14. se scrie functia run();
15. in functia run() se seteaza
culoarea de foreground pe alb pentru contexul offGraphics;
16. se apeleaza functia setXORMode,
care seteaza modul de desenare al acestui context grafic sa alterneze intre
culoarea setata la pasul 15 (alb) si culoarea setata aici (rosu). (vezi clasa
Graphics);
17. in doua cicluri for realizeaza
desenarea cercurilor in 8 pasi cand se maresc si tot 8 cand se micsoreaza;
18. se apeleaza functia repaint()
a apletului;
19. se opreste executia firului pentru 200 de milisecunde.
import java.applet.Applet;
import java.awt.*;
public class OvalThreadExample extends Applet { //pasul
1
Image offImage; //pasul 2
Graphics offGraphics; //pasul
3
public void init() {
offImage=createImage(size().width,size().height); //pasul
4;
offGraphics=offImage.getGraphics(); //pasul
5
}
public void paint(Graphics g) {
g.drawImage(offImage,0,0,null); //pasul
6
}
public void update(Graphics g) { //pasul
7
paint(g);
}
public boolean mouseDown(Event e, int x, int y) { //pasul
8
new DrawTarget(this,x,y,offGraphics); //pasul
9
return true;
}
}
class DrawTarget extends Thread { //pasul
10
int xPos,yPos; //pozitia unde
se vor desena cercurile
Applet applet; //pasul 11
Graphics offGraphics;
public DrawTarget(Applet a, int x, int y, Graphics g) { //pasul
12
xPos=x;
yPos=y;
applet=a;
offGraphics=g;
start(); //pasul 13
}
public void run() { //pasul
14
int i;
int r;
offGraphics.setColor(Color.white); //pasul
15
offGraphics.setXORMode(Color.red); //pasul
16
for(r=0,i=10;i>-20;i-=20) //
i=(10,-10) //pasul 17
for(r+=i;(r<90)&&(r>0);r+=i) { //
r=(10,20...80,80,70...10)
offGraphics.fillOval(xPos-r,yPos-r,2*r,2*r);
applet.repaint(); //pasul
18
try {
sleep(200); //pasul
19
}
catch (InterruptedException e) {
}
}
}
}
Ei, de data aceasta nu a
mai fost asa usor, nu-i asa? (m-am gandit ca ar fi bine sa incepeti anul cu
mult avant, ca sa o tineti tot asa pana la sfarsitul lui... glumesc :)))
Asupra tehnicii de
buffering voi reveni cand voi mai prezenta apleturi cu animatii, pentru ca vom
folosi acelasi procedeu, iar asupra firelor de executie voi reveni luna viitoare,
cand vom intra mai amanuntit in sincronizarea firelor de executie.
Pana atunci va urez spor la invatat si succese multe si astept sa-mi scrieti
raspunsuri la probleme, intrebari, propuneri si nu in ultimul rand critici la
adresa mirela.andronescu@er.dntis.ro.