Documente Academic
Documente Profesional
Documente Cultură
//Generic Code
List<Integer> list = new ArrayList<Integer>();
In exemplul anterior ambele linii creaza o lista de obiecte. Prima creza un sir ce accepta orice
tip de obiect. Aceasta inseamna ca anumite metode ale clasei ArrayList necesita conversie
manuala ceea ce poate conduce la aparitia erorilor la executie.
In cea de-a doua linie am folosit cod generic. Tipul elementelor sirului a fost scris intre
semnele <> si determina ca toate elementele sa fie de tip Integer.
Avantajele folosirii tipului generic sunt:
- Colectiile generice stocheaza numai tipuri cunoscute
- Erorile de tip sunt prinse inca in faza de compilare
- In majoritatea cazurilor elimina nevoia de conversie
- Codul este mai usor de inteles
- Timpul de executie este scurtat
- Este usor de trecut de la legacy la generic si invers pentru ca exista backward
compatibilty intre generic si legacy
Dezvantaje:
- Codul generic e greu de dezvoltat
- Codul generic e greu de interpretat
Urmatorul exemplu ilustreaza producerea unei erori la executie din cauza folosirii defectoase
a tipului row (legacy code). Elementele listei se considera de tip Object. Eroare nu a fost
identificata la compilare. In versiuni mai mari de 5, avem atentionari in ceea ce priveste
folosirea tipului row.
import java.util.*;
public class GenericError {
public static void main(String[] args) {
// This list is not type safe, as it can store any type of object
List list = new ArrayList();
list.add(5);
list.add("42");
list.add(5);
list.add("4");
// constructorul
ABC (T aObj) {
obj = aObj
}
class GenTest {
public static void main(String[] args) {
// initializarea instantelor claselor generice Gen
Gen<Integer> a = new Gen<Integer>();
Gen<String> b = new Gen<String>();
a = b; //eroare!
}
}
Putem folosi colectii generice. Acestea pot fi declarate cat si intializate ca generice. Daca o
colectie este initializata ca generica, dar nu are o declaratie generica atunci ea va accepta
obiecte arbitrare. Aceasta este ilustrat in exemplul urmator:
List list =new ArrayList<Integer>();
Apeluri ca:
list.add(5);
list.add("4");
sunt posibile, dar vor genera atentionari la compilare. La executie, insa, urmatoarea sintaxa va
genera eroare:
for (Iterator<Integer> i = list.iterator(); i.hasNext();) {
int value = (i.next()) * 10;
System.out.println(value);
}
O restricie fundamentala in cazul programarii generice este ca un tip generic T nu este
compatibil cu Object. Astfel, nu putem face atribuirea List<T> la un List<Object>.
Motivatia vine din faptul ca un obiect de orice tip ar putea fi inserat in lista.
public class Generic {
public static void main(String[] args) {
List<Account> list = new ArrayList<Account>();
class Account {
}
Tipul List<Object> este practic identic cu codul legacy List.
In anumite situatii trebuie sa facem conversii explicite cand utilizam tipuri generice. Cazul cel
mai frecvent este atunci cand folosim obiecte instante ale unor clase derivate din clasa
declarata ca parametru. Un exemplu este in cele ce urmeaza:
public class Generic {
public static void main(String[] args) {
List<Account> list = new ArrayList<Account>();
// Adaugarea unui element dintr-o clasa derivata din Account
list.add(new Mortgage());
// cand extragem elementul trebuie sa facem un cast
Mortgage mortgage = (Mortgage) list.get(0);
}
}
class Account {
}
class Mortgage extends Account {
}
Putem adauga si liste intregi intre elementele unei liste existente, caz ilustrat mai jos:
import java.util.*;
public class Generic{
public static void main(String[] args) {
List<Mortgage> mortgageList = new ArrayList<Mortgage>();
mortgageList.add(new Mortgage());
List<Deposit> depositList = new ArrayList<Deposit>();
depositList.add(new Deposit());
List<Account> accountList = new ArrayList<Account>();
accountList.addAll(mortgageList);
accountList.addAll(depositList);
}
}
class Account {
}
class Mortgage extends Account {
}
class Deposit extends Account {
}
Daca adaugarea a fost posibila, in cazul extragerii vom avea nevoie de cast. Atribuirea directa
nu este posibila, asa precum am aratat anterior. Urmatorul cod genereaza eroare la compilare:
accountList=mortgageList;
Solutia pentru atribuirea subtipului unui supertip este folosirea sintaxei: <? extends
Account>. ? poarta denumirea de wildcard. Codul din exemplul anterior se modifica, pentru
atribuire, dupa cum urmeaza:
import java.util.*;
public class Generic {
public static void main(String[] args) {
List<Mortgage> mortgageList = new ArrayList<Mortgage>();
mortgageList.add(new Mortgage());
// constructor
Sum (T[] o){
nums = o;
}
return sum;
}
// constructor
Sum (T[] o){
nums = o;
}
return sum;
}
Lint warnings
Lint reprezinta o functionalitate a Javei ce ne permite sa identificam cod nesigur. Lint
warnings se genereaza cand incercam sa compilam cod nesigur. Recompilarea unui cod cu
–Xlint:unchecked, va genera avertismentele de cod nesigur indicand locul in care pot
aparea erori la executie.
public T get() {
return genericValue;
}
gen1.set("World");
System.out.println("gen1.genericValue is " + gen1.get());
}}
Codul declara trei obiecte de tip GenericClass, dar de versiuni diferite.
Subclase generice
Clasele generice pot forma ierarhii de clase.
Cand o clasa generica mosteneste o alta clasa generica tipul parametru al superclasei trebuie
sa fie declarat de subclasa. Aceasta trebuie realizata chiar daca tipul nu este folosit in
subclasa.
class SubClasaGenerica<T,V> extends ClasaGenerica<T>
Cand o clasa generica mosteneste o clasa negenerica nu trebuie sa specificam niciun
argument.
class SubClasaGenerica<T> extends ClasaNegenerica
In continuare vom da un exemplu pentru a ilustra primul caz.
public class Sum {
public static void main(String[] args) {
GenericClass<String> gen1 = new GenericClass<String>("Hello");
GenericClass<Integer> gen2 = new GenericClass<Integer>(8);
GenericClass<Double> gen3 = new GenericClass<Double>(2.34);
gen1.set("World");
System.out.println("gen1.genericValue este " + gen1.get());
// urmatoarea linie ar cauza eroare la compilare
// gen1.set(gen2.get());
if (b1) {
// urmatoarea linie ar cauza eroare la compilare
// GenericClass<Integer> genInt1 = (GenericClass<String>)
subGen;
}
}
class GenericClass<T> {
private T genericValue;
public T get() {
return genericValue;
}
this.subType = subType;
}
public V getSubtype() {
return subType;
}
}
Operatorul instanceof ne permite sa determinam daca un obiect este o instanta a unei clase.
Acelasi lucru poate fi facut si pentru clasele generice.
In codul anterior subGen este o instanta a lui GenericClass.
Linia ce-l declara pe b3 va genera o eroare la compilare. Tipul GenericClass<String> nu
exista la rulare pentru ca informatiile despre tipul obiectelor sunt indepartate la rulare printr-
un proces numit type erasure.
Putem converti o instanta a unei clase generice daca sursa si destinatia au argumente de tip
identice sau compatibile.
Tema: Sa se scrie o clasa utilitar generica, fara a apela la colectii (liste, set, map, etc), cu
metode statice, ce ofera operatii pe un sir de obiecte generice. Apoi, folosind doar metodele
acestei clase sa se afle maximul unui sir, de obiecte concrete, si pozitia pe care acesta se afla.
In final ultimele doua elemente ale sirului sa se inlocuiasca cu valoarea maximului, respectiv
a pozitiei acestuia.
public static <T, S extends T> S max(S[] coll, Comparator<? super T> comp);
public static <T, S extends T> int binarySearch(S[] list, T key, Comparator<?
super T> c);
public static <T extends Comparable<? super T>> void sort(T[] list);
@SafeVarargs
public static <T, S extends T> void modifyRear(S[] c, T... elements);//pentru
modificarea valorilor ultimelor elemente din sir
Tipul Enum
Folosirea constantelor date prin public static final este foarte folositoare, dar exista un anumit
caz in care ar trebui evitata si anume atunci cand constantele ar produce o interpretare
nepotrivita a unei validari intr-un domeniu de verificat.
Fie urmatorul exemplu: O metoda are ca argument un int, care reprezinta o stare dintr-o
multime de stari (sa spunem 0, 1 sau 2). Valoarea lui int trebuie sa fie doar intr-un domeniu
specificat. Metoda ar trebui sa verifice (cel mai probabil printr-un if) daca acest lucru se
intampla.
Acesata situatie poate fi imbunatatita folosind enumerari.
O enumerare este o lista finita de valori constante. Aceasta multime de valori trebuie sa fie
bine definita si specifica unui anumit tip de data.
Java detine cuvantul rezervat enum cu mult timp inainte de a-l fi implementat. In J2SE 5.0
este inclus tipul enumerare care extinde clasa java.lang.Enum. Spre deosebire de alte
limbaje, enumerarile in Java sunt instante ale unei clase.
Cand declaram enumerari putem include orice metoda, constructori si variabile necesare.
Pentru ca enumerarile Java se bucura de toate beneficiile de a fi clase „fully-fledged” (cu
drepturi depline), compilatorul executa verificari de tip (la compilare) cand folosesc instante
de enumerari. Totusi, compilatorul nu permite definirea claselor care extind in mod explicit
java.lang.Enum.
Enumerarile sunt eficiente pentru ca:
- sunt type safe: adica compilatorul nu va permite folosirea unui obiect incorect atunci
cand un anumit tip de enumerare este asteptat.
- pot contine membri arbitrari
- au valori public static final: deci nu pot fi modificate. Putem compara aceste
valori folosind operatorul == sau metoda equals();
- implementeaza interfetele Comparable si Serializable: deci putem folosi metoda
compareTo() pentru a compara si sorta valorile enumerarii.
- pot suprascrie metoda toString(): folosita pentru a returna numele valorilor din
enumerare. Putem suprascrie metoda pentru a personaliza output-ul
- ofera o metoda valueOf(): Enumerarile includ metodele statice values() (public
static enum-type[] values()) si valueOf() (public static enum-type
valueOf(String string)), metode care ne permit sa iteram valorile enumerarii.
Metoda valueOf() este complementul metodei toString(), asadar trebuie sa
suprascriem ambele metode daca dorim sa suprascriem metoda valueOf().
- definesc o metoda finala numita ordinal(): returneaza fiecare pozitie a valorilor
enumerarii, in ordinea in care ele sunt definite.
Declararea unui tip de date enumerare se face folosind cuvantul cheie enum urmat de numele
tipului si setul de valori.
public static enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY }
}
In exemplul anterior am folosit clasa EnumSet din pachetul java.util care gestioneaza
submultimile de valori ale enumerarii.
Putem defini o enumerare intr-o sursa separata, ca in exemplul urmator:
public enum ComplexEnum {
FIRST(1), THIRD(3), TENTH(10) ;
Unit Testing
O unitate de tesatare este un test scris de un programator, pentru o functionalitate singulara a
unei aplicatii. Unit testele trebuie sa fie fine grained. Ele nu testeaza functionalitatea de nivel
inalt a aplicatiei. Testarea functionalitatii aplicatiei se numeste acceptance testing si este
facuta de persoane care inteleg mult mai bine functionalitatea aplicatiei decat programatorii.
Unit testele ofera increderea ca codul lucreaza corect. De indata ce toate testele trec inseamna
ca imbunatatirea facuta in cod nu a stricat nimic din ceea ce exista deja. Aceasta tehnica este
numita dezvoltare test-driven.
Programatorii scriu testele pentru ca se considera ca ei stiu cel mai bine codul si pot astfel
anticipa mare majoritate a problemelor ce pot aparea.
Testele trebuie facute pentru orice metoda ce are o implementare nontriviala. Iata descrierea
unui proces test-driven:
- rularea tuturor unit testelor intregului proiect si asigurarea ca ele trec
- scrierea unui unit test pentru noua implementare facuta
- rularea testului si observarea erorii
- reluarea implementarii si corectarea erorii
- reluarea testului pina ce acesta trece
Un pas important este testarea anumitor aspecte ce pot duce la esuarea executiei, cum ar fi
argumentul ilegal al unei metode. Ultimul pas este retestarea intregului proiect, astfel ca noua
functionalitate introdusa in aplicatie sa nu creeze probleme globale.
Toate acestea trebuie facute inainte de comiterea pe un repository.
Trebuie sa cream, de asemenea, teste atunci cand sunt raportate bug-uri. Procesul este:
- scrierea unui test ce expune bug-ul
- rularea intregii proceduri de testare si observarea erorii
- repararea bug-ului
- rularea intregii proceduri de testare si observarea succesului
Toate testele trebuie scrise in stilul trece/nu trece. Fie urmatoarea iesire a unui test:
Now Testing Person.java:
First Name: Tanner
Last Name: Burke
Age: 1
Acest test a trecut sau nu? Ar trebui cautat in cod pentru a descoperi interpretarea acestui
rezultat. Putem grupa testele si afisa un rezultat mult mai transparent dupa cum urmeaza:
Now Testing Person.java:
Failure: Expected Age 2, but was 1 instead
Now Testing Account.java:
Passed!
Now Testing Deposit.java:
Passed!
Summary: 2 tests passed, 1 failed.
Cateva sfaturi pentru realizarea testelor:
- testarea conditiilor la limita, cum ar fi: indicele maxim sau minim al sirurilor si listelor
- testarea intrarilor ilegale in metode
- testarea stringurilor nule sau goale. Testarea stringurilor ce contin spatii goale
nepermise
Testele de acceptare trebuie sa fie, de asemenea, create pentru a trece sau esua, si trebuie
facute cat mai automate cu putinta. Dar spre deosebire de unit teste, testele de acceptare nu
trebuie sa treaca 100%. Spre exemplu testele de acceptare sunt scrise inainte ca
functionalitatea sa die scrisa.
Refactorizarea
Refactorizarea este o practica de imbunatatire a design-ului codului fara a modifica
functionalitatea. Prin refactorizare se mentine codul cat se poate de simplu. Iata un exemplu:
public class Person {
private String firstName;
public void setFirst(String n) {
this.firstName = n;
}
}
Codul poate fi imbunatatit prin alegerea unui nume mai potrivit argumentului:
public class Person {
private String firstName;
public void setFirst(String firstName) {
this.firstName = firstName;
}
}
Codul poate fi imbunatatit si mai mult prin redenumirea metodei:
public class Person {
private String firstName;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}
Modificarea numelui unei metode determina modificarea referintelor metodei din intreaga
aplicatie.
Eliminarea duplicarilor este un alt scop al refactorizarii. Codul duplicat este greu de intretinut,
pentru ca inseamna modificari in mai multe locuri.
Refactorizarea trebuie facuta constant, pe tot timpul existentei proiectului. Refactorizarea tine
cont de design pattern-i. Scrierea unit testelor este o buna metoda de identificare a portiunilor
de cod ce necesita refactorizare. Cand scriem un test pentru o clasa devenim clienti ai clasei si
putem realiza ca API-ul este prea complicat. Este momentul in care acesta trebuie refactorizat
si facut simplu de utilizat.
Este recomandat ca inainte de refactorizare sa avem un unit test la indemana. El ar trebui sa
treaca si dupa refactorizare.
JUnit
Testele unitare sunt simple, preferabil continute intr-o singura comanda.
Pentru a crea un test case cu JUnit vom crea o subclasa a lui junit.framework.TestCase.
Fiecare unit test este o metoda publica, fara argumente ce are prefixul test. Daca nu vom
urma aceasta conventie de nume JUnit nu va fi capabil sa localizeze metodele de testare
automat. In acest caz va trebui sa scriem o metoda suite() si sa construim instante ale test
case-urilor, trimitand numele metodelor de testare constructorului.
Fie urmatorul exemplu:
import junit.framework.TestCase;
/**
* Sample unit tests for the {@link Person} class.
*/
public class TestPerson extends TestCase {
/**
* A unit test to verify the name is formatted correctly.
*/
public void testGetFullName( ) {
Person p = new Person("Aidan", "Burke");
assertEquals("Aidan Burke", p.getFullName( ));
}
/**
* A unit test to verify that nulls are handled properly.
*/
public void testNullsInName( ) {
Person p = new Person(null, "Burke");
assertEquals("? Burke", p.getFullName( ));
// this code is only executed if the previous
assertEquals passed!
p = new Person("Tanner", null);
assertEquals("Tanner ?", p.getFullName( ));
}
}
O metoda de testare poate arunca si o exceptie. In acest caz, JUnit prinde exceptia si
raporteaza o eroare. El va continua sa execute alte metode de testare.
Unit testele utilizeaza metode assertXXX() pentru testare. Spre exemplu:
assertEquals("Aidan Burke", p.getFullName( ));
Aceasta metoda confirma ca cele doua argumente sunt egale, folosind eventual metoda
equals(). Astfel testul trece. Altfel, testul esueaza si se trimite un raport si restul testului este
sarit.
Pentru a rula un JUnit trebuie sa includem junit.jar in classpath.
La laborator exemple de rulare cu Eclipse.
Putem folosi metoda assertTrue() pentru aproape orice test, dar pentru a face testele mult
mai usor de inteles si pentru a avea mesaje mai bune de eroare folosim assertXXX().
Toate metodele accepta un prim parametru, optional, de tip String. Acesta furnizeaza un
mesaj descriptiv in cazul in care testul esueaza. Spre ex:
assertEquals("Employees should be equal after the clone( ) operation.",
employeeA, employeeB);
Metoda assertSame() compara referintele a doua obiecte, asigurandu-ne ca ele refera
aceeasi adresa de memorie. Urmatoarele apeluri sunt echivalente:
assertSame("Expected the two parts to be identical.", part1, part2);
assertTrue("Expected the two parts to be identical.", part1 == part2);
Metoda assertEquals() poate compara si double-uri si float-uri. Metoda accepta un
argument delta folosit pentru rotunjire.
Testarea valorii de null este simpla:
assertNull("Did not expect to find an employee.",
database.getEmployee("someInvalidEmployeeId"));
In mod asemanator putem testa daca valoarea returnata este ne nula:
assertNotNull("Expected to find an employee with id=" + id,
database.getEmployee(id));
Putem explicit cauza esuarea unui test folosind metoda fail(). Spre ex:
fail("Unable to configure database.");
Fiecare unit test ar trebui sa verifice o functionalitate specifica. Nu ar trebui sa combinam
teste multiple, fara legatura, in aceeasi metoda. Spre exemplu:
public void testGame( ) throws BadGameException {
Game game = new Game( );
Ship fighter = game.createFighter("001");
assertEquals("Fighter did not have the correct identifier", "001",
fighter.getId( ));
Ship fighter2 = game.createFighter("001");
assertSame("createFighter with same id should return same object",
fighter, fighter2);
assertFalse("A new game should not be started yet",
game.isPlaying( ));
}
Acesta este un exemplu de design prost. Metoda testeaza parti neinrudite de functionalitate.
Refactorizat, testul anterior se transforma in mai multe teste ce testeaza aspecte independente.
public void testCreateFighter( ) throws BadGameException {
Game game = new Game( );
Ship fighter = game.createFighter("001");
assertEquals("Fighter did not have the correct identifier", "001",
fighter.getId( ));
game.shutdown( );
}
public void testSameFighters( ) throws BadGameException {
Game game = new Game( );
Ship fighter = game.createFighter("001");
Ship fighter2 = game.createFighter("001");
assertSame("createFighter with same id should return same object",
fighter, fighter2);
game.shutdown( );
}
public void testGameInitialState( ) throws BadGameException {
Game game = new Game( );
assertFalse("A new game should not be started yet", game.isPlaying(
));
game.shutdown( );
}
Esuarea unui test nu duce la esuarea si a celorlalte. Combinarea mai multor asertiuni in
acelasi test se justifica atunci cand cand intr-o succesiune de teste, esuarea primului ar duce
oricum si la esuarea urmatoarelor.
Conventia de nume
O conventie de nume consistenta are doua scopuri: primul este ca face codul mult mai usor de
intretinut si cel de-al doilea ca faciliteaza automatizarea. Am prefixat toate clasele de test,
date ca exemplu pina acum, cu prefixul Test. Spre exemplu TestPerson, care corespunde
clasei Person.
Pe de alta parte creand un test pe fiecare clasa, acesta este mult mai usor de identificat intr-un
director.
Manipularea exceptiilor
In exemplul urmator constructorul clasei Person ar trebui sa arunce o
IllegalArgumentException daca ambele argumente sunt null. Testul esueaza daca nu se
arunca exceptia.
public void testPassNullsToConstructor( ) {
try {
Person p = new Person(null, null);
fail("Expected IllegalArgumentException when both args are
null");
} catch (IllegalArgumentException expected) {
// ignore this because it means the test passed!
}
}
Aceasta este singura tehnica buna de folosit atunci cand asteptam o exceptie. Pentru alte
conditii de eroare, va trebui sa lasam ca exceptia sa se propage catre JUnit. Acesta va prinde
exceptia si o va raporta o eroare de tetare. Mai jos este o situatie ce ar trebui evitata:
public void testBadStyle( ) {
try {
SomeClass c = new SomeClass( );
c.doSomething( );
...
} catch (IOException ioe) {
fail("Caught an IOException");
} catch (NullPointerException npe) {
fail("Caught a NullPointerException");
}
}
Prima problema este ca JUnit oricum prinde exceptiile neprinse, asadar codul scris este inutil,
iar introducerea blocurilor try/catch cresc complexitatea testelor, facandu-le greu de
intretinut. Iata varianta corecta a codului de mai sus:
public void testGoodStyle( ) throws IOException {
SomeClass c = new SomeClass( );
c.doSomething( );
...
}
Exceptii
Aplicatiile intalnesc erori la executie. Aplicatiile robuste trebuie sa manipuleze erorile cat mai
elegant cu putinta. Cateva motive de aparitie a erorilor:
- Aplicatia nu are comportamentul asteptat
- rezultat al unui bug
- factori din spatele aplicatiei: conexiune la baza de date intrerupta, functionarea driver-
elor, etc
In Java, atunci cand folosim resurse externe, compilatorul cere sa manipulam exceptiile ce pot
aparea (spre ex: java.nio, java.sql). A manipula o exceptie inseamna sa adaugam in cod
un bloc de manipulare a erorii (try-catch). A declara o exceptie inseamna ca declaram ca o
metoda poate esua in executie sa (clauza throws).
O exceptie reprezinta, asadar, un eveniment ce intrerupe executia normala a unui program.
Cand se intalneste o exceptie la rulare, metoda in care a aparut exceptia creaza un obiect
exceptie. Metoda poate arunca exceptia inapoi catre metoda apelanta, care probabil o va trata.
Obiectul exceptie include informatii despre exceptie, cum ar fi metoda in care exceptia a
aparut si cauza exceptiei. Metoda apelanta poate trimite exceptia, mai departe, unei alte
metode, care la randul sau a apelat-o. Daca nicio metoda nu manipuleaza exceptia, programul
se termina cand obiectul exceptie atinge metoda main().
Cand apare o exceptie o putem preveni trimitand-o in stiva de apel sau tratand-o, astfel
aplicatia continundu-si executia pe o alta cale.
Blocul try cuprinde codul ce poate cauza o exceptie. Acest bloc este numit si cod protejat
(protected code). Un bloc try este urmat de zero sau mai multe blocuri catch ce specifica
tipurile de exceptie ce pot fi prinse.
catch ar trebui sa fie folosit pentru:
- a reincerca executia unei operatii
- incercarea de a executa o alta operatie
- o iesire eleganta sau apelul lui return
Scrierea unui bloc catch vid este o bad practice.
Blocurile catch se executa in ordinea in care au fost scrise, de sus in jos, pina la intilnirea
primei exceptii ce se potriveste ca tip cu cel al tipului aruncat. Cel mult un bloc catch se
executa. De aceea, vom plasa blocul catch, cel mai general, pe ultima pozitie in blocurile din
sirul blocurilor catch. Daca nu se arunca nicio exceptie atunci nici un bloc catch se executa.
Blocul finally este optional, daca try este urmat de macar un catch. Blocul finally se
executa indiferent daca avem sau nu aruncata exceptie, cu o singura exceptie, atunci cand
executam System.exit.
// sintaxa de baza a lui try
try {
// cod ce poate arunca o exceptie
}
catch (TipExceptie1 Identificator1) {
// cod de manipulare a exceptiei
}
catch (TipExceptie2 Identificator2) {
// cod de manipulare a exceptiei
}
finally {
// 0 sau 1 clauze finally
}
Cand deschidem resurse precum fisiere sau conexiuni la baza de date, trebuie intotdeauna sa
le inchidem. Incercarea de a le inchide in try poate fi problematica deoarece putem incheia
blocul sarind operatia. De aceea este recomandata inchiderea acestora in blocul finally.
Uneori operatia pe care o executam in blocul finally poate genera o exceptie. In acest caz
va trebui sa scrie un bloc try-catch in interiorul lui finally. Putem avea try-catch si in
interiorul lui try sau al lui catch.
public class FinallyExampleMain {
public static void main(String[] args) {
InputStream in = null;
try {
System.out.println("About to open a file");
in = new FileInputStream("missingfile.txt");
System.out.println("File open");
int data = in.read();
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
System.out.println("Failed to close file");
}
}
}
}
Toate exceptiile si erorile Java sunt subclase ale clasei Throwable din pachetul java.lang.
Putem loga exceptiile ce apar pe parcursul aplicatiei folosind Apache Log4j si framework-uri
predefinite din java.util.
In Java SE7 avem la dispozitie un try cu resurse, ce va inchide automat resursele. Astfel
eliminam folosirea lui finally. Orice clasa ce implementeaza java.lang.AutoCloseable
poate fi utilizata ca resursa. Pentru ca o resursa sa fie automat inchisa, referinta sa trebuie
declarata in interiorul parantezelor rotunde ale lui try.
public class FinallyExampleMain {
public static void main(String[] args) {
System.out.println("About to open a file");
try(InputStream in = new FileInputStream("missingfile.txt")) {
System.out.println("File open");
int data = in.read();
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
Putem deschide mai multe resurse daca ele sunt separate prin punct si virgula. Inchiderea lor
se va face in ordine inversa deschiderii.
Daca o exceptie apare in momentul crearii unei resurse AutoCloseable, controlul va fi
preluat imediat de blocul catch.
Daca o exceptie apare in blocul try, toate resursele vor fi inchise inainte de a rula catch.
Daca o exceptie va fi generata in timpul inchiderii resursei, ea va fi suprimata. Controlul va fi
preluat de un bloc catch. Exceptiile ar putea fi afisate astfel:
public class FinallyExampleMain {
public static void main(String[] args) {
System.out.println("About to open a file");
try (InputStream in = new FileInputStream("missingfile.txt")) {
System.out.println("File open");
int data = in.read();
} catch (Exception e) {
System.out.println(e.getMessage());
for (Throwable t : e.getSuppressed())
System.out.println(t.getMessage());
}
}
}
Resursele din try cu resurse trebuie sa implementeze unul dintre:
- java.lang.AutoCloseable, care poate arunca un Exception
- java.io.Closeable, care extinde AutoCloseable si care poate arunca o
IOException
Interfata AutoCloseable are o singura metoda void close() throws Exception;
Metoda close() din AutoCloseable, spre deosebire de cea din Closeable, nu este
idempotenta. Aceasta inseamna ca apelul sau mai mult decat o data poate avea efecte
secundare. Cei care implementeaza metoda din AutoCloseable vor trebui sa o faca
idempotenta
Java SE7 furnizeaza o clauza multi-catch. Avantajele deriva din faptul ca uneori dorim sa
facem anumite actiuni independent de exceptia ce este generata. Multi-catch reduce cantitatea
de cod scris, prin eliminarea necesitatii scrieri mai multor clauze catch cu acelasi
comportament. Un alt avantaj este acela ca face mai putin probabila incercarea de a prinde o
exceptie generica. Alternativele de tip, separate prin bara verticala, nu trebuie sa aiba o relatie
de mostenire.
public class FinallyExampleMain {
public static void main(String[] args) {
System.out.println("About to open a file");
try (InputStream in = new FileInputStream("missingfile.tx");
Scanner s = new Scanner(in);) {
System.out.println("File open");
int data = s.nextInt();
} catch (NoSuchElementException | IOException e) {
System.out.println(e.getClass().getName());
System.out.println("Quiting");
}
}
}
Numai un obiect Throwable poate fi aruncat, si acesta include exceptii sau erori de sistem.
Metodele din Throwable sunt:
- getMessage(), ce returneaza un string cuprinzand mesajul de eroare al obiectului
Throwable
- toString(), returneaza o descriere a obiectului exceptie incluzand si tipul exceptiei
- initCause(), seteaza cauza exceptiei care este intotdeauna un alt obiect Throwable,
ceea ce permite inlantuirea exceptiilor
- printStackTrace(), permite identificarea metodei ce a aruncat o anumita exceptie
Ierarhia Throwable este urmatoarea:
Clasa Throwable are doua subclase directe:
- Error, si clasele derivate din ea sunt folosite pentru erori de sistem sau erori la
compilare ce nu pot fi tratate de aplicatie, ca spre ex.
ExceptionInInitializerError, StackOverflowError si NoClassDefFoundError
- Exception, si clasele derivate din ea sunt utilizate pentru implementarea exceptiilor
pe care o aplicatie se asteapta sa le intilneasca. Fiecare subclasa a lui Exception
reprezinta un tip particular de exceptie
Clasa RuntimeException si subclasele sale descriu obiectele exceptie ce sunt aruncate
automat de JVM, la rulare. Exceptiile la rulare sunt generate, in general, de bug-uri in codul
sursa. Printre subclasele acesteia amintim:
- ArithmeticException, pentru exceptiile ce cuprind incalcarea regulilor matematice,
cum ar fi impartirea la zero
- IndexOutOfBoundsException, cand indexul unui string sau array nu este in domeniul
acceptat
- NegativeArraySizeException, cand incercam sa cream un sir cu numar negativ de
elemente
Pentru a prinde exceptiile la rulare putem folosi blocurile try-catch, dar acest lucru nu este
obligatoriu. Daca try-catch nu este prezent, toate exceptiile la rulare vor fi considerate
exceptii neverificate (unchecked exceptions). Aceasta inseamna ca compilatorul nu verifica
daca ele au fost declarate sau manipulate.
Toate exceptiile ce mostenesc Exception dar nu mostenesc RuntimeException sunt
cunoscute ca clase exceptie verificate. Acestea trebuie declarate intr-o metoda folosind clauza
throws sau tratate explicit in corpul metodei. Altfel se va genera eroare la compilare. Din
categoria acestor exceptii amintim:
- ClassNotFoundException, este aruncata atunci cand un program incearca sa incarce
o clasa, folosind numele ei, dar definirea clasei cu numele specificat nu este gasita.
Aceste exceptii apar atunci cand un program utilizeaza metoda forName() din Class
sau findSystemClass() sau loadClass() din ClassLoader
- InterruptedException, aruncata atunci cand un fir a fost inactiv pentru mult timp si
un alt fir ce foloseste metoda interrupt() din Thread incearca sa-l intrerupa
- IllegalAccessException, aruncata atunci cand executia metodei curente nu are
acces la definitia unui camp pe care doreste sa-l acceseze sau modifice sau la definitia
unei metode pe care doreste sa o invoce
- FileNotFoundException, apartine pachetului java.io si este aruncata atunci cand
aplicatia nu poate deschide un fisier specificat (fisier inaccesibil sau inexistent).
Exceptiile pot fi tratate in interiorul metodei sau in metoda sa apelanta. Cand o exceptie a fost
aruncata trebuie sa implementam interfata ExceptionListener ce receptioneaza doar
exceptii si apeleza metoda exceptionThrown().
Pentru a ne asigura ca o metoda poate arunca exceptii utilizam cuvantul rezervat throws cand
declaram metoda. In clauza throws specificam tipurile de exceptii ce pot fi aruncate, tipuri
separate prin virgula.
In metoda apelanta exceptia poate fi tratata sau trimisa metodei apelante a acesteia adaugand
clauzei throws exceptiile netratate.
Daca o metoda suprascrie o metoda din clasa de baza ea poate arunca numai aceleasi exceptii
ca si metoda din clasa de baza, o submultime a lor sau subclase ale exceptiilor. Metoda
suprascrisa nu poate arunca exceptii ce sunt superclase ale claselor exceptie ale metodei din
clasa de baza, sau exceptii ce apartin unei alte ierarhii. Aceste reguli se aplica numai
exceptiilor verificate.
Fie urmatorul exemplu:
public class ThrowsMain {
public static void main(String[] args) {
try {
int data = readByteFromFile();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
public DAOException() {
super();
}
}
package exceptions;
}
import daos.EntityDAO;
import exceptions.DAOException;
try {
ent.findById(5);
} catch (DAOException e) {
Throwable t = e.getCause();
System.out.println(e.getMessage() + t.getMessage());
}
}
O implementare DAO poate utiliza o exceptie wrapper pentru a pastra abstractia si pentru a
evita netratarea exceptiilor. Iata explicatia: majoritatea metodelor din DAO trebuie sa trateze
exceptii (IOExceptions sau SQLExceptions). Daca am folosi semnaturi fara throws am
determina clientul sa trateze exceptiile, la implementare. Am pierde astfel abstractia. Prin
folosirea exceptiilor wrapper ii dam clientului posibilitatea de a stii care sunt problemele
aparute la rulare.
In versiunile anterioare ale Javei daca aparea o eroare intr-un fir atunci se arunca o eroare
exceptie si firul se termina, exceptia navigand spre radacina ThreadGroup. Numele firului,
numele exceptiei, mesajul exceptiei si stack trace-ul erau afisate. In 1.4 si versiunile
anterioare inseram cod propriu in ThreadGroup pentru a manipula aceste exceptii. In 1.6
manipularea exceptiilor neprinse este simplificata pentru ca putem defini manipulatorul
exceptiilor neprinse, la nivelul firului de executie.
Metoda setUncaughtExceptionHandler(), introdusa in 1.5, ne permite sa preluam
controlul deplin asupra raspunsului unui fir, in cazul exceptiilor neprinse, prin setarea unui
manipulator de exceptii neprinse. Pentru a seta manipulatorul in toate firele vom trimite ca
parametru al acestei metode o implementare a interfetei
Thread.UncaughtExceptionHandler. Metoda acestei interfete este uncaughtException().
Daca niciun manipulator nu este setat obiectul ThreadGroup va functiona ca un manipulator
de exceptie.
Metoda getUncaughtExceptionHandler() returneaza handler-ul invocat atunci cand un fir
se termina din cauza unei exceptii neprinse. Pentru a determina cum sa manipuleze exceptia
neprinsa, JVM mai intai cauta un manipulator de fire pe care sa-l invoce. Daca exista il
invoca. Apoi JVM trimite exceptia in ierarhia ThreadGroup pina ce atinge radacina acesteia,
ceea ce inseamna ca ThreadGroup nu a suprascris uncaughtException(). In cele din urma
JVM apeleza Thread.getDefaultExceptionHandler() pentru a invoca manipulatorul
predefinit.
In exemplul urmator vom crea un manipulator de exceptie, HandlerPropriu, pe care il vom
seta intr-un fir, ExceptieNeprinsa. Metoda run() a acestui fir nu prinde nicio exceptie, desi
face o impartire la zero. Exceptia este preluata automat de catre manipulatorul definit.
class HandlerPropriu implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.err.printf("%s: %s la linia %d in fisierul %s%n",
t.getName(), e.toString(), e.getStackTrace()[0].getLineNumber(),
e.getStackTrace()[0].getFileName());
}
}
Asertiuni (assertions)
Asertiunea ne permite sa testam presupunerea despre cum ruleaza un program. Asertiunea
contine o expresie booleana, avand valoarea predefinita true. Asertiunea este diferita de toate
celelalte teste pentru ca verifica conditii ce nu pot fi violate. Daca expresia booleana are
valoarea false atunci sistemul arunca o AssertionError. Mecanismul de asertiuni este
valoros deoarece ofera debugging mult mai eficient, ajuta la intretinerea codului prin
documentarea lucrului intern al programului, incurajeaza forma de programare contractuala
(contract programming).
Programarea contractuala este o tehnica utilizata pentru a incuraja un inalt nivel al calitatii
softului, reutilizarii si increderii. Furnizorul si consumatorul unui apel de metoda sunt
impreuna de acord sa adere la un contract bine definit, ce este fortat de folosirea asertiunilor.
Asertiunile sunt in mod predefinit dezactivate. Putem sa le activam sau dezactivam folosind
comenzi la rulare. Asertiunile asigura ca o regula stabilita in cod nu poate fi niciodata
modificata. Putem folosi asertiunile ca pe o masura de siguranta, pentru a confirma
presupunerile despre comportamentul unui program.
Exista doua forme de asertiuni:
- simpla, ce poate fi exprimata ca: assert Expresie; unde Expresie este o expresie
logica. Cand asertiunile sunt activate, sistemul ruleaza asertiunile si evalueaza
expresia din ele. Daca aceasta are valoarea false atunci se va arunca o
AssertionError, fara niciun mesaj
- complexa, ce poate fi exprimata ca: assert Expresie1:Expresie2;. Aceasta se
foloseste pentru a furniza un mesaj complex in caz de eroare. Expresie1 este
booleana si va fi evaluata. In caz de eroare valoarea lui Expresie2 va fi trimisa
constructorului lui AssertionError si un mesaj de eroare detaliat va fi afisat.
AssertionError este o eroare ce opreste executia programului si reprezinta o
subclasa a java.lang.Error. Aceasta eroare nu poate fi prinsa.
Nu vom folosi asertiunile pentru:
- verificarea argumentelor metodelor: metodele publice au un contract clar definit cu
utilizatorii, care nu depinde de faptul ca asertiunile sunt activate. In locul asertiunile
pentru argumente ilegale se poate folosi IllegalArgumentException.
- efectuarea functiilor critice cerute de aplicatie. Aceasta problema apare atunci cand
asertiunile sunt dezactivate
- producerea efectelor secundare. Acesta problema apare atunci cand asertiunile sunt
dezactivate
Urmatorul exemplu ilustreaza de ce nu putem folosi asertiunile pentru executarea operatiilor
critice
assert carNames.remove(null);
In cazul in care asertiunile sunt dezactivate metoda remove() nu se va executa, ceea ce ar
produce rezultate inconsitente. Solutia de rezolvare consistenta a situatiei este urmatoarea:
boolean nullsRemoved = carNames.remove( null );
assert nullsRemoved;
Asertiunile pot fi folosite pentru testarea:
- invariantilor interni: un invariant intern este o conditie ce trebuie sa fie true tot
timpul. Asertiunile se folosesc ori de cate ori facem o presupunem asupra unui
invariant. O folosim in if-uri multiple, pe ultima ramura else sau in switch pe
ramura default. Spre exemplu:
if (j % 5 == 0) {
...
} else if (j % 5 == 1) {
...
} else {
assert j % 5 == 2 : "presupunerea este incorecta";
....
}
- invariantilor de control ai fluxului: pentru aceasta plasam asertiunea intr-o locatie de
cod in care presupunem ca nu putem ajunge. Spre exemplu:
void metoda() {
for (...) {
if (...)
return;
}
assert false; // acest punct al executiei nu ar trebui atins
}
De obicei se foloseste assert false;
- Preconditiilor, postconditiilor si invariantilor clasei. O preconditie este o conditie pe
care o presupunem adevarata la inceputul metodei. Putem testa presupunerea intr-o
metoda nepublica, la inceputul acesteia
private void metoda(int param) {
assert ((param >= 50) && (param <= 500)) : param;
...
}
Putem folosi asertiunile pentru a testa postconditiile atat in metode publice cat si
nepublice. Putem testa o post conditie chiar inainte de instructiunea return. Pentru
aceasta insa trebuie sa ne asiguram ca exista o singura iesire din metoda. Un invariant
de clasa este o conditie ce trebuie sa fie adevarata pentru fiecare instanta a clasei.
Presupunem ca avem o metoda booleana privata numita IsCorrect() care trebuie sa
returneze true inainte si dupa ce orice metoda s-a incheiat. In cod adaugam o
asertiune inainte de instructiunea return pentru fiecare metoda publica si constructor.
In acest moment testul devine un invariant.
Folosim comenzi in linia de comanda pentru a activa asertiunile. Putem activa asertiunile
pentru o aplicatie, un pachet sau o clasa. Sintaxa generala este:
java [-enableassertions | -ea] [:package name"..." | :class name]
Comanda urmatoare activeaza asertiunile aplicatiei Application:
java –ea Application
Pentru o clasa folosim comanda:
java –ea:com.vehicle.motorcar.YourClass
iar pentru un pachet:
java –ea:com.vehicle.motorcar…
cele trei puncte din finalul comenzii precizeaza ca asertiunile sunt active pentru pachetul
motorcar si pentru toate subpachetele sale.
Unele clase nu sunt incarcate de class loader ci de JVM. De aceea folosim in completarea
comenzii de activare a asertiunilor clauza -eas sau –enablesystemassertions. Spre
exemplu:
java –esa –ea:com.vehicle.motorcar.YourClass
Dezactivarea asertiunilor se face folosind clauza –da sau –disableassertions.
Cuvantul rezervat assertion nu poate fi folosit cu versiunile mai vechi ale Javei. Compilarea
cu versiunea 1.3 va genera o eroare sau un avertisment. In rest compilarea se face cu: javac
–source 1.x fisier, unde x este 4, 5 sau 6.
Vom exemplifica utilizarea asertiunilor folosind frameworkul JUnit 4 din Eclipse. Vom crea
mai intai o clasa a carei metoda dorim sa o testam.
public class Asertiuni {
import org.junit.Test;
@Test
public final void testMain() {
Asertiuni a=new Asertiuni();
a.numeleZilei(8);
}
}
Observatie: daca folosim asertiuni (ca in cazul exemplului nostru) din
Window>Preferences>Java>JUnit selectam checkbox-ul Add ‘-ea’….
Rulam apoi aplicatia ca pe un JunitTest. Un API complet gasim in org.junit.Assert.
JUnit uses a lot of static methods and Eclipse cannot automatically import static imports. You
can make the JUnit test methods available via the content assists.
Open the Preferences via Window -> Preferences and select Java > Editor > Content Assist >
Favorites. Add then via "New Member" the methods you need. For example this makes the
assertTrue, assertFalse and assertEquals method available.
You can now use Content Assist (Ctrl+Space) to add the method and the import.
· org.junit.Assert.assertTrue
· org.junit.Assert.assertFalse
· org.junit.Assert.assertEquals
· org.junit.Assert.fail
Annotations
Table 1. Annotations
Annotation Description
Annotation @Test identifies that this
@Test public void method()
method is a test method.
Will perform the method() before each
test. This method can prepare the test
@Before public void method()
environment, e.g. read input data,
initialize the class)
Annotation Description
@After public void method() Test method must start with test
Will perform the method before the start
of all tests. This can be used to perform
@BeforeClass public void method()
time intensive activities for example be
used to connect to a database
Will perform the method after all tests
have finished. This can be used to
@AfterClass public void method()
perform clean-up activities for example be
used to disconnect to a database
Will ignore the test method, e.g. useful if
the underlying code has been changed and
@Ignore the test has not yet been adapted or if the
runtime of this test is just to long to be
included.
Tests if the method throws the named
@Test(expected=IllegalArgumentException.class)
exception
Fails if the method takes longer then 100
@Test(timeout=100)
milliseconds
Assert statements
Statement Description
Let the method fail, might be usable to check that a
fail(String)
certain part of the code is not reached.
assertTrue(true); True
assertsEquals([String message], Test if the values are the same. Note: for arrays the
expected, actual) reference is checked not the content of the arrays
assertsEquals([String message], Usage for float and double; the tolerance are the
expected, actual, tolerance) number of decimals which must be the same
assertNull([message], object) Checks if the object is null
assertNotNull([message], object) Check if the object is not null
assertSame([String], expected, actual) Check if both variables refer to the same object
assertNotSame([String], expected, Check that both variables refer not to the same
actual) object
assertTrue([message], boolean
Check if the boolean condition is true.
condition)
Proprietati
Clasa java.util.Properties este utilizata pentru a incarca si salva perechi cheie-valoare,
care sunt stocate in fisiere text si au de obicei extensia .properties. Proprietatile sunt
folosite pentru a seta valorile predefinite ale aplicatiei si sunt in mod obisnuit citite la pornirea
acesteia. O alta aplicabilitate este aceea a internationalizarii unei aplicatii.
// Print Values
System.out.println("Server: " + myProps.getProperty("hostName"));
System.out.println("User: " + myProps.getProperty("userName"));
System.out.println("Password: " + myProps.getProperty("password"));
}
}
Clasa System
Pune la dispozitie urmatoarele facilitati:
- Gestioneaza fluxurile de intrare, de iesire si de eroare
- Returneaza consola asociata JVM curenta
- Incarca fisiere si librarii
- Acceseaza proprietati definite extern
- Copiaza portiuni din siruri
Toti membrii sunt statici. Constructorul este privat.
Putem folosi urmatoarele metode ale clasei pentru a accesa si modifica proprietatile sistem:
- public static String getProperty(String), returneza valoarea unei proprietati
sistem, identificata prin parametru. Exemplu, afisarea class path-ul Javei:
public static void main(String[] args){
System.out.println(System.getProperty("java.class.path"));
}
Metoda arunca NullPointerException daca parametrul are valoarea null. Putem
folosi varianta supraincarcata, cu doi parametri, pentru a defini valoarea returnata daca
proprietatea sistem este invalida, ca al doilea argument
- public static Properties getProperties(), returneza toate proprietatile sistem
simultan. Obiectul Properties returnat contine o lista a proprietatilor si a valorilor
lor corespunzatoare. Acest obiect trebuie transformat apoi intr-o multime de
proprietati sistem ale mediului curent de rulare. Daca sistemul nu gaseste aceasta
multime de proprietati el initializeaza o multime predefinita. Exemplu:
public static void main(String[] args){
System.getProperties().list(System.out);
}
Argumentul lui list() este System.out ceea ce determina listarea continutului
obiectului Properties prin fluxul predefinit de iesire. Metoda poate arunca o
SecurityException daca metoda checkPropertiesAccess() din SystemManager
nu permite accesul la proprietatile sistemului.
- public static String setProperty(String, String), modifica valoarea unei
proprietati. Primul argument este numele proprietatii iar al doilea este noua valoarea.
Metoda poate arunca o SecurityException daca metoda
checkPropertiesAccess() din SecurityManager nu permite accesul la proprietatile
sistemului sau o NullPointerException daca numele proprietatii nu este precizat.
- public static void setProperties(Properties), modifica proprietati aplicatiei
curente prin obiectul argument. Exemplu, setarea proprietatilor sistemului:
public static void main(String[] args){
Properties p = new Properties(System.getProperties());
p.load("UserProperties.txt");
System.setProperties(p);
}
In exemplul anterior am creat un obiect caruia i-am atribuit proprietatile sistemului
apeland metoda load() din clasa Properties, ce are ca argument numele unui fisier
text ce contine noile proprietati, UserProperties.txt. Metoda poate arunca o
SecurityException daca metoda checkPropertiesAccess() din SystemManager
nu permite accesul la proprietatile sistemului
- public static String getenv(String) si
public static Map<String, String> getenv() pentru a gestiona proprietatile de
mediu. Proprietatile de mediu definesc procesele globale in timp ce proprietatile
sistem definesc subprocesele Java. Prima versiune este folosita atunci cand se
specifica numele proprietatii, ca parametru, iar a doua pentru a returna o mapare
nemodificabila a intregului mediu incluzand valorile si cheile nule. Perechile returnate
sunt de forma nume=valoare. Exemplu:
public static void main(String[] args){
System.out.println("proprietatile de mediu sunt:");
System.out.println(java.lang.System.getenv());
}
Cand pornim platforma Java clasa System intializeaza proprietatile ce contin date despre
mediul de dezvoltare. Dam in continuare un tabel cu unele dintre proprietatile sistem:
Clasa java.lang.System gestioneaza fluxurile de intrare iesire ce cuprind:
- intrarile standard (System.in). Metoda System.in.read() citeste intrarile de la
consola, ca intrare, sub forma de text. System.in este referinta pentru un obiect
InputStream
- Iesirile standard (System.out) ce afiseaza datele de iesire, in general la monitor sau
sunt expediate catre imprimanta. System.out este referinta pentru un obiect
PrintStream
- Erorile standard (System.err) ce afiseaza mesajele de eroare. System.err este
referinta pentru un obiect PrintStream
Toate aceste fluxuri se afla in pachetul java.io. Este de evitat reasignarea fluxurilor standard
de iesire, dar putem redirectiona erorile catre un fisier de iesire folosind functia setErr(), ce
are ca parametru un obiect de tip PrintStream.
Unele dispozitive nu au iesire standard si de aceea trebuie sa trimitem obligatoriu iesirea catre
un fisier. Aceasta se face folosind functia setOut(), ce are ca parametru un obiect de tip
PrintStream.
Daca dorim sa testam aplicatia folosind diferite multimi de intrare, memorate intr-un fisier va
trebui sa cream un nou flux de intrare care sa accepte intrari din fisier in loc de cele de la
tastatura. Aceasta se face folosind functia setIn() ce are ca parametru un obiect
InputStream. Daca security manager este instalat acesta poate verifica daca avem
autorizarea de a reasigna fluxurile standard de intrare/iesire.
Clasa System contine urmatoarele metode folositoare:
- public static void arraycopy(Object src, int src_pos, Object dst, int
dst_pos, int length)copiaza o portiune dintr-un array intr-un altul
- public static long currentTimeMillis(), returneaza timpul scurs in
milisecunde de la 1 ianuarie 1970, ora 00.00.00.
- public static void exit(int status), folosita pentru a termina programat toate
firele si incheia executia interpretorului
- public static void gc(), folosita pentru a cere JVM sa faca o operatie de garbage
collector
- public static long nanoTime(), folosita pentru a returna valoarea timpului
exprimat in nanosecunde a cronometrului sistem. Este folosit in special pentru a
calcula timpul scurs intre doua puncte ale unui program, puncte identificate prin doua
apeluri ale metodei. Exemplu:
public static void main(String[] args){
long startTime = System.nanoTime () ;
long interval = System.nanoTime () - startTime ;
System.out.println (interval) ;
}
- public static Console console(), introdusa in 6.0 pentru a returna obiectul
consola si a accesa dispozitivul asociat JVM-ii curente, in mod caracter. De obicei,
cand un program Java este lansat, in mod automat nu avem disponibila o consola si de
aceea metoda va returna NULL. Obiectul Console detine majoritatea functionalitatilor
fluxului standard, dar si functionalitati noi cum ar fi introducerea securizata a parolei.
Exemplu:
public static void main(String[] args){
Console thisConsole = System.console();
if (thisConsole != null) {
thisConsole.writer().print("introduceti parola");
thisConsole.flush();
char[] password = thisConsole.readPassword();
}
}
In exemplul anterior metoda readPassword() citeste parola fara ecou si o returneaza ca pe
un sir de caractere. Exemplul va returna o consola nenula doar in linia de comanda.
Clasa PrintWriter
Scrie caractere in loc de octeti. Clasa implementeaza toate metodele de printare existente in
PrintStream. Clasa PrintStream converteste caracterele in octeti folosind codarea de
caractere predefinita a platformei. Spre deosebire de PrintStream, daca flush-ul automat este
activ, el va fi facut doar cand una dintre metodele println(), printf() sau format() este
invocata, in loc de a fi facut ori de cate ori caracterul linie noua este inclus la iesire.
public class PrintWriterExample {
public static void main(String[] args) {
PrintWriter pw = new PrintWriter(System.out, true);
pw.println("This is some output.");
}
}
In exemplul anterior am creat un obiect ce foloseste optiunea de auto flush. Valoarea true va
forta PrintWriter-ul sa faca flush dupa fiecare linie afisata la consola.
Java furnizeaza mai multe metode de a formata string-uri: metodele printf() si
String.format().
public class PrintfExample {
public static void main(String[] args){
PrintWriter pw = new PrintWriter(System.out, true);
double price = 24.99; int quantity = 2; String color = "Blue";
System.out.printf("We have %03d %s Polo shirts that cost $%3.2f.\n",
quantity, color, price);
System.out.format("We have %03d %s Polo shirts that cost $%3.2f.\n",
quantity, color, price);
String out = String.format("We have %03d %s Polo shirts that cost
$%3.2f.", quantity, color, price);
System.out.println(out);
pw.printf("We have %03d %s Polo shirts that cost $%3.2f.\n", quantity,
color, price);
}
}
Clasa String
Java utilizeaza caractere Unicode pe 16 biti pentru a codifica datele unui string. Aceasta
tehnica permite Javei sa codifice pina la 65536 de caractere. In Java stringurile sunt
incapsulate prin clasele String si StringBuffer. Clasa String extinde Object si
implementeaza interfetele Serializable, Comparable si CharSequence. Obiectele clasei
String nu sunt obligatoriu a fi create prin operatorul new, ci doar printr-o simpla atribuire a
unei valori literale. Exista mai multi constructori asociati clasei String ce ne permit sa cream
instante pentru:
- un sir de bytes, adica un string codificat UTF-8. Intr-un caracter unicode doar un byte
reprezinta caracterul, celalalt este un cod de limba. ( public String(byte[])).
Exemplu:
byte[] encodedBytes = new byte[] {111, 99, 122, 123, 96, 117} ;
String s = new String(encodedBytes) ;
System.out.println(s);
- o submultime a unui sir de bytes (public String(byte[],int indexInceput,int
lung)). Exemplu:
byte[] encodedBytes = new byte[] {111, 99, 122, 123, 96, 117} ;
String s = new String(encodedBytes, 2, 3) ;
System.out.println(s);
- un sir de caractere (public String(char[])). Exemplu:
char[] chars = new char[] {'a', 'r', 'r', 'a', 'y', 's'} ;
String s = new String(chars) ;
System.out.println(s);
- o submultime a unui sir de caractere ( public String(char[],int
indexInceput,int lung))
- un alt string (public String(String))
- un string buffer (public String(StringBuffer))
Unii constructori ne permit sa folosim un string, ca al doilea parametru, pentru a specifica o
codificare specifica a caracterelor. Acest constructor este folosit in special atunci cand mapam
o multime de bytes unui multimi speciale de caractere. Avem mai multe codificari. Printre
acestea: UTF-8 pentru codificarea Unicode, EUC (Extended Unix Code) codifica caracterele
in diverse limbi asiatice, US-ASCII, ISO-8859-1, UTF-16, etc. Sintaxa acestui constructor
este: String (byte[], String charset); Exemplu:
byte[] encodedBytes = new byte[] {15, 22, 22, 53, 96, 17} ;
String s;
try {
s = new String(encodedBytes, "UTF-8");
System.out.println(s);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Odata creat obiectul String acesta nu mai poate fi modificat. Cand asignam o noua valoare
obiectului, practic, cream un nou string.
Orice aplicatie are un pool de stringuri. In momentul compilarii compilatorul compara toate
stringurile intalnite cu cele deja existente in pool si le adauga pe cele inexistente. Daca
stringul exista, compilatorul economiseste memoria si utilizeaza o copie din pool.
Clasa StringBuffer permite modificarea stringului valoare.
Clasa String contine mai multe metode pentru compararea stringurilor: equals() (compara
doua stringuri caracter cu caracter) , equalsIgnoreCase() si contentEquals() (pentru
compararea obiectului curent String cu un obiect StringBuffer). Putem folosi in loc de
equals si operatorul ==, care poate da rezultate bune din cauza poolului de stringuri, dar acest
lucru nu este garantat.
Alte metode ale clasei String:
- int length(): returneaza numarul de caractere ale stringului curent
- boolean startsWith(String): returneaza true daca stringul curent incepe cu
stringul parametru
- boolean endsWith(String): returneaza true daca stringul curent se termina cu
stringul parametru
- String trim(): returneaza un string fara spatiile de inceput si sfarsit ale stringului
curent si fara caracterele de control ASCII
- String substring(int inceput, int sfarsit): returneaza un substring al
stringului cuprins intre indexul inceput si pina la indexul sfarsit
- String toUpperCase(): returneaza un string cu toate literele mari ale stringului
curent
- String toLowerCase(): returneaza un string cu toate literele mici ale stringului
curent
- boolean isEmpty(): returneaza true daca stringul curent este gol sau are lungime 0
In plus, [^abc] este utilizata pentru a specifica orice caracter care nu este a, b sau c, iar |
specifica alternarea.
\D este folosita ca o clasa negare si reprezinta [^0-9].
\W este folosita ca o clasa negare si reprezinta [^a-zA-Z0-9_].
\S este folosita ca o clasa negare si reprezinta [^ \r\t\n\f\0XB].
Caracterele albe sunt: \t – tab, \n – linie noua, \r – retur de car, \f – form feed, \0XB – tab
vertical.
Clasa Pattern este folosita pentru a defini o expresie regulata prin metoda
Pattern.compile() ce returneaza un obiect Pattern. Exemplu:
public static void main(String args[]) {
Pattern sablon = Pattern.compile("[0-9][0-9][a-z]");
Matcher potrivire = sablon.matcher("lmn89t");
boolean potrivireGasita = potrivire.matches();
if (potrivireGasita)
System.out.println("se potriveste!") ;
else
System.out.println("nu se potriveste!") ;
}
In exemplul anterior obiectul sablon defineste o expresie regulata formata din doua cifre
urmate de o litera. Apoi cream un obiect Matcher pentru stringul in care dorim sa facem
cautarea. Cautarea se face prin apelul metodei matcher() a clasei Pattern. Pentru a gasi in
stringul de cautat o potrivire cu expresia regulata apelam metoda booleana matches() a
clasei Matcher.
Alt exemplu:
public class CustomCharClassExample {
public static void main(String[] args) {
String t = "It was the best of times";
Pattern p1 = Pattern.compile("w.s");
Matcher m1 = p1.matcher(t);
if (m1.find()) {
System.out.println("Found: " + m1.group());
}
Pattern p2 = Pattern.compile("w[abc]s");
Matcher m2 = p2.matcher(t);
if (m2.find()) {
System.out.println("Found: " + m2.group());
}
Pattern p3 = Pattern.compile("t[^eou]mes");
Matcher m3 = p3.matcher(t);
if (m3.find()) {
System.out.println("Found: " + m3.group());
}
}
}
Am creat un obiect Pattern pentru a stoca expresia regulata dupa care dorim sa facem
cautarea, am creat obiectul Matcher prin trimiterea textului in care se face cautarea, metodei
din obiectul Pattern anterior creat (aceasta metoda returneaza un Matcher).
Matcher.group() returneaza caracterele ce se potrivesc sablonului.
Unele dintre clasele caracter se folosesc repetat.
public class PredefinedCharClassExample {
public static void main(String[] args){
String t = "Jo told me 20 ways to San Jose in 15 minutes.";
System.out.println(t);
Pattern p1 = Pattern.compile("\\d\\d");
Matcher m1 = p1.matcher(t);
while (m1.find()){
System.out.println("Found: " + m1.group());
}
Pattern p2 = Pattern.compile("\\sin\\s");
Matcher m2 = p2.matcher(t);
if (m2.find()) System.out.println("Found: " + m2.group());
Pattern p3 = Pattern.compile("\\Sin\\S");
Matcher m3 = p3.matcher(t);
if (m3.find()) System.out.println("Found: " + m3.group());
}
}
In exemplu: \\d\\d gaseste oricare doua cifre, \\sin\\s gaseste in inclus intre doua spatii,
\\Sin\\S gaseste in inclus intre doua caractere non-spatiu.
Cuantificatorii permit selectarea usoara a unui domeniu de caractere:
- {n}, caracterul anterior apare de exact n ori
- {m,n}, caracterul anterior apare de m pina la n ori
- {m,}, caracterul anterior apare de cel putin m ori
- (xx){n}, grupul de caractere apare repetat de n ori
public class QuantifierExample {
public static void main(String[] args){
String t = "Longlonglong ago, in a galaxy far far away.";
Pattern p1 = Pattern.compile("ago.*");
Matcher m1 = p1.matcher(t);
if (m1.find()) System.out.println("Found: " + m1.group());
Pattern p2 = Pattern.compile("gal.{3}");
Matcher m2 = p2.matcher(t);
if (m2.find()) System.out.println("Found: " + m2.group());
Pattern p3 = Pattern.compile("(long){2}");
Matcher m3 = p3.matcher(t);
if (m3.find()) System.out.println("Found: " + m3.group());
}
}
In exemplu:
- ago.*, gaseste ago si 0 sau mai multe caractere ramase pe linie
- gal.{3}, gaseste gal si urmatoarele trei caractere
- (long){2}, gaseste long repetat de doua ori
O expresie regulata incearca de fiecare data sa aduca cat mai multe caractere cu putinta.
Acesta este numit principiul „greediness”. Utilizam ? pentru a limita cautarea la cea mai
scurta potrivire posibila.
public class GreedinessExample {
public static void main(String[] args){
String t = "Longlonglong ago, in a galaxy far far away.";
Pattern p1 = Pattern.compile("ago.*far");
Matcher m1 = p1.matcher(t);
if (m1.find()) System.out.println("Found: " + m1.group());
// Produces: ago, in a galaxy far far
Pattern p2 = Pattern.compile("ago.*?far");
Matcher m2 = p2.matcher(t);
if (m2.find()) System.out.println("Found: " + m2.group());
// Produces: ago, in a galaxy far
}
}
In exemplu:
- ago.*far, ilustreaza principiul greediness
- ago.*?far, ilustreaza inhibarea principiului greediness
Caracterele boundary pot fi utilizate pentru a potrivi diferite parti ale unei linii:
- ^, potriveste inceputul liniei
- $, potriveste sfarsitul liniei
- \b, potriveste inceputul sau sfarsitul unui cuvant
- \B, nu potriveste inceputul sau sfarsitul unui cuvant
public class BoundaryCharExample {
public static void main(String[] args){
String t = "it was the best of times or it was the worst of times";
Pattern p1 = Pattern.compile("^it.*?times");
Matcher m1 = p1.matcher(t);
if (m1.find()) System.out.println("Found: " + m1.group());
Pattern p2 = Pattern.compile("\\sit.*times$");
Matcher m2 = p2.matcher(t);
if (m2.find()) System.out.println("Found: " + m2.group());
Pattern p3 = Pattern.compile("\\bor\\b.{3}");
Matcher m3 = p3.matcher(t);
if (m3.find()) System.out.println("Found: " + m3.group());
}
}
Cu expresii regulate putem folosi parantezele pentru a identifica parti ale unui string pentru
potriviri.
public class MatchingExample {
public static void main(String[] args){
String email = "george.washington@example.com";
Pattern p1 = Pattern.compile("(\\S+?)\\.(\\S+?)\\@(\\S+)");
Matcher m1 = p1.matcher(email);
if (m1.find()){
System.out.println("First: " + m1.group(1));
System.out.println("Last: " + m1.group(2));
System.out.println("Domain: " + m1.group(3));
System.out.println("Everything Matched: " + m1.group(0));
}
}
}
In exemplu potrivim partile componente ale unei adrese de email. Fiecare grup de paranteze
este numerotat. Intr-o expresie regulata group(0) sau group() potriveste tot textul.
In exemplu:
- ^it.*?times, reprezinta secventa ce incepe o linie cu it urmata de niste caractere si
times, greediness fiind dezactivat
- \\sit.*times$, reprezinta secventa ce incepe cu it urmata de niste caractere si
times
- \\bor\\b.{3}, cauta or incercuit decuvinte si urmatoarele trei caractere
Clasa String are cateva expresii ce suporta expresii regulate:
- boolean matches(String): descrisa anterior
- String replaceFirst(String exReg, String inlocuitor): returneaza stringul
obtinut prin inlocuirea in stringul curent a primei aparitii a stringului exReg ce se
potriveste cu inlocuitor
- String replaceAll(String exReg, String inlocuitor): returneaza stringul
obtinut prin inlocuirea in stringul curent a tuturor aparitiilor ale stringului exReg ce se
potrivesc cu inlocuitor
public class ReplacingExample {
public static void main(String[] args){
String header = "<h1>This is an H1</h1>";
Pattern p1 = Pattern.compile("h1");
Matcher m1 = p1.matcher(header);
if (m1.find()){
header = m1.replaceAll("p");
System.out.println(header);
}
}
}
- String[] split(String): returneaza un sir de stringuri, obtinut din obiectul curent
prin folosirea parametrului pe post de separator
public class StringSplit {
public static void main(String[] args){
String shirts = "Blue Shirt, Red Shirt, Black Shirt, Maroon Shirt";
System.out.println(sb.toString());
}
}
Folosim StringBuffer-ul cand dorim sa modificam un string, dinamic, prin cod. Clasa are
urmatorii patru constructori:
- StringBuffer() construieste un string gol
- StringBuffer(int) construieste un string gol de capacitate data de parametru. Daca
bufferul depaseste capacitatea aceasta este automata marita cu valoarea parametrului
- StringBuffer(String) construieste un string initializat cu parametrul
- StringBuffer(CharSequence) construieste un string initializat cu parametrul
Metode uzuale din aceasta clasa:
- void setLength(int): modifica lungimea bufferului. Caracterele ramin aceleasi,
cele care sunt eventual in plus vor fi caractere null
- void setCharAt(int,char): modifica caracterul de la pozitia indicata de primul
parametru
- StringBuffer reverse(): inverseaza ordinea caracterelor din buffer
- StringBuffer append(String): adauga stringul parametrului, bufferului
- StringBuffer insert(int, String): insereaza in buffer stringul parametru
incepand de pe pozitia indicata de primul parametru al functiei
Exemplu de folosire a functiilor clasei StringBuffer:
public class StringBufferDemo {
static void printBuffer( StringBuffer buf ) {
System.out.println( "buf = " + buf );
System.out.println( "length = " + buf.length() );
System.out.println( "capacity = " + buf.capacity() );
}
public static void main( String [] args ) {
StringBuffer buf = new StringBuffer( "This is a
StringBuffer." );
printBuffer( buf ); // prints "This is a StringBuffer.",
// length = 23, capacity = 39
buf.setLength ( 7 );
printBuffer( buf ); // prints "This is",
// length = 7, capacity = 39
StringBuffer buf2 = new StringBuffer( 10 );
printBuffer( buf2 ); // prints "",
// length = 0, capacity = 10
buf2.append( "Write once, run anywhere.") ;
printBuffer( buf2 ); // prints "Write once, run anywhere."
// length = 25, capacity = 25
buf2.setCharAt( 10, '.' );
buf2.setCharAt( 12, 'R' );
printBuffer( buf2 ); // prints "Write once. Run anywhere."
// length = 25, capacity = 25
StringBuffer buf3 = new StringBuffer();
buf3 = buf2.insert( 12, "Distribute. " );
printBuffer( buf3 ); // prints "Write once. Distribute. Run
anywhere."
// length = 37, capacity = 41
buf3 = buf3.append( " Hooray!" );
printBuffer( buf3 ); // prints "Write once. Distribute. Run
anywhere. Hooray!"
// length = 45, capacity = 57
CharSequence charSeq = "Java" ;
StringBuffer buf4 = new StringBuffer( charSeq );
printBuffer( buf4 ); // prints "Java"
// length = 4, capacity = 20
buf4.reverse();
printBuffer( buf4 ); // prints "avaJ"
// length = 4, capacity = 20
}
}
StringBuffer a fost creata pentru a fi sigura in firele de executie. StringBuilder este
desemnata sa inlocuiasca StringBuffer, acolo unde bufferul este utilizat de un singur fir.
TEMA:
1. se da o lista de tricouri, caracteristicile unui tricou fiind separate prin virgula. Sa se
afiseze lista de tricouri astfel:
2. fie urmatorul fisier text:
<html>
<title>Gettysburg Address</title>
<style type="text/css">
body {
font-family: sans-serif;
}
h2,h4 {
text-align: center;
}
</style>
<body>
<h2>Gettysburg Address</h2>
<h4>Abraham Lincoln</h4>
<h4>Thursday, November 19, 1863</h4>
<div class="paragraph">
<p class="line">Four score and seven years ago our fathers brought
forth on this continent a new nation, conceived in liberty,
and
dedicated to the proposition that all men are created
equal.</p>
<p class="line">Now we are engaged in a great civil war, testing
whether that nation, or any nation, so conceived and so
dedicated,
can long endure.</p>
<p class="line">We are met on a great battle-field of that war.</p>
<p class="line">We have come to dedicate a portion of that field,
as a final resting place for those who here gave their lives
that
that nation might live.</p>
<p class="line">It is altogether fitting and proper that we should
do this.</p>
</div>
<div class="paragraph">
<p class="line">But, in a larger sense, we can not dedicate, we
can not consecrate, we can not hallow this ground.</p>
<p class="line">The brave men, living and dead, who struggled
here, have consecrated it, far above our poor power to add or
detract.</p>
<p class="line">The world will little note, nor long remember what
we say here, but it can never forget what they did here.</p>
<p class="line">It is for us the living, rather, to be dedicated
here to the unfinished work which they who fought here have
thus far
so nobly advanced.</p>
<p class="line">It is rather for us to be here dedicated to the
great task remaining before uses that from these honored dead
we
take increased devotion to that cause for which they gave the
last
full measure of devotion that we here highly resolve that
these
dead shall not have died in vain that this nation, under God,
shall have a new birth of freedom and that government of the
people, by the people, for the people, shall not perish from
the
earth.</p>
</div>
</body>
</html>
Scrieti o aplicatie ce cauta in fisierul anterior un text folosind expresii regulate. Daca
textul este gasit sa se afiseze numarul liniei si linia intreaga in care se afla textul cautat. (Spre
exemplu: toate liniile ce contin <h4>, toate liniile ce contin cuvantul to, toate liniile ce incep
cu 4 spatii, toate liniile ce contin tag-uri de inchidere HTML)
3. in exemplul anterior inlocuim tag-urile <p> prin <span>. Mai mult, valoarea
atributului class va fi sentence in loc de line. Folositi expresii regulate pentru a gasi
liniile in care dorim sa facem modificarile. Apoi, folositi expresii regulate pentru a
transforma tag-urile si atributele vizate. Noul fisier va fi afisat la consola.
Autoboxing si Unboxing
Simplifica sintaxa si produc cod mai curat si usor de citit.
Integer intObject = new Integer(1);
int intPrimitive = 2;
Integer tempInteger;
int tempPrimitive;
Colectii Java
In Java colectiile inmagazineaza, grupeaza si sorteaza obiecte. Putem implementa interfete si
extinde clase colectie pentru a defini colectii utilizator. JCF (Java Collection Framework) este
o arhitectura ce pune la dispozitie clase si interfete pentru crearea si manipularea colectiilor.
Asadar, JFC contine:
- interfete, unele interfete sunt goale si functioneaza ca markere
- implementari, ce asigura dezvoltarea colectiilor utilizator
- algoritmi, sunt folositi pentru a rezolva anumite probleme cum ar fi sortarea sau
cautarea intr-o colectie. Algoritmii sunt polimorfici.
O colectie este un obiect destinat sa gestioneze un grup de obiecte:
- un obiect din colectie se numeste element
- primitivele nu sunt permise intr-o colectie
Avantajele folosirii JCF:
- ofera colectii predefinite cum ar fi multimile, tabelele de distributie si hartile
- faciliteaza referirea cu ajutorul interfetelor
- reduce efortul de design si implementare
Avem colectii de diferite tipuri ce implementeaza multe structuri de date comune: stive, cozi,
siruri dinamice, tabele de distributie.
Interfetele colectie formeaza o ierarhie. Radacina este formata din Collection. Elementele
unei Collection pot fi sortate sau nu si pot contine duplicari. Nu exista implementari directe
ale acestei interfete. Toate colectiile pot fi traversate folosind sintaxa foreach. Subinterfete
derivate sunt:
- Set, este o colectie ce nu poate contine duplicari si poate contine cel mult un element
null. Set-ul nu are index.
- o interfata direct derivata din Set este SortedSet in care elementele sunt, in plus,
ordonate natural (dupa codurile caracterelor). TreeSet furnizeaza o implementare
sortata
- direct derivata din SortedSet este interfata NavigableSet, care adauga metode de
navigare bidirectionala (crescator sau descrescator) a multimii si returneaza elemente
ce se potrivesc intr-o oarecare masura unui sablon de cautare. Exemplu de folosire a
lui NavigableSet:
public static void main(String args[]) {
//cream o SortedSet ca instanta a unei clase TreeSet
SortedSet<String> s=new TreeSet<>();
//cream un NavigableSet
NavigableSet<String> n=(NavigableSet<String>)s;
//adaugam elemente
n.add("ion");
n.add("Ion");
n.add("Vasile");
n.add("vasile");
n.add("gelu");
Iterator<String> a=n.iterator();
while(a.hasNext()){
System.out.println(a.next());
}
ad.add("Java");
ad.add("VB");
ad.add("C++");
ad.addFirst("Oracle");
//metode de consultare
System.out.println("primul element este :" + ad.peekFirst());
System.out.println("ultimul element este :"+ ad.peekLast());
//metode de stergere
System.out.println("stergem primul element :"+ ad.pollFirst());
System.out.println("stergem ultimul element :"+ ad.pollLast());
}
JCF este sustinuta de anumite interfete auxiliare ce se gasesc in pachetul java.util. Aceste
interfete ne permit sa manipulam elementele unei colectii. Acestea sunt:
- RandomAccess este o interfata marker folosita de liste pentru a inlesni accesul rapid la
elemente listelor cu acces secvential sau aleator. Implementari ca Vector sau
ArrayList folosesc aceasta interfata
- Comparable ce contine metoda compareTo() si permite doar o optiune de sortare
- Comparator ne asigura ca o colectie este ordonata folosind o operatie de ordonare
utilizator. Interfata are doua metode compare() (returneza un intreg) si equals()
(returneza un boolean). Ne permite definirea mai multor optiuni de sortare. Este
implementata partial de clasa abstracta Collator.
- Iterator permite parcurgerea inainte intr-o colectie. Are metodele next(),
hasNext() si remove(). Iteratorul unifica accesul la colectii
- ListIterator este o extensie a Iterator si permite navigarea in ambele directii. Are
metodele add(), remove() si set() pentru manipulare, next() si previous() pentru
parcurgere, nextIndex() si previousIndex() ce returneza indexul si hasNext() si
hasPrevious() pentru a verifica daca mai exista un element dupa respectiv inaintea
elemetului specificat
Interfata List este implementata de clasele ArrayList, LinkedList si Vector. Metodele
interfetei sunt:
- add(element), add(index,element) adauga elementul specificat la sfarsitul listei
sau la un index specificat.
- remove(element), remove(index,element) sterge prima aparitie a elementului
specificat sau de la un index specificat. Stergerea determina mutarea spre stanga a
elementelor listei
- size()returneaza numarul elemetelor din lista.
- contains(element)returneaza true daca lista contine un anumit element.
- set(index,element)seteaza valoarea unui element de pe o anumita pozitie.
In cele ce urmeaza prezentam un cod ce ilustreaza principalele operatii dintr-o lista:
public class WordQuery {
String j = null;
// obtinem un iterator al listei
Iterator<String> i = listaSortata.iterator();
// parcurgem lista, verificand daca mai avem elemente disponibile
while (i.hasNext()) {
// trecem la urmatorul element disponibil
j = (String) i.next();
System.out.println(j);
}
}
if (esteInLista)
System.out.println("acest element este in lista");
else
System.out.println("acest element nu este in lista");
return esteInLista;
}
while (gata) {
try {
System.out.print("Comanda: ");
int argType = Integer.parseInt(query.readLine());
switch (argType) {
case 0: {
gata = false;
break;
}
case 1: {
getWordCount();
break;
}
case 2: {
outputSortedList();
break;
}
case 3: {
checkName(getName());
break;
}
case 4: {
insertName(getName());
break;
}
case 5: {
removeName(getName());
break;
}
case 6: {
printCommands();
break;
}
default: {
System.out.println("Comanda invalida");
break;
}
}
} catch (Exception e) {
System.out.println("Operatie invalida, programul va
iesi" + e.getMessage());
System.exit(0);
}
} // while
} // main
} // class
Implementari ale interfetelor din JCF sunt grupate in urmatoarele categorii:
- implementari cu caracter general, ce cuprind majoritatea cerintelor:
o pot fi serializabile si clonate utilizand metoda clone()
o permit folosirea cheilor, a valorilor duplicat si a elementelor null
o au un comportament consistent
o contin iteratori care pot detecta orice modificari neautorizate si produc oprirea
iterarii
o sunt predefinit desincronizate
Principalul factor in alegerea uneia sau alteia dintre implementari il reprezinta viteza.
Printre aceste clase avem:
o HashMap, TreeMap si LinkedHashMap ce implementeaza interfata Map.
TreeMap este folosit pentru a ne asigura ca rezultatul iteratiilor este ordonat. In
6.0 implementarea lui TreeMap implementeaza si interfata NavigableMap.
Daca ordinea elementelor adaugate intr-un TreeMap nu este importanta putem
folosi HashMap. Ordinea intr-un HashMap nu este garantat ca ramine aceeasi pe
tot parcursul exploatarii. LinkedHashMap contine o iteratie ordonata, dar de
costuri mai mici decat TreeMap. La creare putem specifica ordinea iteratiei si
aceasta este ordinea de inserare, sau putem folosi ordinea predefinita de
accesare de la cea mai recenta cheie accesata la cea mai demult accesata. Clasa
poate fi implementata ca un cash.
o HashSet, TreeSet si LinkedHashSet ce implementeaza interfata Set.
TreeSet utilizeaza o iteratie logaritmica pentru operatii ca adaugarea si
stergerea. Ordinea elementelor ramine aceeasi. In 6.0 TreeSet implementeaza
si interfata NavigableSet. Daca ordinea elementelor adaugate intr-un
TreeSet nu este importanta putem folosi HashSet. LinkedHashSet contine o
iteratie ordonata, dar de costuri mai mici decat TreeSet. Ordinea este de la
ultimul adaugat element la cel mai recent adaugat.
o ArrayList si LinkedList, implementeaza interfata List. ArrayList este mai
rapida decat LinkedList pentru ca utilizeaza metoda locala
System.arraycopy() si ofera timp de acces constant. Adaugarea se face
printr-o operatie de complexitate amortizata. Ordinea elementelor ramine
aceeasi. Celelalte operatii se executa in timp linear
o PriorityQueue, implementeaza interfata Queue si este o coada ordonata ce
foloseste o structura de heap pentru prioritati. Poate fi ordonata in ordine
naturala sau prin implementarea unui Comparator. Capul cozii este accesat
prin metode ca: poll(), remove() sau peek()
o ArrayDeque, implementeaza interfata Deque si este redimensionabila. Nu este
thread safe.
o Hashtable, implementeaza interfata Map. Este identica cu HashMap, dar este
sincronizata si nu poate avea elemente null. Hashtable inmagazineaza
elemente printr-o cheie asociata, ceea ce inseamna acces rapid atunci cand
lucram cu cantitati mari de date. Ordinea elementelor ramane constanta de-a
lungul timpului.
- Implementari comode sau mini-implementari, contin in general metode statice:
o Collections.EMPTY_SET, Collections.EMPTY_MAP si
Collections.EMPTY_LIST, reprezinta un SET, MAP respectiv LIST, vide. Pot fi
utilizate ca date de intrare pentru metode, atunci cand nu dorim sa transmitem
valori. Acestea sunt nemodificabile
o Collections.singleton, returneza un SET nemodificabil ce contine doar un
singur element
o Collections.nCopies, returneza un LIST nemodificabil ce contine copii ale
aceluiasi element. Exemplu: List l = new
ArrayList(Collections.nCopies(100, "vaca")); Am creat o lista
ce contine de 100 de ori cuvantul vaca
o Array.asList, returneza un LIST de dimensiune fixa. Orice adaugare sau
stergere arunca o UnsupportedOperationException pentru ca dimensiunea
este nemodificabila.
- Implementari Wrapper, contin metode factory (care reprezinta un fel de constructor
generic pe care il putem folosi pentru a crea mai multe tipuri de instante ale claselor)
statice ce impacheteaza colectiile actuale si aduc extra functionalitati. Avem
urmatoarele categorii de wrapperi:
o Nemodificabili: previne modificarea colectiei de catre utilizator prin aruncarea
unei exceptii UnsupportedOperationException in cazul in care s-ar
intampla acest lucru. Prin aceasta marim securitatea structurii de date, facand-
o read-only. In aceasta categorie avem urmatoarele metode:
public static Collection unmodifiableCollection(Collection c);
public static Set unmodifiableSet(Set s);
public static List unmodifiableList(List list);
public static Map unmodifiableMap(Map m);
public static SortedSet unmodifiableSortedSet(SortedSet s);
public static SortedMap unmodifiableSortedMap(SortedMap m);
In acest sens iata un exemplu:
public class Unsupported {
static void test(String msg, List<String> list) {
System.out.println("--- " + msg + " ---");
Collection<String> c = list;
Collection<String> subList = list.subList(1, 8);
// Copy of the sublist:
Collection<String> c2 = new ArrayList<String>(subList);
try {
c.retainAll(c2);
} catch (Exception e) {
System.out.println("retainAll(): " + e);
}
try {
c.removeAll(c2);
} catch (Exception e) {
System.out.println("removeAll(): " + e);
}
try {
c.clear();
} catch (Exception e) {
System.out.println("clear(): " + e);
}
try {
c.add("X");
} catch (Exception e) {
System.out.println("add(): " + e);
}
try {
c.addAll(c2);
} catch (Exception e) {
System.out.println("addAll(): " + e);
}
try {
c.remove("C");
} catch (Exception e) {
}
// The List.set() method modifies the value but
// doesn’t change the size of the data structure:
try {
list.set(0, "X");
} catch (Exception e) {
System.out.println("List.set(): " + e);
}
}
Metoda hashCode()
Este membra a clasei Object si este folosita pentru a genera o valoare intreaga pentru orice
obiect. Fiecare obiect genereaza si inmagazineaza o unica valoare hashcode pe post de cheie.
Sintaxa metodei folosita pentru asa ceva este:
public int hashCode(Object);
si ofera suport pentru implementarile Hashtable si HashMap.
Codul unic al unui obiect este determinat de datele continute in obiect. Daca datele continute
in doua obiecte sunt egale atunci valoarea hashcode-ului este acelasi, adica daca
ob1.equals(ob2) atunci ob1.hashCode()==ob2.hashCode().
Putem invoca metoda hashCode() din obiecte ce implementeaza interfata Collection.
Pentru aceasta trebuie sa ne asiguram ca clasele ce suprascriu Object.equals() suprascriu si
Object.hashCode(). Conform contractului daca doua obiecte sunt egale via equals()
trebuie sa aiba acelasi hashCode(). Daca metoda este invocata de mai multe ori din acelasi
obiect ea va produce acelasi intreg pe toata durata aplicatiei.
Pasi pentru monitorizare: inseram plug-in-ul pentru JMX in Eclipse: Help>Install new
Software...>Add
· Name: eclipse-jmx
· URL: http://eclipse-jmx.googlecode.com/svn/trunk/net.jmesnil.jmx.update/
Acceptam termenii. Resetam Eclipse. Intram in perspectiva JMX. La conectare localhost,
port: 8686. portul trebuie aflat din server.log, la pornirea Glassfish-ului. din administration
Console, nodul: admin service>JMX Connector vedem numele portului si trebuie sa ne
asiguram ca la address avem 127.0.0.1.
Tema:
1. Sa se scrie o clasa derivata din ArrayList care poate fi parcursa in ordine naturala,
dar si in doua ordonari custom: din trei in trei sau in ordinea, primul,ultimul, al doilea,
penultimul, etc.
2. Se da o lista de coduri de produse (ex: "1S01", "1S02", "1M01", "1S01", etc) si o
descriere a fiecarui cod sub forma: ("Tricou Albastru", "1S01"), ("Tricou Negru",
"1S02") etc. Sa se creeze un program eficient pentru obtinerea unui raport ce cuprinde
descrierea fiecarui produs si numarul de aparitii in lista de coduri. Raportul va fi
ordonat alfabetic dupa descriere
3. Folositi o structura de date Deque, ca stiva, pentru a testa ca parantezele rotunde se
inchid corect intr-o expresie data de un String. Ca date de intrare vor fi mai multe
expresii, considerate constante (ex: „if ((a == b) && (x != y));”, „if ((a ==
b) && (x != y)));”). Testul va afisa un mesaj de forma:
Expresia 1 – corect
Expresia 2 – false ....
4. Fie un magazin de tricouri, il vom numi Duke’s Choice. Tricourile (id, descriere,
culoare, marime, numar bucati) intra (se achizitioneaza) sau ies (se vand din magazin).
Dorim sa facem un inventar al tricourilor si sa tiparim la consola raportul rezultat,
dupa un numar de tranzactii (id tricou, tip tranzactie, numar bucati tranzactionate).
Dorim doua rapoarte: unul in ordinea crescatoare a numarului de bucati si altul in
ordinea alfabetica a descrierii.
I/O introducere
Java contine o bogata multime de biblioteci ce efectueaza functii de intrare/iesire. Java
defineste un canal de intrare-iesire ca pe un flux (stream). Fluxul este o sursa de intrare sau o
destinatie de iesire. Sursele si destinatiile pot fi de multiple feluri de date precum octeti, tipuri
primitive, caractere si obiecte. Unele fluxuri doar trimit date altele le manipuleaza si le
transforma intr-un mod usor de interpretat.
Un program utilizeaza un flux de intrare pentru a citi date de la o sursa, un item la un moment
dat. Acelasi lucru se intampla cu fluxurile de iesire.
Indiferent cum lucreaza ele intern, toate fluxurile reprezinta o secventa de date.
Sursele si destinatiile se numesc fluxuri nod. Spre exemplu: fisiere, memorie, pipes intre fire
sau procese.
Un dezvoltator de aplicatie utilizeaza in mod uzual fluxurile I/O pentru a citi sau scrie fisiere,
pentru a citi sa scrie informatii de la sau la dispozitive de iesire (tastatura – intrarea standard,
consola – iesirea standard). O aplicatie foloseste un socket pentru a comunica cu o alta
aplicatie sau sistem la distanta.
Java suporta doua tipuri de stream-uri: caracter si byte. Intrarea si iesirea datelor caracter sunt
manipulate de readers si writers, iar a datelor octet de input streams si output streams.
Mai precis, intrarile pentru fluxuri de octeti sunt gestionate de subclase ale lui InputStream
iar iesirile de subclase ale OutputStream. La nivelul caracterelor (codificate Unicode) avem
clasele de baza Reader si Writer.
Fluxurile de octeti sunt in general folosite pentru a citi fisiere imagine, fisiere audio si
obiecte.
Cele trei metode de baza pentru citire din InputStream sunt:
- int read();, returneaza fie un byte citit din flux fie -1, ce indica atingerea
sfarsitului de fisier
- int read(byte[] b);, citeste fluxul intr-un sir de octeti si returneaza numarul de
octeti cititi
- int read(byte[] b, int off, int len);, la fel ca anterioara doar ca ultimele
doua argumente indica un subdomeniu al sirului, care urmeaza a fi umplut
Pentru eficienta se recomanda sa citim date in blocuri cat mai mari sau sa folosim buffere de
flux.
Dupa folosire un flux trebuie inchis. In Java SE 7 InputStream implementeaza
AutoCloseable.
Alte metode:
- int available();, returneaza numarul de octeti ce sunt disponibili imediat pentru a
fi citite din flux. O operatie de citire ce urmeaza acestui apel poate returna mai multi
octeti
- long skip(long n);, ignora numarul specificat de octeti din flux
- boolean markSupported(); void mark(int readLimit); void reset();,
efectueaza operatii push-back pe un stream, daca strimul suporta aceste operatii.
markSupported() returneaza true daca metodele mark() si reset() sunt
operationale pe fluxul curent. mark() indica faptul ca punctul curent din flux va fi
marcat si un buffer suficient de mare cat cel putin numarul de octeti specificat de
argument va fi creat. Parametrul metodei specifica numarul de octeti ce pot fi recititi
apeland reset().
Cele trei metode de scriere din OutputStream sunt:
- void write(int c);
- void write(byte[] buffer);
- void write(byte[] buffer, int offset, int length);
alte metode:
- void close();
- void flush();, care forteaza scrierea in flux
Ca si la citire este recomandata scrierea datelor in blocuri cat mai mari.
In mod asemanator avem metodele de citire din Reader:
- int read();, returneaza fie un int citit din flux, reprezentand un caracter Unicode,
fie -1, ce indica atingerea sfarsitului de fisier
- int read(char[] b);, citeste fluxul intr-un sir de caractere si returneaza numarul
de octeti cititi
- int read(char[] b, int off, int len);, la fel ca anterioara doar ca ultimele
doua argumente indica un subdomeniu al sirului, care urmeaza a fi umplut
Iar pentru scriere metodele din Writer sunt:
- void write(int c);
- void write(char[] buffer);
- void write(char[] buffer, int offset, int length);
- void write(String string);
- void write(String string, int offset, int length);
Fie urmatorul exemplu de copiere a unui flux de caractere, dat intr-un fisier in.txt.
public class CharStreamCopyTest {
Un program foloseste arareori un sigur obiect pentru flux, ci inlantuie o serie de stream-uri
pentru a procesa date. In prima parte a figurii anterioare este dat un exemplu de flux de
intrare: fluxul de fisier este operat de un buffer pentru cresterea eficientei si apoi este
convertit in date. Partea a doua exemplifica drumul invers.
public class BufferedStreamCopyTest {
Fluxuri standard
In clasa System din java.lang avem trei campuri instanta statice:
- out, care reprezinta fluxul de iesire standard. Acesta este in permanenta deschis si
pregatit sa accepte date de iesire. In mod obisnuit acest stream corespunde monitorului
sau unei alte destinatii de iesire specificata de mediul gazda sau de utilizator. Este o
instanta a lui PrintStream
- in, care reprezinta fluxul de intrare standard. Acesta este in permanenta deschis si
pregatit sa furnizeze date de intrare. In mod obisnuit acest stream corespunde tastaturii
sau unei alte surse de intrare specificata de mediul gazda sau de utilizator. Este o
instanta a lui InputStream
- err, care reprezinta fluxul de iesire standard al erorilor. Acesta este in permanenta
deschis si pregatit sa accepte date de iesire. In mod obisnuit acest stream este utilizat
pentru afisarea mesajelor de eroare sau a altor informatii ce ar putea veni in atentia
imediata a unui utilizator. Este o instanta a lui PrintStream
In afara de obiecte PrintStream, System poate accesa instante ale java.io.Console.
Obiectul Console reprezinta o consola bazata pe caracter si asociata JVM curente. Daca o
masina virtuala are o consola aceasta depinde de platforma si de modul in care masina
virtuala este invocata.
public class SystemConsoleTest {
}
}
In exemplu am creat un buffer de dimensiune potrivita, buffer folosit atat la citire cat si la
scriere. Citirea respectiv scrierea se fac intr-o singura operatie cu consecinte importante de
performanta.
Persistenta si serializare
Salvarea datelor intr-un depozit permanent se numeste persistenta.
Un obiect nepersistent exista doar cat masina virtuala Java ruleaza. Serializarea este un
mecanism pentru salvarea obiectelor ca un sir de octeti, care apoi pot fi reconstruiti intr-o
copie a obiectului. Pentru a serializa un obiect al unei clase specifice, aceasta trebuie sa
implementeze interfata java.io.Serializable. Aceasta interfata nu are metode, este o
interfata marker ce indica faptul ca clasa poate fi serializata.
Cand un obiect este serializat, doar campurile obiectului sunt pastrate. Atunci cand un camp
refera un obiect, campurile obiectului referit sunt de asemenea serializate (obiectul referit
trebuie si el serializat). Arborele campurilor unui obiect constituie un graf obiect.
Serializarea traverseaza graful obiect si scrie datele in fluxul de iesire pentru fiecare nod al
grafului.
Unele clase nu sunt serializate pentru ca ele reprezinta informatii specifice tranzitorii.
Daca un obiect graf contine o referinta neserializata, se va arunca o
NotSerializableException si operatia de serializare esueaza. Campurile ce nu trebuie
serializate vor fi marcate prin cuvantul rezervat transient.
Modificatorul de acces al campurilor nu influenteaza serializarea campurilor. De asemenea,
campurile statice nu sunt serializate.
La deserializarea obiectelor valorile campurilor statice sunt setate la valorile declarate in
clasa. Valorile campurilor nestatice tranzitorii sunt setate la valorile predefinite ale tipului.
Pe timpul serializarii un numar de versiune, serialVersionUID, este utilizat pentru a asocia
iesirea serializata cu clasa utilizata in procesul de serializare. La deserializare
serialVersionUID este folosit pentru verificarea faptului ca incarcarea claselor este
compotibila cu obiectul ce este deserializat. Daca se observa o alta valoare a
serialVersionUID, deserializarea va genera un InvalidClassException. O clasa
serializata poate declara un camp static si final de tip long pentru serialVersionUID.
Daca o clasa serializata nu declara in mod explicit un serialVersionUID, atunci la rulare
se va calcula o valoare predefinita pe baza diverselor aspecte ale clasei. Este, insa, puternic
recomandat ca clasele serializate sa declare explicit serialVersionUID. Altfel, la
deserializare pot apare InvalidCastException, pentru ca valoarea lui serialVersionUID
este foarte sensibila la diversele implementari ale compilatorului Java. Este recomandat, de
asemenea, ca serialVersionUID sa fie private, pentru ca nu are niciun rol in procesul de
mostenire.
Fie un exemplu de folosire a unui obiect graf:
public class Stock implements Serializable {
@Override
public String toString() {
double value = getValue();
return "Stock: " + symbol + "\n" + "Shares: " + shares + " @ "
+
NumberFormat.getCurrencyInstance().format(purchasePrice)
+ "\n" + "Curr $: "
+ NumberFormat.getCurrencyInstance().format(currPrice) +
"\n"
+ "Value: " +
NumberFormat.getCurrencyInstance().format(value)
+ "\n";
}
}
public Portfolio() {
}
Clasa java.io.File
Pachetul java.io permite programelor Java sa citeasca si sa scrie date in diverse formate
(texte, grafice, sunete si video). Pachetul contine clase ce permit accesul la date secvential sau
aleator.
Clasele Reader si Writer precum si subclasele lor sunt utilizate pentru acces secvential,
adica un flux secvential de biti.
Clasa RandomAccessFile si subclasele sale sunt folosite pentru a scrie sau citi aleator datele
din fisier.
In exemplul urmator am creat si apoi am adaugat, intr-un fisier cu acces aleator, mai multe
intrari editate intr-un TextField:
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class SampleWriteFile implements ActionListener {
private Frame f;
private Button write, clear, exit;
private TextField txt;
public SampleWriteFile(String s) {
f=new Frame(s);
write=new Button("Scrie in fisier");
write.addActionListener(this);
clear=new Button("sterge");
clear.addActionListener(this);
exit=new Button("iesire");
exit.addActionListener(this);
txt=new TextField(20);
Label l=new Label("introdu un text");
Panel p=new Panel();
Panel p2=new Panel();
p.add(l);p.add(txt);
p2.add(write);p2.add(clear);p2.add(exit);
f.add(p, "North");
f.add(p2);
f.pack();
f.setResizable(false);
f.setVisible(true);
}
if(e.getSource()==clear) {
txt.setText("");
}
if(e.getSource()==exit) {
System.exit(0);
}
if(e.getSource()==write) {
writeFile();
}
}
}
}
Metoda seek() a fost utilizata pentru a specifica locatia din fisier in care are loc o anumita
operatie. In exemplul nostru am facut pozitionarea la sfarsitul fisierului. Metoda
writeBytes() este folosita pentru a scrie datele in fisier.
Clasa java.io.File poate reprezenta un director sau un fisier. Aceasta permite navigarea,
descrierea si accesul la fisiere si directoare. Managerul de securitate Java permite doar acele
operatii care sunt in interiorul unui context de securitate. Deoarece majoritatea browserelor nu
permit orice tip de acces, clasa File si clasele inrudite sunt utilizate in aplicatii si nu in
applet-uri.
Crearea unui obiect File nu inseamna automat crearea unui fiser sau director real. Si
stergerea unui obiect File de catre garbage collector nu inseamna neaparat stergerea fizica a
acestuia. Constructorii clasei sunt:
· File (File directoryObj, String fileName), creaza o noua instanta utilizand
path-ul unui fisier (director) existent, dat ca prim parametru, si avand numele dat de al
doilea parametru
· File (String pathName), creaza o instanta avand numele si calea date de parametru
· File (String pathName, String fileName) , creaza o noua instanta utilizand
path-ul dat de primul parametru, si avand numele dat de al doilea parametru
Avem urmatoarele metode in File:
- exists(), care confirma daca fisierul specificat exista
- getName(), returneaza numele fisierului sau directorului (ultimul element al unui cai
complete)
- getAbsolutePath(), returneaza un string ce este calea absoluta a unui fisier sau
director
- isDirectory()
- isFile()
- canRead()
- getTotalSpace(), returneaza un long ce reprezinta dimensiunea in bytes a partitiei
numita de calea abstracta
- getFreeSpace(), returneaza un long ce reprezinta numarul de bytes disponibili ai
partitiei numita de calea abstracta. Valoarea nu este neaparat certa.
- setWritable(), permite unui fisier sa poata fi scris. Primul parametru boolean, daca
are valoarea true, permite scrierea. Al doilea parametru boolean da permisiune de
scriere doar celui care a creat sau este proprietarul fisierului, daca valoarea este true,
altfel tuturor
- setExecutable(), permite unui fisier sa se execute operatii pe el. Primul parametru
boolean, daca are valoarea true, permite acest lucru. Al doilea parametru boolean
da permisiunea doar celui care a creat sau este proprietarul fisierului, daca valoarea
este true, altfel tuturor
- canExecute(), returneaza true daca fisierul poate fi executat
In exemplul urmator am definit o functie de cautare a unui fisier intr-un director specificat.
Functia returneaza calea completa a fisierului sau mesajul fisier negasit.
static String search (File f, String fileName) {
String contents[] = f.list() ;
int i = 0;
String found = "fisier negasit" ;
System.out.println(search(f,"sampletext.txt"));
}
Functia list() returneaza un sir de stringuri, listand continutul directorului. Daca File nu
este un director list() va returna null.
Cand Java citeste sau scrie date, deschide un flux, citeste si scrie informatii in flux si apoi
inchide fluxul. Clasele stream utilizeaza intrari si iesiri de date generale in timp ce clasele
writer si reader folosesc stringuri Unicode. Datele receptionate de la sau trimise la dispozitive
de I/O constau in bytes. Java suporta si alte tipuri de date precum intregi, caracter sau string.
Java foloseste o ierarhie de clase pentru a lucra cu diferite tipuri de date. Clasele
InputStream si OutputStream sunt abstracte si folosesc fluxuri low-level pentru a citi bytes
de la sau a-i trimite la fisiere, sockets sau pipes. Fluxurile low-level au acces direct la bytes
procesati. Fluxurile high-level sunt construite peste cele low-level, introducand noi
capabilitati.
void write(byte[] b)
void write(byte[] b, int off, int len)
Fluxurile de intrare-iesire high-level comunica cu fluxurile low-level.
Majoritatea claselor de intrare iesire de nivel inalt mostenesc atribute din superclasele
FilterInputStream si FilterOutputStream. Constructorul unei astfel de clase primeste ca
argument un Input/OutputStream. O diagrama a claselor este ilustrata in figura urmatoare:
fostream.close();
}
catch (IOException e) {
}
}
}
De observat, la sfarsitul codului, modalitatea de inchidere a fluxurilor, in ordine inversa fata
de ordinea crearii.
}
}
Constructia este asemanatoare celei din exemplul precedent.
Majoritatea claselor de citire au in corespondenta clase de scriere.
Daca metoda de citire cel mai frecvent utilizata este readLine(), corespondentul sau la
scriere este write(). Scrierea din buffer in destinatie nu se face daca cantitatea de date este
mai mica decat cea a bufferului. Abia cand dimensiunea bufferului a fost atinsa se produce
scrierea. Pentru a preveni pierderea de informatii datorate inchiderii bufferului folosim
metoda flush(). Apelul acesteia determina golirea bufferului si trimiterea tuturor datelor
fisierului cu care este in corespondenta. flush() va fi apelata inainte de close().
Cateva dintre exceptiile importante atunci cand lucram cu operatii de I/O sunt:
- FileNotFoundException, localizarea unui fisier se face fara succes
- EOFException, sfarsitul fisierului atins intr-un mod neasteptat
- IterruptedIOException, intrerupere neasteptata
- ObjectStreamException, clasa de baza pentru erorile aruncate de clasele
ObjectStream
In Java 6 clasa File contine metode noi ce furnizeaza informatii despre utilizarea discului de
memorie:
- getTotalSpace(), returneaza dimensiunea in octeti a partitiei in care se afla fisierul
- getFreeSpace(), returneza dimensiunea in octeti a zonei libere din partitia in care se
afla fisierul
- getUsableSpace(), asemanatoare functiei precedente, dar verifica si restrictiile
sistemului de operare si permisiunile de scriere asociate fisierului.
Alte extensii din versiunea 6 sunt utilizate pentru a restrictiona sau permite citirea, scrierea
sau executarea unui fisier:
- setWritable(), specifica permisiune de scriere asupra unui obiect File. Are doua
semnaturi:
setWritable(boolean writable)
setWritable(boolean writable, boolean ownerOnly)
prima este utilizata pentru a specifica daca fisierul poate fi scris sau nu. A doua
specifica daca permisiunea la scriere este valabila doar proprietarului fisierului
- setReadable(), specifica permisiune de citire asupra unui obiect File. Are doua
semnaturi:
setReadable(boolean readable)
setReadable(boolean readable, boolean ownerOnly)
prima este utilizata pentru a specifica daca fisierul poate fi citit sau nu. A doua
specifica daca permisiunea la citire este valabila doar proprietarului fisierului. Daca
sistemul nu poate decide proprietarul va acorda permisiune tuturor indiferent de ceea
ce stabileste aceasta metoda
- setExecutable(), specifica permisiune de executie asupra unui obiect File. Are
doua semnaturi:
setExecutable(boolean writable)
setExecutable(boolean writable, boolean ownerOnly)
Metoda care verifica posibilitate ca un fisier sa poata fi executat este canExecute().
Pachetul java.nio
Denumirea provine de la „new I/O”. Inlocuieste java.io si are scopul de a facilita operatii de
intrare-iesire neblocabile, dar si blocarea partiala a unui fisier.
NIO are performante mai bune si este mai eficient. Spre exemplu, permite citirea si scrierea
folosind o singura conexiune in doua sensuri FileChannel. Permite lucrul in conjuctie cu
APIul existent. Principalele facilitati sunt:
- Fisiere mapate si blocate: fisierele mapate in memorie creaza o mapare care ne
permite sa lucram cu fisierele de date ca si cum ar ocupa un spatiu in memorie.
Fisierele blocate permit accesul exclusiv al unei singure aplicatii sau accesul mai
multora
- I/O asincrone: ne permite sa adaugam I/O nonblocabile si selectabile in codul Java.
Cand folosim I/O neblocabile toate actiunile necesare pentru citire sau scriere sunt
executate imediat. Cand folosim I/O asincrone firul principal este gata sa selecteze un
flux care este pregatit sa transmita sau sa receptioneze date. El se muta la un alt flux
daca cel original se blocheaza
- Canale: un canal functioneaza ca un punct final pentru comunicatie. Ele seamna cu
fluxurile. Dispozitivele folosesc metodele proprii pentru citirea sau scrierea de date.
Interfata Channel are doar doua metode: isOpen() si close()
- Bufferi: pachetul este construit in jurul obiectelor ByteBuffer, in timp ce java.io
foloseste siruri de octeti. Obiectele ByteBuffer ne permit sa setam bufferele la read-
only si sa urmarim pozitia datelor scrise sau citite dintr-un buffer. Ne permit, de
asemenea, sa scriem toate tipurile de date primitive. Putem seta bufferii ca bufferi
directi, ce utilizeaza bufferi gazduiti de sistemul de operare
- Coderi si decodari de caractere: permit conversia sirurilor de date in caractere si
invers. Clasa Charset din acest pachet permite acest lucru.
Buffer este o subclasa directa a lui Object. Buffer inmagazineaza propria stare. Este
abstracta. Are trei proprietati:
- position: determina pozitia unde urmatorul element va fi citit sau scris in buffer.
- limit: determina pozitia primului element din buffer ce nu va fi scris sau citit. Limita
poate fi intre 0 si capacitatea buferului si poate fi setata la rulare prin apelul metodei
limit()
- capacity: este capacitatea bufferului sau numarul sau de elemente. Capacitatea este
setata la crearea bufferului
Clasele Buffer si ByteBuffer (clasa care implementeaza interfata) au urmatorele metode:
- clear(), seteaza proprietatea position la zero si limit la valoarea capacitatii. Nu
afecteaza datele din buffer. Este folosita atunci cand dorim sa introducem date noi in
buffer. Datele initiale se pastreaza dar vor fi suprascrise
- mark(), seteaza o pozitie in buffer la care ne putem intoarce la un apel viitor. Se poate
folosi pentru a retine locul in care datele au fost scrise
- reset(), seteaza proprietatea position la pozitia data de mark. Nu afecteaza datele
din buffer. Este folositoare atunci cand dorim sa copiem sau sa rescriem date dupa ce
au fost sterse
- put(), este folosita pentru a scrie octeti intr-un ByteBuffer. Pozitia in care scriem
este intotdeauna intre mark si limit.
- get(), este folosita pentru a citi octeti dintr-un ByteBuffer. Pozitia din care citim
este intotdeauna intre mark si limit
- flip(), seteaza limit la position. Apoi seteaza position la 0. Este folosita dupa
scrierea de date in buffer
- compact(), muta toate datele la inceputul bufferului. Apoi seteaza position la
valoarea unde datele se incheie si limit la valoarea capacitatii
In exemplul urmator vom da construirea si gestionarea unui buffer, folosind metodele anterior
descrise:
import java.nio.*;
public class Test{
public static void showBufferProperties(Buffer b){
System.out.println("proprietatile bufferului:");
System.out.println("capacitatea="+ b.capacity());
System.out.println("limita="+ b.limit());
System.out.println("pozitia="+ b.position());
}
}
catch (IOException e) {
myReadWrite = null ;
}
}
In exemplul anterior am creat un FileChannel dintr-un obiect FileOutputStream prin
apelul metodei getChannel(). In mod append, dat de valoarea true al celui de-al doilea
parametru al constructorului, variabila position va avea valoarea dimensiunii fisierului.
public void memoryMapAFile (String fileToMap ) {
// cream un FileChannel si mapam un buffer pentru el
try {
FileChannel mappedFile = new RandomAccessFile (fileToMap,
"rw").getChannel() ;
MappedByteBuffer mappedBuff = mappedFile.map
(FileChannel.MapMode.READ_WRITE, 0, mappedFile.size() ) ;
}
catch (IOException e) {}
}
In exemplul anterior am folosit functia map() pentru a crea un MappedByteBuffer, care
incorporeaza toate metodele din ByteBuffer plus cele referitoare la mapare. Canalul a fost
deschis pentru citire si scriere ( FileChannel.MapMode.READ_WRITE).
public void bulkCopy (String fileIn, String fileOut ) {
try {
FileInputStream inFile = new FileInputStream (fileIn) ;
FileOutputStream outFile = new FileOutputStream (fileOut, true)
;
FileChannel in = inFile.getChannel () ;
FileChannel out = outFile.getChannel () ;
in.close () ;
out.close () ;
}
catch (IOException e) {}
}
In exemplul anterior am creat o copie a unui fisier in Java. Metoda transferTo() transfera
datele intre cele doua canale. Ca parametru am trimis si dimensiuniea fisierului din canalul
sursa. (in.size()).
Metoda wrap(), impacheteaza un sir de octeti intr-un ByteBuffer si il scrie intr-un
ByteBuffer.
myBuff = ByteBuffer.wrap(myBytes) ;
f.myReadWrite.write (myBuff) ;
Metoda force() este folosita pentru ca schimbarile facute asupra continutului unui buffer sa
fie scrise si in canal.
System.out.println ( "Bytes written: " + totalBytes ) ;
System.out.println ( "File length now: " + f.myReadWrite.size () ) ;
f.myReadWrite.force( true ) ;
f.myReadWrite.close() ;
Metoda close() determina inchiderea canalului.
Clasa Scanner
Permite citirea facila, parsarea stringurilor si tipurilor primitive folosind expresii regulate. Se
afla in pachetul java.util. Clasa Scanner citeste intrari de la consola, fisier, string sau orice
alte surse ce implementeza interfata Readable sau ReadableByteChannel.
Simplifica citirea datelor formatate, de intrare, cu ajutorul tokenilor ce urmaresc expresii
regulate. Un token este o secventa de caractere pe care o specificam atunci cand cream un
scanner folosind delimitatori potriviti. In mod predefinit delimitatorul este spatiul.
O expresie regulata este un string ce cuprinde un sablon de potrivire. Modul in care expresia
regulata se potriveste unui token in codul sursa va determina parsarea datelor. Putem folosi
metode predefinite pentru a extrage expresii precum stringuri, double sau intregi.
Putem sa ne definim si propriile expresii folosind metoda next(). Metoda are ca parametru
un Pattern sau un String si returneza un String, care reprezinta urmatorul token ce se
potriveste sablonului. Procesul de scanare implica urmatorii pasi:
- scannerul localizeaza un token folosind delimitatorii specifici
- scannerul identifica potrivirea unui token cu o expresie regulata, daca este specificata
vreuna
- tokenul este stocat intr-o variabila
Scanner are urmatorii constructori folositi pentru a crea obiecte de citire a intrarilor:
- Scanner(InputStream), dintr-un flux de intrare
- Scanner(InputStream sursa, String charsetName), dintr-un flux de intrare
formatat, cel de-al doilea parametru specifica tipul de codificare folosit pentru a
converti fluxul de intrare la caractere
- Scanner(String), dintr-un string
- Scanner(File), dintr-un fisier
- Scanner(File sursa, String charsetName), dintr-un fisier formatat, cel de-al
doilea parametru specifica tipul de codificare folosit pentru a converti continutul
fisierului la caractere
- Scanner(Readable), dintr-un obiect Readable
- Scanner(ReadableByteChannel), dintr-un obiect ReadableByteChannel
- Scanner(ReadableByteChannel sursa, String charsetName), dintr-un obiect
ReadableByteChannel, cel de-al doilea parametru specifica tipul de codificare folosit
pentru a converti canalul de octeti la caractere
Urmatorul exemplu creaza un scanner de la fluxul standard de intrare.
import java.util.*;
}
}
Iar urmatorul dintr-un FileReader
import java.util.*;
import java.io.*;
}
}
Pentru a citi din fluxul de intrare vom urma pasii:
- determinam daca mai exista ceva de citit folosind metoda public boolean
hasNextX(), care este supraincarcata. Printre cele mai utilizate: hasNextBoolean(),
hasNextInt()si hasNext(String), unde parametrul este o expresie regulata
- citim efectiv folosind metoda public String nextX(), care este supraincarcata.
Printre cele mai utilizate: public boolean nextBoolean(),public int
nextInt()si public String next(String), unde parametrul este o expresie
regulata. Daca nu exista niciun token se va arunca o NoSuchElementException.
Metoda blocheaza scannerul pe timpul citirii intrarii. Pe timpul blocarii metoda se
opreste si asteapta ca utilizatorul sa introduca date.
- repetam ultimii doi pasi
Un exemplu de folosire a clasei Scanner este prezentat in cele ce urmeaza:
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
String str = " ";
// afiseaza rezultatul
System.out.println(str);
}
Pentru a modifica delimitatorul predefinit folosim metoda Scanner
useDelimiter(String/Pattern).
Pentru a determina delimitatorul curent folosim Pattern delimiter().
Spre exemplu apelul metodei:
in.useDelimiter("; *");
determina setarea delimitatorului la ; urmat de spatiu si apoi orice caractere.
Iesiri formatate
Incepand cu 5.0 se introduce metoda printf(). Functia poseda control asupra iesirilor
formatate. Functia scrie un string intr-un string de iesire folosind un format specificat si
argumente. Functia se foloseste cu fluxuri print, spre ex System.out sau System.err. Putem
utiliza metoda cu doua tipuri de obiecte:
- PrintStream, pentru o iesire pe octeti
- PrintWriter, pentru o iesire pe caracter
Cele doua forme ale metodei sunt:
public PrintStream printf(Locale, String, Object);
in cazul in care se precizeaza o localizare, respectiv:
public PrintStream printf(String, Object);
ce foloseste o localizare predefinita. Stringul da formatul de care se va tine cont la afisare.
Ultimele argumente corespund formatului dat in string. Putem folosi urmatoarele argumente
de format:
- %n, inserarea unui sfarsit de linie la afisare
- %e sau %E, afisarea cu exponent a numerelor zecimale. Argumentul trebuie sa fie de
tip: Double, BigDecimal sau Float
Vom da, in continuare, un exemplu de folosire a citirii cu formatare:
public static void main(String[] args) {
Date datePrinted = Calendar.getInstance().getTime();
Clasa Formatter
Ne permite sa convertim stringuri, numere si date in aproape orice format. Ne permite, de
asemenea, sa formatam tipuri de date Java: byte, Calendar si BigDecimal. Se afla in
java.util. Clasa Formatter este stricta, ea afiseaza o eroare daca sesizeaza o formatare
invalida.
Exemplu de folosire a acestei clase;
public static void main(String[] args) {
int numVisitor = 100;
System.out.println(formatter);
}
In exemplul anterior format() si printf() au acelasi rol.
Clasa Formatter converteste intrari binare in text formatat, care va fi retinut intr-un buffer.
Putem obtine continutul bufferului invocand metoda out(). Putem, de asemenea, converti
bufferul la un string apeland toString(). Clasa are urmatorii constructori frecvent utilizati:
- Formatter(), creaza un obiect general ce utilizeaza un obiect StringBuilder pentru
a stoca iesirea formatata, la locatia predefinita, specificata de JVM
- Formatter(Appendable), creaza un obiect ce stocheaza iesirea formatata intr-un
buffer
- Formatter(String) throws FileNotFoundException, creaza un obiect pentru a
stoca iesirea formatata intr-un fisier. Daca fisierul nu poate fi creat arunca o exceptie
In plus constructorii clasei pot specifica: locatia obiectului creat, setul de caractere folosit.
Cateva dintre metodele clasei:
System.out.println(formatter);
}
Urmatoarele flag-uri dupa procent modifica formatul:
- 0, in loc de spatii va adauga zerouri nesemnificative
- +, adauga semnul + in fata numerelor pozitive
- (, scrie numerele negative intre paranteze rotunde
- Spatiu, scrie un spatiu in fata numerelor pozitive pentru alinierea cu numerele
negative
- -, aliniere la stanga
- ,, pentru separarea miilor
public static void main(String[] args) {
Date date = new Date();
Calendar cal = Calendar.getInstance();
System.out.println(formatter);
}
Formatarea numerelor:
public static void main(String[] args) {
StringBuilder buffer = new StringBuilder();
Formatter formatter = new Formatter(buffer, Locale.US);
buffer.setLength(0);
printNums(formatter);
}
System.out.println(formatter);
}
NIO2 Introducere
API-ul nio2 aduce urmatoarele imbunatatiri:
- Imbunatateste interfata File System
- Functionalitate completa Socket-Channel
- I/O asincrone scalabile
JSR 51 care sta la baza NIO API este orientat spre buffer-e, channel-uri si charset-uri. In plus
aduce socket-uri scalabile furnizand un API neblocabil si multiplexat, care permite
dezvoltarea serverelor scalabile.
Noul API:
- Functioneaza mai consistent peste platforme
- Face mai usoara scrierea programelor, care sunt in stare sa manipuleze operatiile
esuate ale sistemului de fisiere
- Furnizeaza acces mai eficient la o multime mult mai mare de atribute ale fisierului
- Permite dezvoltarea aplicatiilor sofisticate, facand posibila folosirea facilitatilor
dependente de platforma, acolo unde este absolut necesar
- Furnizeaza suport pentru ca fisierele sistem non-native sa fie „plugged in” platforma
API-ul java.io.File a prezentat de-a lungul timpului numeroase provocari pentru
dezvoltatori:
- Multe metode nu aruncau exceptii atunci cand esuau, astfel incat era imposibil de
obtinut un mesaj de eroare folositor
- Multe operatii lipseau (copierea, mutarea fisierelor, etc)
- Metoda rename nu functiona consistent pe diverse platforme
- Nu exista suport pentru link-urile simbolice
- Lipsa de suport pentru metadate precum: permisiune la fisier, proprietar de fisier,
atribute de securitate
- Accesul metadatelor de fisier era ineficient (orice apel pentru metadate rezulta intr-un
apel de sistem)
- Multe dintre metode nu se scalau (cererea unei liste mari dintr-un director aflat pe
server putea duce la intreruperea conexiunii)
- Nu era posibil scrierea de cod corect ce traversa recursiv o arborescenta de fisiere daca
erau link-uri simbolice circulare
Mai mult, IO nu a fost scris pentru a fi extins. Dezvoltatorii cereau posibilitatea de a scrie
propriile implementari de fisiere. Spre exemplu, pastrarea unui pseudofisier in memorie sau
formatarea fisierelor ca arhive.
In NIO2 fisierele si directoarele sunt reprezentate de o cale, care este o locatie absoluta sau
relativa a fisierului sau directorului.
Asa dupa cum am mai afirmat, inainte de NIO2 fisierele erau reprezentate de clasa
java.io.File. In NIO2 instante ale obiectelor java.nio.file.Path sunt folosite pentru a
reprezenta locatii relative sau absolute ale unui fisier sau director.
Fisierele sunt structuri ierarhice. Ele pot avea unul sau mai multe directoare radacina.
Calea absoluta intotdeauna contine elementul radacina si lista completa a directoarelor pentru
a localiza fisierul: /home/peter/statusReport. Toate informatiile necesare a localiza
fisierul sunt continute in string-ul de cale.
Calea relativa trebuie combinata cu o alta cale pentru a accesa un fisier: clarence/foo. Fara
alte informatii programul nu poate localiza directorul.
Obiectele sistemului fisier sunt directoare si fisiere. Unele dintre ele suporta notatia de link-
uri simbolice, notatie care mai este cunoscuta ca „symlink” sau „soft link”.
Un link simbolic este un fisier special ce serveste ca referinta pentru un alt fisier. Un link
simbolic este de obicei transparent pentru utilizator. Citirea sau scrierea unui link simbolic
este aceeasi cu citirea sau scrierea unui alt fisier sau director.
In imaginea anterioara logFile pare a fi un fisier obisnuit, dar, in realitate, este un link
simbolic catre dir/logs/homeLogFile. homeLogFile este target-ul link-ului.
In NIO2 avem urmatoarele pachete si clase:
- java.nio.file.Path, care localizeaza un fisier sau director folosind o cale
dependenta de sistem
- java.nio.file.Files, care folosind un Path executa operatii pe fisiere sau
directoare
- java.nio.file.FileSystem, care furnizeaza o interfata catre un sistem de fisiere si
un factory pentru crearea unui Path si a altor obiecte ce acceseaza sistemul de fisiere
- toate metodele ce acceseaza sistemul de fisiere arunca o IOException sau o subclasa
a sa, ce furnizeaza detalii despre cauza exceptiei
Diferenta semnificativa intre NIO2 si java.io.File este arhitectura de acces la sistemul de
fisiere. In clasa File metodele folosite pentru a manipula informatii despre cai sunt in aceeasi
clasa cu metodele utilizate pentru a scrie si citi fisiere si directoare.
In NIO2 cele doua concepte sunt separate. Caile sunt create si manipulate folosind interfata
Path, in timp ce operatiile pe fisiere si directoare sunt in responsabilitatea clasei Files, ce
opereaza doar pe obiecte Path.
Interfata Path
Furnizeaza punctul de intrare pentru manipularea fisierelor si directoarelor prin NIO2. Pentru
a obtine un obiect Path:
- obtinem o instanta a sistemului de fisiere predefinit si apoi invocam metoda
getPath().
FileSystem fs = FileSystems.getDefault();
Path p44 = fs.getPath("C:\\teste\\Steeams\\out.txt");
- Apelam metoda get() din helper-ul Paths
Path p1 = Paths.get("/home/student/./bob/file");
Obiectele Path sunt nemodificabile, o data ce au fost create nu mai pot fi schimbate.
Observatie: daca dorim sa lucram cu sistemul de fisiere predefinit, care este sistemul de
fisiere pe care JVM ruleaza, utilitarul Paths ofera metoda cea mai scurta de obtinere a lui
Path. Altfel, cand dorim sa efectuam operatii pe Path, pe un alt sistem de fisiere decat cel
default, va trebui sa obtinem mai intai instanta sistemului si apoi sa construim obiectul Path.
Observatie: sistemul de fisiere windows utilizeaza un back slash in mod predefinit. Windows
accepta ambele delimitatoare: slash si back slash. Back slash-urile in Java sunt caractere
escape, de aceea pentru a reprezenta un back slash va trebui sa-l scriem dublu.
Metodele lui Path cuprind:
- Accesul componentelor unei cai:
o Path getFileName(); returneaza end pointul acestei cai
o Path getParent(); returneaza calea parinte sau null (fisier sau director)
o getNameCount()
- Operare pe o cale: normalize(), toUri(), toAbsolutePath(), subpath(),
resolve(), relativize()
- Compararea cailor: startsWith(), endsWith(), equals()
Este bine sa gandim obiectele Path in acelasi fel ca pe obiectele String. Obiectele Path pot
fi create dintr-un singur text sau dintr-o multime de componente:
- O componenta root, ce identifica ierarhia de sistem
- Un element cu nume, cel mai indepartat de radacina, ce defineste fisierul sau
directorul catre care indica calea
- Alte elemente, separate de un caracter special sau un delimitator ce identifica numele
directoarelor ce sunt parte a ierarhiei
Exemplu:
public class PathOperationsTest {
public static void main(String[] args) {
// Normalize
System.out.println("Normalize");
Path p1 = Paths.get("/home/student/./bob/file");
Path pN1 = p1.normalize();
Path p2 = Paths.get("/home/student/bob/../sally/file");
Path pN2 = p2.normalize();
System.out.format("Normalized paths: %s%n%s%n", pN1, pN2);
// Subpath
System.out.println("Subpath");
Path p3 = Paths.get("D:\\Temp\\foo\\bar");
Path p4 = p3.subpath(0, 2);
System.out.println("Count: " + p3.getNameCount());
System.out.println(p3.getName(0));
System.out.println(p4);
// Resolve
System.out.println("Resolve");
Path p5 = Paths.get("/home/clarence/foo");
Path p6 = p5.resolve("bar");
System.out.println(p6);
Path p7 = Paths.get("foo").resolve("/home/clarence");
System.out.println(p7);
// Relativize
System.out.println("Relativize");
Path p8 = Paths.get("peter");
Path p9 = Paths.get("clarence");
Path p8Top9 = p8.relativize(p9);
Path p9Top8 = p9.relativize(p8);
System.out.println(p8Top9);
System.out.println(p9Top8);
}
}
Descrierea metodelor din exemplu:
- int getNameCount();, returneaza numarul de elemente nume ce formeaza aceasta
cale
- Path getRoot();, returneaza componenta radacina a caii
- boolean isAbsolute();, returneaza true daca calea contine un element radacina. In
Windows elementul radacina contine o litera si doua puncte. Pentru Unix true este
returnat pentru orice cale ce incepe cu slash
- Path toAbsolutePath();, returneaza calea absoluta a caii
- java.net.URI toUri();, returneaza URI-ul absolut
Multe sisteme de fisiere utilizeaza notatia . pentru a marca directorul curent si .. pentru
directorul parinte. Putem intalni astfel situatii in care Path contine informatii redundante.
Metoda normalize() elimina orice elemente redundante. Este de retinut ca metoda nu
verifica sistemul in momentul in care modifica calea, este o operatie pur sintactica.
O portiune a unei cai poate constitui o subcale si se poate obtine prin metoda: Path
subpath(int beginIndex, int endIndex);. Elementul cel mai apropiat de radacina are
indexul 0. Elementul cel mai indepartat de radacina are indexul count-1. Rezultatul este o cale
ce cuprinde elementele ce incep de la beginIndex pina la endIndex-1.
Metoda resolve() este utilizata pentru a combina doua cai. Ea accepta o cale partiala, care
este calea ce nu include elementul radacina, cale ce este adaugata caii originale. Daca
trimitem ca parametru o cale absoluta metoda returnea calea trimisa.
Metoda relativize() ne permite sa construim o cale de la o locatie din sistem la o alta
locatie. Noua cale este relativa la cea originala.
Pachetul java.nio.file si interfata Path sunt „link aware”. Orice metoda poate detecta ce
are de facut atunci cand se intalneste un link simbolic sau ofera posibilitatea de configurare a
comportamentului metodei atunci cand se intalneste un link simbolic.
Unele sisteme de fisiere suporta link-urile hard. Link-urile hard sunt mult mai restrictive
decat link-urile simbolice:
- target-ul link-ului trebuie sa existe
- link-urile hard nu sunt in general permise directoarelor
- hard link-urile nu sunt permise pe partitii sau volume
- un link hard arata si se comporta ca un fisier regulat, de aceea este greu de gasit
- un link hard este aceeasi entitate ca un fisier original. Toate atributele sunt identice
Datorita acestor restrictii hard link-urile nu sunt folosite la fel de des precum link-urile
simbolice.
Clasa Files
Este entry point-ul primar pentru operatii pe obiecte Path. Metodele statice ale clasei citesc,
scriu sau manipuleaza fisiere si directoare reprezentate de obiecte Path. Clasa este, de
asemenea, link aware.
Obiectele Path reprezinta conceptul de locatie e unui fisier sau director. Inainte de a accesa un
fisier sau director va trebui sa accesam sistemul de fisiere pentru a determina daca acesta
exista:
- exists(Path p, LinkOption...option), care testeaza daca fisierul exista. In mod
implicit sunt urmate link-urile simbolice
- notExists(Path p, LinkOption...option), care testeaza daca fisierul nu exista.
In mod implicit sunt urmate link-urile simbolice
Exemplu:
Path p = Paths.get("D:\\Temp\\foo\\bar");
System.out.format("Path %s exists: %b%n", p,
Files.exists(p, LinkOption.NOFOLLOW_LINKS));
Cand testam existenta unui fisier avem urmatoarele posibilitati:
- fisierul este verificat daca exista
- fisierul este verificat ca nu exista
- statutul fisierului este necunoscut. Acest rezultat poate apare atunci cand programul
nu are acces la fisier
Observatie: !Files.exists(path) nu este echivalent cu Files.notExists(path). Daca ambele
returneaza false, existenta fisierului sau directorului nu poate fi determinata. In Windows,
spre exemplu, aceasta poate fi obtinuta atunci cand cerem statutul unui drive off-line, precum
CD-ROM-ul.
Pentru a verifica ca un fisier poate fi accesat avem urmatoarele metode booleene:
isReadable(Path), isWritable(Path), isExecutable(Path). Aceste operatii nu sunt
atomice, cu respectarea altor operatii pe sistemul de fisiere. De aceea rezultatul acestor
metode nu este sigur pentru ca poate fi modificat imediat dupa incheierea metodei.
Metoda isSameFile(Path, Path) testeaza daca doua cai indica catre acelasi fisier. Aceasta
este folositor in sisteme ce suporta link-uri simbolice.
Fisierele si directoarele pot fi create folosind una dintre metodele:
Files.createFile(Path), Files.createDirectory(Path). Metoda
Files.createDirectories(Path) poate fi utilizata pentru a crea directoare ce nu exista, de
sus in jos.
Putem sterge fisiere, directoare sau link-uri. Metoda Files.delete(Path) sterge fisierul sau
arunca o exceptie in caz de esec. Spre exemplu, daca fisierul nu exista arunca o
NoSuchFileException. Metoda Files.deleteIfExists(Path) sterge fisierul, dar nu
arunca exceptii. Aceasta metoda este utila in cazul firelor de executie.
Putem copia un fisier sau director prin Files.copy(Path, Path, CopyOption...). Cand
directoarele sunt copiate, fisierele continute nu sunt copiate. Copierea esueaza daca fisierul
destinatie exista, mai putin atunci cand specificam optiunea REPLACE_EXISTING. Cand
copiem un link simbolic destinatia link-ului este copiata. Daca dorim sa copiem link-ul insusi,
si nu continutul sau, specificam optiunea NOFOLLOW_LINKS sau REPLACE_EXISTING. In cazul
folosirii optiunii REPLACE_EXISTING in cazul copierii unui director nevid, copierea esueaza
cu aruncarea FileAlreadyExistsException. Optiunea COPY_ATTRIBUTES determina
copierea atributelor de fisier asociate, fisierului destinatie.
Avem la dispozitie doua metode de copiere dintr-un flux intr-un fisier: copy(InputStream
source, Path target, CopyOption...options), respeciv dintr-un fisier intr-un stream:
copy(Path source, OutputStream out).
Exemplu de folosire a metodei de copiere dintr-o pagina web intr-un fisier:
Path path = Paths.get("C:/oracle.html");
URI u = URI.create("http://www.oracle.com/");
try (InputStream in = u.toURL().openStream()) {
Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
Exemplul de dinainte poate fi extins pentru a scrie un fisier intr-un socket sau alte tipuri de
fluxuri.
Putem muta un fisier sau director prin utilizarea metodei: Files.move(Path, Path,
CopyOption...). La mutare trebuie sa tinem cont de urmatoarele:
- daca destinatia este un director si directorul este vid, mutarea reuseste daca este setat
REPLACE_EXISTING
- daca directorul destinatie nu exista, mutarea reuseste. Aceasta este de fapt o
redenumire a directorului
- daca directorul exista si nu este vid este aruncata o DirectoryNotEmptyException
- daca sursa este un fisier si destinatia este un director care exista, REPLACE_EXISTING
este setat, mutarea va redenumi fisierul la numele directorului
Pentru a muta un director ce contine fisiere intr-un alt director trebuie sa copiem recursiv
continutul directorului si apoi sa stergem vechiul director.
Putem efectua mutarea ca pe o operatie atomica folosind ATOMIC_MOVE. Daca sistemul de
fisiere nu suporta mutarea atomica se va arunca o exceptie. Cu mutarea atomica putem muta
un fisier intr-un director si avem garantia ca un proces ce urmareste directorul acceseaza
fisierul intreg.
Clasa DirectoryStream furnizeaza un mecanism pentru a itera peste toate intrarile dintr-un
director. Clasa se scaleaza pentru a suporta foarte multe directoare. Exceptia
DirectoryIteratorException este aruncata daca apar eori I/O pe timpul iterarii. Exceptia
PatternSyntaxException este aruncata daca pattern-ul furnizat ca al doilea argument este
invalid.
public class ListDirectory {
public static void main(String[] args) {
Path dir = Paths.get("c:/chestii");
String filter = "*";
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter))
{
for (Path file : stream) {
System.out.println(file.getFileName());
}
} catch (PatternSyntaxException | DirectoryIteratorException | IOException
x) {
//IOException can never be thrown by the iteration.
//In this snippet, it can only be thrown by newDirectoryStream.
System.err.println(x);
}
}
}
Daca avem un fisier de dimensiuni mici si dorim sa-i citim intregul continut intr-un singur pas
putem folosi metodele Files.readAllBytes(Path) sau Files.readAllLines(Path,
Charset). Aceste metode gestioneaza intreaga operatiune precum inchiderea si deschiderea
stream-ului.
Putem folosi una dintre metodele de scriere pentru a scrie octeti sau linii intr-un fisier:
write(Path, byte[], OpenOption...), respectiv write(Path, Iterable<? extends
CharSequence>, Charset, OpenOption...).
Gestiunea Metadatelor
Daca un program necesita mai multe atribute ale fisierului in acelasi timp este ineficient sa
folosim metode ce returneaza un singur atribut. Accesul repetat la sistemul de fisiere pentru a
obtine un singur atribut poate afecta performanta. De aceea clasa Files furnizeaza doua
metode de obtinere a atributelor unui fisier intr-o singura operatie:
- readAttributes(Path, String, LinkOption...)
- readAttributes(Path, Class<A>, LinkOption...)
Exemplu:
public class FileAttributes {
Operatii recursive
Clasa Files furnizeaza o metoda de parcurgere a arborelui de fisiere pentru operatii recursive
precum copierea si stergerea.
Interfata FileVisitor contine metode ce sunt invocate pe fiecare nod vizitat dintr-o
arborescenta de fisiere:
- preVisitDirectory(Path, BasicFileAttributes), invocata pe un director inainte
ca intrarile in director sa fie vizitate
- visitFile(Path, BasicFileAttributes), invocata pentru un fisier intr-un director
- postVisitDirectory(Path, BasicFileAttributes), invocata dupa ce toate
intrarile din director si descendentii lor au fost vizitati
- visitFileFailed(Path, BasicFileAttributes), invocata pentru un fisier ce nu
poate fi vizitat
Rezultatul returnat de fiecare dintre metode determina actiunile ce urmeaza a se lua dupa ce
un nod a fost atins (pre sau post). Aceste actiuni sunt enumerate in clasa FileVisitResult:
- CONTINUE, continua cu nodul urmator
- SKIP_SIBLINGS, continua fara vizitarea fratilor acestui fisier sau director
- SKIP_SUBTREE, continua fara vizitarea intrarilor din acest director
- TERMINATE
Exista o clasa, SimpleFileVisitor, ce implementeaza metodele din FileVisitor cu tipul
returnat FileVisitResult.CONTINUE sau aruncarea unei IOException.
Pornind de la primul nod director si continuand cu fiecare subdirector intalnit metoda
preVisitDirectory() este invocata pe clasa trimisa metodei walkFileTree().
Presupunand ca rezultatul returnat este FileVisitResult.CONTINUE, urmatorul nod este
explorat.
Observatie: traversarea arborescentei de fisiere este in adancime cu FileVisitor invocat
pentru fiecare fisier intalnit. Traversarea arborescentei se incheie cand toate fisierele
accesibile din arborescenta au fost vizitate sau o metode de vizitare returneaza TERMINATE.
Cand o metoda de vizitare se termina din cauza unei I OException, o eroare sau o exceptie
runtime, traversarea se termina si eroarea sau exceptia este propagata apelentului metodei.
Cand este intalnit un fisier in arborescenta, metoda walkFileTree() incearca sa-i citeasca
BasicFileAttributes. Daca fisierul nu este director metoda visitFile() este invocata cu
atributele de fisier. Daca atributele de fisier nu pot fi citite metoda visitFileFailed() este
invocata cu o exceptie I/O.
Dupa vizitarea tuturor descendentilor unui nod metoda postVisitDirectory() este invocata
pe fiecare director.
In exemplul urmator, clasa PrintTree implementeaza fiecare metoda din FileVisitor si
afiseaza tipul, numele si marimea directorului si fisierului la fiecare nod.
public class PrintTree implements FileVisitor<Path> {
// If there is some error accessing the file, let the user know.
// If you don't override this method and an error occurs, an IOException
// is thrown.
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.out.print("vistiFileFailed: ");
System.err.println(exc);
return CONTINUE;
}
}
Gasirea fisierelor
Pentru a gasi un fisier, in mod normal, cautam un director. Putem utiliza un instrument de
cautare sau o comanda precum: dir /s *.java. Aceasta comanda va cauta recursiv in
arborescenta de directoare, incepand cu elementul in care ne aflam, toate fisierele ce contin
extensia java.
Interfata java.nio.file.PathMatcher include o metoda de cautare pentru a determina daca
obiectul Path se potriveste unui string de cautare. Orice sistem de fisiere furnizeaza un
PathMatcher ce poate fi obtinut din factory-ul FileSystems.
(FileSystems.getDefault().getPathMatcher(String syntaxAndPattern))
String-ul argument este de forma syntax:pattern, unde syntax poate fi glob sau regex.
Sintaxa glob este similara expresiilor regulate, dar mai simpla. Urmatoarele reguli sunt
utilizate pentru a interpreta pattern-ii glob:
- Caracterul * potriveste zero sau mai multe caractere unui nume fara a depasi limitele
directorului
- Caracterele ** potriveste zero sau mai multe caractere depasind limitele directorului
- ? potriveste exact un caracter al numelui
- \ este utilizat pentru caractere escape
- [] formeaza o expresie ce potriveste un singur caracter al numelui dintr-o multime de
caractere. Spre exemplu[abc] potriveste a, b sauc. – poate fi utilizat pentru a specifica
un doemniu. Spre exemplu [a-z] specifica domeniul, inclusiv capetele, de la a la z.
Putem avea si [abce-g] insemnand a, b, c, e, f sau g. ! este folosit pentru negare. Spre
exemplu [!a-c] potriveste orice caracter mai putin a, b sau c
- Intr-o expresie cu paranteze caracterele *, ? si \ se potrivesc lor insele. – se potriveste
lui insusi daca este primul caracter sau imediat dupa !
- {} formeaza un grup de subpattern-uri, unde grupul se potriveste daca orice subpattern
din grup se potriveste. , este folosita pentru a separa subpattern-urile. Grupurile nu pot
fi incuibate
- . in numele unui fisier este tratat ca un caracter obisnuit in expresia de potrivire
Cand syntax este regex componenta pattern este o expresie regulata asa cum a fost definita de
clasa Pattern.
Exemplu:
public class Finder extends SimpleFileVisitor<Path> {
In clasa ce contine metoda main() avem doua argumente necesare cautarii. Primul este
radacina arborescentei in care se face cautarea si este testat pentru a fi director. Al doilea este
utilizat pentru a crea un PathMatcher cu o expresie regulata utilizand un factory
FileSystems.
Clasa Finder implementeaza interfata FileVisitor, astfel incat sa poata fi trimisa metodei
walkFileTree(). Aceasta clasa este utilizata pentru a apela metoda de potrivire pe fiecare
dintre fisierele vizitate in arborescenta.
String s = store.toString();
if (s.length() > 20) {
System.out.println(s);
s = "";
}
System.out.format("%-20s %12d %12d %12d\n", s, total, used, avail);
}
/**
* Register the given directory with the WatchService
*/
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE,
ENTRY_MODIFY);
if (trace) {
Path prev = keys.get(key);
if (prev == null) {
System.out.format("register: %s\n", dir);
} else {
if (!dir.equals(prev)) {
System.out.format("update: %s -> %s\n", prev,
dir);
}
}
}
keys.put(key, dir);
}
/**
* Register the given directory, and all its sub-directories, with the
* WatchService.
*/
private void registerAll(final Path start) throws IOException {
// register directory and sub-directories
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir,
BasicFileAttributes attrs) throws IOException {
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Creates a WatchService and registers the given directory
*/
WatchDir(Path dir, boolean recursive) throws IOException {
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new HashMap<>();
this.recursive = recursive;
if (recursive) {
System.out.format("Scanning %s ...\n", dir);
registerAll(dir);
System.out.println("Done.");
} else {
register(dir);
}
/**
* Process all events for keys queued to the watcher
*/
void processEvents() {
for (;;) {
Fire de executie
Sistemele de operare moderne folosesc multitaskuri preventive pentru a aloca timp pe
procesor pentru aplicatii. Avem doua tipuri de task-uri ce pot fi programate pentru executie:
- Procesele: un proces este o zona de memorie ce contine cod si date. Un proces are un
fir de executie ce este programat sa primeasca procesorul pentru portiuni de timp.
Portiunea de timp (time slice) este de obicei masurata in milisecunde. Cand timpul a
expirat taskul este in mod fortat indepartat de pe procesor si un alt task ii ia locul
- Fire: un fir este o executie programata a unui proces. Firele pot fi concurente. Toate
firele unui proces impart aceleasi date de memorie, dar pot urmari diferite cai catre o
sectiune a codului
Pentru a executa un program cat mai repede cu putinta trebuie sa evitam blocajele
(bottleneck). Exista cateva situatii care descriu aceste blocaje:
- Conexiuni la resurse: doua sau mai multe task-uri asteapta pentru utilizarea exclusiva
a unei resurse
- Blocarea din cauza operatiilor de I/O: pe timpul acestor operatii se asteapta transferul
datelor
- Subutilizarea procesorului: o aplicatie intr-un singur fir utilizeaza un singur procesor
Chiar daca nu scriem explicit cod pentru a crea noi fire de executie, codul poate rula intr-un
mediu multifir. Trebuie asadar sa fim avertizati cum lucreaza firele si cum sa scriem cod
thread-safe. Spre exemplu, cand scriem code care ruleaza intr-un alt software (middleware
sau server de aplicatie) trebuie avuta in vedere documentatia produsului pentru a descoperii
daca firele sunt create automat. Intr-un server de aplicatie Java EE exista o componenta
numita servlet utilizata pentru manipularea cererilor HTTP. Servlet-urile trebuie sa fie
intotdeauna thread-safe deoarece serverul porneste un nou fir la fiecare cerere HTTP.
Clasa Thread
Este folosita pentru a crea si porni fire. Codul ce urmeaza a fi executat de un fir trebuie plasat
intr-o clasa ce face una din urmatoarele:
- Extinde clasa Thread (codarea este mai simpla). Va trebui sa suprascriem metoda
run().
- Implementeaza interfata Runnable (mult mai flexibil)
Exemplu:
public class ExampleThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++) {
System.out.println("i:" + i);
}
}
}
Codul de executat in noul fir trebuie plasat in metoda run(). Trebuie sa evitam apelul lui
run() direct. Apelul lui run() nu va porni un nou fir, efectul fiind acelasi al apelului unei
metode obisnuite.
Dupa crearea firului, el trebuie pornit prin apelul metodei start():
public class ExtendingThreadMain {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("i:" + i);
}
}
}
Dupa ce am creat un Runnable trebuie sa il trimitem constructorului lui Thread. Doar Thread
poate porni firul:
public class ImplementingRunnableMain {
public static void main(String[] args) {
ExampleRunnable r1 = new ExampleRunnable();
Thread t1 = new Thread(r1);
t1.start();
}
}
Campurile statice sau campurile instanta sunt potential impartite intre fire.
public class ExampleSharedRunnable implements Runnable {
private int i;
@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println("i:" + i);
}
}
}
Obiectele care sunt referite de mai multe fire au campurile accesate concurent.
public class SharedDataMain {
Operatii atomice
O operatie atomica functioneaza ca o singura operatie. Spre exemplu, o instructiune in Java
nu este intotdeauna atomica. Spre exemplu:
i++; - creaza o copie temporara a lui i, incrementeaza copia si apoi scrie noua valoare inapoi
in i.
l = 0xffff_ffff_ffff_ffff; - este o variabila de 64 de biti ce poate fi accesata folosind
doua operatii separate pe 32 de biti.
O problema posibila cu doua fire de executie ce incrementeaza acelasi camp este ca
modificarea poate fi pierduta. Sa presupunem ca doua fire de executie citesc valoarea 41
dintr-un camp, incrementeaza valoarea cu unu si scriu rezultatul inapoi in camp. Ambele fire
incheie incrementarea dar rezultatul este doar 42. Dependent de cum este implementata JVM
si de tipul de procesor utilizat, putem obtine aceasta situatie rar sau niciodata, dar trebuie avut
in vedere ca ea se poate intampla.
Daca avem un numar lung 0xffff_ffff_ffff_ffff si il incrementam cu 1, rezultatul ar
trebui sa fie 0x0000_0001_0000_0000. Din cauza operatiei ce se face in doua etape putem
avea 0x0000_0001_ffff_ffff sau 0x0000_0000_0000_0000 dependent de care biti se
modifica primii. Daca un al doilea fir are permisiunea sa citeasca un camp de 64 de biti in
timp ce este modificat de un alt fir, o valoare incorecta poate fi obtinuta.
Operatiile efectuate intr-un fir pot sa nu apara la executie in ordine daca vom observa
rezultatele dintr-un alt fir. Aceasta deoarece firele opereaza pe copii cache ale variabilelor
sharuite.
Ca sa ne asiguram de consistenta comportamentului in fire, trebuie sa sincronizam actiunile
lor, adica sa ne asiguram ca o actiune se intampla inaintea alteia, respectiv sa golim cache-ul
variabilelor share-uite.
Fiecare fir are o memorie de lucru in care isi tine copiile de lucru ale variabilelor. Cand firul
executa un program opereaza pe aceste copii. Exista cateva actiuni ce sincronizeaza memoria
de lucru a firului cu memoria principala:
- Citirea sau scrierea volatila a unei variabile
- Blocarea sau deblocarea
- Prima si ultima actiune a unui fir
- Actiuni ce pornesc un fir sau detecteaza ca un fir s-a terminat
Un camp poate fi declarat ca volatile. Scrierea sau citirea unui camp volatil va determina ca
firul sa-si sincronizeze memoria de lucru cu memoria principala. Volatil nu inseamna, insa,
atomic, adica operatia in care este implicat campul nu este thread safe.
Un fir se opreste prin incheierea metodei run().
Vom da un exemplu de folosire a campurilor volatile pentru oprirea unui fir de executie.
public class ExampleVolatileRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread started");
while (!timeToQuit) {
System.out.println("thread running");
}
System.out.println("Thread finishing");
}
}
public Item() {
}
/**
* @return the id
*/
public int getId() {
return id;
}
/**
* @param id
* the id to set
*/
public void setId(int id) {
this.id = id;
}
/**
* @return the description
*/
public String getDescription() {
return description;
}
/**
* @param description
* the description to set
*/
public void setDescription(String description) {
this.description = description;
}
/**
* @return the price
*/
public double getPrice() {
return price;
}
/**
* @param price
* the price to set
*/
public void setPrice(double price) {
this.price = price;
}
}
Deadlock
Blocarea mortala apare atunci cand doua sau mai multe fire sunt blocate definitiv, asteptand
unul dupa altul.
Alte probleme ce pot apare relativ la firele de executie sunt starvation si livelock.
Starvation descrie o situatie in care un fir este incapabil sa obtina acces la resurse share-uite si
nu este capabil sa progreseze. Aceasta se intampla atunci cand resursele impartite sunt
indisponibile pentru o lunga perioada de timp din cauza firelor „lacome”. De exemplu,
presupunem ca un obiect furnizeaza o metoda sincronizata careia ii ia mult timp pentru a se
incheia. Daca un fir invoca aceasta metoda frecvent, alte fire ce necesita acces sincronizat
frecvent la acelasi obiect vor fi deseori blocate.
Deseori un fir actioneaza ca raspuns la actiunea unui alt fir. Daca actiunea celuilalt fir este tot
un raspuns la primul apare livelock. Ca si deadlock, in livelock firele nu sunt capabile sa faca
progrese. Firele nu sunt blocate, dar sunt permanent ocupate raspunzandu-si unul altuia.
Vom examina particularitatile programarii multifir prin problema celor cinci filosofi, enuntata
astfel: cinci filosofi sunt asezati la o masa rotunda incercand sa manance avand la dispozitie 5
betigase. Un betigas este plasat intre fiecare doi filosofi. Un filosof are nevoie de doua
betigase pentru a putea minca.
Rezolvarea aplicatiei din exemplul de mai sus prin fire de executie, determina gestionarea
“starii” unui filosof printr-un fir de executie. Cele cinci fire nu pot opeara independent pentru
ca impart un numar prea mic de betigase si deci un obiect Betigas este folosit la un moment
dat de un sngur fir. In plus, fiecare filosof trebuie sa afle cand un betigas este eliberat de catre
un alt filosof, astfel incat sa-l poata folosi.
Clasa ce modeleaza starea unui betigas este:
class Betigas {
}
Atributul de tip boolean al clasei da starea obiectului, mai
precis daca betigasul este detinut de catre cineva sau nu.
Initiat betigasul este disponibil.
Cele doua metode simuleaza actiunile de modificare a starii: eliberareBetigas() si
luareNetigas(). Ambele sunt sincronizate, prima notifica celelalte fire despre eliberarea
betigasului, prin metoda notifyAll(), a doua blocheaza firul curent pina ce betigasul este
eliberat, prin metoda wait().
Clasa care modeleaza comportamentul unui filosof o vom numi Filosof si implementeaza
Runnable. Clasa are doua atribute, indicele betigaselor cu care maninca filosoful, i cel din
dreapta respectiv j cel din stanga. Initial fiecare filosof are un singur betigas de indice egal cu
cel al sau, din sirul de filosofi. Metoda run() va contine un ciclu infinit in care starile
filosofului se modifica. Initial firul este intrerupt pentru o perioada aleatoare cuprinsa intre 0
si 3 secunde, considerata perioada in care filosoful gandeste (de aia e filosof !). Apoi filosoful
incearca sa ia un betigas prin aplelul metodei luareBetigas(). Dupa aceasta incearca
obtinerea celui de-al doilea betigas prin apelul aceleiasi metode. Avand cele doua betigase
filosoful maninca un timp aleator curins intre 1 si 2 secunde. Perioada mesei este simulata
prin intreruperea firului pe o durata limitata. Dupa masa filosoful elibereaza ambele betigase
prin intermediul metodei eliberareBetigas(). Cum metoda luareBetigas() este
sincronizata nu este posibil ca doi filosofi sa o apeleze simultan. Deci, un singur fir este
notificat la un moment dat de eliberarea obiectului. Folosirea in acest caz a apelului lui
notify() sau notifyAll() este indiferenta.
Problema mare ce apare in cazul exemplului nostru este evitarea punctului mort. Iata un
scenariu. Presupunem ca suntem la inceputul mesei si fiecare filosof sa ia betigasul din
dreapta sa. Nu poate manca doar cu unul si de aceea incearca sa il ia pe al doilea. Fiecare va
apela la nesfarsit metoda wait(). Cum niciun filosof nu are doua betigase metoda
notifyAll() nu va fi apelata vreodata. Programul este in acest moment intr-un punct mort.
Noi am construit rezolvarea prin intreruperea initiala, aleatorie, a fiecarui fir de excutie,
perioada denumita de gandire. Este putin probabil ca fiecare filosof sa gandeasca la fel de
mult, deci punctul mort nu se va instala inca de la inceput. Dar nu este exclus sa se instaleze
pe parcurs. Vom evita instalarea punctului mort prin faptul ca ne asiguram ca la un moment
dat un filosof nu are doar un singur betigas, ci poate sa il ia si pe al doilea. In cod am evitat
blocarea prin faptul ca un filosof este obligat ca mai intai sa-si aleaga betigasul de indice
minim si dupa ce a facut rost de acesta il va lua pe celalalt, care sigur exista. Spre exemplu,
daca filosoful 0 si 1 doresc in acelasi timp sa manince (0 are nevoie de betigasele 0 si 5, iar 1
de 0 si 1) vor astepta ambii dupa betigasul 0 (de indice minim). Daca filosoful 1 va primi
betigasul 0 atunci il va lua si pe 1 care este disponibil, altfel 0 il va lua pe 5. Conform acestei
strategii 5 va fi intotdeauna disponibil ca al doilea betigas.
Vom retine obiectele Filosof intr-un sir de obiecte si vom crea pentru fiecare un fir de
executie. De asemenea, vom crea un sir de obiecte Betigas, initial fiecare filosof detine un
astfel de obiect, de indice egal cu al sau.
Codul claselor Filosof si Test este dat in continuare:
public class Test{
Filosof[] filosofi ;
Thread[] fir ;
Betigas[] betigase ;
public Test () {
filosofi = new Filosof[5] ;
fir = new Thread[5] ;
betigase = new Betigas[5] ;
}
int i, j ;
class Betigas {
class B {
synchronized void met1(A a) {
String name = Thread.currentThread().getName();
System.out.println(name + " apelul metodei B.met1");
try {
Thread.sleep(1000);
} catch(Exception e) {
System.out.println("B este intrerupta");
}
System.out.println(name + " incearca sa apeleze A.met2()");
a.met2();
}
Deadlock() {
Thread.currentThread().setName("Firul Principal");
Thread t = new Thread(this, "Firul Concurent");
t.start();
a.met1(b);
System.out.println("inapoi in firul principal");
}
Concurenta
In Java SE5 s-a introdus pachetul java.util.concurrent ce contine clase utile in
programarea concurenta. Acesta cuprinde:
- Colectii concurente
- Sincronizare si blocare alternativa
- Pool-uri de fire
In pachetul java.util.concurrent.atomic avem clase ce suporta programare thread-safe
fara blocare, pe variabile singulare.
public class AtomicExample {
public static void main(String[] args) {
AtomicInteger ai = new AtomicInteger(5);
if(ai.compareAndSet(5, 42)) {
System.out.println("Replaced 5 with 42");
}
}
}
In exemplul anterior operatia atomica asigura ca valoarea curenta este 5 si apoi este setata la
42. Pentru arhitecturile procesoarelor ce suporta compararea nativa si operatia de setare nu
este nevoie de blocare cand se executa exemplul anterior. Alte arhitecturi pot necesita unele
forme de blocare interna.
Pachetul java.util.concurrent.locks este un framework pentru blocare si asteptarea unor
conditii ce este folosit pentru sincronizare predefinita si monitoare. Una dintre facilitatile
acestui pachet este implementarea multi-reader si blocarea single writer. Astfel, un fir nu va
obtine blocarea la citire atunci cand o actiune de scriere este in derulare. Mai multe fire pot
cere concurent pot dobandi blocarea la citire, dar numai unul blocarea la scriere. Mai mult, un
fir ce a obtinut blocarea pentru scriere poate apela alte metode ce obtin blocarea la scriere fara
frica de blocare a executiei.
public class ShoppingCart {
private final ReentrantReadWriteLock rwl =
new ReentrantReadWriteLock();
Colectii thread-safe
Colectiile din java.util nu sunt thread-safe. Pentru a le face thread-safe vom folosi blocuri
de cod sincronizate pentru accesul la o colectie atunci cand facem scriere, creem un wrapper
sincronizat folosind metode precum
java.util.Collections.synchronizedList(List<T>) sau utilizam colectiile din
java.util.concurrent.
Observatie: facand o colectie sincronizata aceasta nu inseamna ca elementele sale sunt
sincronizate.
Clasa ConcurrentLinkedQueue furnizeaza o coada FIFO eficient scalabila, non-blocabila si
thread-safe. Cinci implementari din java.util.concurrent suporta interfata
BlockingQueue, ce definesc versiuni de blocare pentru a pune si a lua:
LinkedBlockingQueue, ArrayBlockingQueue, SynchronousQueue,
PriorityBlockingQueue si DelayQueue.
Pe linga cozi acest pachet furnizeaza implementari de colectii destinate a fi utilizate in
contexte multifir: ConcurrentHashMap, ConcurrentSkipListSet, CopyOnWriteArrayList
si CopyOnWriteArraySet. Cand se asteapta ca mai multe fire sa acceseze o colectie data un
ConcurrentHashMap este preferat unui HashMap sincronizat, un ConcurrentSkipListMap
este prefeta unui TreeMap sincronizat. Un CopyOnWriteArraySet este preferabil unui
ArrayList sincronizat atunci cand numarul asteptat de citiri si traversari este mai mare decat
numarul update-urilor.
In pachetul java.util.concurrent avem cinci clase ce furnizeaza actiuni relative
sincronizarii. Acestea permit firelor blocarea pina cand o anumita stare este atinsa:
- Semaphore, reprezinta un tool clasic de concurenta. Acesta gestioneaza o multime de
permisiuni. Firele incearca sa obtina permisiuni si se pot bloca pina cand alte fire
elibereaza permisiunile.
- CountDownLatch, un utilitar simplu pentru blocare, cat timp se pastreaza un numar de
semnale, evenimete sau conditii. Unul sau mai multe fire se blocheaza pina cand un
cronometru se incheie. Dupa incheierea cronometrului toate firele continua.
CountDownLatch nu poate fi refolosit.
- CyclicBarrier, un punct resetabil ce permite sincronizarea pe mai multe cai si este
folositoare in programarea paralela. Creaza o numaratoare de fire. Dupa ce un numar
de fire au apelat await() pe CyclicBarrier, ele vor fi eliberate. CyclicBarrier
sunt reutilizabile
- Phaser, furnizeaza o forma mai flexibila de barrier ce poate fi folosita pentru
controlul calculelor de-a lungul firelor multiple. Firele se pot inregistra si deinregistra
afectand astfel numarul de fire din barrier
- Exchanger, permit ca doua fire sa schimbe obiecte intr-un punct de intalnire si este
folositor in diverse proiectari pipeline. Firele sunt blocate pina cand are loc
schimbarea. Este bidirectional, eficient cu memoria si o alternativa la
SynchronousQueue.
Exemplu:
public class CyclicBarrierExample {
new Thread() {
public void run() {
try {
System.out.println("before await - thread 1");
barrier.await();
System.out.println("after await - thread 1");
} catch (BrokenBarrierException|InterruptedException ex) {
}
}
}.start();
new Thread() {
public void run() {
try {
System.out.println("before await - thread 2");
barrier.await();
System.out.println("after await - thread 2");
} catch (BrokenBarrierException|InterruptedException ex) {
}
}
}.start();
}
}
In exemplul anterior am creat un CyclicBarrier care va determina ca doua fire sa apeleze
await() pina cand vor fi deblocate. Daca doar un fir apeleaza await(), firul va fi blocat
permanent. Clasa CyclicBarrier contine si metoda await(long timeout, TimeUnit
unit), care va bloca firul pentru o durata de timp si va arunca o TimeoutException daca
acea durata a fost atinsa.
try {
//Block until all Callables have a chance to finish
es.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
System.out.println("Stopped waiting early");
}
@Override
public boolean equals(Object obj) {
if (obj instanceof RequestResponse) {
RequestResponse lookup = (RequestResponse) obj;
if (host.equals(lookup.host) && port == lookup.port) {
return true;
}
}
return false;
}
@Override
public RequestResponse call() throws IOException {
try (Socket sock = new Socket(lookup.host, lookup.port);
Scanner scanner = new Scanner(sock.getInputStream())) {
lookup.response = scanner.next();
return lookup;
}
}
}
//low inclusive
//high exclusive
public PriceRangeServer(int port, int low, int high) throws IOException {
ss = new ServerSocket(port);
ss.setSoTimeout(250);
double d = Math.random() * (high - low) + low;
price = String.format("%.2f", d);
}
@Override
public void run() {
try {
accept();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
}
System.out.println("Press enter to quit...");
try {
System.in.read();
} catch (IOException ex) {
}
System.out.println("Quiting...");
exSrv.shutdownNow();
}
}
Sistemele moderne contin multiple procesoare. Pentru a folosi avantajul puterii de procesare
va trebui sa executam task-uri in paralel pe aceste procesoare. Situatiile aparute sunt:
- imparte si stapaneste: un task se divide in subtask-uri. Va trebui doar sa identificam
acele subtask-uri ce se pot executa in paralel
- unele probleme sunt dificil a fi executate in task-uri paralele
- serverele ce suporta clienti multipli pot folosiun task separat pentru a manipula fiecare
client
- programarea a prea multor task-uri in paralel pot avea un impact negativ de
performanta
Daca task-urile sunt orientate pe calcule mai degraba decat pe I/O, numarul task-urilor
paralele nu trebuie sa fie mai mare decat numarul de procesoare din sistem. Putem afla
numarul procesoarelor, asa dupa cum am vazut intr-un exemplu anterior.
Daca nu folosim fire de executie, doar o portiune din puterea de procesare a sistemului va fi
folosita. Spre exemplu, daca avem o cantitate mare de date de procesat si doar un fir care
proceseaza aceste date, doar un procesor va fi utilizat. Intr-un sistem cu 4 procesoare, 3 dintre
ele stau in asteptare pe timpul prelucrarii datelor.
O solutie simpla de folosire a paralelismului imparte datele ce urmeaza a fi procesate in mai
multe multimi, distribuind o multime unui procesor si un fir pentru procesarea fiecarui set.
Dupa procesare submultimile trebuie combinate intr-un mod potrivit.
Avem mai multe moduri de a imparti datele de dimensiuni mari. Spre exemplu, in cazul unui
array crearea unui nou array pe fiecare fir ce contine o copie a unei portiuni din firul original
este prea consumatoare de memorie. Solutia mai eficienta este ca fiecare array sa imparta o
referinta dintr-un singur array mare, dar accesul sa fie doar la o submultime intr-un mod non-
blocking thread-safe.
In cazul unei baze de date, impartirea pentru fiecare fir a unei submultimi de dimensiune
egala de inregistrari, poate genera probleme.
Totusi, ideal ar fi ca toate procesoarele sa fie utilizate complet pina cand task-ul este terminat,
dar:
- procesoarele pot rula cu viteza diferita
- task-uri non Java vor necesita procesorul si vor reduce timpul alocat unui fir Java pe
respectivul procesor
- datele ce urmeaza a fi analizate pot necesita variate perioade de timp pentru a fi
procesate
Pentru a tine mai multe fire ocupate:
- impartim datele de procesat intr-un numar mare de submultimi
- asignam submultimile de date unei cozi de fire ce urmeaza a fi procesate
- fiecare fir va avea mai multe submultimi asignate
Daca un fir termina toate submultimile mai repede, poate „fura” submultimile dintr-un alt fir.
Prin impartirea datelor ce urmeaza a fi procesate pina cand avem mai multe submultimi decat
fire, facilitam „work-stealing”. In work-stealing un fir ce ruleaza si isi termina treaba poate
fura o submultime de date din coada de procesare a unui alt fir. Trebuie doar sa decidem
cantitatea optima de adaugat fiecarei cozi de procesare. O supramaruntire a datelor poate duce
la o ineficienta incarcare, in timp ce o submaruntire poate conduce la o subutilizare a
procesoarelor.
In exemplul urmator avem doua task-uri separate ce pot fi executate in paralel. Initializarea
sirului cu valori aleatoare si cautarea in sir a valorii celei mai mari pot fi facute in paralel.
public class Main {
//We use a lot of memory
public static void main(String[] args) {
int[] data = new int[1024 * 1024 * 32]; //32MB
for (int i = 0; i < data.length; i++) {
data[i] = ThreadLocalRandom.current().nextInt();
}
int max = Integer.MIN_VALUE;
for (int value : data) {
if (value > max) {
max = value;
}
}
System.out.println("Max value found:" + max);
}
}
Obiectul ForkJoinTask reprezinta un task ce urmeaza a fi executat:
- un task contine cod si date de procesat, similar lui Runnable sau Callable
- un numar mare de teask-uri sunt create si procesate de un numar mic de fire dintr-un
pool Fork-Join. Un ForkJoinTask creaza mai multe instante ForkJoinTask pina cand
datele de procesat s-au impartit in mod adecvat
- dezvoltatorii folosesc subclasele: RecursiveAction, cand un task nu necesita
returnarea unui rezultat sau RecursiveTask altfel
Prin aceasta metoda acelasi sir este trimis fiecarui task, dar cu diferite valori de start si sfarsit.
Pentru utilizarea framework-ului Fork-Join trebuie sa tinem cont de urmatoarele recomandari:
- sa evitam I/O sau operatiile de blocare. Doar un fir este creat implicit pe procesor.
Operatiile de blocare ne vor tine de la a utiliza toate resursele procesorului
- solutia Fork-Join va avea performante mai mici pe un singur procesor, decat solutia
standard secventiala
- multe probleme conduc la alte probleme daca sunt executate in paralel
}
}
Componentele JPA
Persistenta este mecanismul utilizat de aplicatii pentru a pastra datele, date ce altfel ar fi
pierdute la inchiderea aplicatiei sau a calculatorului, intr-un context persistent, precum o baza
de date.
JPA stabileste relationari statice ale modelului persistent, prin definirea componentelor
entitate. API-ul defineste clasa entitate ca un echivalent al unei tabele din baza de date, pe
partea de business a aplicatiei. O instanta a entitatii este definita ca un obiect, echivalent al
unei linii din tabela bazei de date.
JPA stabileste relationarea dinamica a modelului persistent prin definirea unui obiect entity
manager. Acesta este insarcinat cu sincronizarea datelor continute intr-o instanta entitate cu
datele continute in linia echivalenta din baza de date. Spre exemplu, daca pe timpul executiei
aplicatiei un camp al instantei entitate este modificat, entity managerul modifica linia
echivalenta din baza de date.
JPA furnizeaza programatorilor Java facilitatea de mapare obiect/relatie, pentru a gestiona
modelul relational al bazelor de date implicate in aplicatiile Java. Persistenta in Java consta
din:
- JPA
- Limbajul de interogare (JPQL)
- Java Persistence Criteria API
- Metadatele de mapari obiect/relatie
In figura anterioara avem:
- Entity, obiectul persistent ce reprezinta o inregistrare din tabela bazei de date. Este un
POJO cu anotatii
- Interfata EntityManager, furnizeaza API-ul de interactiune cu entitatea
- EntityManagerFactory este folosit pentru a crea o instanta de EntityManager.
Un obiect Java poate cuprinde date partiale dintr-o tabela sau date din mai multe tabele.
Framework-urile ORM gestioneaza maparea intre tabelele bazei de date relationale si
obiectele Java astfel incat programatorii sa codeze cat mai putin. EclipseLink si Hibernate
sunt exemple de soft-uri ORM.
Observatie: EclipseLink este o versiune continuata a TopLink, pe care Oracle a donat-o
Eclipse-ului.
Entitati
Asadar, o entitate reprezinta o structura de date prin intermediul careia se asigura persitenta.
O entitate este asociata unui tabel al unei baze de date relationale, iar fiecare instanta a
entitatii corespunde unei linii din tabela. Campul unei clase entitate corespunde unei coloane
din tabela bazei de date.
O entitate:
- Este un POJO creata utilizand cuvantul new
- Suporta mostenire si polimorfism
- Este serializata si poate fi utilizata ca un obiect detached
Caracteristicile JPA ale entitatilor includ:
- Persistenta, entitatile create pot fi persistate intr-o baza de date, dar ele sunt tratate ca
obiecte
- Identitatea, fiecare entitate are un unic obiect identitate care este de obicei asociat unei
chei in baza de date, de obicei cheia primara
- Tranzactii, JPA necesita un context tranzactional pentru operatii ce efectueaza
schimbari in baza de date
- Granularitate, entitatile pot fi oricat de fine-grained sau coarse-grained precum doreste
dezvoltatorul, dar reprezinta o singura linie dintr-un tabel
Starea de persistenta a unei entitati este reprezentata prin intermediul acestor campuri sau prin
intermediul proprietatilor persistente (vezi in paragrafele urmatoare diferenta). Toate acestea
folosesc anotatii pentru maparea entitatilor si datelelor modelului relational.
O clasa entitate trebuie sa indeplineasca urmatoarele cerinte:
- Clasa trebuie sa fie anotata cu anotatia javax.persistence.Entity
- Clasa trebuie sa aiba un constructor public sau protected, fara argumente, dar clasa
mai poate avea si alti contructori
- Clasa nu trebuie declarata final, la fel metodele sau instantele clasei
- Daca o instanta a clasei este trimisa ca parametru al unei metode, precum in cazul
interfetelor remote session bean, clasa trebuie sa implementeze interfata
Serializable
- Clasele entitate pot extinde atat clase entitate cat si non-entitate, iar clasele non-
entitate pot extinde clase entitate
- Variabilele instanta trebuie sa fie declarate private, protected sau package-friendly
si pot fi accesate direct doar prin intermediul metodelor claselor entitate. Clientii
trebuie sa acceseze starea entitatii prin accesori sau metode business
- Campul cheie primara sau metoda getter ce corespunde coloanei cheii primare va fi
anotata cu @Id
- Clasele entitate nu definesc metoda finalize().
Setarile predefinite ale unei clase entitate pot fi suprascrise, astfel:
- Suprascrierea numelui tabelei bazei de date. In mod predefinit numele clasei entitate
este numele tabelei (scris cu majuscula). Pentru a modifica acest comportament
folosim anotatia @Table cu elementul name. Ex:
@Entity
@Table(name=”Cust”) //Cust este numele tabelei din baza de date
public class Client {
- Suprascrierea numelui coloanei tabelei bazei de date. In mod predefinit numele
campului sau proprietatii clasei entitate este numele coloanei tabelei (scris cu litera
mica). Pentru a modifica acest comportament folosim anotatia @Column cu elementul
name. Ex:
@Column(name=”cname”)
private String clientName;
- Avem mai multe strategii de generare a cheii primare. Cel mai facil mod pentru a face
aceasta este apelarea autogenerarii de catre container, folosind anotatia cu valoarea
predefinita @GeneratedValue. Ex:
@Id
@GeneratedValue
private int id;
- Excluderea unui camp sau proprietate de la a fi facuta persistenta de catre container
prin folosirea anotatiei @Transient
Starea persistenta a unei entitati poate fi accesata prin intermediul variabilelor instanta sau al
proprietatilor. Proprietatile sau campurile clasei trebuie sa fie de urmatoarele tipuri Java:
- Tipuri primitive
- String
- Alte tipuri serializabile cum ar fi:
o Clasele wrapper ale tipurilor primitive
o java.math.BigInteger
o java.math.BigDecimal
o java.util.Date
o java.util.Calendar
o java.sql.Date
o java.sql.Time
o java.sql.TimeStamp
o tipuri utilizator serializabile
o byte[]
o Byte[]
o char[]
o Char[]
- Tipuri enumerare
- Alte entitati sau colectii de entitati
- Alte clase ce pot fi incorporate
Observatie: Putem folosi anotatia @Lob pentru a persista un camp sau o proprietate ca un
obiect mare intr-o baza de date ce suporta acest tip de obiect. Acest tip poate fi caracter sau
binar.
Starile din ciclul de viata al unei entitati sunt:
- New, se obtine in urma folosirii operatotului new. In acest caz nu este nicio linie din
tabela in corespondenta cu cheia primara a entitatii, din nivelul persistent asociat
- Managed, exista o linie in corespondenta din tabela si datele sunt tinute sincronizate,
de catre persistence provider, cu datele din entitate
- Detached, la fel ca mai sus doar ca datele din entitate nu sunt tinute sincronizate
- Removed, reprezinta o stergere in asteptare a datelor, din linia corespondenta din baza
de date
Acest proces este ilustrat grafic dupa cum urmeaza:
Relationarea entitatilor
Primul pas in modelarea asocierii la nivelul datelor este definirea relationarii ca o multime de
proprietati. Aceste proprietati pot fi apoi utilizate pentru a implementa relationarea obiectelor
prin clasele entitate.
Urmatoarele patru proprietati sunt folosite pentru a descrie asocierea intre obiecte:
- Cardinalitatea unei relatii, care specifica numarul de relationari intre doua entitati
relationate, poate fi de urmatoarele tipuri: unu-unu, unu-multi, multi-unu, multi-multi.
- Directia unei relatii, care determina navigabilitatea si vizibilitatea, poate fi:
o Bidirectionala, fiecare entitate poate vedea cealalta entitate din relatie. Putem
astfel include cod in oricare dintre entitati pentru a naviga catre cealalta
entitate in vederea obtinerii de informatii si servicii de la aceasta. In JPA
natura bidirectionala a unei relatii este specificata prin utilizarea unei anotatii
de cardinalitate in ambele clase entitate situate de cele doua parti ale relatiei
o Unidirectionala, doar o singura entitate dintre cele doua implicate in relatie
poate vedea cealalta entitate. Natura unidirectionala a relatiei este specificata
prin folosirea uneia dintre anotatiile de cardinalitate intr-una dintre entitati.
- Proprietarul relatiei, specifica partea de owning a relationarii, care la randul sau
contine maparea fizica. Cealalta parte a relatiei se numeste inverse side.
- Tipul de propagare a operatiei (cascading type), se refera la propagarea efectului unei
operatii entitatilor asociate. Se exprima prin termenii: All, Persist, Merge, Remove,
Refresh, None
Intr-o relatie bidirectionala fiecare entitate are un camp sau o proprietate ce refera la cealalta
entitate din relatie. Prin acest camp codul entitatii poate accesa obiectul relationat.
Relatiile bidirectionale trebuie sa urmareasca regulile:
- inverse side trebuie sa refere catre owning side utilizand elementul mappedBy al uneia
dintre anotatiile: @OneToOne, @OneToMany sau @ManyToMany. Valoarea acestui
element este campul sau proprietatea din entitatea ce joaca rol de owner al relatiei
- partea “multi” a unei relatii multi-unu, intr-o relationare bidirectionala, nu trebuie sa
defineasca elementul mappedBy. Aceasta parte este considerata intotdeauna owner
- intr-o relatie unu-unu bidirectioanala owning side corespunde partii ce contine cheia
straina corespunzatoare
- intr-o relatie multi-multi ambele parti pot fi owning side
Intr-o relatie unidirectionala doar o singura entitate are un camp sau o proprietate ce refera la
cealalta entitate.
Directionalitatea este necesara pentru a putea naviga intre relatii.
Entitatile ce utilizeaza relatiile au deseori dependente relativ la existenta celeilalte entitati din
relatie. Tipul enumerare javax.persistence.CascadeType defineste operatiile in cascada,
ca valoare a elementului cascade al anotatiilor de relationare. Valorile acestei enumerari sunt
date in tabelul urmator:
Cand entitatea destinatie dintr-o relatie unu-unu sau unu-multi este stearsa din relatie, este
deseori recomandabil sa stergem si entitatea. Asemenea entitati se numesc orfane si atributul
orphanRemoval poate fi folosit pentru a specifica entitatile orfane ce ar trebui sterse. Acest
atribut se foloseste in anotatiile @OneToOne si @OneToMany si are o valoare booleana, implicit
false.
Observatie: in acest tip de asociere entitatea owning este intotdeauna entitatea din partea
multi a relatiei.
Urmatorii pasi descriu procesul de definire a acestui tip de asocieriere intre doua clase
entitate:
1. definirea celor doua clase entitate
2. identificarea clasei owner (cea de pe partea “multi”)
3. in acea entitate cream un camp sau o proprietate pentru a reprezenta relationarea
acestei entitati cu o entitatea tinta
4. anotarea acestuia prin @ManyToOne
5. in cealalata entitate a asocierii cream o proprietate sau un camp ce reprezinta
relationarea
6. anotarea acestuia prin @OneToMany
7. Setam valoarea atributului mappedBy la valoarea proprietatii sau campului din
entitatea owning
Un exemplu de codare este dat in cele ce urmeaza:
@Entity
public class Customer {
@Id
private int id;
@OneToMany(mappedBy=”customer”)
private Collection <Order> orders;
..//
}
@Entity
public class Order {
@Id
private int orderId;
@ManyToOne
private Customer customer;
..//
}
@Entity
public class Project {
@Id
private int pId
@ManyToMany(mappedBy=”projects”)
private Set <Worker> workers;
..//
}
Anotatiile pentru maparea asocierii au anumite atribute optionale. Printre acestea:
- fetch, care seteaza instructiuni catre furnizorul de persistenta referitoare la momentul
incarcarii in memorie a elementelor de asociere. Valorile posibile sunt
o EAGER, indica furnizorului de persistenta sa incarce tinta asocierii in momentul
in care entitatea este incarcata din baza de date
o LAZY, indica furnizorului de persistenta faptul ca incarcarea tintei asocierii sa
se faca in momentul accesului
- cascade, specifica momentul din ciclul de viata al unei entitati in care au loc
modificari asupra relationarilor. In mod predefinit nu apar modificari. Alte valori
posibile sunt indicate in urmatorul tabel
Clase incorporate in entitati
Clasele incorporate sunt utilizate pentru a reprezenta starea unei entitati, dar nu sunt
persistente. Instantele claselor incorporate folosesc identitatea entitatii careia ii apartin. Intr-o
entitate putem avea clase incorporate singulare sau colectii ale acestora.
Clasele incorporate folosesc aceleasi reguli ca si clasele entitate, dar sunt anotate cu
javax.persistence.Embeddable in loc de @Entity.
Entitatile ce folosesc clasele incorporate ca parte a starii de persistenta anoteaza, optional,
campurile sau proprietatile incorporate cu javax.persistence.Embedded.
La randul lor clasele incorporate pot folosi alte clase incorporate pentru a-si defini starea.
Clasele incorporate pot contine relationari catre alte entitati sau colectii de entitati.
Exemplu:
@Embeddable
public class DataNasterii {
int zi;
String luna;
int an;
}
Si clasa ce o contine:
@Entity
public class Persoana {
@Id
protected long id
String nume;
@Embedded
DataNasterii dataNasterii;
...
}
Mostenirea entitatilor
Entitatile suporta mostenirea, asocierea polimorfica si interogarile polimorfice. Clasele
entitate pot extinde clase non-entiate, iar clasele non-entitate pot extinde clasele entitate.
Clasele entitate pot fi abstracte.
O clasa abstracta poate fi declarata entitate prin anotarea cu @Entity. Entitatile abstracte sunt
ca entitatile obisnuite, dar nu pot fi instantiate.
Entitatile abstracte pot fi interogate ca niste entitati obisnuite. Daca intr-o interogare apare o
clasa abstracta, atunci interogarea opereaza pe toate subclasele ce extind entitatea abstracta.
Entitatile pot mosteni superclase ce contin o stare persistenta si informatii mapate, dar nu sunt
entitati. In acest caz superclasele nu sunt decorate cu anotatia @Entity si nu sunt mapate, ca o
entitate, de catre Java persistence provider. Aceste superclase se folosesc de obicei atunci
cand avem stari sau informatii mapate in comun de catre mai multe entitati. Ele se numesc
superclase mapate (mapped superclasses) si sunt anotate cu
javax.persistence.MappedSuperclass.
Superclasele mapate nu pot fi interogate si nu pot fi folosite in operatiile din EntityQuery
sau Query. Nu pot fi, de asemenea, relationate, dar pot fi abstracte. Ele nu au niciun tabel in
corespondenta in baza de date.
Pe linga entitati si superclase mapate, enitatile pot avea si clase non-entitate pe post de
superclase. Si acestea pot fi abstracte. Starea unei superclase non-entitate nu este persistenta
si orice stare mostenita, dintr-o superclasa non-entitate, de catre o entitate nu este persistenta.
Aceste superclase nu pot fi utilizate in operatii ale EntityQuery sau Query. De asemenea, nu
pot fi relationate.
Putem configura modul in care JPA mapeaza entitatile mostenite cu baza de date prin
decorarea clasei radacina a ierarhiei cu javax.persistence.Inheritance. avem
posibilitatea sa alegem dintre:
- O singura tabela pe intreaga ierarhie de clase
- O tabela pe fiecare entitate concreta
- Strategia „join”, in care campurile sau proprietatile unei subclase sunt mapate unei
tabele diferite dacat cea a clasei parinte
Strategiile pot fi configurate prin setarea valoarii elementului strategy din anotatia
@Inheritance cu o valoare din enumerarea:
public enum InheritanceType {
SINGLE_TABLE,
JOINED,
TABLE_PER_CLASS
};
Valoarea implicita este SINGLE_TABLE.
Discutam pe rand fiecare dintre strategii:
- Un singur tabel, adica toate clasele din ierarhie sunt mapate unui singur tabel din baza
de date. Acest tabel are o coloana discriminator ce contine o valoare ce identifica
subclasa caruia ii corespunde instanta liniei curente. Coloana discriminator poate fi
specificata prin utilizarea anotatiei javax.persistence.DiscriminatorColumn in
radacina ierarhiei de clase entitate. Elementele acestei anotatii sunt ilustrate in tabelul
urmator
Tipul enumerare javax.persistence.DiscriminatorType este folosit pentru a seta
tipul coloanei discriminator in baza de date. Valorea elementului
discriminatorType, a anotatiei @DiscriminatorColumn poate fi, asadar:
public enum DiscriminatorType {
STRING,
CHAR,
INTEGER
};
Daca anotatia @DiscriminatorColumn nu este explicit specificata in entitatea
radacina atunci valoarea predefinita este String, iar numele predefinit al coloanei este
DTYPE.
Anotatia javax.persistence.DiscriminatorValue poate fi folosita pentru a seta
valoarea introdusa in coloana discriminator, pentru fiecare entitate din ierarhia de
clase. Doar clasele din ierarhie pot fi decorate cu aceasta anotatie. Daca anotatia nu
este specificata intr-o entitate, JPA va furniza o valoare, dependenta de implementare,
predefinita. Daca valoarea elementului discriminatorType al anotatiei
@DiscriminatorColumn este String, atunci valoarea din tabela este numele entitatii.
Aceasta strategie suporta polimorfismul atat la nivelul relatiilor cat si al interogarilor.
- Un tabel pentru fiecare clasa, adica fiecare clasa este mapata unei tabele separate din
baza de date. Toate campurile sau proprietatile din clasa, inclusiv campurile sau
proprietatile mostenite sunt mapate ca coloane in tabelul clasei, in baza de date. In
general, aceasta strategie nu suporta polimorfismul la nivelul relationarilor iar
interogarile polimorfice au la baza clauza UNION. In mod predefinit GlassFish-ul nu
suporta aceasta strategie
- Subclasa jonctionata, adica clasa radacina este mapata unui tabel din baza de date iar
fiecare subclasa are mapat un tabel separat, care contine doar acele campuri specifice
subclasei. Asadar, tabelul subclasei nu contine coloane pentru campurile sau
proprietatile mostenite. In plus, tabelul subclasei are o coloana sau mai multe coloane
ce reprezinta cheia primara, ce este cheie straina pentru cheia primare a tabelei
superclasa. Aceasta strategie furnizeaza suport pentru relatii polimorfice, dar sunt
necesare una sau mai multe jonctiuni atunci cand instantiem subclasele entitate.
Acelasi situatie se intalneste si in cazul interogarilor. Unii dintre furnizorii de JPA,
incluzand serverul GlassFish, impun o coloana discriminator ce corespunde entitatii
radacina. Daca nu folosim crearea automata a tabelei trebuie sa ne asiguram ca tabela
este setata corect pentru coloana discriminator predefinita sau folosim anotatia
@DiscriminatorColumn pentru a defini o astfel de coloana.
Gestiunea entitatilor
Entitatile sunt gestionate printr-un entity manager reprezentat de o instanta a interfetei
javax.persistence.EntityManager. Fiecare entity manager este asociat unui context
persistent, adica instante ale unora dintre entitatilor ce exista mapate dintr-o baza de date.
Contextul persistent defineste domeniul sub care instantele entitatilor sunt create, salvate sau
sterse. EntityManager defineste metodele utilizate pentru a interactiona cu contextul
persistent.
In cazul unui entity manager gestionat de container, o instanta a EntityManager este automat
propagata de catre container catre toate componentele aplicatiei ce utilizeaza instanta in
cadrul unei tranzactii JTA.
Tranzactiile JTA implica deseori apeluri peste mai multe componente. Pentru a executa o
tranzactie JTA aceste componente au nevoie de acces catre un singur context persistent.
Aceasta apare atunci cand EntityManager este injectat in componentele aplicatiei, prin
anotatia javax.persistence.PersistenceContext. Acest context persistent este propagat
automat impreuna cu tanzactia curenta JTA, iar referintele lui EntityManager sunt mapate
aceluiasi context persistent ca si tranzactia. Prin propagarea automata a contextului persistent
componenetele aplicatiei nu au nevoie sa-si trimita referintele catre instantele
EntityManager in cadrul unei tranzactii. Containerul Java EE gestioneaza ciclul de viata
entity managerilor gestionati de container.
Obtinerea unei instante se face prin constructia sintactica:
@PersistenceContext
EntityManager em;
In cazul unui entity manager gestionat de aplicatie, contextul persistent nu este propagat catre
componentele aplicatiei, iar ciclul de viata al instantelor EntityManager este gestionat de
aplicatie. Acest gen de management al entitatilor este utilizat atunci cand aplicatiile au nevoie
sa acceseze un context ce nu este propagat cu tranzactia JTA de-a lungul instantelor
EntityManager. In acest caz EntityManager creaza un nou context persistent, izolat. O alta
utilizare a acestui tip de gestiune este atunci cand nu putem injecta direct instante ale
EntityManager pentru ca acestea nu sunt thread-safe. In schimb EntityManagerFactory
este.
Aplicatiile creaza instante ale EntityManager folosind metoda createEntityManager() din
javax.persistence.EntityManagerFactory. Obtinerea unei instante a
EntityManagerFactory se face prin injectarea sa in componenta aplicatiei utilizand anotatia
javax.persistence.PersistenceUnit.
@PersistenceUnit
EntityManagerFactory emf;
EntityManager em = emf.createEntityManager();
Gestiunea entitatilor de catre aplicatie nu propaga automat contextul tranzactiei. Aplicatiile
trebuie sa ceara accesul de la managerul de tranzactie si sa adauge informatiile de demarcatie
ale tranzactiei atunci cand executa operatiile entitatii. Interfata
javax.transaction.UserTransaction defineste metodele pentru inceputul, commit-ul sau
roll back-ul unei tranzactii. Injectam o instanta a interfetei UserTransaction prin crearea
unei variabile de instanta anotate cu @Resource.
@Resource
UserTransaction utx;
Pentru a incepe o tranzactie apelam metoda begin() din UserTransaction. Cand toate
operatiile pe entitati sunt incheiate apelam commit() din aceeasi interfata. Metoda
rollback(), din aceeasi interfata, ne permite revenirea la starea de dinaintea inceperii
tranzactiei.
Iata un cadru clasic de folosire a gestiunii de catre aplicatie a entity managerului:
@PersistenceContext
EntityManagerFactory emf;
EntityManager em;
@Resource
UserTransaction utx;
...
em = emf.createEntityManager();
try {
utx.begin();
em.persist(Entitatea1);
em.merge(Entitatea2);
em.remove(Entitatea3);
utx.commit();
} catch (Exception e) {
utx.rollback();
}
Unitati de persistenta
O unitate de persistenta (persistence unit) defineste multimea tuturor claselor entitate
gestionate de instantele EntityManager intr-o aplicatie. Unitatile de persistenta sunt definite
de fisierul de configurare persistence.xml. Un exemplu de astfel de fisier este cel creat in
aplicatia din cursul anterior:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="PrimulEJB">
<jta-data-source>jdbc/account</jta-data-source>
<class>entitati.Customer</class>
<class>entitati.SavingsAccount</class>
</persistence-unit>
</persistence>
Fisierul descrie o unitate persistenta numita PrimulEJB ce foloseste o sursa de date JTA
jdbc/account. Elementele class specifica clasele persitente si anume: clase entitate, clase
incorporate si superclase mapate.
Unitatile persistente pot fi impachetate intr-un fisier WAR sau EJB JAR, sau pot constitui un
JAR ce este inclus intr-un WAR sau EAR.
Tranzactii si JPA
O tranzactie este un mecanism ce manipuleaza un grup de operatii ca si cum ar fi o singura
operatie. Intr-o trnzactie toate operatiile se executa sau niciuna. Operatiile implicate intr-o
tranzactie se pot baza pe mai multe baze de date.
Un exemplu tipic de utilizare a unei tranzactii este urmatorul: o aplicatie client face o cerere
de serviciu ce poate implica operatii multiple de citire si scriere pe o baza de date. Daca orice
invocare este esuata, orice stare ce este scrisa trebuie facuta rolled back.
Concret, fie un transfer interbancar ce se executa prin intermediul unei aplicatii. Operatia de
transfer cere serverului sa faca urmatoarele invocari:
- Metoda debit dintr-un cont al primei banci
- Metoda credit al altui cont al celei de-a doua banci
Daca invocarea pe cea de-a doua banca esueaza, aplicatia trebuie sa faca roll back contului
din prima banca, la valoarea de dinaintea apelului.
O tranzactie este formal definita ca o multime de proprietati incluse in acronimul ACID:
- Atomicitate: o tranzactie este efectuata integral sau deloc. In caz de esec operatiile si
procedurile se considera neefectuate si toate datele revin la starea anterioara
tranzactiei
- Consistenta: o tranzactie transforma un sistem dintr-o stare consistenta intr-o alta stare
consistenta
- Izolare: fiecare tranzactie se efectueaza independent de alte tranzactii ce se efectueaza
in acelasi timp
- Durabilitate: tranzactiile efectuate cu succes devin permanente chiar daca sistemul
esueaza
O aplicatie trebuie sa converseze cu un obiect pe care il gestioneaza si trebuie sa faca multiple
invocari pe instanta obiectului. Conversatia poate fi caracterizata de unul sau mai multe dintre
urmatoarele:
- Datele sunt cache-uite in memorie sau scrise in baza de date in timpul sau dupa fiecare
invocare succesiva
- Datele sunt scrise in baza de date la sfarsitul conversatiei
- Aplicatia client cere ca obiectul sa mentina un context in memory intre fiecare
invocare; fiecare invocare succesiva foloseste datele ce sunt intretinute in memorie
- La sfarsitul conversatiei aplicatia client necesita capabilitatea de a opri toate operatiile
de scriere asupra bazei de date aparute pe timpul sau la sfarsitul conversatiei
Daca tranzactia a fost executata cu succes modificarile pastrate in memorie sunt comise
(persistate).
Operatiile pe entitate sunt tranzactionale, adica este necesar sa fie parte a unei tranzactii.
Avem doua modele tranzactionale suportate de JPA:
- resource-local, reprezinta tranzactiile native suportate de driver-ele JDBC in unitatea
de persistenta
- JTA, sunt parte a server-ului Java EE
Interfata EntityTransaction suporta tranzactii resource-local, si este obtinuta din
EntityManager prin apelul metodei getTransaction(). Metode obisnuite ale unei tranzactii
includ:
- begin(), pornesc un nou context tranzactional
- commit(), incheie contextul tranzactional curent si scrie orice modificari nescrise inca
in baza de date
- rollback(), readuce tranzactia la starea initiala
- setRollbackOnly(), seteaza tranzactia actuala astfel incat la incheiere aceasta sa
faca rollback. Una dintre intrebuintari este stergerea unei tranzactii care dureaza prea
mult
- getRollbackOnly(), returneaza un boolean ce indica daca tranzactia curenta este
marcata pentru rollback
- isActive(), returneaza un boolean ce indica daca resursa tranzactiei este in procesare
interfata EntityManager defineste metodele ce gestioneaza entitatile:
- persistenta unei entitati: preia o entitate ce nu are o reprezentare persistenta in baza de
date si o stocheaza. Dupa persistenta entitatea este in starea managed. Orice alte
urmatoare modificari pe acesta entitate sunt de asemenea persistate
- gasirea unei entitati: daca entitatea este localizata in contextul persistent aceasta este
returnata de metoda find si gestionata de entity manager
- stergerea unei entitati
- modificarea unei entitati: localizarea unei entitati existente si efectuarea modificarilor
ce apoi sunt persistate
Aceste operatii formeaza CRUD (create, read, update, delete) pe baza de date.
Persistarea unei entitati este operatia de scriere a entitatii in baza de date. Este echivalenta
clauzei INSERT din SQL.
public class Test {
public static void main(String[] args) {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("NIO2");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist (brk);
em.getTransaction().commit();
em.close();
emf.close();
}
}
In cod, EntityManagerFactory este obtinut folosind clasa Persistence si numele
configuratiei unitatii de persistenta. Din factory obtinem EntityManager-ul.
Pentru a localiza o entitate in baza de date, EntityManager-ul va localiza o linie in baza de
date folosindu-se de cheia primara.
Metoda find() utilizeaza clasa entitate, trimisa ca prim argument, pentru a determina ce tip
sa foloseasca pentru a cheia primara si pentru tipul returnat. Astfel, o extra conversie nu mai
este necesara. Entitatea returnata se afla in starea managed. Daca nu exista entitatea ceruta se
returneaza null. Cel de-al doilea argument al lui find() este Object. Metoda este
echivalenta cu clauza SELECT din SQL.
Broker emp = em.find (Broker.class, 110);
Dupa ce o entitate se afla in starea managed putem face modificari obiectului entitate fara
persistarea explicita a sa. Acest proces este echivalentul clauzei UPDATE in SQL. Pentru a
updata o entitate ce nu este in starea managed va trebui sa folosim metoda merge() pentru a o
aduce in aceasta stare.
em.getTransaction().begin();
emp.setBrokername(emp.getBrokername()+" Ionescu");
em.getTransaction().commit();
Stergerea unei entitati din baza de date este echivalenta cu clauza DELETE din SQL.
Stergerea entitatii trebuie facuta pe o entitate in starea managed. Dupa remove() obiectul
Java este in memorie.
em.getTransaction().begin();
em.remove(emp);
em.getTransaction().commit();
O entitate in starea managed necesita un context tranzactional. Exista situatii in care dorim sa
„unmanage” o entitate pentru a face schimbarile inaintea de returnarea entitatii la starea
managed.
In exemplu, entitatea a fost detached prin entity manager. Dupa aceasta operarea pe ea se face
ca si pentru un obiect Java oarecare. Dupa ce am facut modificarile si au trecut validarile
entitatea poate fi din nou facuta managed.
em.detach(emp);
emp.setBrokername("Jones");
// ... in some other part of the code
em.getTransaction().begin();
emp = em.merge(emp);
emp.setAddress(emp.getAddress() + "2");
em.getTransaction().commit();
Pentru a putea rula aplicatiile din acest curs va trebui sa creem un Java Project clasic si din
Project>Properties sa setam valorile ca mai jos:
Facet-ul JPA apare in urma setarii:
Fisierul persistence.xml va trebui sa contina:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="NIO2" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>model.Broker</class>
<properties>
<property name="javax.persistence.jdbc.url"
value="jdbc:mysql://localhost:3306/brokertool"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password"
value="root"/>
<property name="javax.persistence.jdbc.driver"
value="com.mysql.jdbc.Driver"/>
</properties>
</persistence-unit>
</persistence>
Iar setarile pe proiect vor fi:
Limbajul Java Persistence Query
Defineste interogari ale entitatilor si starea lor persistenta. Limbajul de interogare utilizeaza
scheme abstracte de persitenta a entitatilor, incluzand relationarea, pentru modelul de date si
defineste operatori si expresii bazate pe modelul de date. Domeniul unei interogari cuprinde
schemele abstracte ale entitatilor ce coexista in acelasi persistence unit. Query language
utilizeaza sintaxa asemanatoare cu SQL pentru a selecta obiecte sau valori bazandu-se pe
tipurile entitate ale schemei abstracte si a relationarii dintre ele.
Avem urmatorul dictionar de termeni frecvent utilizati in cele ce urmeaza:
- Abstract schema, reprezinta o abstractie a schemei persistente, adica entitatile
persistente, starea lor si relationarile, peste care interogarile opereaza. Query language
traduce interogarile, peste aceasta abstractizare a schemei persistente, in interogari ce
sunt executate peste schema bazei de date de unde entitatile sunt mapate
- Abstract schema type, reprezinta tipul la care proprietatea persistenta a unei entitati se
evalueaza in schema abstracta. Astfel, fiecare camp persistent sau proprietate dintr-o
entitate are un camp de stare corespondent, de acelasi tip, in abstract schema. Tipul
schemei abstracte al unei entitati este derivat dintr-o clasa entitate iar informatiile de
metadate sunt furnizate de anotatii
- Backus-Naur Form (BNF), reprezinta o notatie ce descrie sintaxa unui limbaj de nivel
inalt. Toate diagramele, in cele ce urmeaza vor fi BNF.
- Navigation, reprezinta traversarea unei relationari intr-o expresie query language.
Operatorul de navigare este punctul
- Path expression, este o expresie ce navigheaza catre starea unei entitati sau camp al
relatiei
- State field, un camp persistent al unei entitati
- Relationship field, reprezinta un camp persistent al unei relationari dintr-o entitate al
carei tip este un tip in schema abstracta al entitatii relationate
CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
Root<Customer> cust = cq.from(Customer.class);
Metamodel m = em.getMetamodel();
EntityType<Customer> Customer_ = m.entity(Customer.class);
cust.get(custName);
TypedQuery<Customer> q = em.createQuery(cq);
List<Customer> l = q.getResultList();
return lac;
}
Rezultatele unei interogari pot fi restrictionate in acord cu conditiile setate de apelul metodei
CriteriaQuery.where(). Apelul acestei metode este analog cu clauza WHERE din JPQL.
Metoda where() evalueaza instantele interfetei Expression pentru a restrictiona rezultatele
in acord cu conditiile expresiilor. Instantele Expression sunt create utilizand metodele
definite in interfetele Expression si CriteriaBuilder.
Obiectele Expression sunt folosite in clauzele SELECT, WHERE sau HAVING. Metodele
conditionale utilizate in obiectele Expression sunt descrise in tabelul urmator:
In interfata CriteriaBuilder avem si alte metode pentru crearea expresiilor. Aceste metode
corespund operatorilor si functtilor atritmetice, string, data, timp. Tabelul urmator ilustreaza
aceste metode.
Predicatele formate din conditii multiple pot fi specificate utilizand urmatoarele metode din
CriteriaBuilder.
Urmatorul exemplu urmareste crearea unui predicat si folosirea lui pentru a selecta din tabele
customer acele nume care incep cu sil.
public List<AllCustomer> salut(String name) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
Root<Customer> cust = cq.from(Customer.class);
Metamodel m = em.getMetamodel();
EntityType<Customer> Customer_ = m.entity(Customer.class);
Path<String> pa=cust.get(custName);
Predicate p = cb.like(pa,"sil%");
cq=cq.where(p);
TypedQuery<Customer> q = em.createQuery(cq);
List<Customer> l = q.getResultList();
return lac;
}
Pentru interogari ce returneaza mai mult decat un singur rezultat avem uneori nevoie sa
organizam iesirea. In interfata CriteriaQuery avem metoda orderBy() folosita pentru a
ordona rezultatele in raport cu atributele entitatii. Argumentul metodei este un obiect Order,
creat prin apelul uneia dintre metodele: CriteriaBuilder.asc() sau
CriteriaBuilder.desc(). Prima dintre metode este folosita pentru a ordona rezultatele
crescator, dupa expresia parametru, iar cea de-a doua pentru ordonarea descrescatoare.
Metodei orderBy() ii putem trimite mai multe obiecte Order, precedenta fiind determinata
de ordinea aparitiei in lista.
Metoda CriteriaQuery.groupBy() partitioneaza rezultatele interogarii in grupuri. Formarea
acestor grupuri este data de argumentul metodei.
Metoda CriteriaQuery.having()este folosita impreuna cu groupBy() pentru a filtra
grupurile. having() are ca parametru o expresie conditionala.
Pentru a pregati un query pentru executie cream un obiect TypedQuery<T> cu tipul
rezultatului interogarii transmis de obiectul CriteriaQuery lui
EntityManager.createQuery(). Interogarile sunt executate folosind setSingleResult()
sau getResultList() pe obiectul TypedQuery<T>.
Localizare
Localizarea este procesul de adaptare a software-ului unei anumite regiuni sau limbi prin
adaugarea unor componente specifice locale si traducere de text.
In plus fata de modificarea limbii, elementele culturale precum datele, numerele, valuta, etc
trebuie sa fie adaptate.
Scopul este de a crea un design pentru localizare astfel incat sa nu avem nevoie de modificari
de cod.
Localizarea specifica doua elemente:
- Limba: alpha-2 sau alpha-3 ISO 639 (intotdeauna litere mici)
- Tara: ISO 3166 alpha-2 sau codul numeric UN M.49 (intotdeauna majuscule)
Clasa ResourceBundle izoleaza datele local – specifice. Clasa returneaza perechi
cheie/valoare, stocate in fisier clasa sau .properties.
Pasii utilizati pentru folosirea localizarii sunt:
- Crearea unui fisier bundle pentru fiecare locatie
- Apelarea unui locatii specifice pentru aplicatie
Design-ul pentru localizare incepe prin design-ul aplicatiei astfel incat toate textele, sunetele
sau imaginile sa fie inlocuite la rulare cu elementele potrivite regiunii si culturii dorite.
Fiecare cheie din fisierul de proprietati identifica o componenta specifica a aplicatiei.
Conventia de nume pentru fisierul de proprietati este:
MessageBundle_xx_YY.properties, unde xx este codul de limba, iar YY codul de
tara.
Dupa ce a fost creat resource bundle-ul trebuie sa-l incarcam in aplicatie. Mai intai cream un
obiect Locale care specifica limba si tara. Apoi cream resource bundle-ul prin specificarea
numelui fisierului de baza al bundle-ului si localizarea curenta.
Crearea lui Locale se poate face in mai multe moduri:
- Clasa Locale include constante default pentru anumite tari
- Daca constanta nu este disponibila putem defini codurile de tara si de limba pentru a
defini localizarea
- In cele din urma putem folosi metoda getDefault() pentru a obtine locatia default
In aplicatie in loc de text folosim resource bundle-ul, respectiv cheia din fisier
corespunzatoare componetei curente.
Pentru a schimba localizarea vom incarca bundle-ul corespunzator localizarii folosind metoda
getBundle().
Fie urmatorul exemplu care ilustreaza localizarea:
public class SampleApp {
while (!(line.equals("q"))) {
this.printMenu();
try {
line = this.br.readLine();
} catch (Exception e) {
e.printStackTrace();
}
switch (line) {
case "1":
setEnglish();
break;
case "2":
setFrench();
break;
case "3":
setChinese();
break;
case "4":
setRussian();
break;
case "5":
showDate();
break;
case "6":
showMoney();
break;
}
}
}
- MessagesBundle_ru_RU.properties
menu1 = \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c
\u0430\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u0438\u0439
menu2 = \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c
\u0444\u0440\u0430\u043d\u0446\u0443\u0437\u0441\u043a\u0438\u0439
menu3 = \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c
\u043a\u0438\u0442\u0430\u0439\u0441\u043a\u0438\u0439
menu4 = \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c
\u0440\u0443\u0441\u0441\u043a\u0438\u0439
menu5 = \u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0434\u0430\u0442\u0443
menu6 = \u041f\u043e\u043a\u0430\u0436\u0438\u0442\u0435 \u043c\u043d\u0435
\u0434\u0435\u043d\u044c\u0433\u0438!
menuq = \u0412\u0432\u0435\u0434\u0438\u0442\u0435 q
\u0447\u0442\u043e\u0431\u044b \u0432\u044b\u0439\u0442\u0438
menucommand = \u0412\u0432\u0435\u0441\u0442\u0438
\u043a\u043e\u043c\u0430\u043d\u0434\u0443:
- MessagesBundle_zh_CN.properties
menu1=\u8bbe\u7f6e\u6210\u82f1\u6587
menu2=\u8bbe\u7f6e\u6210\u6cd5\u6587
menu3=\u8bbe\u7f6e\u6210\u4e2d\u6587
menu4=\u8bbe\u7f6e\u6210\u4fc4\u6587
menu5=\u663e\u793a\u65e5\u671f
menu6=\u663e\u793a\u91d1\u989d!
menuq=\u6309q\u952e\u9000\u51fa
menucommand=\u8f93\u5165\u547d\u4ee4:
- MessagesBundle.properties
while (!(line.equals("q"))) {
this.printMenu();
try {
line = this.br.readLine();
} catch (Exception e) {
e.printStackTrace();
}
switch (line) {
case "1":
this.setEnglish();
break;
case "2":
this.setFrench();
break;
case "3":
this.setChinese();
break;
case "4":
this.setRussian();
break;
}
}
}
menu1 = \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c
\u0430\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u0438\u0439
menu2 = \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c
\u0444\u0440\u0430\u043d\u0446\u0443\u0437\u0441\u043a\u0438\u0439
menu3 = \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c
\u043a\u0438\u0442\u0430\u0439\u0441\u043a\u0438\u0439
menu4 = \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c
\u0440\u0443\u0441\u0441\u043a\u0438\u0439
menu5 = \u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0434\u0430\u0442\u0443
menu6 = \u041f\u043e\u043a\u0430\u0436\u0438\u0442\u0435 \u043c\u043d\u0435
\u0434\u0435\u043d\u044c\u0433\u0438!
menuq = \u0412\u0432\u0435\u0434\u0438\u0442\u0435 q
\u0447\u0442\u043e\u0431\u044b \u0432\u044b\u0439\u0442\u0438
menucommand = \u0412\u0432\u0435\u0441\u0442\u0438
\u043a\u043e\u043c\u0430\u043d\u0434\u0443:
date1 = \u0414\u0430\u0442\u0430 \u043f\u043e-
\u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e:
date2 = \u0414\u043b\u0438\u043d\u043d\u0430\u044f \u0434\u0430\u0442\u0430:
date3 = \u041a\u043e\u0440\u043e\u0442\u043a\u0430\u044f \u0434\u0430\u0442\u0430:
date4 = \u041f\u043e\u043b\u043d\u0430\u044f \u0434\u0430\u0442\u0430:
date5 = \u041f\u043e\u043b\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f:
date6 = \u0414\u0435\u043d\u044c \u043d\u0435\u0434\u0435\u043b\u0438:
date7 = \u041c\u043e\u0438 \u043b\u0438\u0447\u043d\u044b\u0435
\u0434\u0435\u043d\u044c \u0438 \u0432\u0440\u0435\u043c\u044f:
menu1=\u8bbe\u7f6e\u6210\u82f1\u6587
menu2=\u8bbe\u7f6e\u6210\u6cd5\u6587
menu3=\u8bbe\u7f6e\u6210\u4e2d\u6587
menu4=\u8bbe\u7f6e\u6210\u4fc4\u6587
menu5=\u663e\u793a\u65e5\u671f
menu6=\u663e\u793a\u91d1\u989d!
menuq=\u6309q\u952e\u9000\u51fa
menucommand=\u8f93\u5165\u547d\u4ee4:
date1=\u7f3a\u7701\u65e5\u671f:
date2=\u957f\u65e5\u671f:
date3=\u77ed\u65e5\u671f:
date4=\u5168\u79f0\u65e5\u671f:
date5=\u5168\u79f0\u65f6\u95f4:
date6=\u661f\u671f:
date7=\u5ba2\u6237\u65e5\u671f\u548c\u65f6\u95f4: