Capitolul V
5.2 Expresii
5.2.1 Valoarea și tipul unei expresii
5.2.2 Ordinea de evaluare
5.2.3 Operatori
5.2.3.1 Operatori unari
5.2.3.1.1 Operatorii de preincrementare și
postincrementare
5.2.3.1.2 Operatorul + unar
5.2.3.1.3 Operatorul - unar
5.2.3.1.4 Operatorul de complementare
5.2.3.1.5 Operatorul de negare logică
5.2.3.1.6 Casturi
5.2.3.2 Operatori binari
5.2.3.2.1 Operatori multiplicativi: *, /, %
5.2.3.2.2 Operatori aditivi: +, -, concatenare pentru șiruri de
caractere
5.2.3.2.3 Operatori de șiftare: >>, <<, >>>
5.2.3.2.4 Operatori relaționali: <, >, <=, >=,
instanceof
5.2.3.2.5 Operatori de egalitate: ==, !=
5.2.3.2.6 Operatori la nivel de bit: &, |, ^
5.2.3.2.7 Operatori logici: &&, ||
5.2.3.3 Operatorul condițional ?:
5.2.3.4 Operații întregi
5.2.3.5 Operații flotante
5.2.3.6 Apeluri de metode
5.2.1 Valoarea și tipul unei expresii
Fiecare expresie a limbajului Java are un rezultat și un tip. Rezultatul poate fi:
- o valoare
- o variabilă
- nimic
În cazul în care valoarea unei expresii are ca rezultat o variabilă, expresia
poate apare în stânga unei operații de atribuire. De exemplu expresia:
tablou[i]
este o expresie care are ca rezultat o variabilă și anume locația elementului cu
indexul i din tabloul de elemente numit tablou.
O expresie nu produce nimic doar în cazul în care este un apel de metodă și acest
apel nu produce nici un rezultat (este declarat de tip void).
Fiecare expresie are în timpul compilării un tip cunoscut. Acest tip poate fi o
valoare primitivă sau o referință. În cazul expresiilor care au tip referință,
valoarea expresiei poate să fie și referința neinițializată, null.
5.2.2 Ordinea de evaluare
În Java operanzii sunt evaluați întotdeauna de la stânga spre dreapta. Acest lucru
nu trebuie să ne îndemne să scriem cod care să depindă de ordinea de evaluare pentru
că, în unele situații, codul rezultat este greu de citit. Regula este introdusă doar
pentru a asigura generarea uniformă a codului binar, independent de compilatorul folosit.
Evaluarea de la stânga la dreapta implică următoarele aspecte:
- în cazul unui operator binar, operandul din stânga este întotdeauna complet evaluat
atunci când se trece la evaluarea operandului din dreapta. De exemplu, în expresia:
( i++ ) + i
dacă i avea valoarea inițială 2, toată expresia va avea valoarea 5
pentru că valoarea celui de-al doilea i este luată după ce s-a executat
incrementarea i++.
- în cazul unei referințe de tablou, expresia care numește tabloul este complet
evaluată înainte de a se trece la evaluarea expresiei care dă indexul. De exemplu, în
expresia:
( a = b )[i]
- indexul va fi aplicat după ce s-a executat atribuirea valorii referință b la
variabila referință de tablou a. Cu alte cuvinte, rezultatul va fi al i-lea
element din tabloul b.
- în cazul apelului unei metode, expresia care numește obiectul este complet evaluată
atunci când se trece la evaluarea expresiilor care servesc drept argumente.
- în cazul unui apel de metodă, dacă există mai mult decât un parametru, la evaluarea
parametrului numărul i, toți parametrii de la 1 la i-1 sunt
deja evaluați complet.
- în cazul unei expresii de alocare, dacă avem mai multe dimensiuni exprimate în
paranteze drepte, dimensiunile sunt de asemenea evaluate de la stânga la dreapta. De
exemplu, în expresia:
new int[i++][i]
- tabloul rezultat nu va fi o matrice pătratică ci, dacă i avea valoarea 3,
de dimensiune 3 x 4.
5.2.3.1 Operatori unari
Operatorii unari se aplică întotdeauna unui singur operand. Acești operatori sunt,
în general exprimați înaintea operatorului asupra căruia se aplică. Există însă
și două excepții, operatorii de incrementare și decrementare care pot apare și
înainte și după operator, cu semnificații diferite.
Operatorii unari sunt următorii:
- ++ preincrement
- -- predecrement
- ++ postincrement
- -- postdecrement
- + unar
- - unar
- ~ complementare
- ! negație logică
- cast
5.2.3.1.1 Operatorii de preincrementare și postincrementare
Operatorii ++ preincrement și postincrement au același rezultat
final, și anume incrementează variabila asupra căreia acționează cu 1. Operandul
asupra căruia sunt apelați trebuie să fie o variabilă de tip aritmetic. Nu are sens
să apelăm un operand de incrementare asupra unei valori (de exemplu valoarea unei
expresii sau un literal), pentru că aceasta nu are o locație de memorie fixă în care
să memorăm valoarea după incrementare.
În cazul operatorului prefix, valoarea rezultat a acestei expresii este valoarea
variabilei după incrementare în timp ce, la operatorul postfix, valoarea rezultat a
expresiei este valoarea de dinainte de incrementare. De exemplu, după execuția
următoarei secvențe de instrucțiuni:
int i = 5;
int j = i++;
valoarea lui j este 5, în timp ce, după execuția următoarei
secvențe de instrucțiuni:
int i = 5;
int j = ++i;
valoarea lui j va fi 6. În ambele cazuri, valoarea finală a lui i
va fi 6.
În cazul operatorilor -? predecrement și postdecrement, sunt valabile
aceleași considerații ca mai sus, cu diferența că valoarea variabilei asupra căreia
se aplică operandul va fi decrementată cu 1. De exemplu, următoarele instrucțiuni:
int i = 5;
int j = i--;
fac ca j să aibă valoarea finală 5 iar următoarele instrucțiuni:
int i = 5;
int j = --i;
fac ca j să aibă valoarea finală 4. În ambele cazuri, valoarea
finală a lui i este 4.
Operatorii de incrementare și de decrementare se pot aplica și pe variabile de tip
flotant. În asemenea cazuri, se convertește valoarea 1 la tipul variabilei incrementate
sau decrementate, după care valoarea rezultată este adunată respectiv scăzută din
vechea valoare a variabilei. De exemplu, următoarele instrucțiuni:
double f = 5.6;
double g = ++f;
au ca rezultat final valoarea 6.6 pentru f și pentru g.
Operatorul + unar se aplică asupra oricărei valori primitive
aritmetice. Valoarea rămâne neschimbată.
Operatorul - unar se aplică asupra oricărei valori primitive
aritmetice. Rezultatul aplicării acestui operand este negarea aritmetică a valorii. În
cazul valorilor întregi, acest lucru este echivalent cu scăderea din 0 a valorii
originale. De exemplu, instrucțiunile:
int i = 5;
int j = -i;
îi dau lui j valoarea -5 . În cazul valorilor speciale definite de
standardul IEEE pentru reprezentarea numerelor flotante, se aplică următoarele reguli:
- Dacă operandul este NaN rezultatul negării aritmetice este tot NaN pentru că NaN nu
are semn.
- Dacă operandul este unul dintre infiniți, rezultatul este infinitul opus ca semn.
- Dacă operandul este zero de un anumit semn, rezultatul este zero de semn diferit. ^
5.2.3.1.4 Operatorul de complementare
Operatorul de complementare ~ se aplică asupra valorilor primitive de
tip întreg. Rezultatul aplicării operandului este complementarea bit cu bit a valorii
originale. De exemplu, dacă operandul era de tip byte având valoarea, în binar, 00110001,
rezultatul va fi 11001110. În realitate, înainte de complementare se face și
extinderea valorii la un întreg, deci rezultatul va fi de fapt: 11111111 11111111
11111111 11001110.
5.2.3.1.5 Operatorul de negare logică
Operatorul de negare logică ! se aplică în exclusivitate valorilor
de tip boolean. În cazul în care valoarea inițială a operandului este true
rezultatul va fi false și invers.
Casturile sunt expresii de conversie dintr-un tip într-altul, așa cum deja am arătat
la paragraful destinat conversiilor. Rezultatul unui cast este valoarea operandului
convertită la noul tip de valoare exprimat de cast. De exemplu, la instrucțiunile:
double f = 5.6;
int i = ( int )f;
double g = -5.6;
int j = ( int )g;
valoarea variabilei f este convertită la o valoare întreagă, anume 5,
și noua valoare este atribuită variabilei i. La fel, j primește valoarea
-5.
Să mai precizăm că nu toate casturile sunt valide în Java. De exemplu, nu putem
converti o valoare întreagă într-o valoare de tip referință.
5.2.3.2 Operatori binari
Operatorii binari au întotdeauna doi operanzi. Operatorii binari sunt următorii:
- Operatori multiplicativi: *, /, %
- Operatori aditivi: +, -, + (concatenare)
pentru șiruri de caractere
- Operatori de șiftare: >>, <<, >>>
- Operatori relaționali: <, >, <=,
>=, instanceof
- Operatori de egalitate: ==, !=
- Operatori la nivel de bit: &, |, ^
- Operatori logici: &&, ||
5.2.3.2.1 Operatori multiplicativi: *, /, %
Operatorii multiplicativi reprezintă respectiv operațiile de înmulțire (*),
împărțire (/) și restul împărțirii (%). Prioritatea
acestor operații este mai mare relativ la operațiile aditive, deci acești operatori se
vor executa mai întâi. Exemple:
10 * 5 == 50
10.3 * 5.0 == 51.5
10 / 2.5 == 4.0// împărțire reală
3 / 2 == 1// împărțire întreagă
7 % 2 == 1// restul împărțirii întregi
123.5 % 4 == 3.5 // 4 * 30 + 3.5
123.5 % 4.5 == 2.0 // 4.5 * 27 + 2.0
După cum observați, operanzii sunt convertiți mai întâi la tipul cel mai puternic,
prin promovare aritmetică, și apoi se execută operația. Rezultatul este de același
tip cu tipul cel mai puternic.
În cazul operatorului pentru restul împărțirii, dacă lucrăm cu numere flotante,
rezultatul se calculează în felul următor: se calculează de câte ori este cuprins cel
de-al doilea operand în primul (un număr întreg de ori) după care rezultatul este
diferența care mai rămâne, întotdeauna mai mică strict decât al doilea operand.
5.2.3.2.2 Operatori aditivi: +, -, concatenare pentru șiruri
de caractere
Operatorii aditivi reprezintă operațiile de adunare (+), scădere (-)
și concatenare (+) de șiruri. Observațiile despre conversia tipurilor
făcute la operatorii multiplicativi rămân valabile. Exemple:
2 + 3 == 5
2.34 + 3 == 5.34
34.5 - 23.1 == 11.4
"Acesta este" + " un sir" == "Acesta este un sir"
"Sirul: " + 1 == "Sirul: 1"
"Sirul: " + 3.4444 == "Sirul: 3.4444"
"Sirul: " + null = "Sirul: null"
"Sirul: " + true = "Sirul: true"
Object obiect = new Object();
"Sirul: " + obiect == "java.lang.Object@1393800"
La concatenarea șirurilor de caractere, lungimea șirului rezultat este suma lungimii
șirurilor care intră în operație. Caracterele din șirul rezultat sunt caracterele din
primul șir, urmate de cele dintr-al doilea șir în ordine.
Dacă cel de-al doilea operand nu este de tip String ci este de tip
referință, se va apela metoda sa toString, și apoi se va folosi în
operație rezultatul. Metoda toString este definită în clasa Object
și este moștenită de toate celelalte clase.
Dacă cel de-al doilea operand este un tip primitiv, acesta este convertit la un șir
rezonabil de caractere care să reprezinte valoarea operandului.
5.2.3.2.3 Operatori de șiftare: >>, <<,
>>>
Operatorii de șiftare se pot aplica doar pe valori primitive întregi. Ei reprezintă
respectiv operațiile de șiftare cu semn stânga (<<) și dreapta (>>)
și operația de șiftare fără semn spre dreapta (>>>).
Șiftările cu semn lucrează la nivel de cifre binare. Cifrele binare din locația de
memorie implicată sunt mutate cu mai multe poziții spre stânga sau spre dreapta.
Poziția binară care reprezintă semnul rămâne neschimbată. Numărul de poziții cu
care se efectuează mutarea este dat de al doilea operand. Locația de memorie în care se
execută operația este locația în care este memorat primul operand.
Șiftarea cu semn la stânga reprezintă o operație identică cu înmulțirea cu 2 de n
ori, unde n este al doilea operand. Șiftarea cu semn la dreapta reprezintă
împărțirea întreagă. În acest caz, semnul este copiat în mod repetat în locurile
rămase goale. Iată câteva exemple:
255 << 3 == 2040
// 00000000 11111111 -> 00000111 11111000
255 >> 5 == 7
// 00000000 11111111 -> 00000000 00000111
Șiftarea fără semn la dreapta, mută cifrele binare din operand completând spațiul
rămas cu zerouri:
0xffffffff >>> -1 == 0x00000001
0xffffffff >>> -2 == 0x00000003
0xffffffff >>> -3 == 0x00000007
0xffffffff >>> 3 == 0x1fffffff
0xffffffff >>> 5 == 0x07ffffff ^
5.2.3.2.4 Operatori relaționali: <, >, <=, >=,
instanceof
Operatorii relaționali întorc valori booleene de adevărat sau fals. Ei reprezintă
testele de mai mic (<), mai mare (>), mai mic sau
egal (<=), mai mare sau egal (>=) și testul care ne
spune dacă un anumit obiect este sau nu instanță a unei anumite clase (instanceof).
Iată câteva exemple:
1 < 345 == true
1 <= 0 == false
Object o = new Object();
String s = new String();
o instanceof Obiect == true
s instanceof String == true
o instanceof String == false
s instanceof Object == true
Să mai observăm că String este derivat din Object.
5.2.3.2.5 Operatori de egalitate: ==, !=
Acești operatori testează egalitatea sau inegalitatea dintre două valori. Ei
reprezintă testul de egalitate (==) și de inegalitate (!=).
Rezultatul aplicării acestor operatori este o valoare booleană.
Exemple:
( 1 == 1.0 ) == true
( 2 != 2 ) == false
Object o = new Object();
String s1 = "vasile";
String s2 = s1;
String s4 = "e";
String s3 = "vasil" + s4;
( o == s1 ) == false
( s1 == s2 ) == true // același obiect referit
( s3 == s1 ) == false // același șir de caractere
// dar obiecte diferite
Să observăm că egalitatea a două obiecte de tip String reprezintă
egalitatea a două referințe de obiecte și nu egalitatea conținutului șirului de
caractere. Două referințe sunt egale dacă referă exact același obiect, nu dacă
obiectele pe care le referă sunt egale între ele. Egalitatea conținutului a două
șiruri de caractere se testează folosind metoda equals, definită în
clasa String.
5.2.3.2.6 Operatori la nivel de bit: &, |, ^
Operatorii la nivel de bit reprezintă operațiile logice obișnuite, dacă considerăm
că 1 ar reprezenta adevărul și 0 falsul. Operatorii la nivel de bit, consideră cei doi
operanzi ca pe două șiruri de cifre binare și fac operațiile pentru fiecare dintre
perechile de cifre binare corespunzătoare în parte. Rezultatul este un nou șir de cifre
binare. De exemplu, operația de și (&) logic are următorul
tabel de adevăr:
1 & 1 == 1
1 & 0 == 0
0 & 1 == 0
0 & 0 == 0
Dacă apelăm operatorul & pe numerele reprezentate binar:
00101111
01110110
rezultatul este:
00100110
Primul număr reprezintă cifra 47, al doilea 118 iar rezultatul 38,
deci:
47 & 118 == 38
În mod asemănător, tabela de adevăr pentru operația logică sau (|)
este:
1 | 1 == 1
1 | 0 == 1
0 | 1 == 1
0 | 0 == 0
iar tabela de adevăr pentru operația logică de sau exclusiv (^)
este:
1 ^ 1 == 0
1 ^ 0 == 1
0 ^ 1 == 1
0 ^ 0 == 0
Iată și alte exemple:
1245 ^ 2345 == 3572
128 & 255 == 128
127 & 6 == 6
128 | 255 == 255
127 | 6 == 127
32 ^ 64 == 96 ^
5.2.3.2.7 Operatori logici: &&, ||
Operatorii logici se pot aplica doar asupra unor operanzi de tip boolean. Rezultatul
aplicării lor este tot boolean și reprezintă operația logică de și (&&)
sau operația logică de sau (||) între cele două valori booleene. Iată
toate posibilitățile de combinare:
true && true == true
true && false == false
false && true == false
false && false == false
true || true == true
true || false == true
false || true == true
false || false == false
În cazul operatorului && este evaluat mai întâi operandul
din stânga. Dacă acesta este fals, operandul din dreapta nu mai este evaluat, pentru că
oricum rezultatul este fals. Acest lucru ne permite să testăm condițiile absolut
necesare pentru corectitudinea unor operații și să nu executăm operația decât dacă
aceste condiții sunt îndeplinite.
De exemplu, dacă avem o referință și dorim să citim o valoare de variabilă din
obiectul referit, trebuie să ne asigurăm că referința este diferită de null.
În acest caz, putem scrie:
String s = "sir de caractere";
if( s != null && s.length < 5 ) ?
În cazul în care s este null, a doua operație nu are sens și
nici nu va fi executată.
În mod similar, la operatorul ||, se evaluează mai întâi primul
operand. Dacă acesta este adevărat, nu se mai evaluează și cel de-al doilea operand
pentru că rezultatul este oricum adevărat. Faptul se poate folosi în mod similar ca mai
sus:
if( s == null || s.length == 0 ) ?
În cazul în care s este null, nu se merge mai departe cu
evaluarea.
5.2.3.3 Operatorul condițional ?:
Este singurul operator definit de limbajul Java care acceptă trei operanzi. Operatorul
primește o expresie condițională booleană pe care o evaluează și alte două expresii
care vor fi rezultatul aplicării operandului. Care dintre cele două expresii este
rezultatul adevărat depinde de valoarea rezultat a expresiei booleene. Forma generală a
operatorului este:
ExpresieCondițională ? Expresie1 : Expresie2
Dacă valoarea expresiei condiționale este true, valoarea operației
este valoarea expresiei 1. Altfel, valoarea operației este valoarea expresiei 2.
Cele două expresii trebuie să fie amândouă aritmetice sau amândouă booleene sau
amândouă de tip referință.
Iată și un exemplu:
int i = 5;
int j = 4;
double f = ( i < j ) ? 100.5 : 100.4;
Parantezele nu sunt obligatorii.
După execuția instrucțiunilor de mai sus, valoarea lui f este 100.4.
Iar după:
int a[] = { 1, 2, 3, 4, 5 };
int b[] = { 10, 20, 30 };
int k = ( ( a.length < b.length ) ? a : b )[0];
valoarea lui k va deveni 10.
5.2.3.4 Operații întregi
Dacă amândoi operanzii unei operator sunt întregi atunci întreaga operație este
întreagă.
Dacă unul dintre operanzi este întreg lung, operația se va face cu precizia de 64 de
biți. Operanzii sunt eventual convertiți. Rezultatul este un întreg lung sau o valoare
booleană dacă operatorul este un operator condițional.
Dacă nici unul dintre operanzi nu e întreg lung, operația se face întotdeauna pe 32
de biți, chiar dacă cei doi operanzi sunt întregi scurți sau octeți. Rezultatul este
întreg sau boolean.
Dacă valoarea obținută este mai mare sau mai mică decât se poate reprezenta pe
tipul rezultat, nu este semnalată nici o eroare de execuție, dar rezultatul este
trunchiat.
5.2.3.5 Operații flotante
Dacă un operand al unei operații este flotant, atunci întreaga operație este
flotantă. Dacă unul dintre operanzi este flotant dublu, operația este pe flotanți
dubli. Rezultatul este un flotant dublu sau boolean.
În caz de operații eronate nu se generează erori. În loc de aceasta se obține
rezultatul NaN. Dacă într-o operație participă un NaN rezultatul este de obicei NaN.
În cazul testelor de egalitate, expresia
NaN == NaN
are întotdeauna rezultatul fals pentru că un NaN nu este egal cu nimic. La fel,
expresia:
NaN != NaN
este întotdeauna adevărată.
În plus, întotdeauna expresia:
-0.0 == +0.0
este adevărată, unde +0.0 este zeroul pozitiv iar -0.0 este zeroul negativ. Cele
două valori sunt definite în standardul IEEE 754.
În alte operații însă, zero pozitiv diferă de zero negativ. De exemplu 1.0 / 0.0
este infinit pozitiv iar 1.0 / -0.0 este infinit negativ.
5.2.3.6 Apeluri de metode
La apelul unei metode, valorile întregi nu sunt automat extinse la întreg sau la
întreg lung ca la operații. Asta înseamnă că, dacă un operand este transmis ca
întreg scurt de exemplu, el rămâne întreg scurt și în interiorul metodei apelate. |