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!
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.group = group; } }
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 = s1.toUpperCase(); // s1 does not change; the method returns a reference to a new object which can be accessed using s2 variable s1 = s1.toUpperCase(); // s1 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!
O consecinta a faptului ca obiectele de tip String sunt imutabile este determinata de faptul ca efectuarea de modificari succesive conduce la crearea unui numar foarte mare de obiecte in String pool.
public static String concatenareCuClasaString() { String s = "Java"; for (int i=0; i<10000; i++){ t = t + "POO"; } return t; }
In acest caz, numarul de obiecte create in memorie este unul foarte mare. Dintre acestea doar cel rezultat la final este util. Pentru a preveni alocarea nejustificata a obiectelor de tip String care reprezinta pasi intermediari in obtinerea sirului dorit putem alege sa folosim clasa StringBuilder creata special pentru a efectua operatii pe siruri de caractere.
public static String concatenareCuClasaStringBuilder(){ StringBuilder sb = new StringBuilder("Java"); for (int i=0; i<10000; i++){ sb.append("POO"); } return sb.toString(); }
Cuvantul cheie final poate fi folosit si in alt context decat cel prezentat anterior. De exemplu, aplicat unei clase impiedica o eventuala derivare a acestei clase prin mostenire.
final class ParentClass { } class ChildClass extends ParentClass { // eroare de compilare, clasa ParentClass nu poate fi extinsa }
In mod similar, in cazul in care aplicam cuvantul cheie final unei metode, acest lucru impiedica o eventuala suprascriere a acelei metode
class ParentClass { public final void dontOverride() { System.out.println("You cannot override this method"); } } class ChildClass extends ParentClass { public void dontOverride() // eroare de compilare, metoda dontOverride() din System.out.println("But I want to!"); // clasa parinte nu poate fi suprascrisa } }
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()); } }
Pentru a observa utilitatea variabilelor statice, vom crea o clasa care tine un contor static ce numara cate instante a produs clasa in total.
class ClassWithStatics { static String className = "Class With Static Members"; private static int instanceCount = 0; public ClassWithStatics(){ instanceCount++; } public static int getInstanceCount() { return instanceCount; } } class Test { public static void main(String[] args) { System.out.println(ClassWithStatics.getInstanceCount()); // 0 ClassWithStatics instance1 = new ClassWithStatics(); ClassWithStatics instance2 = new ClassWithStatics(); ClassWithStatics instance3 = new ClassWithStatics(); System.out.println(ClassWithStatics.getInstanceCount()); // 3 } }
Desi am mentionat anterior faptul ca field-urile si metodele statice se acceseaza folosind sintaxa <NUME_CLASA>.<NUME_METODA/FIELD>
acesta nu este singura abordare disponibila in libajul Java. Pentru a referi o entitate statica ne putem folosi si de o instanta a clasei in care se afla metoda/field-ul accesat.
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) { ClassWithStatics instance = new ClassWithStatic(); System.out.println(instance.className); System.out.println(instance.getStaticFields()); } }
Pentru a facilita o initializare facila a field-urilor statice pe care o clasa le detine, limbajul Java pune la dispozitie posibilitatea de a folosi blocuri statice de cod. Aceste blocuri de cod sunt executate atunci cand clasa in cauza este incarcata de catre masina virtuala de java. Incarcarea unei clase se face in momentul in care aceaste este referita pentru prima data in cod (se creaza o instanta, se apeleaza o metoda statica etc.) In consecinta, blocul static de cod se va executa intotdeauna inainte ca un obiect sa fie creat.
class TestStaticBlock { static int staticInt; int objectFieldInt; static { staticInt = 10; System.out.println("static block called "); } } class Main { public static void main(String args[]) { // Desi nu am creat nici o instanta a clasei TestStaticBlock // blocul static de cod este creat, iar output-ul comenzii va fi 10 System.out.println(TestStaticBlock.staticInt); } }
Pattern-ul Singleton este utilizat pentru a restricționa numărul de instanțieri ale unei clase la un singur obiect, deci reprezintă o metodă de a folosi o singură instanță a unui obiect în aplicație.
Pattern-ul Singleton este util în următoarele cazuri:
Singleton este utilizat des în situații în care avem obiecte care trebuie accesate din mai multe locuri ale aplicației:
Exemple din API-ul Java: java.lang.Runtime, java.awt.Toolkit
Din punct de vedere al design-ului și testarii unei aplicații de multe ori se evită folosirea acestui pattern, în test-driven development fiind considerat un anti-pattern. A avea un obiect Singleton a carei referință o folosim peste tot prin aplicație introduce multe dependențe între clase și îngreunează testarea individuală a acestora.
In general, codul care folosește stări globale este mai dificil de testat pentru că implică o cuplare mai strânsă a claselor, și împiedică izolarea unei componente și testarea ei individuală. Dacă o clasă testată folosește un obiect singleton, atunci trebuie testat și singleton-ul. Soluția este simularea mock-up a singleton-ului în teste. Încă o problemă a acestei cuplări mai strânse apare atunci când două teste depind unul de celălalt prin modificarea singleton-ului, deci trebuie impusă o anumită ordine a rulării testelor.
Aplicarea pattern-ului Singleton constă în implementarea unei metode ce permite crearea unei noi instanțe a clasei dacă aceasta nu există, și întoarcerea unei referințe către aceasta dacă există deja. În Java, pentru a asigura o singură instanțiere a clasei, constructorul trebuie să fie private, iar instanța să fie oferită printr-o metodă statică, publică.
În cazul unei implementări Singleton, clasa respectivă va fi instanțiată lazy (lazy instantiation), utilizând memoria doar în momentul în care acest lucru este necesar deoarece instanța se creează atunci când se apelează getInstance()
, acest lucru putând fi un avantaj în unele cazuri, față de clasele non-singleton, pentru care se face eager instantiation, deci se alocă memorie încă de la început, chiar dacă instanța nu va fi folosită (mai multe detalii și exemplu în acest articol)
Fig. 1: Diagrama de clase pentru Singleton
Respectând cerințele pentru un singleton enunțate mai sus, în Java, putem implementa o componentă de acest tip în mai multe feluri, inclusiv folosind enum
-uri în loc de clase. Atunci când îl implementâm trebuie avut în vedere contextul în care îl folosim, astfel încât să alegem o soluție care să funcționeze corect în toate situațiile ce pot apărea în aplicație (unele implementări au probleme atunci când sunt accesate din mai multe thread-uri sau când trebuie serializate).
public class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } ... }
instance
este privateDe ce Singleton și nu clase cu membri statici?
O clasă de tip Singleton poate fi extinsă, iar metodele ei suprascrise, însă într-o clasă cu metode statice acestea nu pot fi suprascrise (overriden) (o discuție pe aceasta temă puteți gasi aici, și o comparatie între static și dynamic binding aici).
PasswordMaker
ce generează, folosind RandomStringGenerator
, o parolă pornind de la datele unei persoane. Această clasă o să conțină următoarele: name
getPassword()
care va returna parola RandomStringGenerator
și cu un alfabet obținut din 10 caractere obținute random din MAGIC_STRINGinstance
este inițializata într-un bloc staticgetInstance()
. E nevoie ca acest contor să fie static?MyImmutableArray
care să conțină:ArrayList<Integer> immutableArray;
neinitializat în primă fazaimmutableArray
immutableArray
să rămână immutableMyImmutableArray
demonstrând faptul că instanțele acestei clase sunt imutabile