Compararea obiectelor. Conversii de date. Operatii de I/o compararea obiectelor Una dintre operatiile cele mai frecvente de comparare a obiectelor este testarea egalitatii. In Java aceasta operatie poate avea 2 sensuri
Una dintre operatiile cele mai frecvente de comparare a obiectelor este testarea egalitatii. In Java aceasta operatie poate avea 2 sensuri:
testarea identitatii: in acest caz ceea ce se compara de fapt sunt 2 referinte de obiect, iar testul presupune a verifica daca cele 2 referinte indica unul si acelasi obiect. Testarea identitatii se realizeaza cu ajutorul operatorului ==;
testarea echivalentei: in acest caz operanzii sunt 2 obiecte propriu-zise si testul presupune a verifica daca cele 2 obiecte au continut asemanator. De regula, pentru testarea echivalentei se utilizeaza o metoda denumita equals, operanzii fiind un obiect receptor si unul dat ca parametru metodei. Aceasta metoda este definita in clasa Object si drept urmare ea este mostenita de toate clasele.
Problema este ca definitia metodei equals, asa cum apare ea in clasa Object, pentru majoritatea claselor utilizatorului nu este satisfacatoare, deoarece metoda returneaza valoarea true doar daca cei 2 operanzi sunt unul si acelasi obiect (deci doar in caz de identitate).
Ca urmare, programatorul va trebui sa-si defineasca propriile variante de equals, acolo unde este cazul. Daca luam spre exemplu clasa Punct, prezentata in lucrarea L2, o posibila implementare a metodei equals pentru ea ar fi urmatoarea:
class Punct{
//. . .
public boolean equals(Punct p) {
return ((x==p.x)&&(y==p.y));
}
}
class ClientPunct {
public void oMetoda( ) {
Punct p1 = new Punct(1,2);
Punct p2 = new Punct(1,2);
Punct p3 = p1;
boolean t1 = (p1==p2); //test de identitate; rezultat: false
boolean t2 = p1.equals(p2); //test de echivalenta; rezultat: true
boolean t3 = (p1==p3); //test de identitate; rezultat: true
//. . .
}
}
S e observa din implementarea metodei equals ca 2 obiecte Punct sunt considerate "egale" in sensul echivalentei, daca valorile coordonatelor lor sunt respectiv egale.
Rezultatele obtinute la testele de egalitate din clasa ClientPunct pot fi mai usor intelese daca se analizeaza fig.4.1 in care este redata configuratia memoriei corespunzatoare secventei de program de mai sus.
Figura 4.1
Cele mai multe dintre clasele predefinite ale mediului Java contin variante adecvate ale metodei equals, care realizeaza testarea continutului obiectelor respective.
Programatorul nu este obligat sa dea numele equals metodelor care testeaza echivalenta pentru clasele definite de el, dar acest nume constituie un fel de standard in programele Java. De exemplu, in clasele predefinite care implementeaza colectii de obiecte (vezi lucrarea L12) operatiile de cautare folosesc implicit pentru comparare metoda equals.
Compararea stringurilor
Clasa String nu face exceptie de la regulile expuse in paragraful precedent. Ca atare, vom utiliza operatorul == pentru a testa identitatea obiectelor String si metoda equals pentru a testa egalitatea sirurilor de caractere continute de obiectele String. Exemplu:
class oClasa {
public void oMetoda( ) {
String s1 = "ababu";
String s2 = "ababu";
Punct s3 = s1;
boolean t1 = (s1==s2); //test de identitate; rezultat: false
boolean t2 = s1.equals(s2); //test de echivalenta; rezultat: true
boolean t3 = (s1==s3); //test de identitate; rezultat: true
//. . .
}
}
Se observa ca referintele s1 si s2 indica obiecte distincte, desi ele au fost initializate cu acelasi sir de caractere. Acest lucru este o consecinta a faptului ca in Java o constanta String este echivalenta cu new String(constanta), ceea ce determina de fiecare data crearea unui nou obiect.
Pe langa metoda equals, clasa String mai contine si alte metode de comparare, si anume:
metoda equalsIgnoreCase, care este similara cu equals, dar la comparare nu diferentiaza literele mari de cele mici. Exemplu:
class oClasa {
public void oMetoda( ) {
String s1 = "ababu";
String s2 = s1.toUpperCase( ); //continutul obiectului indicat de s2 va fi
// "ABABU"
boolean t1 = s1.equals(s2); // rezultat: false
boolean t2 = s1.equalsIgnoreCase(s2); // rezultat: true
//. . .
}
}
metoda compareTo, care primeste ca parametru o referinta String si returneaza ca rezultat un numar intreg a carui valoare este:
negativa, daca sirul din obiectul receptor este < decat sirul din obiectul parametru;
nula, daca cele 2 siruri sunt egale;
pozitiva daca sirul din obiectul receptor este > decat sirul din obiectul parametru;
Exemplu:
class oClasa {
public void oMetoda( ) {
String s1 = "ababu";
String s2 = "ab2za";
String s3 = "ababu";
int t1 = s1.compareTo(s2); // rezultat: o valoare >0
int t2 = s1.compareTo(s3); //rezultat: valoarea 0
//. . .
}
}
Metoda compareTo poate fi folosita pentru realizarea sortarii alfabetice a sirurilor de caractere.
Conversii intre tipul String si tipurile primitive
Intr-un program Java conversiile intre tipul String si tipurile primitive sunt foarte necesare, in primul rand pentru ca datele de intrare ale programului sunt receptate in majoritatea cazurilor sub forma de siruri de caractere: fie ca sunt pasate ca argumente la lansarea in executie a programului, fie ca sunt citite de pe un suport extern. In Java nu avem ceea ce in C se numea functie de citire formatata (scanf). Ca urmare, odata receptionate de catre program, datele de intrare vor trebui convertite in functie de necesitati.
In ceea ce priveste conversiile, in Java exista o regula care spune ca:
intotdeauna tipul spre care se face conversia trebuie sa aiba metoda necesara conversiei.
In acest sens, clasa String este dotata cu metode numite valueOf care asigura conversiile de la tipurile primitive spre String. Exemplu:
class oClasa {
public void oMetoda( ) {
int a = 4;
double b = 13.2;
boolean c = true;
Din secventa de mai sus deducem ca valueOf este o metoda statica, supraincarcata astfel incat sa admita ca parametri valori ale tuturor tipurilor primitive. Efectul metodei este crearea unui obiect String al carui continut este obtinut prin conversia la sir de caractere a valorii parametrului. Ca rezultat, metoda returneaza o referinta la obiectul String creat.
Pentru a realiza conversiile in sens invers, adica de la String spre tipurile primitive se utilizeaza metode ale unor clase speciale, numite clase infasuratoare (wrapper classes).
Clasele infasuratoare
Tuturor tipurilor primitive ale limbajului Java le corespunde cate o clasa infasuratoare. Aceste clase au fost definite din 2 motive:
sa inglobeze metode de conversie intre valori ale tipurilor primitive si obiecte ale altor clase, precum si constante speciale legate de tipurile primare;
sa permita crearea de obiecte care sa contina valori ale tipurilor primare, si care apoi sa poata fi utilizate in cadrul unor structuri unde nu pot sa apara decat obiecte, nu date simple (v. lucrarea L12).
Clasele infasuratoare sunt definite în pachetul java.lang si ele sunt:
Integer pentru int
Short pentru short
Byte pentru byte
Long pentru long
Float pentru float
Double pentru double
Boolean pentru boolean
Character pentru char
Void pentru void
Exceptand clasa Void, restul claselor infasuratoare contin urmatoarele metode:
cate un constructor care accepta ca parametru o valoare a tipului primitiv corespondent. Exemplu:
Integer a = new Integer(5); Double b = new Double(12.13);
cate un constructor care accepta ca parametru o referinta String (exceptie face clasa Character care nu are acest constructor). Se presupune ca stringul respectiv contine o secventa compatibila cu tipul primitiv corespondent. Exemplu:
Integer a = new Integer("5"); Boolean b = new Boolean("true");
o metoda statica valueOf care accepta ca parametru un String si returneaza un obiect al clasei infasuratoare, convertind continutul stringului; apelul acestei metode este echivalent cu a executa un new cu constructorul descris mai înainte. Exemplu:
Integer a = Integer.valueOf("5"); Double b = Double.valueOf("-8.12");
o metoda toString care realizeaza practic conversia de la clasa infasuratoare la String (despre aceasta metoda vom vorbi ceva mai incolo).
o metoda typeValue, unde type este numele tipului primitiv corespondent; aceasta metoda returneaza continutul obiectului ca valoare a tipului primitiv. Exemplu:
Integer a = new Integer("5"); int x = a.intValue( ); Double b = new Double("3.14"); double y = b.doubleValue( );
o metoda equals care realizeaza testul echivalentei obiectelor.
o metoda compareTo care accepta ca parametru un obiect al aceleiasi clase infasuratoare ca si obiectul receptor. Metoda returneaza o valoare de tip int pozitiva, nula sau negativa dupa cum continutul obiectului receptor este mai mare, egal sau mai mic decât cel al parametrului. Exemplu:
Integer a = new Integer(7); Integer b = new Integer(10); int x = b.compareTo(a); //val. x va fi pozitiva
Pe langa aceste metode, unele dintre clasele înfasuratoare mai contin si alte metode si variabile membru specifice. Astfel, clasele care reprezinta tipuri numerice contin constantele statice MIN_VALUE si MAX_VALUE. In plus, clasele Float si Double, care modeleaza lucrul cu numere reale mai contin:
constantele statice POSITIVE_INFINITY si NEGATIVE_INFINITY, reprezentând valorile +infinit si -infinit;
metoda isInfinitecare returneaza true daca valoarea obiectului receptor este fie +infinit, fie -infinit.
Pentru conversiile de la String la tipurile primitive se utilizeaza metode statice ale claselor infasuratoare, si anume:
in cazul claselor care reprezinta tipuri numerice exista metodele parseType, unde Type este numele tipului primitiv corespondent scris cu prima litera majuscula; metoda accepta ca parametru stringul care trebuie convertit si returneaza valoarea corespunzatoare, in reprezentarea tipului primitiv respectiv;
in cazul tipului boolean exista metoda getBoolean care functioneaza similar metodei parseType.
Exemplu:
class oClasa {
public void oMetoda( ) {
String s1 = "67";
String s2 = "13.2";
String s3 = "true";
int a = Integer.parseInt(s1);
double b = Double.parseDouble(s2);
boolean c = Boolean.getBoolean(s3);
//. . .
}
}
!!Observatie: metodele de conversie spre tipurile float,double si boolean prezentate mai sus sunt valabile pentru versiunea Java 2. Pentru versiuni anterioare (cum e cazul compilatorului disponibil la laborator), conversiile spre aceste tipuri necesita crearea cate unui obiect al clasei infasuratoare, folosind constructorul cu parametru String si apoi apelarea metodelor typeValue ale obiectelor respective:
class oClasa {
public void oMetoda( ) {
String s1 = "67";
String s2 = "13.2";
String s3 = "true";
//conversia String -> double
Double aux = new Double(s2);
double b = aux.doubleValue();
//secventa de 2 operatii de mai sus este echivalenta cu urmatoarea operatie:
double b = new Double(s2).doubleValue();
//in mod similar se fac si conversiile:
//String -> float
float a = new Float(s2).floatValue();
//String -> boolean
boolean c = new Boolean(s3).booleanValue();
//. . .
}
}
Daca stringul care se converteste nu contine o secventa in concordanta cu tipul primitiv spre care se face conversia, programul va fi intrerupt cu un mesaj de eroare corespunzator.
Pentru informatii detaliate privind clasele infasuratoare se poate consulta documentatia http://www.utt.ro/upt/ac/jdk1.2.2/docs/api/index.html.
Metoda toString
De multe ori este necesar ca datele continute de obiecte sa fie convertite la siruri de caractere in vederea participarii obiectelor respective ca operanzi in expresii de manipulare a stringurilor. De exemplu functiile print/println necesita ca parametru o referinta String. Convertind un obiect la String, am putea astfel sa "afisam" acel obiect cu ajutorul functiilor print/println.
In sprijinul acestei necesitati limbajul Java are urmatoarea oferta:
Daca intr-o clasa este definita o metoda avand semnatura:
public String toString( );
atunci referintele la obiectele clasei respective pot sa apara ca operanzi in operatii de concatenare de siruri, deoarece in aceste situatii se apeleaza automat metoda toString.
Concluzia: programatorul trebuie sa-si defineasca in clasele sale cate o metoda toString, daca doreste sa converteasca obiectele respective la stringuri.
Exemplu:
class Punct {
//. . .
public String toString( ) {
String s = "("+String.valueOf(x)+","+String.valueOf(y)+")";
return s;
}
}
class ClientPunct {
public void oMetoda( ) {
Punct p1 = new Punct(1,2);
//. . .
System.out.println("Punctul p1="+p1);
}
//. . .
}
Din acest exemplu se poate vedea si cam cum trebuie definita metoda toString pentru o anumita clasa. Practic, se construieste un obiect String din valorile campurilor obiectului.
Stringurile si tablourile de caractere
Desi aparent un string si un tablou de caractere (tipul char[ ] ) modeleaza acelasi lucru, adica o secventa de caractere, cele doua tipuri se comporta diferit.
In primul rand, asa cum am vazut, unui obiect String nu i se poate modifica continutul. Unui tablou de caractere i se poate modifica oricare dintre elemente:
String vs; char[ ] tc; //. . . tc[pos] = 'x'; //corect //pentru vs nu avem o posibilitate similara
Pentru a obtine caracterul aflat la o anumita pozitie pos intr-un String, vs de exemplu, nu putem sa folosim notatia vs[pos] ca în cazul tablourilor, ci trebuie sa utilizam metoda charAt(pos):
Atributul length aplicabil pentru tablouri nu are tocmai aceeasi semnificatie cu metoda length( ) de la stringuri, asa dupa cum va rezulta din urmatoarea secventa:
String vs = ?ababu?;
char[ ] tc = new char[10]; //am creat un tablou cu 10 elemente de tip char
for(int i=0; itc[i] = vs.charAt(i); //am pus in tc aceleasi caractere ca si in vs
System.out.println("Lungimea lui vs = "+vs.length());
System.out.println("Lungimea lui tc = "+tc.length);}
Rezultatul afisat de acest program va fi:
Lungimea lui vs = 5
Lungimea lui tc = 10
Cu alte cuvinte, nu putem sa aflam cate caractere semnificative se afla intr-un tablou de caractere, decat daca parcurgem tabloul element cu element si le testam.
Intre tipurile String si char[ ] exista totusi o legatura, in sensul ca limbajul Java ofera metode de conversie prin care putem sa tranformam un String în tablou de caractere si viceversa:
String vs=?blabla?;
char[ ] tc = vs.toCharArray(); //crearea unui tablou dintr-un String
char[ ] atc = new char[5];
for(int i=0; iatc[i] = '0'+i; //completez elementele tabloului cu caracterele '0','1','2',...
String avs = new String(atc, 0, atc.length); //crearea unui String pe baza
//unui tablou
Metoda toCharArray creaza un tablou ale carui elemente sunt caracterele stringului sursa.
Operatia inversa se realizeaza cu ajutorul unui constructor al clasei String. Acesta în principiu poate initializa un string preluand un subsir din tabloul de caractere. Parametrii necesari sunt: referinta tabloului, indicele primului caracter din subsirul dorit si lungimea acestui subsir. In exemplul nostru subsirul cuprinde tot tabloul.
Operatii de I/O
Pachetul java.io este un set de clase cu ajutorul carora se realizeaza operatiile de intrare/iesire într-un program Java.
In Java operatiile de intrare/iesire se bazeaza pe conceptul de flux de date (stream).
Un flux de date este o secventa de date care se deplaseaza dinspre o sursa externa spre memorie - caz în care avem de a face cu un flux de intrare (input stream) sau din memorie spre o destinatie externa - flux de iesire (output stream).
Pentru operatiile de intrare/iesire cele mai frecvente sursa externa este tastatura, iar destinatia este ecranul monitorului. Acestea se mai numesc si suporturi standard de intrare, respectiv iesire. Corespunzator suporturilor standard, în Java exista 2 obiecte predefinite: System.in pentru tastatura si System.out pentru monitor.
Observatie:in si out sunt variabile membru statice ale clasei System definita în pachetul java.lang. Tipurile acestor variabile sunt clase din pachetul java.io, si anume: System.in este o referinta la clasa InputStream, iar System.out este o referinta la clasa PrintStream.
Daca dorim sa citim sau sa scriem date din/in fisiere de pe disc, va trebui sa creem si sa utilizam alte obiecte ale unor claselor din pachetul java.io. In aceasta lucrare vom studia posibilitatea de a efectua operatii de intrare/iesire la nivel de linii de caractere (adica fisiere de text).
Operatii de citire a liniilor de text
Clasa care modeleaza citirea unei linii dintr-un flux de intrare este BufferedReader, prin operatia readLine.
Aceasta operatie nu are parametri, iar executia ei are ca efect citirea din fluxul de intrare a unei secvente de caractere pana la intalnirea terminatorului de linie. Operatia returneaza o referinta la un obiect String care contine caracterele citite, dar fara a include si terminatorul. Cu alte cuvinte, stringul returnat contine doar caracterele utile (semnificative) ale liniei.
Daca s-a ajuns la sfârsitul fluxului de intrare, operatia returneaza valoarea null. Daca citirea nu se poate desfasura, operatia emite o exceptie de tip IOException (despre exceptii vom discuta in lucrarea L5). De aceea, semnatura unei functii care apeleaza metoda readLine, dar nu trateaza eventualele erori de citire, trebuie sa contina clauza throws IOException.
Observatie: una dintre cele mai frecvente erori intr-un program Java este omiterea clauzelor throws din antetul functiilor utilizatorului care apeleaza functii predefinite dotate cu aceasta clauza. Acest lucru este semnalat la compilare. Despre acestea insa vom discuta in lucrarea urmatoare.
Pentru a crea un obiect al clasei BufferedReader este necesar sa furnizam constructorului acesteia o referinta la un obiect al clasei InputStreamReader. Constructorul aceasteia, la randul lui necesita:
o referinta la un obiect FileInputStream, daca dorim ca citirea sa se faca dintr-un fisier de pe disc, sau
referinta System.in, daca dorim ca citirea sa se faca de la tastatura.
Deci, daca urmeaza sa citim dintr-un fisier al carui nume este dat de o variabila Stringnume_fis, va trebui sa creem un obiect BufferedReader ca în secventa de mai jos:
BufferedReader flux_in = new BufferedReader(new InputStreamReader(new FileInputStream(nume_fis)));
Daca citirea se va face de la tastatura, obiectul BufferedReader se creaza astfel:
BufferedReader flux_in = new BufferedReader(new InputStreamReader(System.in));
In continuare citirea se va realiza cu apelul:
linie = flux_in.readLine();
unde linie este o referinta String. In urma apelului ea va indica spre un obiect care contine caracterele citite.
In functie de natura datelor reprezentate de aceste caractere, uneori pot fi necesare conversii de la String la alte tipuri, in vederea utilizarii datelor respective in diverse calcule.
Exemplu: vom prezenta un program care citeste numere intregi dintr-un fisier de text, calculeaza si afiseaza pe ecran suma acestora. Se presupune ca fiecare numar din fisier se afla pe cate o linie distincta, iar numele fisierului este dat ca argument la executia programului. Frazele din program care au legatura cu clase ale pachetului java.io si sunt implicate in operatiile de intrare/iesire propriu-zise sunt scrise cu rosu.
import java.io.*;
public class Suma {
public static void main (String[ ] args) throws IOException {
BufferedReader flux_in = new BufferedReader (new
InputStreamReader (new FileInputStream(args[0])));
int suma = 0;
String linie;
while ((linie = flux_in.readLine()) != null) // cat timp nu am
// ajuns la sfarsitul fisierului
suma+=Integer.parseInt(linie); //s-a convertit stringul la int
System.out.println("Suma = "+suma);
flux_in.close(); //se inchide fisierul (fluxul)
}
}
Tema: sa se modifice programul de mai sus astfel incat citirea sa se faca de la tastatura.
Operatii de citire a liniilor de text
Afisarea unei linii pe ecran este deja o operatie binecunoscuta, ea se realizeaza apeland metodele print/println definite in clasa PrintStream, pentru obiectul System.out. Pentru a scrie o linie intr-un fisier de pe disc vom folosi aceleasi metode, dar va trebui sa creem un obiect separat al clasei PrintStream. Pentru aceasta trebuie sa furnizam constructorului clasei PrintStream, ca parametru, o referinta la un obiect FileOutputStream, asa ca în secventa de mai jos:
PrintStream flux_out = new PrintStream (new FileOutputStream(nume_fis));
unde nume_fis este o referinta la un obiect String ce contine numele fisierului.
Exemplu: vom prezenta un program care citeste dintr-un fisier de text o secventa de numere reale, dispuse cate unul pe linie, determina numarul lor, suma, media aritmetica, valoarea minima/maxima si tipareste aceste informatii intr-un alt fisier. Numele ambelor fisiere implicate se dau ca argumente la executia programului.
import java.io.*;
public class Statist {
public static void main (String[ ] args) throws IOException {
BufferedReader flux_in = new BufferedReader (new
InputStreamReader (new FileInputStream(args[0])));
PrintStream flux_out = new PrintStream (new FileOutputStream(args[1]));
double suma = 0.0, min, max, val;
int contor=0;
String linie;
while ((linie = flux_in.readLine()) != null) {
contor++;
val = Double.parseDouble(linie); //conversia String -> double
suma+=val;
if(contor==1) { //s-a citit primul numar
min = val; max = val;
}
else {
if(valif(val>max) max =val;
}
}
flux_out.println("S-au citit "+contor+" valori");
flux_out.println("Suma lor este "+suma);
flux_out.println("Media aritmetica este "+(contor>0 ? suma/contor : 0.0));
flux_out.println("Element minim: "+min);
flux_out.println("Element maxim: "+max);
flux_in.close( ); flux_out.close();
}
}
Tema
1. Sa se modifice clasa Complex, elaborata in cadrul temei de la lucrarea L2, astfel incat sa se renunte la metoda de afisare, iar in locul ei sa se defineasca metoda toString. In clasa ComplexClient se vor modifica in mod corespunzator instructiunile de afisare.
2. Sa se scrie o functie care primeste ca parametru un string ce contine un numar intreg si returneaza ca rezultat un string in care numarul respectiv apare cu virgule de separare a grupelor de cate 3 cifre. De exemplu:
"1543729" devine "1,543,729"
Se cere ca functia sa utilizeze tablouri de caractere pentru a realiza transformarea. Se va scrie si o secventa de program care sa apeleze functia, pentru a verifica executia ei.
3. Sa se modifice functia de la punctul 2 astfel incat ea sa primeasca drept parametri si caracterul de separare si numarul de cifre care sa constituie un grup intre 2 separatori.
4. Scrieti un program care citeste un fisier de text ale carui linii au structura:
nume_tip sir_valoare
unde nume_tip este numele unei clase infasuratoare, iar sir_valoare o valoare compatibila cu tipul respectiv. De exemplu:
Integer 3456
Character x
Float 12.4
Programul va crea cate un obiect al clasei indicate de nume_tip care sa contina valoarea din sir_valoare si il va afisa pe ecran folosind referinta obiectului creat. Numele fisierului de intrare va fi furnizat ca argument la lansarea in executie a programului.