Joc de Biliard cu Masa Customizabila
Joc de biliard cu masă customizabilă
Cuprins
Introducere
1. Principiile programării orientate obiect
1.1. Programarea orientată pe obiecte
1.2. Clasa și comunicarea între obiecte
1.3. Principii de bază ale programării orientate pe obiecte
2. Facilitățile limbajului Java
2.1. Platforma și limbajul Java
2.2. Interfețe grafice – JSWING
2.3. Evenimente și ascultarea evenimentelor
2.4. Tipuri generice și colecții
2.4.1. Necesitatea tipurilor generice
2.4.2. Metode generice și restricționarea parametrilor tip
2.4.3. Colecții
2.5. Excepții
2.6. Fire de execuție – Multithreading
2.7. Java 2D
2.7.1. Desenare în Java 2D
2.7.2. Transfornări afine
2.8. Procesarea imaginilor
3. Implementarea aplicației
3.1. Schema aplicației
3.2. Meniul principal și evenimente
3.3. Parsarea fișierelor
3.4. Rezolvarea coliziunilor
3.4.1. Găsirea punctelor de coliziune
3.4.2. Tratarea problemei pentru mai multe bile
4. Facilitățile aplicației
4.1. Jocul
4.2. Editorul de table de joc
Concluzie
Bibliografie
Lista abrevierilor
GUI = Interfața grafică (în engleză: Graphical User Interface sau GUI) este numit sistemul de afișaj grafic-vizual pe un ecran, situat funcțional între utilizator și dispozitive electronice precum computere, dispozitive personale de tip hand-held (playere MP3, playere media portabile, dispozitive de jucat), aparate electrocasnice și unele echipamente de birou.
AWT = Reprezintă colecția de unelte al limbajului Java ce execută operațiile de realiare a ferestrelor, graficii și GUI+ului independent de platofrmă.
JVM = Java virtual machine sau mașina virtuală Java ce se ocupă cu translatarea codului scris în cod de nivel jos
JRE = Java Runtime Environment. Este o implementare a ma;inii virtuale Java ce execută programele.
IDE = Un IDE este un mediu de dezvoltare integrat. Acesta este o aplicație ce oferă facilități ușor de înțeles pentru programatori cu scopul deyvoltării programelor.
JIT = compilator just in time. Este cunoscut ca translatare dinamică și repreyintă o metodă de a spori performanța rulării programelor bayate pe cod de biți.
API = interfața de programare a aplicației repreyintă un protocol menit să fie folosit ca o interfață de către componentele software pentru a comunica între ele.
MVC = model vie controller. Reprezintă un model de arhirtectură software ce separă repreyentarea informației de interacșiunea utiliyatorului cu aceasta.
Introducere
Aplicația “Make your own Pool” este un program realizat în limbajul de programare Java care oferă utilizatorului posibilitatea de a pregăti amplasamentul bilelor, caracteristicile acestora, precum și modul în care va arăta masa de joc folosind imagini proprii, urmând apoi ca acesta să precizeze în mare detaliu zonele de impact și amplasamentul găurilor.
Rolul unei astfel de aplicații este acela de a extinde perioada de interes a unui utilizator față de aplicație, punându-i la dispoziție unelte puternice și ușor de folosit pentru a crea ceva unic în speranța că va mainfesta dorința de a împărtăși experiența cu alți indivizi. Ideea de a crea conținut nou și elemente noi pentru a putea fi refolosite in cadrul aplicației nu este una noua, însă este o filozofie de design în cadrul jocurilor video întâlnită din ce în ce mai des în momentul de față. Beneficiile unei astfel de abordări sunt evidente și imediat înțelese, ăntrucât asemenea aplicații reușesc să sporească interes și popularitatea de la sine, fără necesitatea unui efort suplimentar din partea creatorului programului.
Acesta este de fapt scopul și direcția luată și de această aplicație. Jocul și-a propus să ofere posibilitatea de a crea o masă de joc editabilă în mare detaliu și acest lucru s-a reușit prin editorul de masă de joc inclus în program, având facilitatea de a exporta și importa proiecte, precum și caracteristici individuale ale pozelor în cadrul unor extensii specifice (*.tbl și *.obs). De asemenea s-a dorit găsirea unui algoritm care să permită simularea mișcării în timp al bilelor și detectarea coliziunilor fără consum prea mare de resurse și timp in cazul multor bile aflate pe masa de joc, lucru ce a fost realizat mai eficient prin mutarea bilelor direct către locul coliziunii pentru efectuarea verificărilor de coliziune. Toate aceste hotărâri legate de logica aplicației și design au fost duse la îndeplinire, iar rezultatul este un program care oferă mai mult decât se poate observa la o primă vedere.
Această lucrare este structurată pe capitole, începând cu o scurtă descriere a conceptelor din spatele programării orientate obiect, continuând imediat cu prezentarea facilităților limbajului Java, și apoi cu descrierea funcționalităților speciale ale aplicației, alături de o descriere detaliata a algoritmilor implementați pentru încărcarea și parsarea fișierelor de proiect necesare rulării jocului, precum și a metodelor matematice utilizate în scopul simulării sistemului de interacțiune al bilelor. Apoi este prezentată o scurtă descriere a facilităților programului și încheind apoi cu o concluzie asupra temei abordate.
1. Principiile programării orientate obiect
Programarea Orientată pe Obiecte
Programarea Orientata Obiect (POO) este o paradigmă de programare care utilizează obiecte și interacțiuni între acestea pentru a modela arhitectura unui program.
Până în anii 60, paradigma cea mai utilizată era cea a programării structurate. Această paradigmă consta în utilizarea funcțiilor și procedurilor pentru a realiza un program care să elimine necesitatea implementării apelurilor GOTO. În această perioadă, pe măsură ce programele deveneau din ce în ce mai mari, randamentul programatorilor scădea, iar timpul necesar realizării și vânzării aplicațiilor creștea din ce în ce mai mult.
În scopul adresării acestei probleme, programarea orientată pe obiecte propune utilizarea conceptului de obiect. Obiectele își propun să modeleze, atât cât este posibil, obiecte din lumea reală. Acestea sunt instanțe ale unui tip de date numit clasă. Relația dintre clasă și obiect-instanță poate fi exemplificat prin relația imaginară între conceptul de masă și particularizarea acesteia prin atașarea unei caracteristici precum culoarea, dându-i astfel identitate.
Clasa este conceptul ce realizează o grupare a datelor și a unităților de prelucrare a acestora într-un modul, unindu-le într-o singură entitate. Aceasta, excluzând faptul că abstractizează foarte mult analiza problemei, are proprietatea de generalitate, ea desemnând o mulțime de obiecte care categorizează o serie de proprietăți.
In implementarea efectivă a programului nu se lucrează cu entități abstracte, precum clasele ci se lucrează cu obiecte care sunt "instanțe" ale claselor. Apare însă problema trecerii de la o structură generală la una particulară, mai precis însemnătatea procesului de instanțiere.
Instanțierea, sau acțiunea trecerii de la conceptul de clasă la obiect, înseamnă atribuirea unor proprietăți specifice clasei astfel încât aceasta să indice un obiect anume care se diferențiază de toate celelalte obiecte din cadrul acesteia printr-o serie de atribute.
1.2. Clasa și comunicarea între obiecte
O dată identificate clasele, ele nu rămân izolate ci vor fi grupate în module, pachete, care vor stabili legături între ele. Aceste legături reflectă relațiile ce se stabilesc între clasele și obiectele problemei.
În scopul exemplificării relaței între clase, vom considera clasa ”Fruct” la care se poate adăuga o nouă clasă: "Raft", care va deține următoarele proprietăți: "număr" și "conținut". Vom instanția clasa "Raft" atribuind atributelor "număr", valoarea "1" și atributului "conținut", valoarea "fructe". Aceasta înseamnă că am creat un obiect al clasei "Raft", raft pe care îl putem considera "primul din magazin conținând fructe". Bineînțeles că acest raft va fi în relație cu clasa "Fruct" pe care am exemplificat-o anterior și va el conține obiecte de tip "Fruct".
Relația pe care am enunțat-o mai sus se mai numește și relație de compunere, o relație fundamentală în programarea orientată pe obiecte, iar clasa "Raft" se numește clasă compusă (sau Agregate), fiindcă în componența ei intră alte clase, în cazul nostru "Fruct", cum se observă în diagrama de mai jos (Figura 1.2.1):
Figura 1.2.1
O clasă compusă
Să considerăm în continuare faptul că în magazin există și fructe care trebuie păstrate la temperaturi joase. Pentru acestea vom avea nevoie de un raft special. Acest nou raft (denumit raft frigorific) este în esență tot un raft doar că are în plus proprietatea de răcire. Acest lucru conduce la faptul că se poate reutiliza codul scris pentru "Raft" pentru a implementa o clasă numită "Raft Frigorific". Altfel spus, dorim ca noua noastră clasă să fie o subclasă a clasei "Raft", întrucât ea deține toate proprietățile clasei "Raft" la care se adaugă altele particulare ce o diferențiază. Acest lucru, în mod intuitiv, este numit moștenire.
Moștenirea este o relație care pune în legătură o clasă cu alta sau mai multe, astfel încât clasa rezultată să preia toate atributele clasei sau claselor pe care o sau le moștenește. Clasa care moștenește atributele altei clase se numește "clasă derivată" iar clasa ce deține moștenitori se numește "clasă de bază".
Unul din avantajele moștenirii, ce se observă direct, este acela al reutilizării codului: clasa derivată nu va mai implementa metodele clasei de bază, ci va implementa numai metodele ei specifice. Mai mult, clasa derivată va conține, prin intermediul moștenirii, toate atributele, respectiv date și subrutine sau "metode" ale clasei de bază. Astfel spus și clasa "Raft Frigorific" va avea atributele "număr" si "conținut".
Următoarea diagramă ilustrează moștenirea (Figura 1.2.2):
Moștenirea este de asemenea o relație fundamentală în programarea orientata obiect, ce este recunoscută ca fiind un principiu de bază, alături de Abstractizare, Încapsulare, și Polimorfism.
Se observă faptul că o clasă poate moșteni o altă clasă, ceea ce înseamnă că o entitate preia toate atributele altei entități. Putem avea, de asemenea, mai multe clase care moștenesc o clasă. Fie clasa "A" și clasele "B", "C" și "D", să presupunem că "B", "C", "D" moștenesc pe "A". În acest caz putem remarca faptul că "B, C și D sunt de tip A". Aceasta înseamnă că B, C și D moștenesc toate caracteristicile clasei A, identificându-se cu o clasă de tip A.
Deoarece, prin moștenire, clasele moștenitoare preiau toate atributele clasei moștenite, subrutinele preluate de la sursă, nu sunt întotdeauna potrivite necesității claselor ce o particularizează. Acest lucru înseamnă că această subrutină trebuie să se particularizeze pentru fiecare clasă în parte, reflectând un comportament adecvat cu proprietățile acesteia. Acest lucru mai este numit și polimorfism.
Principiul polimorfismului asigură faptul că fiecare clasă se comportă diferit (polimorfic) la un anumit mesaj trimis către obiect. Se observă faptul că polimorfismul este în strânsă legătură cu moștenirea, fără de care nu ar exista. O clasă preia din clasa de bază doar acele proprietăți care sunt comune și reflectă un comportament adecvat structurii sale. Altfel spus, prin polimorfism se poate realiza o moștenire selectivă.
1.3. Principii de bază ale programării orientate pe obiecte
Abstractizarea – Este posibilitatea ca un program de a ignora anumite aspecte ale informației pe care o manipulează, adică posibilitatea de a se concentra asupra esențialului. Fiecare obiect în sistem are rolul unui “actor” abstract, care poate executa acțiuni, își poate modifica și comunica starea și poate comunica cu alte obiecte din sistem fără a dezvălui cum au fost implementate acele facilitați. Procesele, funcțiile sau metodele pot fi de asemenea abstracte și în acest caz sunt necesare o varietate de tehnici pentru a extinde abstractizarea.
Încapsularea – Denumită și ”ascunderea de informații”, asigură faptul că obiectele nu pot schimba starea internă anumit mesaj trimis către obiect. Se observă faptul că polimorfismul este în strânsă legătură cu moștenirea, fără de care nu ar exista. O clasă preia din clasa de bază doar acele proprietăți care sunt comune și reflectă un comportament adecvat structurii sale. Altfel spus, prin polimorfism se poate realiza o moștenire selectivă.
1.3. Principii de bază ale programării orientate pe obiecte
Abstractizarea – Este posibilitatea ca un program de a ignora anumite aspecte ale informației pe care o manipulează, adică posibilitatea de a se concentra asupra esențialului. Fiecare obiect în sistem are rolul unui “actor” abstract, care poate executa acțiuni, își poate modifica și comunica starea și poate comunica cu alte obiecte din sistem fără a dezvălui cum au fost implementate acele facilitați. Procesele, funcțiile sau metodele pot fi de asemenea abstracte și în acest caz sunt necesare o varietate de tehnici pentru a extinde abstractizarea.
Încapsularea – Denumită și ”ascunderea de informații”, asigură faptul că obiectele nu pot schimba starea internă a altor obiecte în mod direct, ci doar prin metode puse la dispoziție de obiectul respectiv. Doar metodele proprii ale obiectului pot accesa starea acestuia. Fiecare tip de obiect expune o interfață pentru celelalte obiecte care specifică modul în care acele obiecte pot interacționa cu el.
Polimorfismul – Este abilitatea de a procesa obiectele în mod diferit, în funcție de tipul sau de clasa lor. Mai exact, este abilitatea de a redefini metode pentru clase derivate. De exemplu pentru o clasă denumită ”Figura” putem defini o metodă cu numele ”arie”. Dacă alte clase precum ”Cerc”, ”Dreptunghi” și altele vor extinde clasa ”Figura”, acestea pot redefini metoda ”arie”.
Moștenirea – Acest concept organizează și facilitează polimorfismul și încapsularea, permițând definirea și crearea unor clase specializate plecând de la clase generale deja definite, împărtășind și extinzâmd comportamentul lor, fără a fi nevoie de a-l redefini. Aceasta se face de obicei prin gruparea obiectelor în clase și prin definirea de clase ca extinderi ale altora deja existente. Conceptul de moștenire permite construirea unor clase noi, care păstrează caracteristicile și comportarea, deci datele și funcțiile membru, de la una sau mai multe clase definite anterior, numite clase de bază, fiind posibilă redefinirea sau adăugarea unor date și funcții noi. O clasă moștenitoare a uneia sau mai multor clase de bază se numește clasă derivată.
În concluzie obiectele sunt de obicei reprezentări ale obiectelor din viața reală (domeniul problemei), astfel încât programele realizate prin tehnica POO sunt mai ușor de înțeles, de depanat și de extins decât programele procedurale. Această afirmație este adevărată mai ales în cazul proiectelor software complexe și de dimensiuni mari, care se gestionează făcând apel la ingineria programării.
2. Facilitățile limbajului Java
2.1. Platforma și limbajul Java
Java este un limbaj de programare orientat-obiect, puternic tipizat, conceput de către James Gosling la compania Sun Microsystems (în momentul de față filială Oracle) la începutul anilor ‘90, fiind lansat în 1995. Cele mai multe aplicații distribuite sunt scrise în limbajul Java, iar noile evoluții tehnologice permit utilizarea sa și pe dispozitive mobile precum telefon, agendă electronică, palmtop etc. În felul acesta se creează o platformă unică, la nivelul programatorului, ce este utilizată în prezent cu succes și pentru programarea aplicațiilor web.
Java a pornit ca o platformă de programare pentru sisteme embedded. Țelurile principale ale proiectului erau:
Independența de sistem
utilizarea POO.
Limbajul împrumută o mare parte din sintaxă de la limbajele C și C++, dar are un model al obiectelor mai simplu și prezintă mai puține facilități de nivel jos. Un program Java compilat, corect scris, poate fi rulat fără modificări pe orice platformă pe care este instalată o mașină virtuală Java (engleză: Java Virtual Machine, prescurtat JVM). Acest nivel de portabilitate (inexistent pentru limbaje mai vechi cum ar fi C) este posibil deoarece sursele Java sunt compilate într-un format standard numit cod de octeți (engleză: byte-code) care este intermediar între codul mașină (dependent de tipul calculatorului) și codul sursă.
Mașina virtuală Java este mediul în care se execută programele Java. În prezent, există mai mulți furnizori de JVM, printre care Sun, IBM, Bea, Oracle, FSF. În 2006, Sun a anunțat că face disponibilă varianta sa de JVM ca open-source.
Există 3 platforme Java furnizate de Sun Microsystems:
Java Platform, Micro Edition (Java ME) — pentru hardware cu resurse limitate, gen PDA sau telefoane mobile,
Java Platform, Standard Edition (Java SE) — pentru sisteme gen Workstation, este cea care se găsește pe PC-uri,
Java Platform, Enterprise Edition (Java EE) — pentru sisteme de calcul mari, eventual distribuite.
Java este un mediu (platformă) de programare care constă din:
un limbaj de programare (Java) care descrie programatorului ce instrucțiuni sunt valide si ce face fiecare.
un compilator (javac.exe (Windows) / javac (Linux)) care transformă fișierul sursă într-un limbaj intermediar numit bytecode.
o mașină virtuală, Java Virtual Machine (JVM), care permite transformarea codului intermediar în instrucțiuni executabile pe procesorul curent.
o bibliotecă puternică ce răspunde foarte bine nevoilor apărute în practică (class library).
Dezvoltatorul, pentru a lucra cu acest limbaj, trebuie în primul rând să instaleze Java Development Kit (JDK) care constă în principal din:
Java Runtime Environment (JRE), ce conține JVM.
compilator.
Compilatorul este aplicat codului scris și se obțin fișiere conținând bytecode. Aceste fișiere au în Java extensia .class.
Prin urmare ordinea operațiilor este următoarea:
Clasamea.java —-compilare—> Clasamea.class [pe mașina de dezvoltare]
Acest pas corespunde cu invocarea compilatorului astfel:
javac Clasamea.java
Apoi bytecodeul este distribuit utilizatorului. El are instalat JRE, aceasta fiind mașina care interpretează bytecode-ul și îl transformă într-un șir de instrucțiuni pentru procesorul utilizatorului (exista un JRE pentru fiecare procesor și sistem de operare folosit).
Diagrama arată astfel:
| șir de bytecode | ——> | JRE | —-> | șir instrucțiuni native |
Pasul corespunde cu invocarea mașinii virtuale astfel:
java Clasamea
Rezultatul instrucțiunilor native afectează șirul de instrucțiuni bytecode, astfel încât rolul JRE nu este doar o etapă de preprocesare. Nu se aplică o simplă transformare de instrucțiuni ca să se obțină o imagine, după care să se trimită imaginea de executabil nativ la procesor. Mașina virtuala “interpretează” instrucțiuni tot timpul. Bytecodeul este numit interpretat din aceasta cauză.
Cel mai important avantaj în acest șir este că permite obținerea independenței de sistem. Dezvoltatorul are nevoie de compilator funcțional pentru platforma pe care face dezvoltarea, iar utilizatorii, indiferent de platformă (sistem de operare + procesor), pot utiliza programul cât timp au o mașină virtuală Java existentă pentru acea platformă.
Un dezavantaj este viteză scăzută a codului Java din cauza acțiunilor adiționale realizate de JRE tot timpul rulării programului. Pentru a combate acest dezavantaj au apărut compilatoare just-in-time (JIT) (Figura 2.1.1) care permit transformarea bytecodeului în cod executabil la prima rulare a unei secvențe de instrucțiuni bytecode, apoi stocarea acestuia pentru refolosire. Aici mașina virtuală nu este folosită decât o dată. Modelul clasic aici este C#, care folosește acest artificiu încă de la apariția sa. (Și întârzierea cauzată de prima pornire a aplicației .NET respective este vizibilă în multe cazuri). Trebuie reținut faptul că modelul clasic Java este unul format din compilator și interpretor (mașină virtuală).
Conform paradigmei POO, programul este compus din clase. Pentru a avea un loc de început al programului (precum funcția main în limbajul C), trebuie scrisă o clasă precum următoarea:
class HelloWorld {
public static void main(String[] args) {
System.out.println(“Hello world!”);
}
}
Se observă faptul că:
o clasă se definește prin listarea metodelor, cuprinse între acolade, după declararea ei prin cuvântul cheie class urmat de numele clasei.
semnătura funcției de intrare în program este: public static void main(String[] args)
linia System.out.println(“Hello world!”) va afișa mesajul de întâmpinare
Un IDE (engleză: integrated development environment) este un mediu de lucru ce permite dezvoltarea de aplicații folosind anumite limbaje de programare (cele suportate de IDE, adică cele pentru care a fost creat acel IDE). Pentru Java sunt folosite următoarele:
JCreator – gratuit JCreator LE
Eclipse – gratuit
NetBeans – gratuit
BEA Workshop
BlueJ – gratuit
CodeGuide – comercial
DrJava – gratuit
IntelliJ IDEA – gratuit Idea Community Edition
JBuilder – comercial
JDeveloper – comercial, platformă multiplă
KDevelop – gratuit (platformă GNU/Linux, Cygwin)
2.2. Interfețe grafice – JSwing
Programarea folosind interfața grafică Java ("Java GUI") implică folosirea a două pachete: setul de dezvoltare de ferestre abstracte ("AWT") și pachetul Swing, mai nou decat AWT. Componenetele Swing au prefixele J pentru a putea fi distinse de cele din pachetul AWT (e.g. JFrame în loc de Frame). Pentru a include componentele Swing și metodele într-un proiect trebuie importate pachetele java.awt.*, java.awt.event.* si javax.swing.*.
Cadrele ce pot fi afișate sunt containere de nivel înalt, cum ar fi JFrame, JWindows, JDialog și JApplet, ce comunică cu managerul de ferestre al sistemului de operare. Panourile de continut care nu sunt afisabile, sunt containere intermediare precum JPanel, JOptionsPane, JScrollPane și JSplitPane.
Așadar, containerele sunt module sau controale ale interfeței grafice care sunt folosite pentru a îngloba sau a grupa alte module, precum casete de text. casete de validare, butoane radio etc. În .NET, principala interfață grafică, numită Windows Form, deține controale care sunt trase și plasate pe suprafața de control. Orice interfață grafică începe cu o fereastră menită sa afișeze diverse lucruri. În Swing sunt 3 tipuri de ferestre: Applet, Dialog și Frame. Acestea comunică cu managerul de ferestre. În Swing, un obiect cadru (frame) este numit JFrame.
Un JFrame este un container de cel mai înalt nivel (Figura 2.2.1). Acestea mai sunt denumite cadre afișabile (displayable frames). Panourile de conținut, care nu sunt afișabile, sunt containere intermediare precum JPanel, JScrollPane, JLayeredPane, JSplitPane și JTabbedPane, care organizează structura planului interfeței când mai multe controale sunt folosite. Panoul de conținut este locul unde plasăm câmpurile text sau alte module și, deci, pentru a adăuga și afișa controale ale interfeței grafice, trebuie să specificăm că panoul de conținut este cel la care le adaugăm. Așadar, panoul de conținut este la cel mai învalt nivel al unei ierarhii de containere, în care această ierarhie asemănătoare unui arbore, are containerul JFrame de cel mai înalt nivel. Mergând mai in jos pe arborele ierarhiei, vom găsi alte containere de nivel înalt, precum JPanel. care deține componentele.
Aceasta este o porțiune de cod simplă ce poate fi folosită drept punct de pornire pentru crearea unei aplicații în Swing:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*; //notice javax
public class Frame1 extends Jframe {
JPanel pane = new JPanel();
Frame1(){ // the frame constructor method
super("My Simple Frame");
setBounds(100,100,300,100);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container con = this.getContentPane(); // inherit main frame
con.add(pane); // add the panel to frame
setVisible(true); // display this frame
}
public static void main(String args[]) {
new Frame1();
}
}
Majoritatea componentelor Swing sunt lightweight(ușoare), acest lucru însemnând faptul că toate componentele nu sunt dependente de setări native pentru a se afișa. În schimb ele utilizează primitive grafice simplificate pentru a se desena pe sine pe ecran și pot chiar să permită unor porțiuni de ecran să fie transparente. Această abilitate de a crea cmponente ușoare a apărut pentru prima dată în JDK 1.1, totuși majoritatea componentelor AWT nu au profitat de pe urma acestui lucru.
Înainte de aceasta, programatorii Java nu aveau de ales decât să își creeze propriile lor componente. Java a alocat un obiect din sistemul de operare pentru a reprezenta componentele, forțând fieare dintre acesta să se comporte ca și cum ar fi propria fereastră, prin urmare preluând o formă dreptunghulară , solidă. De aceea, aceste componente au primit numele de “heavyweight”, întrucât acestea dețineau frecvent bagaj în pluc la nivel nativ pe care Java nu îl folosea.
Swing folosește arhitectura model-view-controller (MVC) pe post de design fundamental din spatele fiecărui component. Fiecare dintre aceste elemente joacă un rol esențial asupra modului în care componentul se comportă.
Modelul cuprinde starea datelor pentru fiecare component. Sunt diferite modele pentru diferite tipuri de componente. De exemplu, modelul unui component de tip scrollbar poate conține informații legate de poziția curentă al bării akustabile, valori minime și maxime și lățimea bării (relativ la intervalul valorilor). Un meniu, pe de altă parte, poate să conțină pur li simplu o listă de itemi pe care utilizatorul le poate alege. Această informație rămâne la fel indiferent de modul în care componentul este desenat pe ecran. Datele modelului sunt mereu independente de reprezentarea vizuală a componentului.
Vizualizarea(View) se seferă la modul în care seobservă componenta pe ecran. Pentru un exemplu bun legat de modul în care pot diferi vizualizările, este indicată observarea ferestrei aplicașiei pe două platforme cu interfață grafică diferită. Totuși, bara de titlu poate avea un buton de închidere pe partea stângă (precum în MacOS) sau poate avea butonul de închidere în partea dreaptă ca și pe platforma Windows.
Controllerul reprezintăporțiunea interfeșei care dictează modul în care componentele interacționează cu evenimentele. Evenimentele îmbracă multe forme (un click, primirea sau pierderea focusului, un eveniment generat de tastatură). Controllerul decide modul în care fiecare componentă reacționează la eveniment sau dacă reacționează.
Figura 2.2.2 arată un model, o vizualizare (view) și un controller care lucrează împreună pentru a crea un component de tip scrollbar.
Figura 2.2.2
Modelul, vizualizarea și controllerul lucrând împreună
Scrollbarul folosește informația din model pentru a determina cât de departe se va desena bara și cât de lată trebuie să fie. Trebuie ținut cont de faptul că modelul specifică această informație relativ la minim și maxim. Nu oferă posibilitatea sau lățimea în pixeli a bării, întrucât viewul calulează aceasta. Controllerul este responsabil cu tratarea evenimentelor cauzate de către maus asupra componentelor. Controllerul știe, de exemplu, faptul că tragerea bării este o acșiune legitimă a scrollbarului în cadrul limitelor determinate de puntele de margine.
2.3. Evenimente și ascultarea evenimentelor
API-urile fundamentale ale limbajului java precum Toolkitul abstract de ferestre (Abstract Window Toolkit – AWT) și Swing precum și alte componente ale limbajului depind foarte mult de evenimente. Evenimentele reprezintă o parte integrală a platformei Java. Un eveniment reprezintă o acțiune sau întâmplare, de multe ori generată de către utilizator, la care programul ar putea să răspundă. De exemplu: apăsări de taste, clickuri sau mișcări ale mouseului.
Evenimentele nu sunt specifice platformei Java. Acestea au existat înainte de Java și au fost popularizate de către GUIuri precum cel al Windows-ului. Aceste sisteme intensive din punct de vedere graphic sunt dirijate de către utilizator prin evenimente. În contrast cu modelul programării tradiționale, aplicația controlează firul de execuție al programelor. Acest
fir de execuție este predeterminat de condițiile din cod. (Figura 2.3.1)
Un exemplu de acest tip de aplicație este utilizarea liniei de comandă. Odată invocată ea își efectuează propria funcție depinzând de comenzile date și în concordanță cu logica codificată, independentă de factori externi.
Evenimentele nu aderă la abordarea tradițională întrucât ele au loc în afara controlului programului. Atunci când are loc un eveniment, aplicația este anunțată, cauzând execuția unei porțiuni de cod. De obicei, această notificare implică apelul unei proceduri sau funcții, pasându-i suficiente informații pentru a putea identifica eventul astfel ca aplicația să își realizeze logica rulării. Evenimentele pot avea loc în orice moment și fără o anumită ordine predefinită, astfel că un model de programare diferit este necesar. În acest model, aplicația așteaptă în mod pasiv după evenimente, care în schimb determină ce și când se execută. Mai mult, aplicația trebuie codificată în așa fel încât să nu depindă de ordinea evenimentelor.
Acest model de programare definește aplicația condusă de evenimente (event-driven), precum o aplicație interactivă ce adoptă un GUI. Reacționează doar la acțiunile utilizatorului (evenimente) și la acțiunile pe care aceasta le realizează depinzând de evenimentul curent. Mai mult, utilizatorul poate apăsa butoane, să selecteze opțiuni, să scrie ceva la orice moment și în orice ordine. Evenimentele sunt aleatoare în sensul că acestea se realizează la discreția utilizatorului.
În Java, evenimentele sunt generate de către obiecte. Un eveniment este reprezentat de o subclasă a java.util.EventObjects ce poartă informații legate de evenimente. Există subclase pentru fiecare eveniment. Întotdeauna cel puțin poartă o referință către obiectul care a generat evenimentul (sursa evenimentului), însă fiecare subclasă definește informații adiționale relevante pentru eveniment.
De exemplu, un obiect de tip javax.swing.JButton generează un java.awt.event.ActionEvent atunci când este apăsat. Acesta creează o instanță a clasei java.awt.event.ActionEvent pe care apoi o populează cu informații legate de eveniment, incluzând o referință la el însușii și anunță orice element interesat de existența acestui eveniment.
Mecanismul de notificare al evenimentelor implică apelul la o metodă specifică al oricăror obiecte care doresc să fie anunțate de existența evenimentului precum și pasarea obiectului eveniment ca parametru. Aceste metode sunt denumite mânuitoare de evenimente (event handlers). Pentru ca un obiect să fie anunțat de eveniment, acesta trebuie să implementeze o interfață specifică și să fie înregistrat cu sursa evenimentului. Aceste obiecte sunt denumite ascultătoare de evenimente (event listeners).
Interfața pe care obiectul trebuie să o implementeze depinde de evenimentul pe care îl dorește să îl primească. Există interfețe pentru fiecare tip de eveniment, toate fiind subinterfețe ale interfeței java.util.EventListener. Fiecare interfață definește una sau mai multe metode care sunt apelate de către sursa evenimentului. Fiecare metodă primește un obiect eveniment ca parametru. Trebuie luat în vedere faptul că este destul de comun ca o interfață să definească mai mult decât o singură metodă și câteva evenimente sunt deseori reprezentate folosind aceeași clasă. De exemplu atât apăsările de taste cât și eliberarea tastelor generează evenimente, însă aceeași clasă (java.awt.event.KeyEvent), este folosită pentru a reprezenta ambele. Cu toate că obiectul eveniment întotdeauna identifică care din tipurile de evenimente au avut loc, este mai ușor dacă o metodă diferită este apelată pentru fiecare pentru a nu fi nevoit utilizatorul să le testeze individual.
Prin urmare, corespondentul interfeței pentru evenimentele java.awt.event.KeyEvent, java.awt.event.KeyListener, definește o metodă pentru apăsări de taste keyPressed(), și alta pentru eliberări de taste, keyReleased().
Odată ce un obiect implementează interfața corectă, trebuie să fie înregistrat ca un listener al evenimentului dorit împreună cu sursa evenimentului. Fiecare clasă care generează evenimente oferă metode pentru a adăuga și să elimine listeneruri pentru fiecare tip de eveniment pe care îl generează. Orice număr de listeneruri pot fi înregistrați cu o anumită sursă. Atunci când un eveniment este declanșat, fiecare este apelat pe rând.
Programarea bazată pe evenimente este un element cheie al platformei Java. Acesta oferă căi pentru a dezvolta aplicații flexibile în care utilizatorul determină firul de execuție al programului. Implementarea handlerelor de evenimente în Java este de cele mai multe ori lipsită de dificultăți, însă este important să știm care abordare este cea mai bună pentru fiecare situație.
2.4. Tipuri generice și colecții
Necesitatea tipurilor generice
Scopul introducerii tipurilor generice în limbajul Java a fost cel de a ajuta la descoperirea mai eficientă a bug-urilor. Unele bug-uri sunt mai ușor de depistat, de exemplu cele descoperite la compilare indică imediat neregulile din cod. Tipurile generice ajută la scrierea unui cod mai sigur tocmai prin determinarea unor bug-uri de a fi detectabile la compilare.
Tipurile generice sunt intens folosite de clasele care implementează colecții de date. O astfel de clasă, fără a folosi tipuri generice, ar controla o colecție de obiecte de tip Object. Adăugarea de obiecte de orice tip (mai puțin cele primare) se poate face fără probleme. Probleme apar atunci când avem nevoie să extragem un obiect de un anumit tip dintr-o colecție de date. Obiectul obținut este de tip Object, iar noi trebuie să facem o conversie adecvată explicită. Aici este punctul unde pot apărea problemele.
Folosind o conversie explicită la un tip pe care îl știm noi că este cel corect, nu permite compilatorului în nici un fel să verifice dacă conversia este una permisă. Dacă un anumit obiect extras dintr-o colecție de date nu este de tipul așteptat, atunci încercarea de conversie va genera o excepție în tipul execuției.
Luăm ca exemplu o clasă ce conține un obiect de un anumit tip și face anumite prelucrări asupra acelui obiect. Cum dorim ca funcționalitatea oferită de această clasă să o putem folosi asupra unor obiecte de orice tip, vom folosi o instanță la un obiect de tip Object. Astfel de clase ce implementează funcționalități ce se pot aplica unor obiecte de diverse tipuri sunt des întâlnite și utile (colecții de date, stive, cozi, liste) și este lesne de înțeles că nu dorim să facem câte o nouă implementare pentru fiecare tip de obiect cu care avem nevoie la un moment dat să populăm o astfel de clasă. Pentru simplitate exemplul nostru va fi o clasă ce va conține un singur obiect.
package work;
public class Container {
private Object element;
public void setElement(Object element) {
this.element = element;
}
public Object getElement() {
return element;
}
}
Pentru a testa clasa scrisă vom seta un element și-l vom extrage. Observăm că valoarea întreagă pe care o setăm este o valoare primitivă (nu este o referință la un obiect, deci în mod normal nu poate fi convertită la tipul Object pe care metoda îl așteaptă). O ”convertire” implicită este realizată de către compilator de la primitivul int la clasa ce înglobează un int: Integer. Când extragem elementul obținem un obiect Object pe care îl convertim la tipul Integer (știm că este de acest tip) iar ”convertirea” la tipul primitiv int al obiectului val se face implicit. Aceste convertiri implicite se numesc boxing/unboxing.
package work;
public class ContainerTest {
public static void main(String[] args) {
Container container = new Container();
container.setElement(10);
int val =(Integer) container.getElement();
System.out.println("value : " + val);
}
}
Se observă faptul că punctul vulnerabil al codului este momentul când extragem valoarea și o convertim la acel tip pe care știm noi că l-am folosit când am introdus variabila în Container. Într-o aplicație reală cu un grad mai ridicat de complexitate există posibilitatea să nu mai ”știm” tipul real al obiectului extras sau ce tip de obiect trebuie introdus în container. Să încercăm de exemplu în aplicația noastră de test să introducem un element de tip String:
container.setElement("10");
De asemenea se remarcă faptul că la compilare nu suntem avertizați în nici un fel, iar dacă vom rula din nou aplicația vom obține o excepție:
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at work.ContainerTest.main(ContainerTest.java:10)
Pentru declararea unui tip generic se folosește o variabilă de tip, numită ”T”. Această variabilă de tip poate fi folosită oriunde în cadrul clasei și peste tot va avea înțelesul tipului ce va fi dat ca parametru la instanțierea clasei. Dacă inițial, în exemplul nostru, foloseam tipul Object pentru a putea adapta implementarea la orice tip de obiecte, acum vom folosi tipul T ce va putea înlocui orice tip. Clasa noastră, folosind generice se va transforma astfel:
public class Container<T> {
private T element;
public void setElement(T element) {
this.element = element;
}
public T getElement() {
return element;
}
}
Folosirea clasei se modifică prin adăugarea tipului dorit ca parametru. Folosirea unui tip generic se mai numește și invocare a unui tip parametrizat.
Container<Integer> container = new Container<Integer>();
Aplicația de test a tipului se modifică astfel:
public class ContainerTest {
public static void main(String[] args) {
Container<Integer> container = new Container<Integer>();
container.setElement(10);
int val = container.getElement();
System.out.println("value : " + val);
}
}
La extragerea elementului nu mai este necesară nici o conversie, deoarece metoda returnează acum un obiect de tipul dat ca parametru (Integer) și nu Object. Problema tipului returnat este astfel rezolvată, o încercare de a converti obiectul returnat la un tip incompatibil va genera o eroare de compilare corespunzătoare. Mai observăm de asemenea că la setarea elementului, metoda așteaptă un obiect de tipul Integer, iar dacă încercăm din nou să setăm o variabilă String, vom obține de această dată o eroare la compilare.
Pe lângă depistarea acestor greșeli în timpul compilării, observăm că tipurile generice mai aduc unele avantaje: codul este mai lizibil și mai ușor de înțeles, iar când folosim cod scris de altcineva este ușor de observat ce tipuri putem folosi atunci când lucrăm cu colecții de date sau altfel de implementări comune.
Tehnica tipurilor generice se poate aplica atât în cazul claselor cât și în cazul interfețelor.
Un tip generic poate avea mai mulți parametri de tip. De exemplu clasa din exemplul anterior s-ar declara astfel dacă ar avea 2 parametri de tip: Container<T,U>. Prin convenție, parametrii de tip se numesc începând cu litera T (Type).
2.4.2. Metode generice și restricționarea parametrilor tip
Parametrii tip pot fi aplicați și metodelor (inclusiv constructorilor). Acestea se vor numi metode generice (constructori generici). Tehnica este similară cu cea de la tipurile generice, decât că scopul parametrilor tip se rezumă la metoda (constructorul) în care aceștia au fost declarați.
package work;
public class GenericMethod {
public static <U> void testMethod(U u) {
System.out.println(u.getClass().getName());
}
public static void main(String[] args) {
GenericMethod.<String>testMethod("test");
GenericMethod.testMethod(10);
}
}
În această clasă putem vedea un exemplu de metodă generică. La primul apel observăm ca se specifică tipul dorit, dar acest lucru nu este obligatoriu, deoarece compilatorul poate deduce singur tipul necesar (type inference). La al doilea apel observăm că nu mai specificăm tipul dorit, iar dacă rulăm testul vom vedea că tipul dedus este Integer (clasa în care se înglobează un tip primitiv int).
Până acum am observat că atunci când folosim generice nu există nici un fel de restricționare în ceea ce privește parametrii tip, în sensul că la invocarea tipurilor parametrizate putem folosi orice fel de tip. Pot apărea totuși situații când dorim ca o clasă să se ocupe numai cu un anumit tip de obiecte. Acesta înseamnă că dorim să restricționăm parametrii tip numai la tipuri care extind anumite clase și/sau implementează anumite interfețe.
În acest scop se folosește cuvântul cheie extends, dar sensul său este unul general, adică se referă atât la extinderea de clase cât și la implementarea de interfețe. De exemplu dacă dorim ca exemplul anterior Container să se ocupe numai de valori numerice, atunci folosim pentru restricționare clasa Number, care este clasa de bază pentru toate clasele ce înglobează o valoare primitivă numerică (cum este Integer pentru int). Clasa devine astfel:
package work;
public class Container<T extends Number> {
private T element;
public void setElement(T element) {
this.element = element;
}
public T getElement() {
return element;
}
}
În cadrul aplicației de test putem folosi acum numai tipuri numerice precum în exemplul următor:
package work;
public class ContainerTest {
public static void main(String[] args) {
Container<Integer> intContainer = new Container<Integer>();
intContainer.setElement(10);
int intVal = intContainer.getElement();
System.out.println("int value : " + intVal);
Container<Float> floatContainer = new Container<Float>();
floatContainer.setElement(10.5f);
float floatVal = floatContainer.getElement();
System.out.println("float value : " + floatVal);
}
}
Dacă vom încerca să instanțiem tipul parametrizat cu un tip care nu extinde Number, String de exemplu, vom obține o eroare la compilare:
Exception in thread "main" java.lang.Error: Unresolved compilation problems:
Bound mismatch: The type String is not a valid substitute for the bounded parameter <T extends Number> of the type Container<T>
În cazul claselor obișnuite există o relație între clasele de bază și clasele care le extind. În exemplul anterior am folosit clasa Integer care extinde Number. Putem spune că Integer este un Number și oriunde trebuie să oferim o instanță de tipul Number putem oferi un obiect Integer cu succes. Cu alte cuvinte o subclasă specializează clasa sa de bază dar poate fi folosită la nevoie doar ca o instanță a clasei de bază.
În cazul tipurilor generice situația este diferită. Dacă un obiect Integer este și un obiect Number, un obiect Container<Integer> nu este și un obiect Container<Number>. Deși pare neașteptată, această idee are totuși sens. Dacă un obiect Integer este un obiect Number cu niște proprietăți suplimentare și poate deci înlocui cu succes un obiect Number, un obiect de tip Container<Integer> controlează un obiect Integer în mod special, și nu se poate asuma faptul că poate controla cu succes orice obiect de tip Number.
2.4.3. Colecții
Denumirea de Collection sau Container se referă în general la un simplu obiect care grupează mai multe elemente și care ajută la salvarea, manipularea și transmiterea acelui grup de elemente. Necesitatea lucrului cu colecții de date este des întâlnită în programarea aplicațiilor, fapt pentru care limbajul Java oferă o arhitectură comună de reprezentare și manipulare a colecțiilor de date. Această arhitectură se numește collection framework. Un astfel de framework trebuie să conțină în general următoarele parți:
Interfețe: tipuri abstracte de reprezentare a colecțiilor. Ajută la manipularea colecțiilor în mod independent de implementările lor și formează de regulă ierarhii.
Implementări: implementările concrete ale reprezentărilor abstracte menționate anterior
Algoritmi: implementări ale unor procesări comune aplicate colecțiilor, cum ar fi sortări și căutări. Acești algoritmi sunt polimorfici: pot fi aplicați diferitelor implementări ale reprezentărilor abstracte.
Figura 2.4.1 prezintă ierarhia interfețelor din Frameworkul Collections
Figura 2.4.1
Ierarhia inferențelor din Frameworkul Collection
O observație importantă este faptul că toate interfețele din această ierarhie sunt generice.
Collection înseamnă reprezentarea abstractă a unui grup de obiecte numite elemente.
Dintre operațiile de bază, menționez numărul de elemente (size, isEmpty), existența unui element în colecție (contains), adăugare și eliminare de elemente (add, remove) și oferirea unui iterator.
Se pot folosi două moduri pentru a parcurge valorile unei colecții: folosind instrucțiunea for-each sau folosind un iterator. Instrucțiunea for-each ajută la traversarea valorilor dintr-o colecție sau dintr-un array:
for (Object elem : collection) {
// procesează elementul curent
}
Un iterator este un obiect ce implementează metodele interfeței Iterator: hasNext, next și remove.
Lucrul cu grupuri întregi de valori se realizează folosind metodele: containsAll, addAll, removeAll, retainAll, clear, iar tipurile de colecții (în general) sunt următoarele:
Set: colecție care nu poate conține valori duplicate.
List (secvență): o colecție ordonată ce poate conține și valori duplicate. Suplimentar față de Collection, List oferă unele facilități: acces pozițional (get(int index), set(int index, E element)), căutare (indexOf, lastIndexOf),Iteration, interfață care extinde Interator (hasPrevious, previous, nextIndex, previousIndex, set, add).
Queue: colecție ce reține elementele înainte de a fi procesate.
Operațiile de bază asupra acestora sunt următoarele: add și offer (adaugă un element, în caz de depășire a capacității prima metodă generează o excepție iar a doua returnează false), remove și poll (șterg și returneză din coadă primul element, în caz că nu există nici un element, prima metodă generează o excepție iar a doua returnează null), element și peek (acționează la fel ca precedentele operații fără să șteargă elementul din coadă).
Map este un obiect care mapează chei și valori. Nu poate conține chei duplicate.
Un Map poate fi foolosit și pe post de colecție prin colecțiile pe care le oferă: keySet, values, entrySet. Cheile fiind unice, observăm că prima și ultima colecție sunt Set-uri. Interfața Entry definită în cadrul interfeței Map oferă următoarele metode: getKey, getValue, setValue.
Următorul este un tabel ce prezintă implementările cu scop general oferite în Framework Collection împreună cu tipul de implementare:
Tabel 2.4.1
Implementările oferite de Framework-ul Collection și tipul de implementare
Listele sunt probabil cele mai des folosite colecții folosite în practică în cadrul limbajului Java. O listă este o colecție care spre deosebire de un set, poate conține dubluri ș i spre deosebire de cozi, oferă utilizatorului vizibilitate totală și control asupra comandării elementelor. Interfața Frameworkului Colections asociată este List (Figura 2.4.2)
Sunt trei implementări concrete ale interfeței List din cadrul Frameworkului Collections (Figura 2.4.3), deosebindu-se prin modul și viteza cu care realizează diverse operații definite de interfață și cum se comportă în fața modificărilor concurenteș spre deosebire de Set și Queue (seturi si cozi). Totuși List nu are subinterfețe față de care să specifice diferențe în comportamentul funcțional.
Arrayurile sunt oferite ca o parte din limbajul Java și au o sintaxă foarte ușor de utilizat, însă cel mai mare dezavantaj al lor este faptul că odată creat, nu poate fi redimensionat, iar acest lucru în face mai puțin popular decât implementări ale List care pot fi extensibile la infinit.
LinkedList este de cele mai multe ori mai bine evitat dacă aplicația necesită accesări aleatoare, întrucât lista trebuie să itereze intern pentru a ajunge la poziția dorită, iar metodele de adăugare și eliminare au complexitate liniară în medie. LinkedList are totuși un avantaj de performanță asupra lui ArrayList atunci când se adaugă și se elimină elementeoriunde în afară de coada listei. Pentru LinkedList aceasta necesită timp constant spre deosebire de timpul liniar necesar pentru arrayuri cu implementări neciclice.
CopyOnWriteArrayList reprezintă un set de implementări menite să ofere o implementare thread-safe împreună cu acces rapid. CopyOnWriteArrayList este o implementare a lui List cu același scop propus. Combinarea de siguranță la threaduri împreună cu acces la citire foarte rapid este folositor în unele programe concurente, în special atunci când o colecție de obiecte observatoare au nevoie se primească notificări de evenimente frecvente. Costul este faptul că arrayul care însoțește colecția trebuie tratat ca imobil, așa că o nouă copie este creată de fiecare dată când apar modificări asupra colecției. Acest cost poate fi prea mare dacă schimbările în setul de observatori apar doar foarte rar.
Cea mai comună implementare a List este de fapt ArrayList, aceasta fiind o listă luată împreună cu un vector (Array) .
Implementarea standard a clasei ArrayList stochează elementele din List în locații subsecvente sub formă de arrayuri, cu primul element întotdeauna stocat la indexul 0 în array. Necesită un array cel puțin suficient de mare pentru a conține elementele, împreună cu o cale de aține sub observație numărul de locații ocupate (dimensiunea listei). Dacă un ArrayList a crescut la punctul în care dimensiunea este egala cu capacitatea sa, încercarea de a adăuga un alt element va necesita înlocuirea array-ului însoțitor cu unul mia mare capabil de a reține conținuturile vechi și noul element și cu o margine pentru expansiune ulterioară (implementarea standard folosește de fapt un nou array care are o lungime dublă față de cel vechi). Aceasta conduce la un cost amortizat de O(1).
Performanța obiectelor din clasa ArrayList reflectă performanța pentru ”operații cu acces aleatoriu”: set și get necesită timp constant. Dezvantajul unei astfel de implementări apare la inserarea sau eliminarea elementelor de pe poziții arbitrare, întrucât este posibil să necesite ajustarea pozițiilor altor elemente. Totuși performanța metodelor de adăugare și eliminare sunt mult mai importante pentru liste decât este iterator.remove pentru cozi.
Figura 2.4.4 arată un nou obiect ArrayList după ce au fost adăugate trei elemente folosind următorul cod:
List<Character> charList = new ArrayList<Character>();
Collections.addAll(charList, 'a', 'b', 'c');
Dacă dorim acum să eliminăm elementul de la indexul 1 al unui array, implementarea trebuie să prezerve ordinea elementelor rămase și să asigure că regiunea ocupată ale arrayului este tot la startul indexului 0. Prin urmare elementul la indexul 2 trebuie mutat la indexul 1, cel de la indexul 3 mutat la indexul 2 și așa mai departe. Din moment ce fiecare element trebuie mutat pe rând, complexitatea de timp al acestei operații este proporțională cu dimensiunea listei, chiar dacă din motiv că această operație poate fi în mod obișnuit implementată hardware, factorul constant este scăzut.
Este de menționat și o altă implementare a lui List, Vector. Aceasta este o clasă thread-safe, însă a fost declarat depășit. Vector sincronizează pe fiecare operație individuală. Acest lucru este rareori ce se dorește de la o aplicație. În general se dorește sincronizarea pe o anumită secvență de operații. Sincronizarea individuală a operațiilor nu este doar și mai nesigură (iterarea peste un Vector neecesită eliberarea unui lacăt pentru a evita schimbarea colecției de către altă sursă simultan) , dar este și mai lentă (scoaterea unui lacăt în mod repetat nu este necesară). Pe lângă aceste aspecte, această clasă creează un lacăt fără ca utilizatorul sa ceară acest lucru.
Este considerată o abordare cu multe slăbiciuni spre încercarea de sincronizare în foarte multe situații. O soluție alternativă este apelul Collections.synchronisedList (folosit și în cadrul aplicației). Faptul că vectorul combină atât implementarea de redimensionare al arrayului din Collections cu ideea sincronizării fiecărei operații este prea mult din punct de vedere al performanței și siguranței așa că folosirea Collections.synchronisedList este mai indicată, întrucât această clasă constă în mod exclusiv din metode statice care operează pe sau returnează colecții și conține algoritmi polimorfici care operează pe colecții și prin urmare elimină ideea de sincronizare constantă oferind totuși protecție pe threaduri.
Tabelul 2.4.2 descrie performanța comparativă pentru metodele discutate ce se pot efectua de către clasele din List. La fel ca la orice cozi, trebuie pusă întrebarea dacă aplicația necesită siguranță la threaduri. Dacă da se poate folosi CopyOWriteArrayList în cazul în care scrierile pe listă vor fi rare. Dacă nu, se poate folosi un wrapper sincronizat în jurul unui ArrayList sau LinkedList.
O listă poate fi parcursă folosind un obiect Iterator beneficiind astfel de accesul la poziția elementelor din listă această metodă fiind echivalentă metodei de parcurgere cu for-each:
List<Tip_Element> list = new ArrayList();
Iterator<Tip_Element> iterator = list.iterator();
while(iterator.hasNext()){
Tip_Element element = iterator.next();
//execută cod legat de element
i.remove();
}
Acest exemplu poate elimina elemente selectiv, ținând seama de dimensiunea listei chiar și între iterații, lucru imposibil în metoda cu for-each.
2.5. Excepții
Excepțiile sunt erori care apar la rularea aplicației (în timpul execuției, nu la compilare). Folosind sistemul de control al excepțiilor oferit de Java se pot trata astfel erorile ce pot apărea la execuție. În alte limbaje de programare anumite coduri de eroare erau returnate de unele metode a căror execuție eșua, iar aceste coduri trebuiau verificate manual de fiecare dată când o astfel de metodă era apelată. Această abordare era dificilă dar și predispusă la provocarea erorilor în scrierea codului. Sistemul oferit de Java permite definirea unui bloc de cod (exception handler) care să fie executat atunci când apare o eroare. Nu mai este necesară astfel verificarea anumitor coduri de eroare.
În Java, excepțiile sunt reprezentate de clase și toate sunt extinse direct sau indirect din clasa Throwable. Asta înseamnă că atunci când o excepție apare în execuția codului, o astfel de clasă este generată. Există două clase extinse direct din Throwable (Figura 2.5.1):
Clasa Error reprezintă excepții legate de erori ce apar chiar în mașina virtuală Java și nu în codul scris de noi. Aceste tipuri de erori nu sunt tratate de aplicațiile scrise de utilizator.
Clasa Exception tratează erori ce rezultă din execuția codului scris de utilizator (ex: împărțire la 0, indecși în afara limitelor sau erori la accesarea unor fișiere). Aplicațiile trebuie să trateze corespunzător erori de acest fel. O subclasă importantă este RuntimeException care reprezintă diverse erori ce pot apărea la execuție.
Sistemul de tratare a erorilor din Java implică folosirea a 5 cuvinte cheie: try, catch, throw, throws și finally. Blocul de cod monitorizat în vederea prinderii și tratării erorilor este încadrat între cuvintele cheie try și catch. Dacă o eroare apare în acest bloc de cod, o excepție este generată (”aruncată” – thrown). Aceste excepții sunt prinse în blocurile catch și tratate după dorință.
Excepțiile generate de sistem sunt generate și aruncate automat. Pentru a genera manual o excepție în urma unei stări pe care o considerăm eronată, folosim cuvântul cheie throw. În cazurile în care o excepție este generată și nu este captată în corpul metodei curente, atunci metoda trebuie să specifice explicit că poate genera o anume excepție prin cuvântul cheie throws. Porțiunile de cod care trebuie executate obligatoriu indiferent dacă se generează sau nu o excepție în blocul try-catch sunt încadrate într-un bloc finally.
Blocul try/catch stă la baza sistemului de tratare a erorilor. O porțiune de cod nu poate fi monitorizată pentru tratarea excepțiilor fără a fi inclusă într-un astfel de bloc.
Forma generală a unui bloc try/catch este următoarea:
try {
//blocul ce trebuie monitorizat
}
catch (ExceptionTypeA e) {
//tratarea pentru o excepție de tipul ExceptionTypeA
}
catch (ExcpetionTypeB e) {
//tratarea pentru o excepție de tipul ExcpetionTypeB
}
Se observă faptul că pot fi mai multe blocuri catch asociate aceluiași bloc try, iar dacă o excepție este aruncată în blocul try aceasta este prinsă în blocul catch corespunzător. Blocul catch ce urmează să trateze excepția se alege în funcție de tipul excepției asociat cu acest catch astfel: dacă un tip de excepție specificat de o instrucțiune catch se potrivește cu tipul excepției ce trebuie tratate, atunci blocul acelei instrucțiuni catch se execută iar eventualele blocuri catch care urmează sunt ignorate. Excepția generată ce trebuie tratată se obține prin parametrul instrucțiunii catch. Un aspect important de remarcat este că unul din blocurile catch se execută numai în cazul în care o excepție este generată în corpul try corespunzător și tipul acestei excepții se potrivește cu cel specificat de instrucțiunea catch respectivă. Dacă nici o excepție nu este generată, execuția decurge normal sărind peste blocurile catch așa cum se poate observa în exemplul următor:
public class ExempluTryCatch {
public static void main(String[] args) {
int values[] = new int[10];
try {
System.out.println("Inainte de generare exceptie");
values[11] = 10; // index gresit
System.out.println("dupa generare executie. Nu se va executa!");
} catch (ArrayIndexOutOfBoundsException ex) {
System.out.println("prindere exceptie");
}
System.out.println("cod dupa bloc catch");
}
}
Ordinea operaíilor în cadrul acestui bloc de execuție sunt următoarele:
Inainte de generare exceptie
prindere exceptie
cod dupa bloc catch
Atunci când se încearcă accesarea unei poziții greșite dintr-un vector, o excepție este aruncată în afara blocului try, execuția acestui bloc se întrerupe iar controlul este preluat de blocul catch. Aspectul important atunci când o excepție este generată este că o instrucțiune catch nu este apelată ci pur și simplu firul execuției este transferat acestui bloc. Acest lucru înseamnă că atunci când codul catch își termină execuția firul execuției nu revine în blocul try acolo unde s-a generat execuția ci trece la prima instrucțiune după blocurile catch.
Întregul cod cuprins într-un bloc catch este monitorizat pentru captarea excepțiilor, inclusiv codul din metodele apelate din acest bloc try:
public class ExempluTryCatch {
public static void testMethod() {
int values[] = new int[10];
values[11] = 10; // index gresit
}
public static void main(String[] args) {
try {
System.out.println("Inainte de generare exceptie");
testMethod();
System.out.println("dupa generare executie. Nu se va executa!");
} catch (ArrayIndexOutOfBoundsException ex) {
System.out.println("prindere exceptie");
}
System.out.println("cod dupa bloc catch");
}
}
În urma rulării programului de test obținem același rezultat. Observăm că excepția generată în cadrul metodei de test nu este prinsă și tratată prin urmare aceasta va fi prinsă în blocul try/catch din metoda apelatoare.
Dacă o excepție este generată și nu este tratată nicăieri, atunci însăși execuția programului va fi întreruptă, execuția fiind prinsă de mașina virtuală. Un astfel de comportament al aplicației în cazul generării excepțiilor poate fi util doar în cazul rulării pentru test sau pentru debug. Este ușor de înțeles de ce trebuie să prindem și să tratăm corespunzător excepțiile. Dacă codul care provoacă generarea este cuprins într-un bloc try/catch dar nici o instrucțiune catch nu specifică un tip de excepție potrivit cu cel al excepției generate, atunci acea excepție este aruncată mai departe, urmând să fie prinsă de un alt eventual bloc try/catch sau dacă nu de mașina virtuală.
Atunci când se stabilește ce bloc catch trebuie să trateze o anumită excepție, contează potrivirea dintre tipul acesteia și tipul specificat de instrucțiunea catch. Această potrivire ține cont și de ierarhia de clase ce reprezintă excepțiile. Astfel, o instrucțiune catch ce specifică tipul Throwable va capta orice tip de excepție. Putem astfel să tratăm în mod explicit unele excepții iar restul să le prindem pe toate și să oferim o tratare generică. Trebuie avut în vedere însă ordinea în care specificăm aceste tipuri: un subtip trebuie specificat înaintea oricărui tip de bază al său, altfel codul lor nu va mai putea fi executat niciodată.
public class ExempluSubclase {
public static void main(String[] args) {
int values[] = { 0, 1, 2 };
for (int i = 0; i <= 3; i++) {
try {
int nr = 10 / values[i];
System.out.println("rezultat : " + nr);
} catch (ArrayIndexOutOfBoundsException ex) {
System.out.println("exceptie : index gresit");
} catch (Throwable ex) {
System.out.println("o exceptie a aparut : " + ex.getMessage());
}
}
}
}
Din acest exemplu reiese faptul că excepția provocată de indexul greșit este tratată în mod expres, iar celelalte sunt prinse pentru a permite continuarea execuției codului.
În exemplele anterioare s-a putut observa tratarea excepțiilor generate automat de către mașina virtuală. Folosind instrucțiunea throw excepțiile pot fi provocate și manual. Parametrul instrucțiunii throw trebuie să fie un obiect de tipul subclasei Thowable așa cum se observă în următorul exemplu:
public class ExempluThrow {
public static void main(String[] args) {
try {
System.out.println("inainte de throw");
throw new Exception("exceptie generata manual");
} catch(Exception ex) {
System.out.println("exceptie : " + ex.getMessage());
}
}
}
Excepția generată trebuie creată (new). Există posibilitatea să se arunce mai departe o excepție deja existentă. Un astfel de comportament ar fi de dorit atunci când dorim să tratăm aceeași excepție la mai multe niveluri logice, fiecare ocupându-se de un anumit aspect.
Într-un bloc catch tratăm un anumit tip de eroare în funcție de informațiile ce se pot obține din obiectul excepție primit ca parametru. Toate excepțiile sunt subclase Thowable și oferă următoarele metode utile:
printStackTrace(): afișează apelurile succesive care au dus la codul ce a generat excepția
getMessage(): descriere a excepției
toString(): returnează un obiect String ce conține descrierea excepției
Atunci când se dorește ca anumite porțiuni de cod să se execute indiferent dacă blocul try s-a executat normal sau a generat o excepție se folosește blocul finally.
Dacă o metodă conține cod ce ar putea genera o excepție și totuși în cadrul metodei nu există un bloc try/catch care să prindă acea excepție, atunci acea metodă trebuie să specifice prin clauza throws că poate genera acea excepție. Totuși nu se supun acestei reguli excepțiile care sunt subclase ale claselor Error și RuntimeExcepțion. Încercarea de a omite clauza throws cu o listă corespunzătoare de excepții pe care metoda le poate genera va rezulta într-o eroare la compilare.
Metoda care va apela această metodă va fi obligată la rândul ei să prindă și să trateze excepția sau să specifice prin clauza throws faptul că poate genera mai departe excepția respectivă precum se observă înurmătorul exemplu:
import java.io.IOException;
public class TestThrows {
public static char testMethod() throws IOException {
System.out.println("introiduceti un caracter");
return (char) System.in.read();
}
public static void main(String[] args) {
try {
char ch;
ch = testMethod();
System.out.println("ch : " + ch);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.6. Fire de execuție – Multithreading
“Multithreading” înseamnă capacitatea unui program de a executa mai multe secvențe de cod în același timp. O astfel de secvență de cod se numește fir de execuție sau thread. Limbajul Java suportă multithreading prin clase disponibile în pachetul java.lang. În acest pachet există 2 clase Thread și ThreadGroup, și interfața Runnable. Clasa Thread și interfața Runnable oferă suport pentru lucrul cu threaduri ca entități separate, iar clasa ThreadGroup pentru crearea unor grupuri de threaduri în vederea tratării acestora într-un mod unitar. Există 2 metode pentru crearea unui fir de execuție: se creează o clasă derivată din clasa Thread, sau se creează o clasă care implementeză interfața Runnable.
Pentru crearea unui fir de execuție prin extinderea clasei Thread se urmează următoarele etape:
se creează o clasă derivată din clasa Thread
se suprascrie metoda public void run() moștenită din clasa Thread
se instanțiază un obiect Thread folosind new
se pornește threadul instanțiat, prin apelul metodei start() moștenită din clasa Thread.
Apelul acestei ultime metode menționate, conduce la crearea de către mașina virtuală Java a contextul de program necesar unui thread înainte de aplearea metoda run().
Următorul exemplu prezintă un thread ce execută mai mulți pași în mod independent de restul codului din acea metodă:
public class Fir {
public static void main(String args[]) {
FirdeExecutie fir=new FirdeExecutie();
fir.start();
System.out.println("Revenim la main");
}
}
class FirdeExecutie extends Thread {
public void run() {
for(int i=0;i<10;i++)
System.out.println("Pasul "+i);
System.out.println("Run s-a terminat");
}
}
Metoda main() are propriul său fir de execuție. Prin apelul start() se face apel către mașina virtuală Java (JVM) crearea și pornirea unui nou fir de execuție. Din funcția start() se va ieși imediat. Firul de execuție corespunzător metodei main() își va continua execuția independent de noul fir de execuție creat.
Să considerăm un alt exemplu simplu în care se vor crea două fire de execuție ce rulează concomitent. Metoda sleep() cere oprirea rulării firului de execuție curent pentru un interval de timp specificat.
public class Fir1 {
public static void main(String args[]) {
FirdeExecutie fir1=new FirdeExecutie();
FirdeExecutie fir2=new FirdeExecutie();
fir1.start();
fir2.start();
System.out.println("Revenim la main");
}
}
class FirdeExecutie extends Thread {
public void run() {
for(int i=0;i<10;i++) {
System.out.println("Pasul "+i);
try{
sleep(500);//oprirea pt. 0,5 secunde a firului de executie
}
catch(InterruptedException e) {
System.err.println("Eroare");
}
}
System.out.println("Run s-a terminat");
}
}
Java definește 3 constante pentru selectarea priorităților firelor de execuție:
public final static int MAX_PRIORITY; // 10
public final static int MIN_PRIORITY; // 1
public final static int NORM_PRIORITY; // 5
O metodă importantă în contextul utilizării priorităților este
public static native void yield()
care scoate procesul curent din execuție și îl pune în coada de așteptare.
Următorul este un exemplu simplu care demonstrează cum se lucrează cu prioritățile firelor de execuție. Metoda getName() (moștenită din clasa Thread) returnează numele procesului curent.
public class Fir2 {
public static void main(String args[]) {
FirdeExecutie fir1=new FirdeExecutie("Fir 1");
FirdeExecutie fir2=new FirdeExecutie("Fir 2");
FirdeExecutie fir3=new FirdeExecutie("Fir 3");
fir1.setPriority(Thread.MIN_PRIORITY);
fir2.setPriority(Thread.MAX_PRIORITY);
fir3.setPriority(7);
fir1.start();
fir2.start();
fir3.start();
System.out.println("Revenim la main");
}
}
class FirdeExecutie extends Thread {
public FirdeExecutie(String s) {
super(s);
}
public void run() {
String numeFir=getName();
for(int i=0;i<5;i++) {
if(numeFir.compareTo("Fir 3")==0)
yield();
System.out.println(numeFir+ " este la pasul "+i);
try{
sleep(500);
}
catch(InterruptedException e) {System.err.println("Eroare");}
}
System.out.println(numeFir+ " s-a terminat");
}
}
Implementările Java depind de platformă. Un program Java care folosește fire de execuție poate avea comportări diferite la execuții diferite pentru aceleași date de intrare.
Platformele Windows folosesc cuante de timp (firele de execuție sunt administrate într-o manieră RoundRobin).
Crearea unui fir de execuție se poate realiza și cu ajutorul interfeței Runnable
Este o modalitate extrem de utilă atunci când clasa de tip Thread care se dorește a fi
implementată moștenește o altă clasă (Java nu permite moștenirea multiplă). Interfața Runnable descrie o singură metodă run().
Pentru a rula un fir de execuție cu ajutorul interfeței Runnable trebuie urmați următorii pași:
se creează o clasă care implementează interfața Runnable
se implementează metoda run() din interfață
se instanțiază un obiect al clasei folosind new
se creează un obiect din clasa Thread folosind un constructor care are ca parametru un obiect de tip Runnable (un obiect al clasei ce implementează interfața)
se pornește threadul creat la pasul anterior prin apelul metodei start().
public class Fir3 {
public static void main(String args[]) {
FirdeExecutie fir = new FirdeExecutie();
Thread thread = new Thread(fir);
thread.start();
System.out.println("Revenim la main");
}
}
class A {
public void afis() {
System.out.println("Este un exemplu simplu");
}
}
class FirdeExecutie extends A implements Runnable {
public void run() {
for(int i=0;i<5;i++)
System.out.println("Pasul "+i);
afis();
System.out.println("Run s-a terminat");
}
}
Un fir de execuție se poate afla la un moment dat în una din următoarele stări: running (rulează), waiting (adormire, blocare, suspendare), ready (gata de execuție, prezent în coada de așteptare), dead (terminat) așa cum se observă în figura 2.6.1. Fiecare fir de execuție are o prioritate de execuție. În general threadul cu prioritatea cea mai mare este cel care va accesa primul resursele sistem.
O altă clasă aparte de threaduri sunt cele Daemon care sunt threaduri de serviciu (aflate în serviciul altor fire de execuție). Când se pornește mașina virtuală Java, există un singur fir de execuție care nu este de tip Daemon și care apelează metoda main(). JVM rămâne pornită cât timp există un thread activ care să nu fie de tipul Daemon.
Pentru a demonstra principiile excluderii mutuale și sincronizării, presupunem că două fire de execuție incrementează valoarea unui întreg partajat. Pot apărea probleme în sensul că cele 2 fire de execuție incrementează în același timp întregul, sărindu-se astfel peste valori ale acestuia. Problema se rezolvă dacă cel mult un fir de execuție are acces la acea dată la un moment dat. Java implementează excluderea mutuală prin specificarea metodelor care partajează variabile ca fiind synchronized. Aceste metode trebuie să fie din aceeași clasă. Orice fir de execuție care încearcă să acceseze o metodă synchronized a unui
obiect atât timp cât metoda este utilizată de un alt fir de execuție, este blocat până când primul fir de execuție părăsește metoda.
Această tehnică este folosită și în cadrul aplicației “MakeYourOwnPool”.
2.7. Java 2D
2.7.1. Desenare în Java2D
Java2D este un API(Application Programming Interface) destinat desenării grafici bidimensionale folosind limbajul de programare Java. Fiecare operație de desenare pe care o poate efectua Java2D poate fi clasificat în cele din urmă în operații de umplere(fill) a unei forme folosind metode precum paintComponent și compunerea rezultatului pe ecran.
De cele mai multe ori în Swing este folosit un obiect din clasa JPanel drept planșă de desen. Se poate suprascrie (Override) metoda sa “paintComponent(Graphcs)” folosind instrucțiuni personale de desenare. Este bine de știut faptul că având în vedere faptul că toate componentele Swing lightweight, se pot suprascrie metodele acestora de desen pentru a adăuga noi featureuri displayului. Spre exemplu, se poate crea un buton cu aspect personalizat pentru starea de apăsat și neapăsat.
Metoda paintComponent(Graphics g) include o instanță a clasei Graphics ca argument. Contextul grafic reprezintă o suprafață de desen și toate setările sale precum culoarea de fundal și prim plan, fonturile ce trebuie folostie pentru Stringuri, etc. Această clasă mai este denumită și clasa contextului grafic din moment ce oferă contextul sub care comenzile grafice operează pentru o componentă.
Contextul grafic nu necesită să reprezinte o componentă vizibilă, însă poate fi o imagine în afara ecranului.
După versiunea 1.2 al limbajului, acesta a conținut o subclasă a clasei Graphics denumită Graphics2D. Aceasta oferă un set mult mai extins și mai sofisticat de capacități de desen. O instanță a clasei Graphics2D poate fi castată(operația de schimbare a tipului de obiect în altul) de la instanța Graphics pasată în cadrul metodei paint(Graphics g) sau paintComponent(Graphics g) provenite de la JComponent.
Trebuie observat faptul că sistemul de coordonate pentru metodele contextul grafic sunt reprezentate in felul următor:
Origine(0,0)-colțul din stânga sus
Axa ox(în pixeli)-crește spre dreapta
Maxim x = lațime – 1
y(în pixeli)-crește spre partea inferioară
Maxim y = înălțime – 1
Se pot obține dimensiunile panoului fie prin metoda getSize() care întoarce o instanță a clasei Dimension care oferă acces direct la variabilele sale height și width. Încă de la versiunea Java 1.2, clasa Component include și metodele getHeight() și getWidth() pentru a putea obține fiecare din aceste dimensiuni separat. Valorile negative și pozitive ce depășesc lățimea și înălțimea zonei de desen nu cauzează erori ci sunt tratate ca coordonate valide, însă orice desen în acele zone nu vor fi văzute.
Următoarele sunt doar câteva din metodele clasei Graphics:
setColor(Color c)
Setează culoarea de desen cu una din cele standard:
Color.black, Color.blue, Color.red, etc.
sau cu o culoare personală:
new Color(r, g, b); sau new Color(r, g, b, a);
drawLine(int x1, int y1, int x2, int y2)
Desenează o linie între punctele (x1,y1) și (x2,y2)
drawRect (int x, int y, int width, int height) și
fillRect (int x, int y, int width, int height)
Desenează sau umple un dreptunghi, cu (x,y) coordonata colțului din stanga sus, și cu colțul din dreapta jos la: (x+width,y+height).
drawOval (int x, int y, int width, int height) and
fillOval (intx, int y, int width, int height)
Desenează sau umple un oval mărginit de dreptunghiul descris de parametrii.
draw3DRect (int x, int y, int width, int height, int arcWidth,
boolean raised) și
fill3DRect (int x, int y, int width, int height, boolean raised)
Desenează sau umple un dreptunghi cu margini umbrite care oferă un aspect tridimensional.
drawRoundRect (int x, int y, int width, int height, int arcWidth,
int arcHeight)
fill3DRect (int x, int y, int width, int height, int arcWidth, int
arcHeight)
Desenează sau umple un dreptunghi cu margini rotunjite.
drawArc(int x,int y,int width, int height, int startAngle, int arcAngle)
Desenează un arc cu lungimea unghiul de pornire și un continuitatea unghiului in raport cu unghiul de start dați ca parametrii
drawPolyline (int[] x, int[] y, int N)
Desenează linii ce conectează cele N puncte date de vectorii x și y
drawPolygon (int[] x, int[] y, int N)
Desenează linii ce conectează punctele date de vectorii x și y. Conectează ultimulpunct cu primul dacă nu reprezintă deja același punct.
Fiecare formă 2D precum un dreptunghi sau o linie este considerată un obiect. Se poate trimite apoi forma către suprafața de desen (contextul Graphics2D) într-o singură operație decât prin comenzi incrementale.
Diagrama de mai jos arată obiectele de tip Shape care sunt definite de aplicații care implementează interfața java.awt.Shape (Figura 2.7.1).
Așa cum și numele pentru clasa Line2D sugerează, fiecare clasă abstractă de tip ”[nume]2D” coníne o clasă reală .Float și o clasă .Double care pot fi instanțiate. Acest lucru a fost realizat în acest fel pentru a permite utilizatorului să specifice proprietățile formei dorite precum tipul de date pentru coordonate (float sau double) sau punctul de inceput și sfârșit al unui de segment în cazul acestuia.
Și mai mult, chiar și operațiile de desenare pot fi tratate ca pe niște obiecte. De exemplu atunci când dorim să realizăm o operație de transformare asupra acestor obiecte 2D precum o translație sau rotație, se crează o instanță a clasei Transform care conține metodele necesare efectuării operației.
2.7.2. Transformări afine
Java.awt.geom.AffineTransform este o clasă care reprezintă coordonatele sub forma unei matrice 3×3 și o înmulțire. Întrucât operațiile realizate în spate sunt pur matematice, vor fi discutate aceste aspecte într-un capitol ulterior., iar în acest capitol vor fi prezentate câteva posibilități ale acestei clase din punct de vedere practic.
Această clasă poate translata punctul de origine al panoului de dese, să roteasca perspectiva, să schimbe valoarea versorilor axelor de coordonate și mult mai mult, permițând realizarea unor efecte și rezultate vizuale care în mod normal nu ar fi posibile cu ușurință.
Pentru a translata întreaga perspectivă și prin urmare toate punctele de pe contextul grafic, se poate folosi funcția AffineTransform.getTranslateInstance(double a, double b), iar mutarea perspectivei se realizează cu AffineTransform.transform(Point2D before, Point2D after).
//transformare care mută puncte cu 10 pixeli la dreapta și 20 de pixeli sus
AffineTransform transformer = AffineTransform.getTranslateInstance ( 10.0d, -20.0d );
//crearea unui punct
Point2D before = new Point2D.Double( 3.0d , 6.0d );
//creara unui punct pentru a ține rezultatul
Point2D after = new Point2D.Double();
// transformarea punctului (translatare)
after = transformer.transform ( before, after );
// afișează 13.0,-14.0
System.out.println( after.getX() + "," + after.getY() );
Așa cum s-a menționat, această clasă permite și realizarea de rotații ale axelor și prin urmare a tuturor punctelor, insă și schimbarea dinstanței dintre două coordonate unitate de pe axa ox și oy, realizând un efect de micșorare sau lărgire în cazul unor texturi de exemplu, lucru folositor mai mult în cazul lucrului cu imagini.
//aplicatea unei rotații
transformer.rotate ( Math.toRadians( 30 ) );
//rotirea în sens invers acelor de ceasornic cu 90 de grade
transformer.quadrantRotate( 1 );
/*setarea distanței unitate între puncte pe axe (aici având un efect de inversare ale axelor)*/
transformer.scale( 1.0, -1.0 );
În exemplul de cod de mai sus se poate observa cum setarea scalei coordonatelor (dimensiunea versorilor corespunzatori axelor x și y) și atribuirea unor valor negative poate inversa sistemul de axe, realizând efecte care nu sunt posibile altfel, spre exemplu oglindirea unei imagini.
Trebuie menționat și faptul că exista metode de tranlație, rotație și scalare și în cadrul clasei Graphics2D, însă AffineTransform este uneori mai indicat întrucât permite memorarea unei transformări pentru apeluri ușoare ulterior.
Un aspect foarte important ce poate fi folositor în cazul interacțiunii utilizatorului cu interfața grafică este acela al translatării coordonatelor mausului în cadrul noilor coordinate sau aflarea coordonatelor în alt sistem de coordinate fiind date punctele amplasate în cadrul celui current. Pentru aceasta este nevoie de o copie memorată a inversei matricei corespunzătoare sistemului de coordinate destinnație pentru a putea translate punctual din sistemul de coordinate current.
//transformare ce mută puncte cu 10 pixeli la dreapta și 20 în sus
AffineTransform transformer = AffineTransform.getTranslateInstance( 10.0d, -20.0d );
//crearea unei transformări pentru a inversa translația
AffineTransform reverse = transformer.createInverse();
//detransformarea manuală a coordonatelor mausului folosind inversa matricei
Point mousePositionUC = new Point();
reverse.transform( new Point( mousex, mousey ), mousePositionUC );
După cum se poate observa, această clasă este extrem de puternică și ușor de folosit. În capitolul următor se va discuta despre lucrul cu imagini și cum transformările pot ajuta în diverse situații.
2.8. Procesarea imaginilor
Procesările de imagini în Java au fost suportate încă de la prima apariție, prin intermediul pachetelor java.awt și java.awt.image.
Primele versiuni ale Java AWT API au fost constituite dintr-un pachet de clase utile care urmăreau afișarea de imagini aflate în Internet, dar fără nici o funcție necesară pentru procesările de imagini mai complexe. Acest pachet permitea generarea de imagini simple prin desenarea de linii și forme. De asemenea se putea citi un număr foarte mic de formate de fișiere de imagine (doar formatele GIF și JPEG), prin intermediul unui obiect de tip Toolkit din pachetul java.awt. Odată citită, imaginea putea fi doar afișată, deoarece nu existau operatori pentru procesarea ei.
Java 2D API extinde pachetul de început AWT prin adăugarea unui suport mai ridicat pentru operații generale de grafică și afișare de imagini. Java 2D a adăugat clase speciale pentru definirea de primitive geometrice, formatarea textelor, definirea fonturilor, spații de culoare și reprezentare de imagini. Noile clase suportă un set limitat de operatori de procesare de imagini cum ar fi blurr, sharpen, transformări geometrice, îmbunătățire de contrast și binarizare. Extensiile Java 2D au fost adăugate la nucleul Java AWT odată cu lansarea platformei J2SE (Java 2 Standard Edition) versiunea 1.2.
Aplicațiile mai pretențioase au nevoie însă de un API de procesare a imaginilor cu facilități mult mai dezvoltate. Astfel au fost create de către diverse companii o serie de APIuri de procesare de imagini care au avut un succes destul de remarcabil, dar nici unul dintre acestea nu au fost universal acceptate deoarece au eșuat în a se adresa unui segment specific al domeniului procesărilor de imagini sau nu aveau putere în a îndeplini anumite nevoi. Astfel multe companii au trebuit să-și elaboreze propria implementare în încercarea de a îndeplinii aceste nevoi.
Un exemplu de librărie(colecție de clase și interfeț) destinată procesării imaginilor este si “thumbnailator” folosit și în cadrul programului MakeYourOwnPool. Scopul principal al acestei librării este de a ușura munca necesară procesării imaginilor folosind APIul Image I/O și Java2D.
Iată un exemplu de cod ce realizează o imagine este redimensionată la 640 de pixeli pe 480 păstrând raportul de aspect și este salvată în format JPEG într-un director:
Thumbnails.of(new File("calea_director").listFiles())
.size(640, 480)
.outputFormat("jpg")
.toFiles(Rename.PREFIX_DOT_THUMBNAIL);
După cum se poate observa, această librarie execută operații complexe extrem de ușor și cu un limbaj aproape natural.
Figura 2.8.1 este o imagine aparținând creatorului librăriei ce reprezintă o comparație între timpul de rulare al acestei librării în cadrul unei operații de redimensionare de imagine relativ la alte metode din clasa Graphics și Image, precum și o comparație asupra calității rezultatului. Imaginea originală folosită are formatul JPEG și dimensiunile 2112 x 2816 pixeli, iar a doua imagine de pe cel de-al doilea rând este o imagine în format PNG de diemensiune 3000 x 3000 pixeli. Întrucât nu sunt tehnici relevante de procesare de imagini în cadrul proiectului MakeYourOwnPool voi menționa doar pe scurt faptul că metoda interpolării liniare utilizată în a doua imagine, este o metodă de redimensionare a imaginilor ce utilizeaza patru puncte de referință, mai exact colțurile imaginii, precum și distanța față de punctul sursă și destinația punctului final, realizând apoi medii de valori și alte calcule pentru a determina astfel culoarea fiecărui pixel. Metoda interpolării bicubice este o extensie a interpolării cubice ce ia în considerare 16 puncte de referință spre deosebire de cea menționată anterior, rezultând într-o calitate a imaginii mai bună.
Figura 2.8.1
Comparație asupra vitezei de rulare și calitatea rezultatelor procesării imaginilor
Din această comparație se poate observa ca librăria thumbnailator este mult mai rapidă decât metodele de redimensionare ale clasei Image, cunoscute ca fiind lente și în profida dezavantajului de viteză în raport cu metodele de interpolare din clasa Graphics2D:
setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
Aceasă librărie oferă totuși o calitate comparabilă cu Image.SCALE_SMOOTH, așa că este de la sine înțeles de ce această librărie este extrem de utilă.
O analiză mai amănunțită a codului din spatele acestei librării dexvăluie secretul. Librăria deține multe clase abstracte ce permit scrierea codului astfel, iar la fiecare trecere prin aceste clase, modifică valori ce urmează la final să indice modul în care desenarea va avea loc (sunt valori constante din clasa RenderingHints). În momentul în care se dorește o redimensionare de exemplu se poate observa că tot ce face această librărie (în cazul cererii obținerii unui obiect de tip BufferedImage) este să creeze dimensiunea acesteia, să preia contextul grafic strict al obiectului și aplicând apoi tehnicile rapide de interpolare biliniară de mai multe ori pe aceeași poză redimensionând rezultatul treptat până la atingerea dimensiunii dorite.
Iată o porțiune de cod din metoda resize(BufferedImage srcImage, BufferedImage destImage) aparținând clasei ProgressiveBilinearResizer demonstrând acest fapt:
public void resize(BufferedImage srcImage, BufferedImage destImage)
throws NullPointerException {
…
//diverse operații
…
// Temporary image used for in-place resizing of image.
BufferedImage tempImage = new BufferedImage(
currentWidth,
currentHeight,
destImage.getType()
);
Graphics2D g = tempImage.createGraphics();
g.setRenderingHints(RENDERING_HINTS);
g.setComposite(AlphaComposite.Src);
…
//diverse operaíi
…
// Perform an in-place progressive bilinear resize.
while ( (currentWidth >= targetWidth * 2) && (currentHeight >= targetHeight * 2) )
{
currentWidth /= 2;
currentHeight /= 2;
if (currentWidth < targetWidth)
{
currentWidth = targetWidth;
}
if (currentHeight < targetHeight)
{
currentHeight = targetHeight;
}
g.drawImage(
tempImage,
0, 0, currentWidth, currentHeight,
0, 0, currentWidth * 2, currentHeight * 2,
null
);
}
g.dispose();
BufferedImage este o subclasă a clasei Image mult mai rapidă și mult mai potrivită pentru lucrul cu imagini. Aceasta descrie o imagine cu un buffer accesibil de date. Aceasta conține un ColorModel și un Raster de date de imagine. Toate obiectele BufferedImage au o coordonată stânga sus de (0,0).
ColorModel este o clasă abstractă care încapsulează metodele pentru translatarea valorii unui pixel spre componentele culorii (exemplu: roșu, verde, albastru) și o componentă alfa, iar Raster este o clasă care deține valori pentru pixeli ocupând o anumită zonă dreptunghiulară a unui plan, nu neapărat incluzând (0,0).
Clasa BufferedImage gestionează imaginea în memorie asigurând metode de stocare, interpretare și reprezentare a datelor pixelilor spre un context Graphics sau Graphics2D.
Pentru a crea un obiect BufferedImage, se apelează metoda Component.createImage. Aceasta returnează un obiect de tip BufferedImage a cărui caracteristici de reprezentare se potrivesc cu cele ale componentei folosite pentru creare. Imaginea creată este opacă și are culorile de suprafață și fundal ale obiectului Component, cu posibilitatea de modificare a transparenței. Acest lucru este exemplificat pe secțiunea de cod următoare:
BufferedImage offImg;
public Graphics2D
createMyG2D(Graphics g) {
Graphics2D g2 = null;
int width = getSize().width;
int height = getSize().height;
if (offImg == null || offImg.getWidth()!= width || offImg.getHeight() != height) {
offImg = (BufferedImage)createImage(width, height);
}
if (offImg != null) {
g2 = offImg.createGraphics();
g2.setBackground(getBackground());
}
// șterge componenta
g2.clearRect(0, 0, width, height);
return g2;
}
Clasa BufferedImage poate fi folosită la pregătirea elementelor grafice în afara ecranului și apoi afișarea lor pe ecran. Această tehnică este folositoare atunci când un element grafic este utilizat în mod repetat.
Facilitățile pachetului java.awt permit folosirea buferelor de pregătire în afara ecranului, astfel modificarea unei imagini se face în același mod ca și cum ea ar fi afișată într-o fereastră. Toate facilitățile de reprezentare Java 2D pot fi aplicate atunci când se lucrează cu imagini în afara ecranului.
Cea mai simplă metodă de a crea o imagine, care poate fi folosită ca și un bufer în afara ecranului, este prin apelul metodei Component.createImage.
Clasa BufferedImage suportă câteva tipuri de imagine predefinite:
TYPE_3BYTE_BGR – imagine cu componente de culoare RGB pe 8 biți, corespunzătoare modelului de culoare RGB din Windows, cu culorile Blue, Green și Red stocate pe 3 bytes.
TYPE_4BYTE_ABGR – imagine cu componente de culoare RGBA pe 8 biți, cu culorile Blue, Green și Red stocate pe 3 bytes și un byte pentru canalul alfa.
TYPE_4BYTE_ABGR_PRE – imagine cu componente de culoare RGBA pe 8 biți cu culorile Blue, Green și Red stocate pe 3 bytes și un byte pentru alfa
TYPE_BYTE_BINARY – imagine opacă pe 1, 2 sau 4 biți
TYPE_BYTE_GRAY – imagine scală de gri pe un unsigned byte, neindexată.
TYPE_BYTE_INDEXED – imagine pe un byte indexată.
TYPE_CUSTOM – imagine nerecunoscută, de tip utilizator.
TYPE_INT_ARGB_PRE – imagine cu componente de culoare RGBA pe 8 biți compusă din pixeli integer.
TYPE_INT_ARGB – imagine cu componente de culoare RGBA pe 8 biți compusă din pixeli integer.
TYPE_INT_BGR – imagine cu componente de culoare RGBA pe 8 biți, corespunzător pentru modelele de culoare din Windows sau Solaris, cu culorile Blue, Green și Red împachetate în pixeli integer.
TYPE_INT_RGB – reprezintă o imagine cu componente de culoare RGB pe 8 biți împachetate în pixeli integer.
TYPE_USHORT_555_RGB – imagine cu componente de culoare 5-5-5 RGB (5-biți Red, 5-biți Green, 5-biți Blue) fără canal alfa.
TYPE_USHORT_565_RGB – imagine cu componente de culoare 5-6-5 RGB (5-biți Red, 6-biți Green, 5-biți Blue) fără canal alfa.
TYPE_INT_GRAY – imagine în scală de gri stocată pe tipul unsignrd short, neindexată.
Pentru reprezentarea într-un obiect de tip BufferedImage, se apelează metoda BufferedImage.createGraphics, care returnează un obiect Graphics2D. Cu acest obiect se pot apela toate metodele pentru reprezentarea primitivelor grafice, sau reprezentarea altor imagini în imagine. În fragmentul de cod următor se ilustrează utilizarea buferării în afara ecranului:
public void update(Graphics g) {
Graphics2D g2 = (Graphics2D)g;
if(firstTime) {
Dimension dim = getSize();
int w = dim.width;
int h = dim.height;
area = new Rectangle(dim);
bi = (BufferedImage)createImage(w, h);
big = bi.createGraphics();
rect.setLocation(w/2-50, h/2-25);
big.setStroke(new BasicStroke(8.0f));
firstTime = false;
}
// Șterge suprafața care a fost desenată anterior
big.setColor(Color.white);
big.clearRect(0, 0, area.width, area.height);
// Desenează și umple un nou dreptunghi în bufer.
big.setPaint(drept1);
big.draw(rect);
big.setPaint(drept2);
big.fill(rect);
// Desenează un buffered image pe ecran.
g2.drawImage(bi, 0, 0, this);
}
Acestea fiind spune, BufferedImage este alegerea ideală pentru lucrul cu imagini în aplicaíi, această clasă fiind utilizată intensiv în cadrul proiectului MakeYourOwnPool. În capitoul următor va fi prezentată schema aplicației, precum și clasele mai deosebite din cadrul implementării acesteia, împreună cu problemele apărute și souluțiile găsite.
3. Implementarea aplicației
3.1. Schema aplicației:
Figura 3.1.1
Schema UML a pachetului poolpackage
Figura 3.1.2
Schema UML a pachetului TableEditoPackage
Figura 3.1.3
Schema UML a pachetului FrameComponents
Figura 3.1.4
Schema UML a pachetului GeneralMethods
3.2. Meniul principal și evenimente
Clasa MainPanel din cadrul pachetului “poolpackage” conține toate clasele necesare rulării jocului efectiv de la meniul “PLAY NOW” și pornirii editorului.
Această clasă este de fapt meniul principal al aplicației. Este o clasă extinsa din clasa JPanel și amplasată pe fereastra principală ce este un obiect de tip poolApplication ce extinde clasa JFrame. Clasa MainPanel implementează interfețele MouseListener și ActionListener pentru accesarea meniurilor cu ajutorul mausului adăugând apoi un listener cu parametru this. Această metodă este folosită in general pe tot parcursul implementării aplicației, întrucât astfel toate evenimentele sunt ascultate de un singur obiect, în cazul acesta panoul părinte, și pot fi ușor diferențiate apoi prin comparare cu elementele cărora li s-a adăugat acel listener.
În cazul unui click pe unul din obiectele de tip JLabel respectiv pe imaginile meniurilor, se declanșează evenimentul și este tratat în metoda suprascrisă mouseReleased(Mouse Event e):
@Override
public void mouseReleased(MouseEvent e) {
Object source = e.getSource();
if (source == menus[PLAY_NOW]) {
showMapChooser();
}
if (source == menus[TABLE_EDITOR]) {
FrameComponents.WindowCloserClass.closeWindow(parent, 1);
}
if (source == menus[EXIT_GAME]) {
FrameComponents.WindowCloserClass.closeWindow(parent, 0);
}
if(source == menus[BACK]){
showMainMenu();
}
if(source == menus[PLAY]){
if(mapTable.getSelectedRowCount() == 0){
ErrorClass.showNoTableSelectedError(parent);
}
else{
showGame();
}
}
if(source == menus[LEAVE_TABLE]){
showMapChooser();
}
}
După cum se poate observa, sursa este comparată cu fiecare element posibil de a fi accesat și se iau masurile corespunzătoare, pregătindu-se un alt submeniu, închiderea aplicației sau confirmarea selectării unei table de joc valide înaintea începerii jocului la cererea utilizatorului.
Imaginile meniurilor sunt stocate într-un vector de șase elemente, iar indexul lor, în cazul unui eveniment, este comparat cu variabile finale de tip byte reprezentând comenzile (exemplu: final byte PLAY_NOW = 0).
De asemenea panoul are adăugat și un KeyListener alcărui parametru este un obiect de tip KeyAdapter. Acest obiect conține metodele suprascrise de apăsare ale tastelor și eliberare, executând într-un bloc switch ce îndeplinește acelașii rol precum clickul pe meniuri, însă în funcție de tastele apăsate.
3.3. Parsarea fișierelor
Cea mai deosebită metodă din această clasă atât datorită importanței ei asupra rulării programului, cât și din punctul de vedere al complexității operațiilor executate este pe departe metoda protected void scanForTablesIn(String path, boolean reset).
protected void scanForTablesIn(String path, boolean reset){
fileNameBox.setText(path);
tableModel.setRowCount(0);
if(reset){
hadError = false;
}
try{
nrTables = 0;
GeneralMethods.StaticMethod.deleteAllFilesIn(tempFolder, parent);
File dir = new File(path);
for(File file : dir.listFiles()){
if(file.getName().endsWith(".tbl")){
gamePanel.clearAllAdditions();
File ini = extractTblTo(file, tempFolder.concat("\\"+(++nrTables)));
if(!parseTextOfINIForProject(ini)){
throw new Exception("Error parsing the project .ini file at \n"+ ini.getAbsolutePath());
}
for(File infoTxt : ini.getParentFile().listFiles()){
if(!infoTxt.getName().endsWith(".txt") && !infoTxt.getName().endsWith(".ini")){
if(!parseTextOfOBSForThumbnail( new File(ini.getParentFile().getAbsolutePath() + "\\" + (infoTxt.getName().substring(0, infoTxt.getName().lastIndexOf('.'))) + ".txt"),
infoTxt.getAbsolutePath() )){
throw new Exception("Error parsing .obs information for file at \n" + infoTxt.getAbsolutePath());
}
}
}
if(!gamePanel.setTranslatedLinesAndHoles()){
throw new Exception("Error translating coordinates of .obs file elements");
}
tableRow[0] = file.getName();
tableRow[1] = gamePanel.tableWidth + " x " + gamePanel.tableHeight;
gamePanel.setPreferredSize(new Dimension(gamePanel.tableWidth, gamePanel.tableHeight));
if( "-1".equals(tableRow[2] = Integer.toString(gamePanel.getNrPlayerBall()))){
tableRow[2] = "Balls not even for players ERROR";
}
if(!gamePanel.balls.isEmpty()){
tableRow[3] = Double.toString(gamePanel.balls.get(0).width);
tableRow[4] = Double.toString(gamePanel.balls.get(0).mass);
}
else{
tableRow[3] = "No balls ERROR";
tableRow[4] = "No balls ERROR";
}
tableModel.addRow(tableRow);
}
}
}
catch(Exception e){
if(hadError == false){
gamePanel.clearAllAdditions();
ErrorClass.showCouldNotLoadError(path, parent);
tableModel = new DefaultTableModel(columnNames,0);
hadError = true;
}
}
}
Această metodă are rolul de a scana directorul din caseta de text din a doua stare a meniului principal, respectiv “alegătorul de mese” la care se ajunge prin meniul ”PLAY NOW”.
Prima etapă este să șteargă toate fișierele și directoarele în mod recursiv din directorul ”Temp” ce se regăsește în directorul aplicației printr-o funție statică declarată în pachetul GeneralMethods respectiv clasa StaticMethods:
GeneralMethods.StaticMethod.deleteAllFilesIn(tempFolder, parent);
Apoi începe căutarea pe rând a fișierelor din directorul dorit în încercarea de a găsi pe cele cu extensia *.tbl:
if(file.getName().endsWith(".tbl")){
//operații
}
Se elimină orice element din panoul de joc(dacă acestea au rămas de la o pornire anterioară) și se extrage conținutul fișierului *.tbl dacă este găsit. Acest lucru se realizează prin metoda private File extractTblTo(File loadedFile, String path):
private File extractTblTo(File loadedFile, String path) {
File dir = new File(path),
ini = null;
StaticMethod.makeFolder(dir, parent);
GeneralMethods.ZipFileHandler zfh = new GeneralMethods.ZipFileHandler(loadedFile.getAbsolutePath());
zfh.extractFilesTo(path);
for (File f : dir.listFiles()) {
if (f.getName().endsWith(".obs")) {
zfh = new GeneralMethods.ZipFileHandler(f.getAbsolutePath());
if(!zfh.extractFilesTo(path)){
ErrorClass.showCouldNotSaveObjectsError(path, parent);
return null;
}
try {
f.delete();
} catch (Exception ex) {
ErrorClass.showCouldNotDeleteError(f.getAbsolutePath(), parent);
return null;
}
}
if(f.getName().endsWith(".ini")){
ini = new File(f.getAbsolutePath());
}
}
return ini;
}
Această metodă folosește un obiect din clasa ZipFileHandler delcarată în pachetul GeneralMethods care conține cod de extragere a fișierelor *.zip sau orice alt tip de fișier arhivă cu proprietăți similare. Întrucât fișierele *.tbl sunt create prin utilizarea claselor din limbajul Java destinate lucrului cu arhive, nu pot apărea probleme în ceea ce privește procesarea acestor fișiere.
GeneralMethods.ZipFileHandler zfh = new GeneralMethods.ZipFileHandler(loadedFile.getAbsolutePath());
zfh.extractFilesTo(path);
Fișierele din interiorul arhivei sunt extrase către directorul din parametru care în cazul acesta va fi un director creat în cadrul directorul “Temp” numerotat pentru a evita suprascrierea în cazul în care există mai multe mese găsite. Apoi se scanează folderul unde a fost despachetat conținutul și se extrage și conținutul fișierelor *.obs în acelașii loc. Pe scurt, aceasta clasă realizează două dezarhivări în adâncime penru a extrage toate fișierele necesare.
Revenind la metoda scanForTablesIn, se observă faptul că în momentul acesta s-a obținut fișierul *.ini ce conține informații esențiale pentru organizarea elementelor jocului.
Este necesară parsarea acestui fișier pentru a memora informații legate de dimensiunea mesei de joc, caracteristicile fiecărei bile și altele. Parsarea se realizează în cadrul metodei:
private boolean parseTextOfINIForProject(File file) al cărei parametru se dorește a fi un fișier *.ini cu informații valide, returnând true dacă operațiile au avut loc fără erori, altfel parsarea nu a avut loc și se lansează o excepție personalizată:
throw new Exception("Error parsing the project .ini file at \n" + ini.getAbsolutePath());
Această clasă, parseTextOfINIForProject, începe prin a instanția un obiect de tip BufferedReader necesar citirii unor linii întregi de text:
br = new BufferedReader(new FileReader(file));
Obiectul String, strLine va memora acele șiruri de caractere pentru comparaíi.
Formatul textului în cadrul fișierelor *.ini trebuie să respecte cu exactitate ordinea logică în care au fost generate inițial. De aceea editarea manuală a acestor fișiere este foarte descurajată. Iată un exemplu de fișier valid *.ini:
TableDimensions:
1000,1000
NumberOfThumbnails:
8
Balls:
4,20.0,100.0,458.0,291.0
3,20.0,100.0,606.0,274.0
1,20.0,100.0,408.0,198.0
2,20.0,100.0,719.0,279.0
PicturesOnTable:
5,0,84.0,76.0,148.0,151.0
2,0,232.0,75.0,579.0,152.0
6,0,408.0,490.0,128.0,96.0
3,1,536.0,227.0,355.0,673.0
6,1,99.0,318.0,96.0,128.0
4,0,258.0,324.0,164.0,101.0
Codul acestei metode preia fiecare linie de text pe rând și compară textul găsit cu titlul acestor categorii exact în ordinea în care apar și aici. Daca este găsit textul ”TableDimensions:”, căruia i se elimină touși spațiile albe dacă există, atunci în cadrul cauzei conditionale se mai preia o linie și se citesc și se parsează valorile găsite către întregi cu metoda Integer.parseInt(String s). Dacă nu s-a găsit niciunul din titluri la linia curentă nu este tosuși nici o problem, întrucât se fa prelua o nouă linie până când se identifică o linie de text care se va potrivi cu unul din cele patru, așa caă este totuși important ca aceste categorii să nu apară de mai multe ori.
Bilele conțin date în ordinea următoare: tipul bilei, lățimea(diametrul), masa(greutatea), coordonataX, coordonataY.
Iar imaginile conțin următoarele date în următoarea ordine: indexul imaginilor obținute din fișiere *.obs pentru preluara căii absolute, număr reprezentând de câte ori este rtită spre stânga la 90 de grade, coordonataX, coordonataY, lățime, înălțime.
În cazul acetor ultime două categorii parsarea se face în interiorul condiției if până când următoarea linie cu spații libere eliminate este goală.
Revenind din nou la metoda scanForTablesIn, aceasta continuă execuția prin a scana, în urma extragerii fișierelor, după fișierele text ce erau inițial în interiorul arhivelor *.obs, identificându-le în mulțimea de fișiere prin găsirea oricărui fișier care nu are extensia *.txt sau *.ini, întrucât astfel de fișiere nu ar trebui să fie nimic altceva decât imagini, iar dacă nu sunt atunci este vina voluntară a utilizatorului pentru plasarea unui tip de fisier inaccesibil în cadrul arhivei sau în cadrul folderului Temp în timpul rulării. Acest lucru ajută la găsirea imaginii associate fișierului de obstacole asociat cu ea. Este prin urmare absoult necesar ca utilizatorul să nu redenumească fișierele din interiorul arhivelor. În urma problemelor am constatat că această abordare este cea mai bună, întrucât amintește utilizatorul de propria vină și nu apar erori datorită simplei existențe a altor fișiere text în cadrul folderului, cu toate că existența acelor fișiere este tot o urmare a vinei utilizatorului.
Această metodă realizează parsarea prin aceeași metodă ca și în cazul metode de parsare a fișierelor *.tbl, singura diferență constând în existența variabilei ”part” al cărei rol este de a fi un semafor pentru executarea operațiilor după verificarea condițiilor.
Următorul este un exemplu de un fișier *.obs parsabil:
Lines:
40.0,37.0,25.0,113.0
72.0,21.0,135.0,32.0
129.0,57.0,138.0,110.0
Holes:
51.0,45.0,74.0,74.0
Index:
5
RotateValue:
0
NumberOfUses:
1
Segmentele de coliziune(marcaje de coliziune) au următoarele informaíi: coordonatele punctelor x1, y1, x2, y2, unde (x1, y1) și (x2, y2) sunt capetele segmentului.
Găurile au informațiile în aceeași ordine ca și elementele constructorului unui obiect de clasă Ellipse2D.Double.
Indexul reprezintă al câtelea element trebuie să fie această imagine în cadrul unui ArrayList.
Valoarea de rotație are aceeași insemnătate ca și în cazul fișierelor *.tbl, iar NumberOfUses reprezintă de câte ori a fost utilizată imaginea în contextul grafic, fapt care este mai important în cadrul editorului.
După această etapă, toate segmentele de coliziune și găurile sunt translatate prin metoda protected boolean setTranslatedLinesAndHoles() din cadrul clasei GamePanel pentru a se raporta la masa de joc, întrucât inițial sunt raportate la imaginea de care aparțin, și masa de joc este adăugată în cadrul listei de mese de joc (Figura 3.3.1)
Figura 3.3.1
O masă de joc gata pentru a fi rulată în cadrul jocului
3.4. Rezolvarea coliziunilor
Găsirea punctelor de coliziune
Odată pornit jocul, un thread este pornit al cărui rol este de a redesena panoul la intervale regulate de timp redate de o variabilă:
public static final int FRAMES_PER_SECOND = 100
Aceasta înseamnă după cum se paote observa, faptul că se dorește ca programul să ruleze în 100 de cadre pe secundă. Este necesară o cuantă de timp ce reprezintă a 100-a parte din 1000 de milisecunde pentru a simula mișcarea bilelor per cadru:
long delay = 1000/FRAMES_PER_SECOND
Următoarele rânduri reprezintă pe scurt idea din spatele algoritmului ce avansează timpul:
//avansarea animaíei cu timp delta
delta = TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS) – lastTime;
lastTime += delta;
resolveCollisions();
startTime += delay;
long timeDelay = startTime – TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS);
Thread.sleep(Math.max(0, timeDelay));
repaintGameTable();
//repetarea codului
La fiecare rulare a acestui algoritm se execută o metodă care va rezolva coliziunile între bile. În următoarele rânduri vom analiza din punct de vedere mathematic modul în care se reazlizează coliziunile.
Există multe metode de a realiza o coliziune intre două obiecte circulare. Mai întâi trebuie menționat faptul că mișcarea bilelor se realizează cu ajutorul vectorilor ce compun segmental viteză. Acest lucru fiind spus. o metodă mai simpă este cea în care se mută bilele și se verifică coliziunea după efectuarea translației (Figura 3.4.1).
Figura 3.4.1
Deplasarea bilelor pe întreaga distanță și verificarea coliziunii ulterior
În imaginea de mai sus, δt reprezintă produsul dintre cuanta de timp (o valoare subunitară ce masoară cat din o secundă este timpul curent de translație) și vitezele bilei la un moment dat.
Problema care apare aici este dată de faptul că bilele se pot suprapune, sau dacă sunt prea mici sau prea rapide, să nu se ciocnească deloc. Este evident ca nu este sufficient de bună această metodă.
O altă abordare ar fi aceea în care se ia în considerare ce mai rapidă bilă din sistem și se găsește cea mai mică cuantă de timp necesară ca distanța parcursă de cea mai rapidă bilă să nu depășească o valoare mică predefinită(de exempli 0.1 dintr-un pixel), permițând astel erorilor să apară întrucât sistemul are totuși credibilitate. Problema care apare aici este faptul că necesită foarte multă putere de procesare, iar în cazul existenței simultane a peste 40 de bile, pe o mașină de calcul cu un processor Intel Core 2 Duo la 2500Mhz are deja toate nucleele încărcate peste medie și se pot observa clare întârzieri în animație, precum și avertizări asupra neîncadrării operaților în intervalul de timp necesar pentru rularea în 60 de cadre pe secnudă.
O metodă sigură este cea în care se pot muta toate bilele direct cu o cuantă de timp ce reprezintă timpul la care prima coliziune din sistem are loc. Pentru a depista valoarea timpului la care are loc acea coliziune este necesar să ne imaginăm faptul că una din cele două bile testate este staționară. Apoi să ne imaginăm că cealaltă bilă se deplasează cu o viteză egală cu diferența dintre vitezele celor două bile. Bila ce se deplasează o micșorăm până la un punct, iar bila staționară este ”umflată” adăugândui-se la rază, raza celeilalte bile. (Figura 3.4.2)
Figura 3.4.2
Cele două bile din imagine se ciocnesc dacă și doar dacă
punctul ce se deplasează se ciocnește cu bila staționară
Această abordare de a simplifica determinarea coliziunii a două figuri geometrice prin “desumflarea” uneia până la un singur punct este destul de generală. Figura umflată este cunoscută drept “Suma Minkowski” a figurilor originale.
Astfel am reușit să simplificăm această problemă la ideea depistării coliziunii dintre un cerc și un segment (Figura 3.4.3).
Figura 3.4.3
Verificarea intersecției liniei de la p către p + d cu cercul de centru q și rază r
Ne întrebăm dacă linia de la p la p + d intersectează cercul de centru q și rază r. Un punct de pe linie are poziția generală p + td pentru un parametru t. Un astfel de punct se află pe cerc atunci când |p + td – q| = r, adică atunci când:
t2d.d + 2t(d.p – d.q) + p.p + q.q – 2p.q – r2 = 0
Aceasta este o ecuație pătrată în t și vom nota:
a = d.d,
b = 2(d.p – d.q),
c = p.p + q.q – 2p.q – r2
Atunci dacă b2 < 4ac, nu există soluții reale pentru t (linia ratează cercul și nu are loc coliziune), altfel cele două puncte de intersecție sunt soluțiile ecuației:
t = −b ± / 2a
Vom denumi aceste soluții t- și t+. Segmentul original intersectează cercul dacă intervalul [t−,t+] intersectează intervalul [0, 1]. În cazul obișnuit în care p este în afara cercului, aceasta reduce condiția la 0 ≤ t− ≤ 1.
Dacă traducem rezultatul înapoi la problema inițială, soluția t- este prin urmare timpul primei coliziuni a celor două bile reprezentat în ca o valoare subunitară δt explicată anterior.
Metoda aceasta a sumei Minkowski presupune sumarea (dilatarea) a două seturi de vectori de poziție A și B în spațiu Euclidian și este formată prin adăugarea fiecărui vector în A cu fiecare vector din B formând un nou set:
A + B = {a + b | a A}
De exemplu, fie două seturi A și B, fiecare constând din trei vectori de poziție (în mod informal, trei puncte), reprezentând laturile celor două triunghiuri din R2 cu coordonatele:
A = {(1, 0), (0, 1), (0, −1)}
și
B = {(0, 0), (1, 1), (1, −1)} ,
Atunci suma Minkowski este:
A + B ={(1, 0), (2, 1), (2, −1), (0, 1), (1, 2), (1, 0), (0, −1), (1, 0), (1, −2)} , care arată ca un hexagon cu trei puncte repetate la (1, 0). (Figura 3.4.4)
Pentru sumarea Minkowski, setul zero {0}, conín\nd doar vectorul 0, este un element identitate. Pentru fiecare subset S al unui spatiu vectorial:
S + {0} = S;
Setul vid este important în sumarea Minkowski, întrucât setul vid anihilează orice alt subset.
3.4.2. Tratarea problemei pentru mai multe bile
Abordarea explicată în subcapitoul anterior rezolvă complet problema coliziunii a două bile. Însă dacă deținem un sistem cu mai mult decât două obiecte, apare posibilitatea ca un singur obiect să realizeze o coliziune de mai multe ori în durata de timp δt. Prin urmare trebuie găsită o metodă de rezolvare a acestei probmele.
Va trebui să sacrificăm viteza de calcul pentru acuratețe. O simplă soluție este să deplasăm bilele una câte una. Alegem prima bilă și o deplasăm pe traiectoria ei originală până când este depistată prima coliziune. Putem găsi prima coliziune prin identificarea tipilor t- al tuturor posibilelor coliziuni și alegerea celei cu cea mai scăzută valoare. Dacă mai rămâne timp, mutăm bila pe traiectoria ei nouă până la cea de-a doua coliziune și tot așa până se termină timpul. Apoi mutăm și celelalte bile pe rănd cu aceeași metodă.
Această metodă este funcțională, însă nu este încă potrivită pentru o simulare fizică de mare acuratețe precum cea necesară la un joc de biliard.
Este absolut necesar să mutăm bilele simultan. Acest lucru se poate realiza mutând bilele atât timp cât nici una nu a depistat o coliziune. Prin urmare iată pașii unui astfel de algoritm:
P1: Considerăm toate coliziunile posibile între bile și găsim pe cea mai recentă dintre ele la timpul t1.
P2: Mutam toate bilele cu timpul t1. Nu pot exista coliziuni în această perioadă, întrucât cea mai recentă posibilă coliziune a fost la timpul t1.
P3: Înnoim vitezele pentru cele două bile care s-au ciocnit.
P4: Considerăm toate posibilele coliziuni între bile la pozițiile noi și viteze. Se găsește cea mai recentă coliziune la timpul t2.
P5: Deplasăm toate bilele până la timpul t2
P6: Se repetă până când tn > δt.
Figura 3.4.5 arată trei bile aproape de coliziune cu săgeți indicând mișcarea plănuită pentru un interval de timp luat în considerare.
Figura 3.4.6 arată, pentru fiecare pereche de bile, timpul coliziunii acelei perechi.
Figura 3.4.6
Timpii de coliziune
Prin urmare bilele A și C se ciocnesc primele la timpul t=0.5 așa că putem muta toate bilele până la acest interval de timp și să calculăm noile viteze pentru bilele A și C care tocmai au avut o coliziune. Figura 3.4.7 arată noile poziții la timpul t=0.5 cu săgeți indicând mișcările rămase pentru această etapă.
Figura 3.4.7
Poziția la t = 0.5
4. Facilitățile aplicației
4.1. Jocul
Primul ecran întâmpinat este meniul principal (Figura 4.1.1) în care utilizatorul poate alege opțiuni folosind mausul sau tastele direcționale. Opțiunile se accesează folosind un click sau tasta enter.
Opțiunea “Play Now” trimite utilizatorul către următorul ecran unde se va realiza alegerea mesei de joc. A doua opțiune “Table Editor” închide fereastra jocului și deschide o noua fereastră cu programul de creare și editare de mese de joc. Ultima opțiune, așa cum este și denumirea ei “Exit Game”, are rolul de închidere a aplicației.
Al doilea ecran, respectiv ecranul meselor de joc (Figura 4.1.2) conține o listă cu toate fișierele cu extensia *.tbl din directorul selectat. Primul director selectat atunci când aplicația afișează această fereastră pentru prima dată este locația directorului “Maps” din locația aplicației. Utilizatorul se poate întoarce acum la meniul anterior, să închidă aplicația sau să pornească jocul dacă a fost selectată o masă de joc.
La pornirea jocului, utilizatorul este prezentat cu patru ferestre de dialog consecutive. Primele două întreabă care este numele primului și celui de-al doilea jucător, iar in caz de introducere eronată reapare acelașii dialog până când se realizează o alegere validă, mesajul oferind toate informațiile legate despre ce presupune o alegere validă. (Figura 4.1.3)
Figura 4.1.3
Dialogul de introducere al numelui
La sfârșit unilizatorul, sau în acest caz, utilizatorii, sunt întrebați care jucător are drept la prima lovitură, iar in caz de închidere a ferestrei, jucătorul 1 are prima lovitură. De asemenea, dacă orice dialog înaintea acestuia este închis, numele jucatorului va fi cel inițial propus de program, respectiv “Player 1” sau “Player 2”.
După aceea jocul a început! (Figura 4.1.4)
Figura 4.1.4
Un joc de 8 ball în curs cu opțiunea de vizualizare a markerelor bifată
Dacă utilizatorii nu doresc să fie păcăliți de aspectul mesei de joc, au opțiunea de a arăta umbre transparente(markere) ce marchează liniile de coliziune, precum și locurile unde sunt găurile. Jocul nu adoptă toate regulile standardizate ale 8 ballului, întrucât este un joc de tip sandbox ce încurajează utilizatorii să creeze propriile lor scenarii.
4.2. Editorul de table de joc
Editorul (Figura 4.2.1) este a doua aplicație ce se poate rula din meniul principal al jocului. Acesta reprezintă o parte deosebit de importantă a aplicației ca un întreg, întrucât este locul unde se pot crea și modifica mese de joc folosind imagini importate în cadrul acesteia.
La prima pornire, acest program prezintă utilizatorului o interfață intuitivă, punând la dispoziție cat mai multe unelte posibile încă de la început. Cel mai izbitor aspect legat de aceasta este absența meniurilor tradiționale. Această aplicație conține toate facilitățile în partea dreaptă, lăsând cât mai mult loc posibil pentru panoul de proiectare a mesei de joc. Inițial, dimensiunea mesei de joc va fi setată la o dimensiune de 1000 x 1000 de pixeli, însă utilizatorul poate schimba aceste valori folosind butonul . Trebuie menționat faptul că orice imagine găsită în afara noii dimensiuni va fi eliminată din panoul de desen.
Programul oferă posibilitatea selectării unei valori de transparență pentru imaginile de pe masa de joc pentru a identifica mai ușor elementele individuale în cazul în care pot apărea confuzii. Tot în același scop, programul mai oferă si butonul cu ajutorul căruia se poate schimba culoarea de margine ce delimitează imaginile amplasate în momentul în care se intră cu mausul în interiorul panoului de desen.
Butonul mod de selecție () comută între starea de selecție și modul de amplasare.
În primul mod menționat utilizatorul poate selecta mai multe elemente deja amplasate pe panou cu un dreptunghi de selecție, urmând apoi ca elementele selectate să fie șterse cu tasta delete. Comutarea la modul selecție va dezactiva utilizarea butonului de redimensionare a panoului de desen, precum si setările legate de imagini importate și poziționare.
Butonul de comutare la modul bile () este de același timp ca și cel menționat anterior în sensul că acesta comută între lucrul cu bile presupunând amplasarea și ștergerea lor după cum este necesar, respectiv modul standard în care se lucrează cu imagini. Aceste două butoane cu două stări acționează în funcție de starea celuilalt, respectiv modul selecție este folosit pentru bile dacă modul bile este activ.
Butonul deschide un panou (Figura 4.2.3) conținând un set de setări legate de bile. Aici se poate seta dimensiunea tuturor bilelor, programul având grijă ca acestea să nu se intersecteze fie cu ele însele, cu marginea mesei, cu delimitatorii de obstacole sau marcările de găuri precum și faptul că trebuie să fie mereu mai mari decât 10 pixeli. De asemenea se poate alege greutatea bilelor ce nu poate depăși 1000 de unități precum și tipul de bilă ce se dorește a fi adăugată. Există și restricții pentru a asigura faptul că mereu va fi cel mult o singură bilă albă și neagră pe masă.
Urmează apoi setările legate de poziționarea imaginilor, importarea lor în cadrul proiectului curent precum și manipularea orientării acestora. Toate acestea se găsesc în grupul de opțiuni “Imported images” (Figura 4.2.3). În stânga acestui grup se găsesc două casete de bifare. Caseta “To nearest” în cazul în care este bifată, indică faptul că imaginea selectată pentru a fi inserată va încerca să se atașeze de cea mai apropiată imagine alăturată. Opțiunea “Across screen” atunci când este bifată, indică faptul că imaginea curentă va încerca să se atașeze de cea maia propiata imagine de aceasta din întreg panoul pe orizontală sau verticală în funcție de direcția în care a fost mișcată, oferind și indicații vizuale sub forma unor linii ce marchează direcția pe care și locul unde are loc atașarea.
Butonul din mijloc permite importarea unor imagini sau a unui fișier cu extensia *.obs conținând o imagine și caracteristicile sale sau a unui proiect (fișier cu extensia *.tbl) deja creat.
La activarea acestui buton va apărea o fereastră de dialog (Figura 4.2.4) în care utilizatorul este rugat să aleagă o opțiune din cele menționate sau să anuleze comanda.
După aceea utilizatorul poate să caute locația fișierelor dorite și să le importe în program. În cazul importării unui proiect se va pierde tot progresul curent în cazul programului.
Odată importate imagini în cadrul programului, acestea vor fi avea afișată o mini iconiță reprezentând aspectul lor, însă redimensionat pentru a încăpea într-un pătrat de dimensiuni predefinite și relative la acest panou și la restul conținutului acestuia (Figura 4.2.5).
Utilizatorul poate selecta mai multe elemente din lista de imagini folosind aceași tehnică folosită și la selecția multiplă a fișierelor în sistemele de operare Windows, respective folosind tastele control și shift împreună cu mouse-ul. După aceea se pot șterge elementele selectate cu tasta delete.
Pentru a putea fi amplasată o imagine pe panoul de desen trebuie să fie selectată una singură, iar aplicația nu permite ca o imagine să intersecteze pe celelalte sau marginea panoului.
Butoanele “Step 1” și “Step 2” (Figura 4.2.6) au drept scop comutarea între modul de amplasare a imaginilor și bilelor și modul de delimitare a obstacolelor și amplasarea marcajelor pentru găuri în cazul pozelor utilizate. În funcție de starea curentă a programului doar un buton din cele menționate va fi vizibil pe ecran, starea inițială fiind etapa 1. În etapa 2 iconițele imaginilor importate vor fi înlocuite cu iconițele doar acelor imagini care au fost amplasate cel puțin o data în cadrul panoului de desen, iar selecția multiplă va fi dezactivată, întrucât ștergerea imaginilor care au corespondent pe planșa de desen nu este permisă decât în cazul în care imaginea respectivă nu a fost utilizată.
În cadrul etapei 2 butoanele și setările corespunzătoare imaginilor sunt dezactivate împreună cu butonul ce permite redimensionarea panoului de desen. Acest lucru se întâmplă deoarece panoul de desen îndeplinește o cu totul altă funcție aici. Scopul acestuia este să afișeze doar imaginea selectată pentru a urma să îi fie adăugate caracteristici. Aceste caracteristici sunt un set de segmente și un set de discuri circulare care au rolul de a delimita pozițiile obstacolelor și găurilor cu care vor interacționa bilele.
Butonul “Hole Mode” () apare doar în etapa 2 a programului și are un rol similar cu butonul de comutare la modul bilă în sensul că fa schimba între trasarea marcajelor pentru găuri sau marcajele pentru obstacole.
Butonul “Finalise!” va deschide un dialog (Figura 4.2.7) ce va oferi utilizatorului opțiunea de a salva toate imaginile folosite împreună cu delimitatorii de obstacole și marcajele de găuri în fișiere importabile cu extensia *.obs sau opțiunea de a salva toate setările și imaginile într-un singur proiect și salvându-l cu extensia *.tbl.
Daca utilizatorul dorește să salveze lucrul ca un proiect, acesta trebuie să îndeplinească câteva criterii esențiale altfel un dialog (Figura 4.2.8) îl va preveni să progreseze. În primul rând accesul la a doua etapă a editării nu este posibilă fără cel puțin o imagine amplasată pe panou. Deschiderea panoului de finalizare este apoi posibil însă în momentul salvării unui proiect utilizatorul trebuie să fi amplasat o singură bilă albă, o singură bilă neagră și un număr egal de bile pline și dungate diferit de zero. De asemenea trebuie să fie cel puțin o imagine având marcată cel puțin o gaură și totalitatea găurilor marcate din cadrul imaginilor utilizate să fie mai mari decât bilele. Dacă toate condițiile menționate sunt îndeplinite atunci crearea proiectului este posibilă, altfel utilizatorul va fi întâmpinat de un mesaj de dialog care va explica motivele pentru care crearea proiectului nu este posibilă. Dacă toate condițiile sunt îndeplinite, atunci se oferă opțiunea de a salva proiectul în directorul standard, acesta fiind localizat în interiorul directorului principal al aplicației și poartă numele “Maps”. Această opțiune este oferită și pentru salvarea fișierelor de setări individuale (*.obs) în directorul “Obstacles” din aceeași locație. Dacă utilizatorul dorește o locație specială atunci va avea și această posibilitate.
Se poate comuta ușor la etapa unu, așa cum am menționat, folosind butonul din partea de joc a aplicației pentru ca utilizatorul să își corecteze greșelile sau să adauge mai multe elemente mesei de joc. Dacă masa de joc este prea mare pentru a fi afișată corespunzător pe ecran, se poate naviga pe lungimea și lățimea acesteia folosind barile de derulare orizontale și verticale aflate lângă panoul de desen.
Programul oferă o imagine de ansamblu a întregului panou în miniatură atunci când se derulează una din aceste bare (Figura 4.2.9).
Acest editor este prin urmare o unealtă puternică ce pune puterea direct în mâinile utilizatorului, dându-i șansa să își exprime creativitatea și să realizeze o masă de joc cu care va putea să interacționeze în joc. Lipsa meniurilor tradiționale și înlocuirea lor cu butoane atractive care fac din operații în mod normal dificile o parte a jocului sporind longevitatea și atractivitatea aplicației este cu siguranța ceva ce mulți utilizatori vor aprecia.
Concluzie
Această aplicație, în pofida simplității conceptului la o primă vedere, este totuși complexă, tratând probleme de simultaneitate, sincronizarea firelor de execuție, precum și tratarea corectă a erorilor. Interacțiunile cu interfața grafică, împreună cu posibilele conflicte între evenimente și metoda de desenare, nu sunt deloc aspecte ușor ignorate, întrucât o simplă omitere de a actualize valorile variabilelor, pot avea efecte dezastruase asupra celorlate elemente ale programelor.
Realizarea unui proiect prin separarea claselor în pachete corespunzătoare este de multe ori un lucru pozitiv, ușurând programarea ulterioară datorită structurii și legăturii logice între obiecte.
În cazul în care această partajare este realizată într-o manieră greșită, poate genera mai multe probleme decât să resolve forțând utilizatorul să creeze metode și variabile publice pentru a oferi accesul din alte pachete, însă astfel crește și pericolul ca și acele variabile și metode să fie modificate sau apelate în mod greșit, distrugând logica plicației.
În final, MakeYourOwnPool este o aplicație flexibilă ce tratează probleme de sincronizare ale thread-urilor și probleme legate de sincronizarea simulărilor fizice oferind de asemenea facilități puternice utilizatorului sporind astfel interesul acestuia pe o perioadă de timp cât mai lungă.
Bibliografie
Marc Loy, Robert Eckstein, Dave Wood, James Elliot, Brian Cole, Developing GUIs in Java: Java Swing Second Edition, O’Reilley, ISBN: 9780596004088.
Scott Oaks, Henry Wong, Understanding and Mastering Concurrent Programming: Java Threads Third Edition, O’Reilley.
Croft, H. T.; Falconer, K. J.; and Guy, R. K. Unsolved Problems in Geometry. New York: Springer-Verlag, p. 3, 1991.
Gray, A. Modern Differential Geometry of Curves and Surfaces with Mathematica, 2nd ed. Boca Raton, FL: CRC Press, p. 130, 1997.
Zwillinger, D. (Ed.). "Affine Transformations." §4.3.2 in CRC Standard Mathematical Tables and Formulae. Boca Raton, FL: CRC Press, pp. 265-266, 1995.
Arrow, Kenneth J.; Hahn, Frank H. (1980). General competitive analysis. Advanced textbooks in economics. 12 (reprint of (1971) : Holden-Day, Inc. Mathematical economics texts. 6 ed.). : North-Holland. ISBN 0-444-85497-5. MR 439057.
NTNUJAVA Virtual Physics Laboratory, Topic: 2D Collision, [http://www.phy.ntnu.edu.tw/ntnujava/index.php?topic=4], accesat noiembrie 2012.
Landau LD and Lifshitz EM (1976) Mechanics, 3rd. ed., Pergamon Press. ISBN 0-08-021022-8.
Raymond, David J.. "10.4.1 Elastic collisions". A radically modern approach to introductory physics: Volume 1: Fundamental principles. Socorro, NM: New Mexico Tech Press. ISBN 978-0-9830394-5-7.
Goetz, Brian; Joshua Bloch, Joseph Bowbeer, Doug Lea, David Holmes, Tim Peierls (2006). Java Concurrency in Practice. Addison Wesley. ISBN 0-321-34960-1.
Bibliografie
Marc Loy, Robert Eckstein, Dave Wood, James Elliot, Brian Cole, Developing GUIs in Java: Java Swing Second Edition, O’Reilley, ISBN: 9780596004088.
Scott Oaks, Henry Wong, Understanding and Mastering Concurrent Programming: Java Threads Third Edition, O’Reilley.
Croft, H. T.; Falconer, K. J.; and Guy, R. K. Unsolved Problems in Geometry. New York: Springer-Verlag, p. 3, 1991.
Gray, A. Modern Differential Geometry of Curves and Surfaces with Mathematica, 2nd ed. Boca Raton, FL: CRC Press, p. 130, 1997.
Zwillinger, D. (Ed.). "Affine Transformations." §4.3.2 in CRC Standard Mathematical Tables and Formulae. Boca Raton, FL: CRC Press, pp. 265-266, 1995.
Arrow, Kenneth J.; Hahn, Frank H. (1980). General competitive analysis. Advanced textbooks in economics. 12 (reprint of (1971) : Holden-Day, Inc. Mathematical economics texts. 6 ed.). : North-Holland. ISBN 0-444-85497-5. MR 439057.
NTNUJAVA Virtual Physics Laboratory, Topic: 2D Collision, [http://www.phy.ntnu.edu.tw/ntnujava/index.php?topic=4], accesat noiembrie 2012.
Landau LD and Lifshitz EM (1976) Mechanics, 3rd. ed., Pergamon Press. ISBN 0-08-021022-8.
Raymond, David J.. "10.4.1 Elastic collisions". A radically modern approach to introductory physics: Volume 1: Fundamental principles. Socorro, NM: New Mexico Tech Press. ISBN 978-0-9830394-5-7.
Goetz, Brian; Joshua Bloch, Joseph Bowbeer, Doug Lea, David Holmes, Tim Peierls (2006). Java Concurrency in Practice. Addison Wesley. ISBN 0-321-34960-1.
Copyright Notice
© Licențiada.org respectă drepturile de proprietate intelectuală și așteaptă ca toți utilizatorii să facă același lucru. Dacă consideri că un conținut de pe site încalcă drepturile tale de autor, te rugăm să trimiți o notificare DMCA.
Acest articol: Joc de Biliard cu Masa Customizabila (ID: 159676)
Dacă considerați că acest conținut vă încalcă drepturile de autor, vă rugăm să depuneți o cerere pe pagina noastră Copyright Takedown.
