Administrativ
Laboratoare
Tema
Teste
Resurse utile
Alte resurse
Arhiva Teme
Administrativ
Laboratoare
Tema
Teste
Resurse utile
Alte resurse
Arhiva Teme
This is an old revision of the document!
Scopul acestui laborator este familiarizarea voastră cu noțiunile de constructori și de referințe în limbajul Java.
Aspectele urmărite sunt:
Există uneori restricții de integritate care trebuie îndeplinite pentru crearea unui obiect. Java permite acest lucru prin existența noțiunii de constructor, împrumutată din C++. Astfel, la crearea unui obiect al unei clase se apelează automat o funcție numită constructor. Constructorul are numele clasei, nu returnează explicit un tip anume (nici măcar void
) și poate avea oricâți parametri.
Crearea unui obiect se face cu sintaxa:
class MyClass { ... } ... // constructor call instanceObject = new MyClass(param_1, param_2, ..., param_n);
Se poate combina declararea unui obiect cu crearea lui propriu-zisă printr-o sintaxă de tipul:
// constructor call MyClass instanceObject = new MyClass(param_1, param_2, ..., param_n);
De reținut că, în terminologia POO, obiectul creat în urma apelului unui constructor al unei clase poartă numele de instanță a clasei respective. Astfel, spunem că instanceObject
reprezintă o instanţă
a clasei MyClass
.
Să urmărim în continuare codul:
String myFirstString, mySecondString; myFirstString = new String(); mySecondString = "This is my second string";
Acesta creează întâi un obiect de tip String
folosind constructorul fără parametru (alocă spațiu de memorie și efectuează inițializările specificate în codul constructorului), iar apoi creează un alt obiect de tip String
pe baza unui șir de caractere constant.
Clasele pe care le-am creat până acum însă nu au avut nici un constructor. În acest caz, Java crează automat un constructor implicit (în terminologia POO, default constructor) care face iniţializarea câmpurilor neinițializate, astfel:
null
0
boolean
) se inițializează cu false
Pentru a exemplifica acest mecanism, să urmărim exemplul:
public class SomeClass { private String name = "Some Class"; public String getName() { return name; } } class Test { public static void main(String[] args) { SomeClass instance = new SomeClass(); System.out.println(instance.getName()); } }
La momentul execuției, în consolă se va afișa “Some Class”
și nu se va genera nici o eroare la compilare, deși în clasa SomeClass
nu am declarat explicit un constructor de forma:
public SomeClass() { ... // variables initialization }
Să vedem acum un exemplu general:
public class Student { private String name; public int averageGrade; // (1) constructor without parameters public Student() { name = "Unknown"; averageGrade = 5; } // (2) constructor with two parameters; used to set the name and the grade public Student(String n, int avg) { name = n; averageGrade = avg; } // (3) constructor with one parameter; used to set only the name public Student(String n) { this(n, 5); // call the second constructor (2) } // (4) setter for the field 'name' public void setName(String n) { name = n; } // (5) getter for the field 'name' public String getName() { return name; } }
Declararea unui obiect de tip Student
se face astfel:
Student st;
Crearea unui obiect Student
se face obligatoriu prin apel la unul din cei 3 constructori de mai sus:
st = new Student(); // first constructor call (1) st = new Student("Gigel", 6); // second constructor call (2) st = new Student("Gigel"); // third constructor call (3)
class Student { private String name; public int averageGrade; public Student(String n, int avg) { name = n; averageGrade = avg; } public static void main(String[] args) { // ERROR: the implicit constructor is hidden by the constructor with parameters Student s = new Student(); } }
După cum știți din laboratorul 1, obictele se alocă pe heap
. Pentru ca un obiect să poată fi folosit, este necesară cunoașterea adresei lui. Această adresă, așa cum știm din limbajul C, se reține într-un pointer.
Limbajul Java nu permite lucrul direct cu pointeri, deoarece s-a considerat că această facilitate introduce o complexitate prea mare, de care programatorul poate fi scutit. Totuși, în Java există noțiunea de referinţe care înlocuiesc pointerii, oferind un mecanism de gestiune transparent.
Astfel, declararea unui obiect:
Student st;
creează o referință care poate indica doar către o zonă de memorie inițializată cu patternul clasei Student
fără ca memoria respectivă să conțină date utile. Astfel, dacă după declarație facem un acces la un câmp sau apelăm o funcție-membru, compilatorul va semnala o eroare, deoarece referința nu indică încă spre vreun obiect din memorie. Alocarea efectivă a memoriei și inițializarea acesteia se realizează prin apelul constructorului împreună cu cuvântul-cheie new.
Managementul transparent al pointerilor implică un proces automat de alocare și eliberare a memoriei. Eliberarea automată poartă și numele de Garbage Collection, iar pentru Java există o componentă separată a JRE-ului care se ocupă cu eliberarea memoriei ce nu mai este utilizată.
Un fapt ce merită discutat este semnificația atribuirii de referințe. În exemplul de mai jos:
Student s1 = new Student("Bob", 6); s2 = s1; s2.averageGrade = 10; System.out.println(s1.averageGrade);
se va afișa 10.
În concluzie, atribuirea de referințe nu creează o copie a obiectului, cum s-ar fi putut crede inițial. Efectul este asemănător cu cel al atribuirii de pointeri în C.
Transferul parametrilor la apelul de funcții este foarte important de înțeles. Astfel:
st = new Student()
) în apelul funcției și modificările făcute după ele, NU VOR FI VIZIBILE după apel, deoarece ele modifică o copie a referinței originale. class TestParams { static void changeReference(Student st) { st = new Student("Bob", 10); } static void changeObject(Student st) { st.averageGrade = 10; } public static void main(String[] args) { Student s = new Student("Alice", 5); changeReference(s); // 1 System.out.println(s.getName()); // 1' changeObject(s); // 2 System.out.println(s.averageGrade); // 2' } }
Astfel, apelul (1) nu are nici un efect în metoda main
pentru că metoda changeReference
are ca efect asignarea unei noi valori referinței s
, copiată pe stivă. Linia (1') va afișa textul: Alice
.
Apelul (2) metodei changeObject
are ca efect modificarea structurii interne a obiectului referit de s
prin schimbarea valorii atributului averageGrade
. Linia (2') va afișa textul: 10
.
Cuvântul cheie this
se referă la instanța curentă a clasei și poate fi folosit de metodele, care nu sunt statice, ale unei clase pentru a referi obiectul curent. Apelurile de funcții membru din interiorul unei funcții aparținând aceleiași clase se fac direct prin nume. Apelul de funcții aparținând unui alt obiect se face prefixând apelul de funcție cu numele obiectului. Situația este aceeași pentru datele membru.
Totuși, unele funcții pot trimite un parametru cu același nume ca și un câmp membru. În aceste situații, se folosește cuvântul cheie this
pentru dezambiguizare, el prefixând denumirea câmpului când se dorește utilizarea acestuia. Acest lucru este necesar pentru că în Java este comportament default ca un nume de parametru să ascundă numele unui câmp.
În general, cuvântul cheie this
este utilizat pentru:
diferenta
între câmpuri ale obiectului curent și argumente care au același nume argument
unei metode o referință către obiectul curent (vezi linia (1) din exemplul următor)constructorilor
din alți constructori, evitându-se astfel replicarea unor bucăți de cod (vezi exemplul de la constructori)
Iată un exemplu în care vom extinde clasa Student
pentru a cunoaște grupa din care face parte:
class Group { private int numberStudents; private Student[] students; Group () { numberStudents = 0; students = new Student[10]; } public boolean addStudent(String name, int grade) { if (numberStudents < students.length) { students[numberStudents++] = new Student(this, name, grade); // (1) return true; } return false; } }
class Student { private String name; private int averageGrade; private Group group; public Student(Group group, String name, int averageGrade) { this.group = group; // (2) this.name = name; this.averageGrade = averageGrade; } }
Variabilele declarate cu atributul final
pot fi inițializate o singură dată. Observăm că astfel unei variabile de tip referință care are atributul final
îi poate fi asignată o singură valoare (variabila poate puncta către un singur obiect). O încercare nouă de asignare a unei astfel de variabile va avea ca efect generarea unei erori la compilare.
Totuși, obiectul către care punctează o astfel de variabilă poate fi modificat intern, prin apeluri de metode sau acces la câmpuri.
Exemplu:
class Student { private final Group group; // a student can change the group he was assigned in private static final int UNIVERSITY_CODE = 15; // declaration of an int constant public Student(Group group) { // reference initialization; any other attempt to initialize it will be an error this.grupa = grupa; } }
Dacă toate atributele unui obiect admit o unică inițializare, spunem că obiectul respectiv este immutable
, în sensul că nu putem schimba obiectul in sine (informatia pe care o stocheaza, de exemplu), ci doar referinta catre un alt obiect.. Exemple de astfel de obiecte sunt instanțele claselor String
și Integer
. Odată create, prelucrările asupra lor (ex.: toUpperCase()
) se fac prin instantierea de noi obiecte și nu prin alterarea obiectelor înseși.
Exemplu:
String s1 = "abc"; String s2 = s.toUpperCase(); // s does not change; the method returns a reference to a new object which can be accessed using s2 variable s = s.toUpperCase(); // s is now a reference to a new object
String pool
pentru a limita memoria utilizată. Asta înseamnă că dacă mai declarăm un alt literal “abc”, nu se va mai aloca memorie pentru încă un String, ci vom primi o referință către s-ul inițial. În cazul în care folosim constructorul pentru String se aloca memorie pentru obiectul respectiv și primim o referință nouă. Pentru a evidentia concret cum functioneaza acest String pool
, sa luam urmatorul exemplu:
String s1 = "a" + "bc"; String s2 = "ab" + "c";
În momentul în care compilatorul va încerca să aloce memorie pentru cele 2 obiecte, va observa că ele conțin, de fapt, aceeași informație. Prin urmare, va instanția un singur obiect, către care vor pointa ambele variabile, s1 și s2. Observați că această optimizare (de a reduce memoria) e posibilă datorită faptului că obiectele de tip String sunt immutable.
O intrebare legitimă este, așadar, cum putem compara două String-uri (ținând cont de faptul că avem referințele către ele, cum am arătat mai sus). Să urmărim codul de mai jos:
String a = "abc"; String b = "abc"; System.out.println(a == b); // True String c = new String("abc"); String d = new String("abc"); System.out.println(c == d); // False
equals
. Același lucru este valabil și pentru oricare alt tip referință: operatorul "==" testează egalitatea referințelor (i.e. dacă cei doi operanzi sunt de fapt același obiect).
Dacă vrem să testăm “egalitatea” a două obiecte, se apelează metoda: public boolean equals(Object obj)
.
Reţineţi semnătura acestei metode!
După cum am putut observa până acum, de fiecare dată când cream o instanță a unei clase, valorile câmpurilor din cadrul instanței sunt unice pentru aceasta și pot fi utilizate fără pericolul ca instaţierile următoare să le modifice în mod implicit.
Să exemplificăm aceasta:
Student instance1 = new Student("Alice", 7); Student instance2 = new Student("Bob", 6);
În urma acestor apeluri, instance1
și instance2
vor funcționa ca entități independente una de cealaltă, astfel că modificarea câmpului nume
din instance1
nu va avea nici un efect implicit și automat în instance2
. Există însă posibilitatea ca uneori, anumite câmpuri din cadrul unei clase să aibă valori independente de instanțele acelei clase (cum este cazul câmpului UNIVERSITY_CODE
), astfel că acestea nu trebuie memorate separat pentru fiecare instanță.
Aceste câmpuri se declară cu atributul static și au o locație unică în memorie, care nu depinde de obiectele create din clasa respectivă.
Pentru a accesa un câmp static al unei clase (presupunând că acesta nu are specificatorul private
), se face referire la clasa din care provine, nu la vreo instanță. Același mecanism este disponibil și în cazul metodelor, așa cum putem vedea în continuare:
class ClassWithStatics { static String className = "Class With Static Members"; private static boolean hasStaticFields = true; public static boolean getStaticFields() { return hasStaticFields; } } class Test { public static void main(String[] args) { System.out.println(ClassWithStatics.className); System.out.println(ClassWithStatics.getStaticFields()); } }
Exercițiile se rezolvă în ordine.
Punct
care să conțină: float
) ce reprezintă coordonatele. changeCoords
ce primește două numere reale și modifică cele două coordonate ale punctului. (x, y)
.Poligon
cu următoarele: Punct
). Punct
obținute din parametrii primiți. Punct
. new Integer(2+3)
și 2+3
, după modelul de mai jos:“abc”
și măsurați memoria utilizată. Apoi, umpleți vectorul cu new Strîng(“abc”)
și măsurați memoria utilizată. Măsurarea memoriei utilizate se poate face folosind următoarea metodă:RandomStringGenerator
ce generează un String de o anumită lungime fixată, conținând caractere alese aleator dintr-un alfabet. Această clasă o să conțină următoarele: myGenerator = new RandomStringGenerator(5, "abcdef");
next()
care va returna un nou String random folosind lungimea și alfabetul primite de constructor. char[]
(char array). Pentru a construi un String pornind de la un char array procedăm ca în exemplul următor:toCharArray()
a String-ului de convertit. PasswordMaker
ce generează, folosind RandomStringGenerator
, o parolă pornind de la datele unei persoane. Această clasă o să conțină următoarele: firstName
, un String numit lastName
și un int numit age
getPassword()
care va returna parola (age % 3)
litere din firstNameRandomStringGenerator
și cu un alfabet obținut din 10 caractere obținute random din MAGIC_STRING