Sunteți pe pagina 1din 123

Java basics

Obiective
Scopul acestui laborator este familiarizarea studenților cu noțiunile de bază ale programării în
Java.

Aspectele urmărite sunt:

 organizarea unui proiect Java


 familiarizarea cu IDE-ul
 definirea noțiunilor de clasă, câmpuri, proprietăți, metode, specificatori de acces
 folosirea unor tipuri de date

Organizarea unui proiect Java


În cadrul acestui laborator veți avea de ales între a folosi IntelliJ IDEA (tutorial instalare)
folosind contul de mail de la facultate (tutorial activare) sau Eclipse (tutorial instalare).
Primul pas este să vă familiarizați cu structura unui proiect și a unui fișier sursă Java.

Înainte de a începe orice implementare, trebuie să vă gandiți cum grupați logica întregului
program pe unități. Elementele care se regăsesc în același grup trebuie să fie conectate în
mod logic, pentru o ușoară implementare și înțelegere ulterioară a codului. În cazul Java,
aceste grupuri logice se numesc pachete și se reflectă pe disc conform ierarhiei din cadrul
proiectului. Pachetele pot conține atât alte pachete, cât și fișiere sursă.

Următorul pas este delimitarea entităților din cadrul unui grup, pe baza unor trăsături
individuale. În cazul nostru, aceste entități vor fi clasele. Pentru a crea o clasă, trebuie mai
întâi să creăm un fișier aparținând proiectului nostru și unui pachet (dacă este cazul și
proiectul este prea simplu pentru a-l împărți în pachete). În cadrul acestui fișier definim una
sau mai multe clase, conform urmatoarelor reguli:
 dacă dorim ca această clasă să fie vizibilă din întreg proiectul, îi vom pune
specificatorul public (vom vorbi despre specificatori de acces mai în detaliu în cele ce
urmează); acest lucru implică însă 2 restricții:
o fișierul și clasa publică trebuie să aibă același nume
o nu poate exista o altă clasă/interfață publică în același fișier (vom vedea în
laboratoarele următoare ce sunt interfețele)
 pot exista mai multe clase în același fișier sursă, cu condiția ca maxim una să fie
publică

Pentru mai multe informații despre cum funcționează mașina virtuală de Java (JVM) precum
și o aprofundare în POO și Java, consultați acest link.

Pentru un exemplu de creare a unui proiect, adăugare de pachete și fișiere sursă, consultați
acest link pentru IntelliJ Idea si acest link pentru Eclipse.

Tipuri primitive
Conform POO, orice este un obiect, însă din motive de performanță, Java suportă și tipurile
de bază, care nu sunt clase.

boolean isValid = true;


 
char nameInitial = 'L';
 
byte hexCode = (byte)0xdeadbeef;
 
short age = 23;
 
int credit = -1;
 
long userId = 169234;
 
float percentage = 0.42;
 
double money = 99999;

Biblioteca Java oferă clase wrapper (“ambalaj”) pentru fiecare tip primitiv. Avem astfel
clasele Char, Integer, Float etc. Un exemplu de instanțiere este următorul:

new Integer(0);

Clase
Clasele reprezintă tipuri de date definite de utilizator sau deja existente în sistem (din class
library - set de biblioteci dinamice oferite pentru a asigura portabilitatea, eliminând
dependența de sistemul pe care rulează programul). O clasă poate conține:

 membri (variabile membru (câmpuri) și proprietăți, care definesc starea


obiectului)
 metode (functii membru, ce reprezintă operații asupra stării).
Prin instanțierea unei clase se înțelege crearea unui obiect care corespunde unui șablon
definit. În cazul general, acest lucru se realizează prin intermediul cuvântului cheie new.

Procesul de inițializare implică: declarare, instanțiere și atribuire. Un exemplu de inițializare


este următorul:

Integer myZero = new Integer(0);

Un alt exemplu de clasă predefinita este clasa String. Ea se poate instanția astfel (nu este
necesară utilizarea new):

String s1, s2;


 
s1 = "My first string";
s2 = "My second string";

Aceasta este varianta preferată pentru instanțierea string-urilor. De remarcat că și varianta


următoare este corectă, dar ineficientă, din motive ce vor fi explicate ulterior.

s = new String("str");

Câmpuri (membri)
Un câmp este un obiect având tipul unei clase sau o variabilă de tip primitiv. Dacă este un
obiect atunci trebuie inițializat înainte de a fi folosit (folosind cuvântul cheie new).

class DataOnly {
int i;
float f;
boolean b;
String s;
}
 

Declarăm un obiect de tip DataOnly și îl inițializăm:

DataOnly d = new DataOnly();


 
// set the field i to the value 1
d.i = 1;
 
// use that value
System.out.println("Field i of the object d is " + d.i);
 

Observăm că pentru a utiliza un câmp/funcție membru dintr-o funcție care nu aparține clasei
respective, folosim sintaxa:

classInstance.memberName
 
Proprietăți
O proprietate este un câmp (membru) căruia i se atașează două metode ce îi pot expune sau
modifica starea. Aceste doua metode se numesc getter si setter.

class PropertiesExample {
String myString;
 
String getMyString() {
return myString;
}
 
void setMyString(String s) {
myString = s;
}
}
 

Declarăm un obiect de tip PropertiesExample și îi inițializăm membrul myString astfel:

PropertiesExample pe = new PropertiesExample();


 
pe.setMyString("This is my string!");
 
System.out.println(pe.getMyString());
 

Metode (funcții membru)


Putem modifica programul anterior astfel:

String s1, s2;


 
s1 = "My first string";
s2 = "My second string";
 
System.out.println(s1.length());
System.out.println(s2.length());

Va fi afișată lungimea în caractere a șirului respectiv. Se observă că pentru a aplica o funcție


a unui obiect, se folosește sintaxa:

classInstance.methodName(param1, param2, ..., paramN);

Funcțiile membru se declară asemănător cu funcțiile din C.

Specificatori de acces
În limbajul Java (şi în majoritatea limbajelor de programare de tipul OOP), orice clasă, atribut
sau metodă posedă un specificator de acces, al cărui rol este de a restricţiona accesul la
entitatea respectivă, din perspectiva altor clase. Există specificatorii:
Tip primitiv Definitie
private limitează accesul doar în cadrul clasei curente
default accesul este permis doar în cadrul pachetului (package private)
accesul este permis doar în cadrul pachetului si în clasele ce moștenesc clasa
protected
curentă
public permite acces complet
Atenţie, nu confundaţi specificatorul default (lipsa unui specificator explicit) cu protected

Încapsularea se referă la acumularea atributelor şi metodelor caracteristice unei anumite


categorii de obiecte într-o clasă. Pe de altă parte, acest concept denotă şi ascunderea
informaţiei de stare internă a unui obiect, reprezentată de atributele acestuia, alături de
valorile aferente, şi asigurarea modificării lor doar prin intermediul metodelor.

Utilizarea specificatorilor contribuie la realizarea încapsulării.

Încapsularea conduce la izolarea modului de implementare a unei clase (atributele acesteia şi


cum sunt manipulate) de utilizarea acesteia. Utilizatorii unei clase pot conta pe
funcţionalitatea expusă de aceasta, indiferent de implementarea ei internă (chiar şi dacă se
poate modifica în timp).

Exemplu de implementare
Clasa VeterinaryReport este o versiune micșorată a clasei care permite unui veterinar să
țină evidența animalelor tratate, pe categorii (câini/pisici):

public class VeterinaryReport {


int dogs;
int cats;
 
public int getAnimalsCount() {
return dogs + cats;
}
 
public void displayStatistics() {
System.out.println("Total number of animals is " +
getAnimalsCount());
}
}
 

Clasa VeterinaryTest ne permite să testăm funcționalitatea oferită de clasa anterioară.

public class VeterinaryTest {


public static void main(String[] args) {
VeterinaryReport vr = new VeterinaryReport();
 
vr.cats = 99;
vr.dogs = 199;
 
vr.displayStatistics();
System.out.println("The class method says there are " +
vr.getAnimalsCount() + " animals");
}
}
 
Observații:

 Dacă nu inițializăm valorile câmpurilor explicit, mașina virtuală va seta toate


referințele (vom discuta mai mult despre ele în laboratorul următor) la null și tipurile
primitive la 0 (pentru tipul boolean la false).
 În Java fișierul trebuie să aibă numele clasei (publice) care e conținută în el. Cel mai
simplu și mai facil din punctul de vedere al organizării codului este de a avea fiecare
clasă în propriul fișier. În cazul nostru, VeterinaryReport.java și
VeterinaryTest.java.

Obiectele au fiecare propriul spațiu de date: fiecare câmp are o valoare separată pentru fiecare
obiect creat. Codul următor arată această situație:

public class VeterinaryTest {


public static void main(String[] args) {
VeterinaryReport vr = new VeterinaryReport();
VeterinaryReport vr2 = new VeterinaryReport();
 
vr.cats = 99;
vr.dogs = 199;
vr2.dogs = 2;
 
vr.displayStatistics();
vr2.displayStatistics();
 
System.out.println("The first class method says there are " +
vr.getAnimalsCount() + " animals");
System.out.println("The second class method says there are " +
vr2.getAnimalsCount() + " animals");
}
}

Se observă că:

 Deși câmpul dogs aparținând obiectului vr2 a fost actualizat la valoarea 2, câmpul
dogs al obiectului vr1 a rămas la valoarea inițiala (199). Fiecare obiect are spațiul
lui pentru date!

 System.out.println(…) este metoda utilizată pentru a afișa la consola standard de


ieșire

 Operatorul '+' este utilizat pentru a concatena două șiruri

 Având în vedere cele două observații anterioare, observăm că noi am afișat cu succes
șirul “The first/second class method says there are ” + vr.getAnimalsCount() + “
animals”, deși metoda getAnimalsCount() întoarce un întreg. Acest lucru este posibil
deoarece se apelează implicit o metodă care convertește numărul întors de metodă în
șir de caractere. Apelul acestei metode implicite atunci când chemăm
System.out.println se întâmplă pentru orice obiect și primitivă, nu doar pentru
întregi.
Având în vedere că au fost oferite exemple de cod în acest laborator, puteți observa că se
respectă un anumit stil de a scrie codul in Java. Acest coding style face parte din informațiile
transmise in cadrul acestei materii și trebuie să încercați să îl urmați încă din primele
laboratoare, devenind un criteriu obligatoriu ulterior în corectarea temelor. Puteți gasi
documentația oficială pe site-ul Oracle. Pentru început, încercați să urmați regulile de
denumire: http://www.oracle.com/technetwork/java/codeconventions-135099.html

Arrays
Vectorii sunt utilizați pentru a stoca mai multe valori într-o singură variabilă. Un vector este
de fapt o matrice (array) unidimensională.

Exemplu definire vector de String-uri cu valorile deja cunoscute


String[] cars = { "Volvo", "BMW", "Ford" };
Exemplu creare și populare vector cu valori de la 1 la 20
int[] intArray = new int[20];
for (int i = 0; i < intArray.length; i++) {
intArray[i] = i + 1;
}

 Înainte să populăm vectorul, trebuie declarat (int[] intArray) și alocată memorie


pentru 20 elemente de tip int (new int[20]).
 Pentru a accesa lungimea vectorului, folosim câmpul length păstrat în vector.
 Indexarea elementelor într-un array începe de la 0.

Nice to know
 Fiecare versiune de Java aduce și concepte noi, dar codul rămâne cross-compatible
(cod scris pt Java 8 va compila si rula cu Java 12). Dintre lucrurile adaugate in
versiuni recente avem declararea variabilelor locale folosind var, ceea ce face implicit
inferarea tipului. Codul devine astfel mai ușor de urmărit (Detalii și exemple)

var labString = "lab 1" // type String

 Când folosiți biblioteci third-party o să observați că pachetele au denumiri de forma


com.organizationname.libname. Acesta este modul recomandat pentru a le denumi,
vedeți mai multe detalii pe pagina oficială. Există situații în care acest stil este
obligatoriu, de exemplu o aplicație Android nu poate fi publicată pe Play Store dacă
numele pachetelor sale nu respectă aceasta convenție.

Summary
 Codul Java este scris în interiorul claselor, enum-urilor și interfețelor (todo link lab cu
interfete)
 Un fișier de tip java poate conține mai multe clase
 Numele singurei clase publice dintr-un fișier trebuie să fie același cu numele fișierului
(fără extensia .java)
 Fișierele sunt grupate în pachete
 În interiorul unei clase declarăm
o variabile
o metode
o alte clase (lab clase interne)
 Clasele și metodele ar trebui să aibă o singură responsabilitate. Evitați metodele
lungi și clase cu mai mult de un rol. Nu puneți toată logica în metoda main.
Metoda main ar trebui să conțină doar câteva linii cu instațieri si apeluri de
metodele specializate.
 În declarația claselor, câmpurilor, metodelor putem adăuga specificatori de acces sau
diverse keywords pe care le vom studia în următoarele laboratoare
o specificatori de access: public, private, default, protected
 Java are coding style-ul său și este importantă respectarea lui, altfel proiectele, mai
ales cele cu mulți dezoltatori ar arăta haotic.
 Tipuri de date primitive: int, long, boolean, float, double, byte, char, short. Câmpurile
primitive se inițializează automat la instanțierea unei clase cu valori default: e.g. 0
pentru int.
 Clasa String oferă multe metode pentru manipularea șirurilor de caractere.

Exerciții
În cadrul laboratorului și la teme vom lucra cu Java 12. Când consultați documentația uitați-
vă la cea pentru această versiune.

Prerequisites

 Java JDK 12 instalat


 IDE (Intellij, eclipse) instalat
 Verificat din linia de comandă versiunea de java:
o javac -version - comanda javac este folosită pentru compilare
o java -version - comanda java este folosită pentru rulare

Task 1 (3p)

1. Creați folosind IDE-ul un nou proiect și adăugați codul din secțiunea Exemplu de
implementare. Rulați codul din IDE.
2. Folosind linia de comandă, compilați și rulați codul din exemplu
3. Mutați codul într-un pachet task1 (sau alt nume pe care il doriți să îl folosiți). Folosiți-
vă de IDE, de exemplu Refactor → Move pentru IntelliJ. Observați ce s-a schimbat în
fiecare fișier mutat.

Task 2 (5p)

Creați un pachet task2 (sau alt nume pe care îl doriți să îl folosiți). În el creați clasele:

 Student cu proprietățile: name (String), year (Integer)


 Course
o cu proprietățile: title (String), description (String), students (array de clase
Student - exemplu arrays).
o cu metoda: filterYear care întoarce o listă de studenți care sunt intr-un an dat
ca parametru.
 Nu folosiți vreun modificator de acces pentru variabile (aka “nu puneți nimic în fața
lor în afară de tip”)
 Test cu metoda main. La rulare, ca argument în linia de comandă se va da un integer
reprezentând anul în care este un student

o creați un obiect Course și 3-4 obiecte Student. Populați obiectul Course.


o afișați toți studenții din anul dat ca parametru. Hint: folositi
Arrays.toString(listStudenti). In clasa Student folositi IDE-ul pentru a
genera metoda toString (pt Intellij Code→Generate…)
o rulați atât din IDE (modificati run configuration) cât și din linie de comandă
2. Opțional, în loc de arrays pentru filterYear puteți să folosiți și obiecte de tip List, e.g.
ArrayList (exemplu)

Task 3 (1p)

1. Creați două obiecte Student cu aceleași date în ele. Afișați rezultatul folosirii equals()
între ele. Discutați cu asistentul despre ce observați și pentru a vă explica mai multe
despre această metodă.
o documentație

Task 4 (1p)

1. Adăugați modificatorul de acces 'private' tuturor variabilelor claselor Student și


Course (e.g. private String name;)
2. Rezolvați erorile de compilare adăugând metode getter și setter acestor variabile.
1. Ce ați făcut acum se numește încapsulare (encapsulation) și este unul din
principiile de bază din programarea orientată pe obiecte. Prin această
restricționare protejați accesarea și modificarea variabilelor.
 Hint: pentru a vă eficientiza timpul, folosiți IDE-ul pentru a generarea
aceste metode
 Eclipse: Source → Generate Getters and Setters
 IntelliJ: Code → Generate… → Getters and Setters
Constructori și referințe
Obiective
Scopul acestui laborator este familiarizarea voastră cu noțiunile de constructori și de
referințe în limbajul Java.

Aspectele urmărite sunt:

 tipurile de contructori și crearea de instanţe ale claselor folosind acești constructori


 utilizarea cuvântului-cheie this

Constructori
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 {
...
}
 
...
 
MyClass instanceObject;
 
// 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:

 referințele la obiecte se inițializează cu null


 variabilele de tip numeric se inițializează cu 0
 variabilele de tip logic (boolean) se inițializează cu false

Pentru a exemplifica acest mecanism, să urmărim exemplul:

SomeClass.java
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:

Student.java
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)
Atenție! Dacă într-o clasă se definesc doar constructori cu parametri, constructorul default,
fără parametri, nu va mai fi vizibil! Exemplul următor va genera eroare la compilare:
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();
}
}

În Java, există conceptul de copy constructor, acesta reprezintând un constructor care ia ca


parametru un obiect de același tip cu clasa în care se află constructorul respectiv. Cu ajutorul
acestui constructor, putem să copiem obiecte, prin copierea membru cu membru în
constructor.

public class Student {


private String name;
private int averageGrade;
 
public Student(String name, int averageGrade) {
this.name = name;
this.averageGrade = averageGrade;
}
 
// copy constructor
public Student(Student student) {
// name este camp privat, noi il putem accesa direct (student.name)
// deoarece ne aflam in interiorul clasei
this.name = student.name;
this.averageGrade = student.averageGrade;
}
}

Referințe. Implicații în transferul de parametri


Obiectele 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.
Motivul este că s1 și s2 sunt două referințe către ACELASI obiect din memorie. Orice
modificare făcută asupra acestuia prin una din referințele sale va fi vizibilă în urma accesului
prin orice altă referință către el.

Î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:

 pentru tipurile primitive se transfera prin COPIERE pe stivă: orice modificare în


funcția apelată NU VA FI VIZIBILĂ în urma apelului.
 pentru tipurile definite de utilizator și instanțe de clase în general, se COPIAZĂ
REFERINȚA pe stivă: referința indică spre zona de memorie a obicetului, astfel că
schimbările asupra câmpurilor vor fi vizibile după apel, dar reinstanțieri (expresii de
tipul: 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.

TestParams.java
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); // apel (1)
System.out.println(s.getName()); // "Alice"
 
changeObject(s); // apel (2)
System.out.println(s.averageGrade); // "10"
}
}
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ă. Se 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. Se va afișa textul: 10.

Cuvântul-cheie "this". Întrebuințări


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:

 a se face diferenta între câmpuri ale obiectului curent și argumente care au același
nume
 a pasa ca argument unei metode o referință către obiectul curent (vezi linia (1) din
exemplul următor)
 a facilita apelarea 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:

Group.java
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;
}
}
Student.java
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;
}
}
Metoda toString()
Cu ajutorul metodei toString(), care este deja implementată by default pentru fiecare
clasă în Java, aceasta întorcând un string, putem obține o reprezentare a unui obiect ca string.
În cazurile claselor implementate de utilizator, este de recomandat să implementăm (mai
precis, să suprascriem - detalii despre suprascrierea metodelor în următoarele laboratoare)
metoda toString() în clasele definite de către utilizator.

Un exemplu de implementare a metodei toString():

Student.java
public class Student {
private String name;
private int averageGrade;
 
public Student(String name, int averageGrade) {
this.name = name;
this.averageGrade = averageGrade;
}
 
@Override
public String toString() {
return "Nume student: " + name + "\nMedia studentului: " +
averageGrade;
}
}

Folosirea metodei toString():

Student st1 = new Student("Decebal Popescu", 5);


/*
nu este neaparat sa scriem st1.toString() la apelul metodei println
println apeleaza in mod automat metoda toString in acest caz
*/
System.out.println(st1);
/*
se va afisa urmatorul string
 
Nume student: Decebal Popescu
Media studentului: 5
*/

Exerciții
Task 1 - 3 puncte

Să se creeze o clasă numită Complex, care are doi membri de tip int (real și imaginary), care
vor fi de tip private. Realizați următoarele subpuncte:

 să se creeze trei constructori: primul primește doi parametri de tip int (primul
desemnează partea reală a unui număr complex, al doilea partea imaginară), al doilea
nu primește niciun parametru și apelează primul constructor cu valorile 0 și 0, iar al
treilea reprezinta un copy constructor, care primește ca parametru un obiect de tip
Complex, care este copiat în obiectul this
 să se scrie metode de tip getter și setter (remember primul laborator - proprietăți), prin
care putem accesa membrii privați ai clasei
 să se scrie o metodă de tip void numită addWithComplex, care primește ca parametru
un obiect de tip Complex, prin care se adună numărul complex dat ca parametru la
numărul care apelează funcția (adică la this)
 să se scrie o metodă de tip void numita showNumber, prin care se afișează numărul
complex

Task 2 - 2 puncte

Aveți in scheletul laboratorului un cod, care conține două greșeli legate de referințe. Rolul
vostru este să corectați aceste greșeli încât codul să aibă comportamentul dorit (există
comentarii în cod despre modul cum ar trebui să se comporte).

Task 3 - 3 puncte

Să se implementeze o clasă Point care să conțină:

 un constructor care să primească cele două numere reale (de tip float) ce reprezintă
coordonatele.
 o metodă changeCoords ce primește două numere reale și modifică cele două
coordonate ale punctului.
 o funcție de afișare a unui punct astfel: (x, y).

Să se implementeze o clasă Polygon cu următoarele:

 un constructor care preia ca parametru un singur număr n (reprezentând numărul de


colțuri al poligonului) și alocă spațiu pentru puncte (un punct reprezentând o instanță
a clasei Point).
 un constructor care preia ca parametru un vector, cu 2n numere reale reprezentând
colțurile. Acest constructor apelează constructorul de la punctul de mai sus și
completează vectorul de puncte cu cele n instanțe ale clasei Point obținute din
parametrii primiți.

La final, afișați coordonatele poligonului utilizând metodă de afișare a clasei Point.

Task 4 - 2 puncte

În scheletul de cod aveți definită clasa Book, în care trebuie să implementați metoda
toString(), și o clasă Main, în care se testează metoda toString() din Book.
Agregare și moștenire
Obiective
Scopul acestui laborator este familiarizarea studenților cu noțiunile de agregare și de
moștenire a claselor.

Aspectele urmărite sunt:

 studierea mecanismului de moștenire

 înțelegerea diferenței între moștenire și agregare

 downcasting și upcasting

Agregare și Compunere
Agregarea și compunerea se referă la prezența unei referințe pentru un obiect într-o altă clasă.
Acea clasă practic va refolosi codul din clasa corespunzatoare obiectului. Exemplu:

Compunere:

public class Foo {


private Bar bar = new Bar();
}

Agregare:

public class Foo {


private Bar bar;
 
// Obiectul Bar poate continua să existe chiar dacă obiectul Foo nu
există
Foo(Bar bar) {
this.bar = bar;
}
}

Exemplu practic:

class Page {
private String content;
public int numberOfPages;
 
public Page(String content, int numberOfPages) {
this.content = content;
this.numberOfPages = numberOfPages;
}
}
 
class Book {
private String title; // Compunere
private Page[] pages; // Compunere
private LibraryRow libraryRow = null; // Agregare
 
public Book(int size, String title, LibraryRow libraryRow) {
this.libraryRow = libraryRow;
this.title = title;
 
pages = new Page[size];
 
for (int i = 0; i < size; i++) {
pages[i] = new Page("Page " + i, i);
}
}
}
 
class LibraryRow {
private String rowName = null; // Agregare
 
public LibraryRow(String rowName) {
this.rowName = rowName;
}
}
 
class Library {
 
public static void main(String[] args) {
LibraryRow row = new LibraryRow("a1");
Book book = new Book(100, "title", row);
 
// După ce nu mai există nici o referință la obiectul Carte,
// Garbage Collector-ul va șterge (la un moment dat, nu
// neapărat imediat) acea instanță, dar obiectul LibraryRow
// transmis constructorului nu este afectat.
 
book = null;
}
}

 Agregarea (aggregation) - obiectul-container poate exista și în absența obiectelor


agregate de aceea este considerată o asociere slabă (weak association). În exemplul
de mai sus, un raft de bibliotecă poate exista și fără cărți.

 Compunerea (composition) - este o agregare puternică (strong), indicând că


existența unui obiect este dependentă de un alt obiect. La dispariția obiectelor
conținute prin compunere, existența obiectului container încetează. În exemplul de
mai sus, o carte nu poate exista fără pagini.

Inițializarea obiectelor conținute poate fi făcută în 3 momente de timp distincte:

 la definirea obiectului (înaintea constructorului: folosind fie o valoare inițială, fie


blocuri de inițializare)

 în cadrul constructorului
 chiar înainte de folosire (acest mecanism se numește inițializare leneșă (lazy
initialization))

Moștenire (Inheritance)
Numită și derivare, moștenirea este un mecanism de refolosire a codului specific limbajelor
orientate obiect și reprezintă posibilitatea de a defini o clasă care extinde o altă clasă deja
existentă. Ideea de bază este de a prelua funcționalitatea existentă într-o clasă și de a adăuga
una nouă sau de a o modela pe cea existentă.

Clasa existentă este numită clasa-părinte, clasa de bază sau super-clasă. Clasa care extinde
clasa-părinte se numește clasa-copil (child), clasa derivată sau sub-clasă.

Spre deosebire de C++, Java nu permite moștenire multiplă (multiple inheritance), astfel că nu putem
întâlni ambiguități de genul Problema Rombului / Diamond Problem. Mereu când vom vrea să ne
referim la metoda părinte (folosind cuvântul cheie super, cum vom vedea mai jos), acel părinte este
unic determinat.

Agregare vs. moștenire


Când se folosește moștenirea și când agregarea?

Răspunsul la această întrebare depinde, în principal, de datele problemei analizate dar și de


concepția designerului, neexistând o rețetă general valabilă în acest sens. În general,
agregarea este folosită atunci când se dorește folosirea trăsăturilor unei clase în interiorul
altei clase, dar nu și interfața sa (prin moștenire, noua clasă ar expune și metodele clasei de
bază). Putem distinge două cazuri:

 uneori se dorește implementarea funcționalității obiectului conținut în noua clasă și


limitarea acțiunilor utilizatorului doar la metodele din noua clasă (mai exact, se
dorește să nu se permită utilizatorului folosirea metodelor din vechea clasă). Pentru a
obține acest efect se va agrega în noua clasă un obiect de tipul clasei conținute și
având specificatorul de acces private.

 obiectul conținut (agregat) trebuie/se dorește a fi accesat direct. În acest caz vom
folosi specificatorul de acces public. Un exemplu în acest sens ar fi o clasă numită
Car care conține ca membrii publici obiecte de tip Engine, Wheel etc.

Moștenirea este un mecanism care permite crearea unor versiuni “specializate” ale unor
clase existente (de bază). Moștenirea este folosită în general atunci când se dorește
construirea unui tip de date care să reprezinte o implementare specifică (o specializare oferită
prin clasa derivată) a unui lucru mai general. Un exemplu simplu ar fi clasa Dacia care
moștenește clasa Car.
Diferența dintre moștenire și agregare este de fapt diferența dintre cele 2 tipuri de relații
majore prezente între obiectele unei aplicații :

 is a - indică faptul că o clasă este derivată dintr-o clasă de bază (intuitiv, dacă avem o
clasă Animal și o clasă Dog, atunci ar fi normal să avem Dog derivat din Animal, cu
alte cuvinte Dog is an Animal)

 has a - indică faptul că o clasă-container are o clasă conținută în ea (intuitiv, dacă


avem o clasă Car și o clasă Engine, atunci ar fi normal să avem Engine referit în
cadrul Car, cu alte cuvinte Car has a Engine)

Upcasting și Downcasting
Convertirea unei referințe la o clasă derivată într-una a unei clase de bază poartă numele de
upcasting. Upcasting-ul este facut automat și nu trebuie declarat explicit de către
programator.

Exemplu de upcasting:

class Instrument {
public void play() {}
 
static void tune(Instrument i) {
i.play();
}
}
 
// Obiectele Wind sunt instrumente
// deoarece au aceeași interfață:
public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // !! Upcasting automat pentru că metoda
primește
// un obiect de tip Instrument, nu un
obiect de tip Wind
// Deci ar fi redundant să faci un cast
explicit cum ar fi:
// Instrument.tune((Instrument) flute)
}
}

Deși obiectul flute este o instanță a clasei Wind, acesta este pasat ca parametru în locul unui
obiect de tip Instrument, care este o superclasa a clasei Wind. Upcasting-ul se face la
pasarea parametrului. Termenul de upcasting provine din diagramele de clase (în special
UML) în care moștenirea se reprezintă prin 2 blocuri așezate unul sub altul, reprezentând cele
2 clase (sus este clasa de bază iar jos clasa derivată), unite printr-o săgeată orientată spre
clasa de bază.

Downcasting este operația inversă upcast-ului și este o conversie explicită de tip în care se
merge în jos pe ierarhia claselor (se convertește o clasă de bază într-una derivată). Acest cast
trebuie făcut explicit de către programator. Downcasting-ul este posibil numai dacă obiectul
declarat ca fiind de o clasă de bază este, de fapt, instanță clasei derivate către care se face
downcasting-ul.

Iată un exemplu în care este folosit downcasting:

class Animal {
public void eat() {
System.out.println("Animal eating");
}
}
 
class Wolf extends Animal {
public void howl() {
System.out.println("Wolf howling");
}
 
public void eat() {
System.out.println("Wolf eating");
}
}
 
class Snake extends Animal {
public void bite() {
System.out.println("Snake biting");
}
}
 
class Test {
public static void main(String[] args) {
Animal a [] = new Animal[2];
 
a[0] = new Wolf(); // Upcasting automat
a[1] = new Snake(); // Upcasting automat
 
for (int i = 0; i < a.length; i++) {
a[i].eat(); // 1
 
if (a[i] instanceof Wolf) {
((Wolf)a[i]).howl(); // 2
}
 
if (a[i] instanceof Snake) {
((Snake)a[i]).bite(); // 3
}
}
}
}

Codul va afișa:

Wolf eating
Wolf howling
Animal eating
Snake biting

În liniile marcate cu 2 și 3 se execută un downcast de la Animal la Wolf, respectiv Snake


pentru a putea fi apelate metodele specifice definite în aceste clase. Înaintea execuției
downcast-ului (conversia de tip la Wolf respectiv Snake) verificăm dacă obiectul respectiv
este de tipul dorit (utilizând operatorul instanceof). Dacă am încerca să facem downcast
către tipul Wolf al unui obiect instantiat la Snake mașina virtuală ar semnala acest lucru
aruncând o excepție la rularea programului.

Apelarea metodei eat() (linia 1) se face direct, fără downcast, deoarece această metodă este
definită și în clasa de bază Animal. Datorită faptului că Wolf suprascrie (overrides) metoda
eat(), apelul a[0].eat() va afișa “Wolf eating”. Apelul a[1].eat() va apela metoda din
clasă de bază (la ieșire va fi afișat “Animal eating”) deoarece a[1] este instantiat la Snake,
iar Snake nu suprascrie metoda eat().

Upcasting-ul este un element foarte important. De multe ori răspunsul la întrebarea: este nevoie de
moștenire? este dat de răspunsul la întrebarea: am nevoie de upcasting? Aceasta deoarece
upcasting-ul se face atunci când pentru unul sau mai multe obiecte din clase derivate se execută
aceeași metodă definită în clasa părinte.

Să încercăm să evităm folosirea instanceof

Totuși, deși v-am ilustrat cum instanceof ne poate ajuta să ne dăm seama la ce să facem
downcasting, este de preferat să ne organizăm clasele și designul codului în așa fel încât să
lăsăm limbajul Java să facă automat verificarea tipului și să cheme metoda corespunzătoare.
Vom refactororiza codul anterior pentru a nu fi nevoie de instanceof:

class Animal {
public void eat() {
System.out.println("Animal eating");
}
 
public void action() {
// avem nevoie de această metodă deoarece vom crea un vector
// cu instanțe Animal și vom apela această metodă pe ele
}
}
 
class Wolf extends Animal {
public void action() {
System.out.println("Wolf howling");
}
 
public void eat() {
System.out.println("Wolf eating");
}
}
 
class Snake extends Animal {
public void action() {
System.out.println("Snake biting");
}
}
 
class Test {
public static void main(String[] args) {
Animal a [] = new Animal[2];
 
a[0] = new Wolf();
a[1] = new Snake();
 
for (int i = 0; i < a.length; i++) {
a[i].eat();
 
// acum că ele sunt numite la fel, putem apela metoda action
// din clasa Animal (observați de ce a fost nevoie să definim
// metoda action în clasa Animal), iar metoda corespunzătoare
// va fi apelată pentru tipul specific al instanței a[i]
 
a[i].action();
}
}
}

Codul va afișa:

Wolf eating
Wolf howling
Animal eating
Snake biting

Implicații ale moștenirii


În Java, clasele și membrii acestora (metode, variabile, clase interne) pot avea diverși
specificatori de acces, prezentați pe wiki în Organizarea surselor și controlul accesului.

 specificatorul de acces protected - specifică faptul că membrul sau metoda


respectivă poate fi accesată doar din cadrul clasei înseși sau din clasele derivate din
această clasă. Clasele nu pot avea acest specificator, doar membrii acestora!

 specificatorul de acces private - specifică faptul că membrul sau metoda respectivă


poate fi accesată doar din cadrul clasei înseși, nu și din clasele derivate din această
clasă. Clasele nu pot avea acest specificator, doar membrii acestora!
Constructorii nu se moștenesc și pot fi apelați doar în contextul unui constructor copil.
Apelurile de constructor sunt înlănțuite, ceea ce înseamnă că înainte de a se inițializa obiectul
copil, mai întâi se va inițializa obiectul părinte. În cazul în care părintele este copil la rândul
lui, se va înițializa părintele lui (până se va ajunge la parintele suprem – root).

Pe lângă reutilizarea codului, moștenirea dă posibilitatea de a dezvolta pas cu pas o aplicație


(procedeul poartă numele de incremental development). Astfel, putem folosi un cod deja
funcțional și adaugă alt cod nou la acesta, în felul acesta izolându-se bug-urile în codul nou
adăugat. Pentru mai multe informații citiți capitolul Reusing Classes din cartea Thinking în
Java (Bruce Eckel)

Suprascrierea, supraîncărcarea si ascunderea metodelor


statice
Suprascrierea (overriding) presupune înlocuirea funcționalității din clasa/clasele părinte
pentru instanța curentă. Supraîncărcarea (overloading) presupune furnizarea de
funcționalitate în plus, fie pentru metodele din clasa curentă, fie pentru clasa/clasele părinte.

public class Car {


public void print() {
System.out.println("Car");
}
 
public void init() {
System.out.println("Car");
}
 
public void addGasoline() {
// do something
}
}
 
class Dacia extends Car {
public void print() {
System.out.println("Dacia");
}
 
public void init() {
System.out.println("Dacia");
}
 
public void addGasoline(Integer gallons) {
// do something
}
 
public void addGasoline(Double gallons) {
// do something
}
}

Metodele dependente de instanță sunt polimorfice (la runtime pot avea diferite implementări)
deci ele pot fi suprascrise sau supraîncarcăte. Metoda print este suprascrisă în clasa Dacia
ceea ce înseamnă că orice instanță, chiar dacă se face cast la tipul Car metoda ce se va apela
va fi mereu metoda print din clasa Dacia. Metoda addGasoline este supraîncărcată ceea
ce înseamnă că putem executa metode cu semnături diferite dar același nume (cel mai folosit
in crearea metodelor de conversie).

Car a = new Car();


Car b = new Dacia();
Dacia c = new Dacia();
Car d = null;
 
a.print(); // afișează Car
b.print(); // afișează Dacia
c.print(); // afișează Dacia
d.print(); // aruncă NullPointerException

Suprascrierea nu se aplică și metodelor statice pentru că ele nu sunt dependente de instanță.


Dacă în exemplul de mai sus facem metodele print din Car și din Dacia statice, rezultatul va
fi următorul:

Car a = new Car();


Car b = new Dacia();
Dacia c = new Dacia();
Car d = null;
 
a.print(); // afișează Car
b.print(); // afișează Car pentru că tipul dat la inițializare al lui b
este Car
c.print(); // afișează Dacia pentru că tipul dat la inițializare al lui c
este Dacia
d.print(): // afișează Car pentru că tipul dat la inițializare al lui b
este Car
O să punem accent pe aceste concepte în laboratorul visitor

Sintaxa Java permite apelarea metodelor statice pe instanțe (e.g. a.print în loc de Car.print), dar
acest lucru este considerat bad practice pentru că poate îngreuna înțelegerea codului.

Suprascrierea corecta a metodei equals(Object o)

Una din problemele cele mai des întâlnite este suprascrierea corectă a metodei equals. Mai
jos putem vedea un exemplu de suprascriere incorectă a acestei metode.

public class Car {


public boolean equals(Car c) {
System.out.println("Car");
return true;
}
 
public boolean equals(Object o) {
System.out.println("Object");
return false;
}
}

Prima metodă este o supraîncărcare a metodei equals iar a doua metodă este suprascrierea
metodei equals.

Car a = new Car();


Dacia b = new Dacia();
Int c = new Int(10);
 
a.equals(a); // afișează Car
a.equals(b); // afișează Car deoarece se face upcasting de la Dacia la Car
a.equals(c); // afișează Object deoarece se face upcasting de la Int la
Object

Problema care se poate observa este că putem pasa ca argumente metodei equals si tipuri de
date diferite de Car, lucru ce ar putea arunca excepții de cast sau când vrem să accesăm
anumite proprietăți din instanță. Mai jos este modul corect de suprascrie metoda equals.

public class Car {


public boolean equals(Car c)
{
return true;
}
 
public boolean equals(Object o)
{
if (o == this) {
return true;
}
 
if (!(o instanceof Car)) {
return false;
}
 
return equals((Car) o);
}
}

De reținut că folosirea instanceof nu este recomandată, însă în acest caz este singurul mod
prin care ne putem asigura ca instanța de obiect trimisă metodei este de tip Car.

Cuvântul cheie super. Întrebuințări


Cuvântul cheie super se referă la instanța părinte a clasei curente. Acesta poate fi folosit în
două moduri: apelând o metoda suprascrisă (overriden) sau apelând constructorul părinte.

Apelând o metodă suprascrisă


public class Superclass {
 
public void printMethod() {
System.out.println("Printed in Superclass.");
}
}
 
public class Subclass extends Superclass {
 
// overrides printMethod in Superclass
public void printMethod() {
super.printMethod(); // apelează metoda părinte
 
System.out.println("Printed in Subclass.");
}
 
public static void main(String[] args) {
Subclass s = new Subclass();
s.printMethod();
}
}

Codul va afișa:

Printed in Superclass.
Printed in Subclass.
Apelând constructorul părinte
class Superclass {
public Superclass() {
System.out.println("Printed in Superclass constructor with no
args.");
}
 
public Superclass(int a) {
System.out.println("Printed in Superclass constructor with one
integer argument.");
}
}
 
class Subclass extends Superclass {
public Subclass() {
super(); // apelează constructorul părinte
// acest apel trebuie să fie pe prima linie a
constructorului !!
 
System.out.println("Printed in Subclass constructor with no
args.");
}
 
public Subclass(int a) {
super(a); // apelează constructorul părinte
// acest apel trebuie să fie pe prima linie a
constructorului !!
 
System.out.println("Printed in Subclass constructor with one
integer argument.");
}
 
public static void main(String[] args) {
Subclass s1 = new Subclass(20);
Subclass s2 = new Subclass();
}
}

Codul va afișa:

Printed in Superclass constructor with one integer argument.


Printed in Subclass constructor with one integer argument.
Printed in Superclass constructor with no args.
Printed in Subclass constructor with no args.
Invocarea constructorului părinte trebuie să fie prima linie dintr-un constructor al unei subclase,
dacă invocarea părintelui există (se poate foarte bine să nu apelăm super din constructor).

Chiar dacă nu se specifică apelul metodei super(), compilatorul va apela automat constructor-ul
implicit al părintelui însă dacă se dorește apelarea altui constructor, apelul de super(args)
respectiv este obligatoriu
Summary
Relații între obiecte:

 Agregare - has a

 Moștenire - is a

Upcasting

 convertire copil ⇒ parinte

 realizată automat

Downcasting

 convertire parinte ⇒copil

 trebuie facută explicit de către programator

 încercați să evitați folosirea operatorului instanceof

Suprascrierea

 înlocuirea functionalitații metodei din clasa de bază în clasa derivată

 pastreaza numele și semnatura metodei

Supraincarcarea

 în interiorul clasei pot exista mai multe metode cu acelasi nume, cu condiția ca
semnătura (tipul, argumentele) să fie diferită

super

 instanța clasei parinte

 amintiți-vă din laboratorul anterior că this se referă la instanța clasei curente

Exerciții
Gigel vrea să-i faca mamei sale un cadou de ziua ei și știe că-i plac foarte mult bomboanele.
El are nevoie de ajutorul vostru pentru a construi cel mai frumos și gustos cadou:

Task 1 [2p]

Veti proiecta o clasa CandyBox, care va conține câmpurile private flavor (String) și origin
(String). Clasa va avea, de asemenea:
 un constructor fără parametri

 un constructor ce va inițializa toate campurile

 o metoda de tip float getVolume(), care va intoarce valoarea 0;

 Întrucât clasa Object se află în rădăcina arborelui de moștenire pentru orice clasă,
orice instanta va avea acces la o serie de facilități oferite de Object. Una dintre ele
este metoda toString(), al cărei scop este de a oferi o reprezentare unei instanțe sub
forma unui șir de caractere, utilizata in momentul apelului System.out.println().
Adaugati o metoda toString(), care va returna flavor-ul si regiunea de proveniență a
cutiei de bomboane.

Task 2 [2p]

Din ea derivați clasele Lindt, Baravelli, ChocAmor. Pentru un design interesant, cutiile vor
avea forme diferite:

 Lindt va contine length, width, height (float);

 Baravelli va fi un cilindru. Acesta va conține un camp radius și unul height (float);

 ChocAmor, fiind un cub, va conține un camp length (float);

Clasele vor avea:

 constructori fără parametri

 constructori care permit inițializarea membrilor. Identificați o modalitate de reutilizare


a codului existent. Pentru fiecare tip de cutie veti initializa, in constructor, campurile
flavor și origin cu tipul corespunzator

 Suprascrieti metoda getVolume() pentru a intoarce volumul specific fiecarei cutii de


bomboane, in functie de tipul sau.

 Suprascrieti metoda toString() în clasele derivate, astfel încat aceasta să utilizeze


implementarea metodei toString() din clasa de bază. Returnați un mesaj de forma
“The ” + origin + “ ” + flavor + “ has volume ” + volume;

Task 3 [1p]

Adăugați o metodă equals() în clasa CandyBox. Justificați criteriul de echivalentă ales.


Vedeți metodele clasei Object, moștenită de toate clasele - Object are metoda equals, a cărei
implementare verifică echivalența obiectelor comparând referințele.

Hint: Puteti genera automat metoda, cu ajutorul IDE. Selectați câmpurile considerate și analizați în
ce fel va fi suprascrisă metoda equals.

Task 4 - Upcasting [2p]


Acum că am stabilit tipul cutiilor de bomboane, putem construi cadoul, ramanand la
latitudinea vostra care va fi designul lui. In pachetul java.util se gaseste clasa ArrayList,
care definește un resizable array, cu metodele specifice (add, size, get, lista lor completa este
in documentatie). Creati o clasă CandyBag, care va conține un ArrayList cu mai multe cutii
din fiecare tip. Creați obiecte de tip Lindt si testați egalitatea lor;

Task 5 - Downcasting [1p]

Adaugati clasei Baravelli, functia printBaravelliDim(), care va afișa dimensiunile razei


și inaltimii. În mod analog, procedati cu celelalte tipuri de cutii, adaugand metodele
printChocAmorDim() si printLindtDim(), în care să afișați dimensiunile fiecarei cutii.

Task 6 - Agregare [2p]

Gigel va vrea sa trimită prin curier cadoul, pentru a nu-l gasi mama lui mai devreme. Ajutați-l
să determine locația, creând clasa “Area”, care va conține un obiect de tip CandyBag, un
camp “number” (int) și un câmp “street” (String) Clasa va avea, de asemenea:

 un constructor fără parametri

 un constructor ce va inițializa toate campurile

 Acum ca am finalizat construcția, îi vom oferi mamei informații despre cadoul ei


printr-o felicitare. Creați o metoda getBirthdayCard(), care va afișa, pe primul
rand, adresa completă, iar apoi un mesaj de la multi ani.

 Tot aici, parcurgeți array-ul, apeland metoda toString() pentru elementele sale.

 Parcurgeți array-ul și, folosind downcasting la clasa corespunzătoare, apelați metodele


specifice fiecărei clase. Pentru a stabili tipul obiectului curent folosiți operatorul
instanceof

 In final, modificați cum considerati programul anterior astfel încât să nu mai aveți
nevoie de instanceof.

Static și final; Singleton Design Pattern


Obiective
 Înțelegerea conceptului de static în contextul claselor și instanțelor

 Utilizarea keywords-urilor static și final din Java

 Folosirea design-pattern-ului Singleton

Cuvântul-cheie "final". Obiecte immutable


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
Observăm că în acest exemplu am folosit un String literal. Literalii sunt păstrați într-un 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
Operatorul "==" compară referințele. Dacă am fi vrut să comparăm șirurile în sine am fi folosit
metoda 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;
}

În acest caz, numărul de obiecte create în memorie este unul foarte mare. Dintre acestea doar
cel rezultat la final este util. Pentru a preveni alocarea nejustificată a obiectelor de tip String
care reprezintă pași intermediari în obținerea șirului dorit putem alege să folosim clasa
StringBuilder creată special pentru a efectua operații pe șiruri de caractere.

public static String concatenareCuClasaStringBuilder(){


StringBuilder sb = new StringBuilder("Java");
for (int i=0; i<10000; i++){
sb.append("POO");
}
return sb.toString();
}

Cuvântul cheie final poate fi folosit și în alt context decât cel prezentat anterior. De exemplu,
aplicat unei clase împiedică o eventuală derivare a acestei clase prin moștenire.

final class ParentClass {


}
 
class ChildClass extends ParentClass {
// eroare de compilare, clasa ParentClass nu poate fi extinsa
}
În mod similar, în cazul în care aplicăm cuvântul cheie final unei metode, acest lucru
împiedică o eventuală 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
}
}

Cuvântul-cheie "static"
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 ține un contor static ce
numără câte instanțe a produs clasa în 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
}
}

Deși am menționat anterior faptul că field-urile și metodele statice se accesează folosind


sintaxa <NUME_CLASA>.<NUME_METODA/FIELD> acesta nu este singura abordare disponibilă în
libajul Java. Pentru a referi o entitate statică ne putem folosi și de o instanța a clasei în care se
află metodă/field-ul accesat.

În acest caz nu este relevant dacă tipul obiectului folosit este diferit de cel al referinței în care e
stocat(i.e. avem o referință a clasei Animal care referă un obiect de tipul Dog). Pentru apelul unei
metode statice se va lua în considerare numai tipul referinței, nu și cel al instanței pe care o referă
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());
}
}
Deși putem accesa o entitate statică folosind o referință, acest lucru este contraindicat. Field-urile și
metodele statice aparțin clasei și nu ar trebui să fie in nici un fel dependențe de existența unei
instanțe.

Pentru a facilita o initializare facilă a field-urilor statice pe care o clasa le deține, limbajul
Java pune la dispoziție posibilitatea de a folosi blocuri statice de cod. Aceste blocuri de cod
sunt executate atunci când clasa în cauza este încărcată de către mașina virtuală de java.
Încărcarea unei clase se face în momentul în care aceaste este referita pentru prima dată in
cod (se crează o instanță, se apelează o metodă statică etc.) În consecință, blocul static de cod
se va execută întotdeauna înainte că un obiect să 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);
}
}

Singleton Pattern
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.

Utilizari

Pattern-ul Singleton este util în următoarele cazuri:

 ca un subansamblu al altor pattern-uri:

o impreună cu pattern-urile Abstract Factory, Builder, Prototype etc. De


exemplu, în aplicație dorim un singur obiect factory pentru a crea obiecte de
un anumit tip.

 în locul variabilelor globale. Singleton este preferat variabilelor globale deoarece,


printre altele, nu poluează namespace-ul global cu variabile care nu sunt necesare.
Singleton este utilizat des în situații în care avem obiecte care trebuie accesate din mai multe
locuri ale aplicației:

 obiecte de tip logger

 obiecte care reprezintă resurse partajate (conexiuni, sockets etc.)

 obiecte ce conțin configurații pentru aplicație

 pentru obiecte de tip Factory.

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.

Încercați să nu folosiți în exces metode statice și componente Singleton.

Implementare

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;
}
...
}

 Instanța instance este private

 Constructorul este privat ca sa nu poata fi apelat decat din clasa respectivă

 Instanța este inițial nulă

 Instanța este creată la prima rulare a getInstance()

De 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).

Exerciții
1. (3p) Să se implementeze o clasă PasswordMaker ce generează, folosind
RandomStringGenerator, o parolă pornind de la datele unei persoane. Această clasă
o să conțină următoarele:

o o constantă MAGIC_NUMBER având orice valoare doriți

o un String constant MAGIC_STRING, lung de minim 20 de caractere, generat


random

o un constructor care primește: un String numit name


o o metodă getPassword() care va returna parola

 parola se construiește concatenand următoarele șiruri:

 un șir random de lungime MAGIC_NUMBER, generat cu


RandomStringGenerator și cu un alfabet obținut din 10
caractere obținute random din MAGIC_STRING

 și șirul format prin conversia la String a lungimii lui name + un


numar intreg generat random din intervalul [0,100]

o Pentru subșiruri și alte metode utile consultați documentația clasei String

2. (3p) Modificați implementarea clasei PasswordMaker astfel încât să respecte


conceptul de Singleton pattern (să permită instanțierea unei singur obiect)

o Pornind de la exemplul de Singleton din textul laboratorului implementați o


versiune care urmează principiul de Eager Initialization (singura instanța a
clasei este creată la pornirea aplicației, indiferent dacă este necesar sau nu)

o Implementați o versiune de Singleton în care variabila instance este


inițializata într-un bloc static

o Adăugați un contor care să numere de câte ori a fost accesată metodă


getInstance(). E nevoie ca acest contor să fie static?

o Tema de gândire: Ce se va întâmplă dacă folosim conceptul de Singleton


pattern într-un program paralelizat, care rulează pe mai multe linii de execuție
(thread-uri). Ce probleme ar putea să apară?

3. (3p) Să se implementeze o clasă MyImmutableArray care să conțină:

o un field de ArrayList<Integer> immutableArray; neinitializat în primă


faza

o un constructor care primește un ArrayList<Integer> și copiază toate


elementele din acel array în immutableArray

o o metodă getArray implementată în așa fel încât field-ul immutableArray să


rămână immutable

4. (1p) Testați clasa MyImmutableArray demonstrând faptul că instanțele acestei clase


sunt imutabile
Clase abstracte și interfețe
Obiective
Scopul acestui laborator este prezentarea conceptelor de interfață și de clasă abstractă și
utilizarea lor în limbajul Java.

Conceptele de metode și clase abstracte și de interfețe sunt prezente și în alte limbaje OOP,
fiecare cu particularitățile lor de sintaxă. E important ca în urma acestui laborator să înțelegeți
ce reprezintă și situațiile în care să le folosiți.

Aspectele urmărite sunt:

 Prezentarea interfețelor și claselor abstracte

 Moștenirea în cazul interfețelor si claselor abstracte

 Diferențele dintre interfețe și clase abstracte

Introducere
Fie următorul exemplu (Thinking in Java) care propune o ierarhie de clase pentru a descrie o
suită de instrumente muzicale, cu scopul demonstrării polimorfismului:
Clasa Instrument nu este instanţiată niciodată pentru că scopul său este de a stabili o
interfaţă comună pentru toate clasele derivate. În același sens, metodele clasei de bază nu vor
fi apelate niciodată. Apelarea lor este ceva greșit din punct de vedere conceptual.

Clase abstracte vs Interfețe


Folosim o clasă abstractă atunci când vrem:

 sa implementăm doar unele din metodele din clasă

 ca respectiva clasă să nu poată fi instanțiată

Folosim o interfață atunci când vrem:

 să avem doar o descriere a structurii, fără implementări

 ca interfața în cauză să fie folosită împreună cu alte interfețe în contextul moștenirii

Clase abstracte
Dorim să stabilim interfaţa comună pentru a putea crea funcţionalitate diferită pentru fiecare
subtip și pentru a ști ce anume au clasele derivate în comun. O clasă cu caracteristicile
enumerate mai sus se numește abstractă. Creăm o clasă abstractă atunci când dorim să:
 manipulăm un set de clase printr-o interfaţă comună

 reutilizăm o serie metode si membrii din această clasă in clasele derivate.

Metodele suprascrise în clasele derivate vor fi apelate folosind dynamic binding (late
binding). Acesta este un mecanism prin care compilatorul, în momentul în care nu poate
determina implementarea unei metode în avans, lasă la latitudinea JVM-ului (mașinii
virtuale) alegerea implementării potrivite, în funcţie de tipul real al obiectului. Această legare
a implementării de numele metodei la momentul execuţiei stă la baza polimorfismului. Nu
există instanţe ale unei clase abstracte, aceasta exprimând doar un punct de pornire pentru
definirea unor instrumente reale. De aceea, crearea unui obiect al unei clase abstracte este o
eroare, compilatorul Java semnalând acest fapt.

Metode abstracte
Pentru a exprima faptul că o metodă este abstractă (adică se declară doar interfaţa ei, nu și
implementarea), Java folosește cuvântul cheie abstract:

abstract void f();

O clasă care conţine metode abstracte este numită clasă abstractă. Dacă o clasă are una sau
mai multe metode abstracte atunci ea trebuie să conţină în definiţie cuvântul abstract.

Exemplu:

abstract class Instrument {


...
}

Deoarece o clasă abstractă este incompletă (există metode care nu sunt definite), crearea unui
obiect de tipul clasei este împiedicată de compilator.

Clase abstracte în contextul moștenirii


O clasă care moștenește o clasă abstractă este ea însăși abstractă daca nu implementează
toate metodele abstracte ale clasei de bază. Putem defini clase abstracte care moștenesc alte
clase abstracte ș.a.m.d. O clasă care poate fi instanţiată (adică nu este abstractă) și care
moștenește o clasă abstractă trebuie să implementeze (definească) toate metodele abstracte pe
lanţul moștenirii (ale tuturor claselor abstracte care îi sunt “părinţi”). Este posibil să declarăm
o clasă abstractă fără ca ea să aibă metode abstracte. Acest lucru este folositor când
declarăm o clasă pentru care nu dorim instanţe (nu este corect conceptual să avem obiecte de
tipul acelei clase, chiar dacă definiţia ei este completă).

Iată cum putem să modificăm exemplul instrument cu metode abstracte:


Interfeţe
Interfeţele duc conceptul abstract un pas mai departe. Se poate considera că o interfaţă este o
clasă abstractă pură: permite programatorului să stabilească o “formă” pentru o clasă
(numele metodelor, lista de argumente, valori întoarse), dar fară nicio implementare. O
interfaţă poate conţine câmpuri dar acestea sunt în mod implicit static și final. Metodele
declarate în interfață sunt în mod implicit public.

Interfaţa este folosită pentru a descrie un contract între clase: o clasă care implementează o
interfaţă va implementa metodele definite în interfaţă. Astfel orice cod care folosește o
anumită interfaţă știe ce metode pot fi apelate pentru aceasta.

Pentru a crea o interfaţă folosim cuvântul cheie interface în loc de class. La fel ca în cazul
claselor, interfaţa poate fi declarată public doar dacă este definită într-un fișier cu același
nume ca cel pe care îl dăm acesteia. Dacă o interfaţă nu este declarată public atunci
specificatorul ei de acces este package-private. Pentru a defini o clasă care este conformă
cu o interfaţă anume folosim cuvântul cheie implements. Această relaţie este asemănătoare
cu moștenirea, cu diferenţa că nu se moștenește comportament, ci doar “interfaţa”. Pentru a
defini o interfaţă care moștenește altă interfaţă folosim cuvântul cheie extends. Dupa ce o
interfaţă a fost implementată, acea implementare devine o clasă obișnuită care poate fi extinsă
prin moștenire.
Iata exemplul dat mai sus, modificat pentru a folosi interfeţe:

Codul arată astfel:

interface Instrument {
 
// Compile-time constant:
int FIELD = 5; // static & final
 
// Cannot have method definitions:
void play(); // Automatically public
 
String what();
 
void adjust();
}
 
class WindInstrument implements Instrument {
 
public void play() {
System.out.println("WindInstrument.play()");
}
 
public String what() {
return "WindInstrument";
}
 
public void adjust() {
}
}
 
class Trumpet extends WindInstrument {
 
public void play() {
System.out.println("Trumpet.play()");
}
 
public void adjust() {
System.out.println("Trumpet.adjust()");
}
}

Un exemplu pentru o interfață care extinde mai multe interfețe:

interface A{
void a1();
void a2();
}
 
interface B {
int x = 0;
void b();
}
 
interface C extends A, B {
// this interface will expose
// * all the methods declared in A and B (a1, a2 and b)
// * all the fields declared in A and B (x)
}

Implicit, specificatorul de acces pentru membrii unei interfeţe este public. Atunci când
implementăm o interfaţă trebuie să specificăm că funcţiile sunt public chiar dacă în interfaţă
ele nu au fost specificate explicit astfel. Acest lucru este necesar deoarece specificatorul de
acces implicit în clase este package-private, care este mai restrictiv decât public.

Moștenire multiplă în Java


Interfaţa nu este doar o formă “pură” a unei clase abstracte, ci are un scop mult mai înalt.
Deoarece o interfaţă nu specifică niciun fel de implementare (nu există nici un fel de spaţiu
de stocare pentru o interfaţă) este normal să “combinăm” mai multe interfeţe. Acest lucru este
folositor atunci când dorim să afirmăm că “X este un A, un B si un C”. Acest deziderat este
moștenirea multiplă și, datorită faptului ca o singură entitate (A, B sau C) are implementare,
nu apar problemele moștenirii multiple din C++.
interface CanFight {
void fight();
}
 
interface CanSwim {
void swim();
}
 
interface CanFly {
void fly();
}
 
class ActionCharacter {
public void fight() {
}
}
 
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
 
public void swim() {
}
 
public void fly() {
}
}
 
public class Adventure {
 
static void t(CanFight x) {
x.fight();
}
 
static void u(CanSwim x) {
x.swim();
}
 
static void v(CanFly x) {
x.fly();
}
 
static void w(ActionCharacter x) {
x.fight();
}
 
public static void main(final String[] args) {
Hero hero = new Hero();
 
t(hero); // Treat it as a CanFight
 
u(hero); // Treat it as a CanSwim
 
v(hero); // Treat it as a CanFly
 
w(hero); // Treat it as an ActionCharacter
}
}

Se observă că Hero combină clasa ActionCharacter cu interfeţele CanSwim etc. Acest lucru
se realizează specificând prima data clasa concretă (sau abstractă) (extends) și abia apoi
implements. Metodele clasei Adventure au drept parametri interfeţele CanSwin etc. si clasa
ActionCharacter. La fiecare apel de metodă din Adventure se face upcast de la obiectul
Hero la clasa sau interfaţa dorită de metoda respectivă.

Coliziuni de nume la combinarea interfeţelor


Combinarea unor interfețe care conţin o metodă cu același nume este posibilă doar dacă
metodele nu au tipuri întoarse diferite și aceeași listă de argumente. Totuși este preferabil ca
în interfețe diferite care trebuie combinate să nu existe metode cu același nume deoarece
acest lucru poate duce la confuzii evidente (sunt amestecate în acest mod 3 concepte:
overloading, overriding și implementation).

Extinderea interfeţelor
Se pot adăuga cu ușurinţă metode noi unei interfeţe prin extinderea ei într-o altă interfaţă:

Exemplu:

interface Monster {
void menace();
}
 
interface DangerousMonster extends Monster {
void destroy();
}

Deoarece câmpurile unei interfeţe sunt automat static și final, interfeţele sunt un mod
convenabil de a crea grupuri de constante, asemănătoare cu enum-urile din C , C++.

Iniţializarea câmpurilor în interfeţe


În interfeţe nu pot exista blank finals (câmpuri final neiniţializate) însă pot fi iniţializate cu
valori neconstante. Câmpurile fiind statice, ele vor fi iniţializate prima oară când clasa este
iniţializată.
Exerciţii
1. (2p) Implementaţi interfaţa Task (din pachetul first) în cele 3 moduri de mai jos.

 Un task care să afișeze un mesaj la output. Mesajul este specificat în constructor.


(OutTask.java)

 Un task care generează un număr aleator și afișează un mesaj cu numărul generat la


output. Generarea se face în constructor. (RandomOutTask.java)

 Un task care incrementează un contor global și afișează valoarea contorului după


fiecare incrementare (CounterOutTask.java).Notă: Acesta este un exemplu simplu
pentru Command Pattern

2. (3p) Interfaţa Container (din pachetul second) specifică interfaţa comună pentru colecţii
de obiecte Task, în care se pot adăuga și din care se pot elimina elemente. Creaţi două tipuri
de containere care implementează această clasă: 1. (1.5p) Stack - care implementează o
strategie de tip LIFO

2. (1.5p) Queue - care implementează o strategie de tip FIFO

Evitaţi codul similar în locuri diferite!

Hint: Puteţi reţine intern colecţia de obiecte, utilizând clasa ArrayList din SDK-ul Java. Iată
un exemplu de iniţializare pentru șiruri:

ArrayList<String> list = new ArrayList<String>();


3. (2p) Implementaţi interfaţa IFactory (clasa ContainerFactory, din pachetul third) care
conţine o metodă ce primește ca parametru o strategie (enum Strategy) și care întoarce un
container asociat acelei strategii. Din acest punct înainte, în programul vostru veţi crea
containere folosind doar această clasă (nu puteţi crea direct obiecte de tip Stack sau Queue).
Evitaţi instanţierea clasei Factory implementate de voi la fiecare creare a unui container!
Notă:Acest mod de a crea obiecte de tip Container elimină problema care apare în momentul
în care decidem să folosim o implementare diferită pentru un anumit tip de strategie și nu
vrem să facem modificări și în restul programului. De asemenea o astfel de abordare este utilă
când avem implementări care urmăresc scopuri diferite (putem avea un Factory care să creeze
containere optimizate pentru viteză sau un Factory cu containere ce folosesc minimum de
memorie). șablonul acesta de programare poartă denumirea de Factory Method Pattern.
4. (3p) Extindeţi clasa AbstractTaskRunner (din pachetul fourth) în 3 moduri:

 (1p) PrintTimeTaskRunner - care afișează un mesaj după execuţia unui task în care
se specifică ora la care s-a executat task-ul (vedeți clasa Calendar).

 (1p) CounterTaskRunner - incrementează un contor local care ţine minte câte task-
uri s-au executat.

 (1p) RedoBackTaskRunner - salvează fiecare task executat într-un container în


ordinea inversă a execuţiei și are o metodă prin care se permite reexecutarea task-
urilor.

Clase interne
Obiective
 prezentarea tipurilor de clase interne

 utilizarea claselor interne

 utilizarea sintaxei specifice claselor interne

Introducere
Clasele declarate în interiorul unei alte clase se numesc clase interne (nested classes). Acestea
permit gruparea claselor care sunt legate logic și controlul vizibilității uneia din cadrul
celorlalte.

Clasele interne sunt de mai multe tipuri:

 clase interne normale (regular inner classes)


 clase anonime (anonymous inner classes)

 clase interne statice (static nested classes)

 clase interne metodelor (method-local inner classes) sau blocurilor

 O clasă internă se comportă ca un membru al clasei în care a fost declarată

 O clasă internă are acces la toți membrii clasei în care a fost declarată, inclusiv cei
private

 O clasă internă poate fi public, final, abstract dar și private, protected și


static, însumând modificatorii claselor obișnuite și cei permiși metodelor și
variabilelor

 Nested classes vs. Inner classes

Clase interne "normale"


O clasă internă este definită în interiorul unei clase și poate fi accesată doar la runtime printr-
o instanță a clasei externe (la fel ca metodele și variabilele ne-statice). Compilatorul creează
fișiere .class separate pentru fiecare clasă internă, în exemplul de mai jos generând fișierele
Outer.class și Outer$Inner.class, însă execuția fișierului Outer$Inner.class nu este
permisă.

Test.java
interface Engine {
public int getFuelCapacity();
}
 
class Car {
class OttoEngine implements Engine {
private int fuelCapacity;
 
public OttoEngine(int fuelCapacity) {
this.fuelCapacity = fuelCapacity;
}
 
public int getFuelCapacity() {
return fuelCapacity;
}
}
 
public OttoEngine getEngine() {
OttoEngine engine = new OttoEngine(11);
return engine;
}
}
 
public class Test {
public static void main(String[] args) {
Car car = new Car();
 
Car.OttoEngine firstEngine = car.getEngine();
Car.OttoEngine secondEngine = car.new OttoEngine(10);
 
System.out.println(firstEngine.getFuelCapacity());
System.out.println(secondEngine.getFuelCapacity());
}
}
student@poo:~$ javac Test.java
student@poo:~$ ls
Car.class Car$OttoEngine.class Engine.class Test.class Test.java

Urmăriți exemplul de folosire a claselor interne de mai sus. Adresați-vă asistentului pentru
eventuale neclarități.

Dintr-o clasă internă putem accesa referința la clasa externă (în cazul nostru Car) folosind
numele acesteia și keyword-ul this:

Car.this;
Rulați codul din exemplu.

Modificatorii de acces pentru clase interne

Așa cum s-a menționat și în secțiunea Introducere, claselor interne le pot fi asociați orice
identificatori de acces, spre deosebire de clasele top-level Java, care pot fi doar public sau
package-private. Ca urmare, clasele interne pot fi, în plus, private și protected, aceasta
fiind o modalitate de a ascunde implementarea.

Folosind exemplul anterior, modificați clasa OttoEngine pentru a fi privată. Observați erorile
de compilare care rezultă.

 Tipul Car.OttoEngine nu mai poate fi accesat din exterior. Acest neajuns poate fi
rezolvat cu ajutorul interfeței Engine. Asociindu-i clasei interne Car.OttoEngine
supertipul Engine prin moștenire, putem instanția clasa prin upcasting.

 Fiind privată, clasa internă are implicit toți constructorii privați. Totuși, putem
instanția obiecte de tipul Car.OttoEngine în interiorul clasei Car, urmând să le
întoarcem folosind tot upcasting la Engine. Astfel, folosind metode getEngine,
ascundem complet implementarea clasei Car.OttoEngine.

Clase anonime
În dezvoltarea software, există situații când o componentă a aplicației are o utilitate suficient
de mare pentru a putea fi considerată o entitate separată (sau clasă). De multe ori, însă,
aceasta nu este utilizată decât într-o porțiune restrânsă din aplicație, într-un context foarte
specific (într-un lanț de moșteniri sau ierarhie de interfețe). Într-o aplicație reală, crearea unei
clase pentru fiecare astfel de componentă poate duce la fenomenul de Class Explosion, care,
pe scurt, ar aduce după sine o aplicație cu performanțe scăzute și greu de modificat/extins.

Pentru a evita acest fenomen, putem folosi clase interne anonime în locul definirii unei clase
cu număr de utilizări reduse. Acestea nu au nume și apar în program ca instanțe ale unei clase
moștenite (sau a unei interfețe extinse), care suprascriu (sau implementează) anumite metode.
Întorcându-ne la exemplul cu clasa top-level Car, putem rescrie metoda getEngine() a
acesteia astfel:

[...]
class Car {
public Engine getEngine(int fuelCapacity) {
return new Engine () {
private int fuelCapacity = 11;
 
public int getFuelCapacity() {
return fuelCapacity;
}
};
}
}
[...]
Modificați implementarea clasei Car. Rulați codul. Urmați instrucțiunile de mai jos pentru a restabilit
funcționalitatea programului. Adresați-vă asistentului pentru neclarități.

Metoda folosită mai sus elimină necesitatea creări unei clase interne “normale”, reducând
volumul codului și crescând lizibilitatea acestuia. Sintaxa return new Engine() { … } se
poate citi astfel: “Crează o clasă care implementează interfața Engine, conform următoarei
implementări”.

Observații:

 acest obiect este instanțiat imediat după return, folosind new (referința întoarsă de
new va fi upcast la clasa de bază: Engine)

 numele clasei instanțiate este absent (ea este anonimă), însă ea este de tipul Engine,
prin urmare, va implementa metoda/metodele din interfață(cum e metoda
getFuelCapacity). Corpul clasei urmeaza imediat instanțierii.

IntelliJ sugerează înlocuirea cu funcții lambda însă acest concept nu este acoperit în laborator.
Pentru detalii suplimentare urmăriți acest exemplu

O clasă internă anonimă poate extinde o clasă sau să implementeze o singură interfață, nu poate
face pe ambele împreună ca la clasele ne-anonime (interne sau nu), și nici nu poate să implementeze
mai multe interfețe.

Constructori

Clasele anonime nu pot avea constructori din cauză că nu au nume (nu am ști cum să numim
constructorii). Această restricție asupra claselor anonime ridică o problemă: în mod implicit,
clasă de bază este creată cu constructorul default.

Ce se întâmplă dacă dorim să invocăm un alt constructor al clasei de bază? În clasele


normale acest lucru era posibil prin apelarea explicită, în prima linie din constructor a
constructorului clasei de bază cu parametrii doriți, folosind super. În clasele interne acest
lucru se obține prin transmiterea parametrilor către constructorul clasei de bază direct la
crearea obiectului de tip clasă anonimă:

new Engine("Otto") {
// ...
}

În acest exemplu, am instanțiat o clasa anonimă, ce implementează interfața Engine, apelând


constructorul clasei de bază cu parametrul “Otto”.

Clase interne statice


În secțiunile precedente, s-a discutat doar despre clase interne ale caror instanțe există doar în
contextul unei instanțe a clasei exterioare, astfel că poate accesa membrii obiectului exterior
direct. De asemenea, am menționat că fiind membri ai claselor exterioare, clasele interne pot
avea modificatorii disponibili pentru metode și variabile, dintre care și static (clasele
exterioare nu pot fi statice!). Așa cum pentru a accesa metodele și variabilele statice ale unei
clase nu este nevoie de o instanță a aceteia, putem obține o referință către o clasă internă fără
a avea nevoie de o instanță a clasei exterioare.

Pentru a înțelege diferența dintre clasele interne statice și cele nestatice trebuie să reținem
următorul aspect: clasele nestatice țin legătura cu obiectul exterior în vreme ce clasele
statice nu păstrează această legătură.

Pentru clasele interne statice:

 nu avem nevoie de un obiect al clasei externe pentru a crea un obiect al clasei interne

 nu putem accesa câmpuri nestatice ale clasei externe din clasă internă (nu avem o
instanță a clasei externe)

 Pornind de la codul de aici, modificați clasa internă OttoEngine pentru a fi statică.

 Observați că trebuie modificat felul prin care aceasta este instanțiată. Pentru clasele
interne statice, apelăm new Car.OttoEngine().

 Rulați codul. Observați că în interiorul clasei Car, putem instanția clasa internă statică
folosind doar new OttoEngine(), datorită contextului.

* Clasele interne statice nu au nevoie de o instanță a clasei externe → atunci de ce le facem interne
acesteia?

 pentru a grupa clasele, dacă o clasă internă statică A.B este folosită doar de A, atunci
nu are rost să o facem top-level.

 Avem o clasă internă A.B, când o facem statică?

 în interiorul clasei B nu avem nevoie de nimic specific instanței clasei externe A, deci
nu avem nevoie de o instanță a acesteia → o facem statică
Clase interne în metode și blocuri
Primele exemple prezintă modalitățile cele mai uzuale de folosire a claselor interne. Totuși,
design-ul claselor interne este destul de complex și exista modalitati mai “obscure” de a le
folosi: clasele interne pot fi definite și în cadrul metodelor sau al unor blocuri arbitrare de
cod.

Clase interne în metode

În exemplul următor, clasa internă a fost declarată în interiorul funcției getInnerInstance.


În acest mod, vizibilitatea ei a fost redusă pentru ca nu poate fi instanțiată decât în această
funcție.

Singurii modificatori care pot fi aplicați acestor clase sunt abstract și final (binențeles, nu
amândoi deodată).

Test.java
[...]
class Car {
public Engine getEngine() {
class OttoEngine implements Engine {
private int fuelCapacity = 11;
 
public int getFuelCapacity() {
return fuelCapacity;
}
}
 
return new OttoEngine();
}
}
[...]

 Schimbați implementarea clasei Car de aici folosind codul de mai sus

 Observați că trebuie OttoEngine este vizibilă doar în interiorul metodei

 Modificați metoda main astfel încât să ruleze (Hint: upcast)

Clasele interne declarate în metode nu pot folosi variabilele declarate în metoda respectivă și nici
parametrii metodei. Pentru a le putea accesa, variabilele trebuie declarate final, ca în exemplul
următor. Această restricție se datorează faptului că variabilele si parametrii metodelor se află pe
segmentul de stivă (zonă de memorie) creat pentru metoda respectivă, ceea ce face ca ele să nu
existe la fel de mult cât clasa internă. Dacă variabila este declarată final, atunci la runtime se va
stoca o copie a acesteia ca un câmp al clasei interne, în acest mod putând fi accesată și după
execuția metodei. Pentru o explicație detaliată citiți Link1 și Link2.

Clase interne în blocuri

Exemplu de clasa internă declarata într-un bloc:


[...]
class Car {
public Engine getEngine(int fuelCapacity) {
if (fuelCapacity == 11) {
class OttoEngine implements Engine {
private int fuelCapacity = 11;
 
public int getFuelCapacity() {
return fuelCapacity;
}
}
 
return new OttoEngine();
}
 
return null;
}
}
[...]

 Schimbați implementarea clasei Car de aici folosind codul de mai sus

 Observați că trebuie OttoEngine este vizibilă doar în interiorul blocului

 Modificați metoda main astfel încât să ruleze (Hint: upcast)

În acest exemplu, clasa internă OttoEngine este defintă în cadrul unui bloc if, dar acest lucru
nu înseamnă că declarația va fi luată în considerare doar la rulare, în cazul în care condiția
este adevarată.

Semnificația declarării clasei într-un bloc este legată strict de vizibilitatea acesteia. La compilare clasa
va fi creată indiferent care este valoarea de adevăr a condiției if.

Moștenirea claselor interne


Deoarece constructorul clasei interne trebuie sa se atașeze de un obiect al clasei exterioare,
moștenirea unei clase interne este puțin mai complicată decât cea obișnuită. Problema rezidă
în nevoia de a inițializa legătura (ascunsă) cu clasa exterioară, în contextul în care în clasa
derivată nu mai există un obiect default pentru acest lucru.

class Car {
class Engine {
public void getFuelCapacity() {
System.out.println("I am a generic Engine");
}
}
}
 
class OttoEngine extends Car.Engine {
OttoEngine() {
} // EROARE, avem nevoie de o legatura la obiectul clasei exterioare
 
OttoEngine(Car car) { // OK
car.super();
}
}
 
public class Test {
public static void main(String[] args) {
Car car = new Car();
OttoEngine ottoEngine = new OttoEngine(car);
ottoEngine.getFuelCapacity();
}
}

Observăm ca OttoEngine moșteneste doar Car.Engine însa sunt necesare:

 parametrul constructorului OttoEngine trebuie sa fie de tipul clasei externă (Car)

 linia din constructorul OttoEngine: car.super().

 Rulați codul de mai sus

 Ștergeți constructorul OttoEngine(), rulați din nou codul

Utilizarea claselor interne


Clasele interne pot părea un mecanism greoi și uneori artificial. Ele sunt însă foarte utile în
următoarele situații:

 Rezolvăm o problemă complicată și dorim să creăm o clasă care ne ajută la


dezvoltarea soluției dar:

o nu dorim să fie accesibilă din exterior sau

o nu mai are utilitate în alte zone ale programului

 Implementăm o anumită interfață și dorim să întoarcem o referință la acea interfață,


ascunzând în același timp implementarea.

 Dorim să folosim/extindem funcționalități ale mai multor clase, însă în JAVA nu


putem extinde decât o singură clasă. Putem defini însă clase interioare. Acestea pot
moșteni orice clasă și au, în plus, acces la obiectul clasei exterioare.

 Implementarea unei arhitecturi de control, marcată de nevoia de a trata evenimente


într-un sistem bazat pe evenimente. Unul din cele mai importante sisteme de acest
tip este GUI (graphical user interface). Bibliotecile Java Swing, AWT, SWT sunt
arhitecturi de control care folosesc intens clase interne. De exemplu, în Swing, pentru
evenimente cum ar fi apăsarea unui buton se poate atașa obiectului buton o tratare
particulară al evenimentului de apăsare în felul următor:
button.addActionListener(new ActionListener() { //interfata implementata e
ActionListener
public void actionPerformed(ActionEvent e) {
numClicks++;
}
});

Exerciții
Task 1 - Meta (4p)

Îndrumarul este o componentă importantă a laboratorului de POO, iar citirea și înțelegerea


acestuia nu trebuie neglijate. În îndrumarul din acest laborator există mai multe căsuțe de tip
note, de forma celei de mai jos:

Do stuff

Identificați toate căsuțele de tip note din laborator și urmați instrucțiunile din acestea. Puteți
folosi scheletul pus la dispoziție. Pentru a ușura procesul de evaluare, creați fișiere separate
pentru fiecare task din note.

Task 2 - Car Dealership: The Beginning (2p)

Scheletul de cod conține implementarea unui dealership de mașini. Acesta vinde autoturisme
doar la cerere, conform cu tipul de mașină cerut de client. Totuși, patronul este nemulțumit.
El ar dori ca:

 toate tipurile de mașini vândute să nu fie vizibile decât de către angajați

 dealership-ul să rămână unul cu specific și tradiție, continuând să vândă veșnic


tipurile curente de mașini

Hint: aceasta este o decizie de business teribilă din partea patronului,


dar acesta v-a promis că veți putea închiria orice mașină de la el, dar
doar după ce îl ajutați.

Task 3 - Car Dealership: The Crisis (2p)

Timpul a trecut peste dealership, iar vânzările au fost chiar bune, cu o singură excepție,
mașinile de tip RACING. Aceste tipuri de mașini sunt făcute pe comandă, iar cererea a fost atât
de mică, încât nici după un an nu s-au vândut cele din primul lot. Astfel, dealership-ul riscă să
dea faliment, din cauza datoriilor generate (mașinile de tip RACING sunt și cele mai scumpe).

Din fericire, patronul a făcut o înțelegere cu Ferrari, de unde a și cumpărat mașinile.


Aceștia îl vor scăpa complet de datorii dacă acesta va elimina orice asociere dintre
Dealership și Ferrari, lăsând totuși posibilitatea pentru clienți de a-și ridica mașinile de la
Dealership. Patronul este disperat și vă cere din nou ajutorul. Va trebui să eliminați orice
asociere dintre cele două entități, păstrând funcționalitatea aplicației.

Hint: Clienții vor mai putea să își ridice mașinile Ferrari, dar acestea nu
vor mai fi administrate de către Dealership.
Task 4 - Car Dealership: The New Age (2p)

Business-ul merge bine, astfel că patronul nostru s-a hotărât să se extindă. El vrea să aducă în
Dealership mașini de tip ELECTRIC, Tesla, și a vorbit chiar cu Elon Musk în acest sens.
Această extindere este una importantă, astfel că patronul nostru vrea o performanță cât mai
ridicată. Patronul vă roagă să îl ajutați cu o comparație între:

 comandarea mașinilor Tesla folosind clase anonime

 comandarea mașinilor Tesla folosind funcții lambda

Un tutorial despre folosirea funcțiilor lambda îl găsiți în laborator, la secțiunea Clase


anonime. (opțional) Pentru o mică prezentare în discuția Anonymous vs. Lambda, puteți
verifica secțiunea de Referințe a laboratorului.

Overriding, Overloading & Visitor pattern


Obiective
Scopul acestui laborator este aprofundarea noțiunilor de programare orientată pe obiecte
întalnite in laboratoarele precedente, prezentarea design pattern-ului Visitor și familiarizarea
cu situațiile în care acesta este util de aplicat.

Overloading
Supraîncarcarea se referă la posibilitatea de a avea într-o clasă mai multe metode cu același
nume, dar implementari diferite. În Java, compilatorul poate distinge între metode pe baza
semnăturii lor, acesta fiind mecanismul din spatele supraîncărcarii.

Semnătura (signature) unei metode constă în:

 numele metodei

 numărul și tipul parametrilor


Opțional, pe lângă semnătura metodei poate fi menționat și tipul excepțiilor ce pot fi aruncate
din codul acesteia.

Tipul de return al unei metode nu face parte din semnătura acesteia. Din acest motiv simpla
modificare a tipului de return al unei metode nu este suficientă pentru supraîncărcare. Ceea ce vom
primi este o eroare de compilare.
public class TRex {
public void eat(Triceratops victim) {
System.out.println("Take 5 huge bites");
}
 
public boolean eat(Triceratops victim) {
boolean satisfaction = false;
if (victim.isJuicy()) {
System.out.println("Eat and be satisfied");
satisfaction = true;
}
return satisfaction;
}
 
// Error "Duplicate method eat(Triceratops)" in type TRex

Observăm de asemenea că la compilare nu se ține cont de numele dat parametrilor. Astfel


modificarea acestuia din victim în dino, spre exemplu, nu constituie o supraîncărcare validă.

Mai jos avem un exemplu valid de supraîncărcare pentru metoda eat:

public class TRex {


 
public void eat(Triceratops victim) {
System.out.println("Take 5 huge bites");
}
 
public void eat(Dromaeosaurus victim) { // parametru
cu tip diferit
System.out.println("Take 1 single bite");
}
 
public void eat(Human firstCourse, Human secondCourse) { // numar si
tipuri diferite de parametrii
System.out.println("No humans to eat at the time");
}
 
public int eat(Grass desert) { // parametru
cu tip diferit, return type este irelevant
System.out.println("Rather starve");
return 0;
}
 
public static void main(String [] args) {
TRex john = new TRex();
 
john.eat(new Triceratops()); // "Take 5 huge
bites"
john.eat(new Dromaeosaurus()); // "Take 1 single
bite"
john.eat(new Human("Ana"), new Human("Andrei")); // "No humans to
eat at the time"
john.eat(new Grass()); // "Rather starve"
}
}

O clasă poate supraîncărca metodele moștenite. Constructorii pot fi supraîncărcați.

Supraîncărcarea are loc la compilare, motiv pentru care mai este numită și polimorfism
static (compile time polymorphism). În aceasta fază compilatorul decide ce metodă este
apelată pe baza tipului referinței și prin analiza numelui și a listei de parametri. La runtime,
când este întalnit apelul unei metode supraîncărcate, deja se știe unde este codul care trebuie
executat.

Overriding
Suprascrierea se referă la redefinirea metodelor existente în clasa părinte de către clasa copil
în vederea specializării acestora. Metodele din clasa parinte nu sunt modificate. Putem
suprascrie doar metodele vizibile pe lanțul de moștenire (public, protected). O metodă din
clasa copil suprascrie metoda din clasa părinte dacă are același tip de return și aceeași
semnatură (nume și parametri).

Spre deosebire de supraîncărcare care face acest lucru la compilare, în cazul suprascrierii se
determină ce metodă va fi apelată, în mod dinamic, la runtime. Explicația este că decizia se
face pe baza tipului obiectului care apelează metoda, deci a instanței (cunoscută la runtime).
Din acest motiv, suprascrierea este cunoscută și ca polimorfism dinamic (Runtime
polymorphism). Polimorfismul reprezintă abilitatea unei clase să se comporte ca o altă
clasă de pe lanțul de moștenire, și de aceea conceptul de suprascriere a metodelor este
foarte strâns legat. Supraîncărcarea, fiind la compile-time, nu are legătură cu acest
polimorfism dinamic.

La apelarea unei metode suprascrise, Java se uită la tipul intern al obiectului pentru care este apelată
metoda, NU la referință. Astfel dacă referința are tipul clasei părinte, dar tipul este al clasei copil,
JVM va apela metoda din clasa copil.

Câteva reguli pentru suprascriere sunt:

 metoda suprascrisă are același tip de return și semnatură ca metoda inițială

 putem avea un tip de return diferit de cel al metodei inițiale, atâta timp cat este un tip
ce moștenește tipul de return al metodei inițiale

 specificatorul de access al metodei suprascrise nu poate fi mai restrictiv decât cel al


metodei inițiale

 nu poate arunca mai multe excepții sau excepții mai generale, poate însă arunca mai
puține sau mai particulare sau excepții unchecked (de runtime)

 metodele de tip static și final nu pot fi suprascrise


 constructorii nu pot fi suprascriși

În exemplul de mai jos, metodele purr și getFeatures au fost suprascrise de tipul GrumpyCat.

class CatFeatures { }
class GrumpyCatFeatures extends CatFeatures { }
class GrumpyFeatures { }
 
class Cat {
 
public void purr() {
System.out.println("purrrr");
}
 
public CatFeatures getFeatures() {
System.out.println("Cat getFeatures");
return new CatFeatures();
}
 
public final void die() {
System.out.println("Dying! frown emoticon");
}
}
 
class GrumpyCat extends Cat {
@Override
public void purr() {
System.out.println("NO!");
}
 
@Override
public GrumpyCatFeatures getFeatures() {
System.out.println("Grumpy getFeatures");
return new GrumpyCatFeatures();
}
 
// compiler would complain if you included @Override here
//@Override
//public void die() { } // Cannot override the final method from
Cat
 
public static void main(String [] args) {
ArrayList<Cat> cats = new ArrayList<Cat>();
cats.add(new Cat());
cats.add(new GrumpyCat());
 
for (Cat c : cats) {
c.purr();
c.die();
c.getFeatures();
}
}
}
Adnotarea (Annotation) @Override este complet opțională. Totuși este indicat să o includeți
mereu când suprascrieți o metodă. Motivele sunt simple:

 Compilatorul vă va anunța printr-o eroare dacă ați greșit numele metodei sau tipul
parametrilor și această nouă metodă nu suprascrie de fapt o metodă a părintelui
 Face codul vostru mai ușor de citit, pentru că devine evident când o metodă suprascrie
o altă metodă

super

În laboratorul cuvantul-cheie-super-intrebuintari am folosit cuvântul cheie super pentru a


invoca un anumit constructor din clasa părinte dar și pentru a apela în mod explicit metoda
din clasa părinte în cazul metodelor suprascrise.

Rescriem metoda purr() din clasa GrumpyCat astfel:

@Override
public void purr() {
super.purr();
System.out.println("NO!");
}

La apelul metodei pe o instanță a clasei GrumpyCat output-ul va fi:

purrrr
NO!

Visitor
Design pattern-urile reprezintă soluții generale și reutilizabile ale unei probleme comune în
design-ul software. Un design pattern este o descriere a soluției sau un template ce poate fi
aplicat pentru rezolvarea problemei, nu o bucata de cod ce poate fi aplicata direct. În general
pattern-urile orientate pe obiect arată relațiile și interacțiunile dintre clase sau obiecte, fără a
specifica însă forma finală a claselor sau a obiectelor implicate.

Design pattern-ul Visitor oferă o modalitate de a separa un algoritm de structura pe care


acesta operează. Avantajul constă în faptul că putem adauga noi posibilităţi de prelucrare a
structurii, fără să o modificăm. Extrapolând, folosind Visitor, putem adăuga noi funcţii care
realizează prelucrări asupra unei familii de clase, fără a modifica efectiv structura claselor.

Acest pattern este comportamental (behavioral) pentru că definește modalități de comunicare


între obiecte.

Cum recunoaștem o situație în care Visitor e aplicabil?

 Mai multe obiecte și operații pentru acestea

 Schimbarea/adăugarea operațiilor fără a modifica clasele

 Elemente heterogene - tipuri diferite de obiecte pe care se aplică mai multe operații

Decizia de utilizare a pattern-ului Visitor este în strânsă legătură cu stabilitatea ierarhiilor de


clase prelucrate: dacă noi clase copil sunt adăugate rar, atunci se poate aplica acest pattern
(într-o manieră eficientă), altfel nu este indicat.
Structură

Fig. 1: Componente pattern Visitor

Visitor - o interfață pentru operația aplicată Visitable - o interfață pentru obiecte pe care pot
fi aplicate operațiile (în diagramă este numită Element)

 metoda accept e independentă de tipul concret al Visitor-ului

 în accept se folosește obiectul de tip Visitor

Pentru fiecare algoritm/operație ce trebuie aplicată, se implementează clase de tip Visitor. În


fiecare obiect de tip Visitor trebuie să implementăm metode care aplică operația pentru
fiecare tip de element vizitabil.

În figure 2 este reprezentat flow-ul aplicării acestui pattern:

1. Clientul este cel care folosește o colecție de obiecte de unul sau mai multe tipuri, și
dorește să aplice pe acestea diferite operații (în exercițiile din laborator clientul este
practic programul vostru de test - main-ul). Clientul folosește obiecte Visitor create
pentru fiecare operație necesară.

2. Clientul parcurge colecția și în loc să aplice operaţia direct pe fiecare obiect de tip
Element, îi oferă acestuia un obiect de tip Visitor.

3. Obiectul de tip Element apelează metoda de “vizitare” oferită de Visitor.

4. Pe obiectul Visitor se apelează metoda visit corespunzătoare obiectului, iar în ea se


efectuează operația. ( în Visitor folosim conceptul de overloading pentru fiecare
metodă visit)
Fig. 2: Interacțiunile dintre componentele pattern-ului Visitor

Visitor și structurile de date

Aparent, folosirea lui accept este artificială. De ce nu declanşăm vizitarea unui obiect,
apelând direct v.visit(e) atunci când dorim vizitarea unui obiect oarecare? Răspunsul vine
însă chiar din situaţiile în care vrem să folosim pattern-ul; vrem să lăsăm structura internă a
colecţiei să facă aplicarea vizitatorilor. Cu alte cuvinte vizitatorul se ocupă de fiecare obiect
în parte, iar colecţia îl “plimbă” prin elementele sale. De exemplu, când dorim să vizităm un
arbore:

 declanşarea vizitării se va face printr-un apel accept pe un prim obiect (e.g. rădacina
arborelui)

 elementul curent este vizitat, prin apelul v.visit(this)

 pe lângă vizitarea elementului curent, este necesar sa declanşăm vizitarea tuturor


elementelor accesibile din elementul curent (e.g. nodurile-copil din arbore etc).
Realizăm acest lucru apelând accept pe fiecare dintre aceste elemente. Acest
comportament depinde de logica structurii.

Scenariu Visitor

Pentru a înţelege mai bine motivaţia din spatele design-pattern-ului Visitor, să considerăm
următorul exemplu.

Before

Fie ierarhia de mai jos, ce defineşte un angajat (Employee) şi un şef (Manager), văzut, de
asemenea, ca un angajat:

Test.java
class Employee {
String name;
float salary;
public Employee(String name, float salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public float getSalary() {
return salary;
}
}
class Manager extends Employee {
float bonus;
public Manager(String name, float salary) {
super(name, salary);
bonus = 0;
}
public float getBonus() {
return bonus;
}
public void setBonus(float bonus) {
this.bonus = bonus;
}
}
public class Test {
public static void main(String[] args) {
Manager manager;
List<Employee> employees = new
LinkedList<Employee>();
employees.add(new Employee("Alice", 20));
employees.add(manager= new Manager("Bob", 1000));
manager.setBonus(100);
}
}

Ne interesează să interogăm toţi angajaţii noştri asupra venitului lor total. Observăm că:

 anagajaţii obişnuiţi au salariul ca unic venit

 şefii posedă, pe lângă salariu, un posibil bonus

Varianta la îndemână ar fi să definim, în fiecare din cele doua clase, câte o metodă,
getTotalRevenue(), care întoarce salariul pentru angajaţi, respectiv suma dintre salariu şi
bonus pentru şefi:

class Employee {
...
public float getTotalRevenue() {
return salary;
}
}
class Manager extends Employee {
...
public float getTotalRevenue() {
return salary + bonus;
}
}
Acum ne interesează să calculăm procentul mediu pe care îl reprezintă bonusul din venitul
şefilor, luându-se în considerare doar bonusurile pozitive. Avem două posibilităţi:

 Definim câte o metodă, getBonusPercentage(), care în Employee întoarce mereu 0, iar


în Manager raportul real. Dezavantajul constă în adăugarea în interfeţe a unor funcţii
prea specializate, de detalii ce ţin doar de unele implementări ale acestora.

 Parcurgem lista de angajaţi, testăm, la fiecare pas, tipul angajatului, folosind


instanceof, şi calculăm, doar pentru şefi, raportul solicitat. Dezavantajul este
tratarea într-o manieră neuniformă a structurii noastre, cu evidenţierea
particularităţilor fiecărei clase.

Datorită acestor particularităţi (în cazul nostru, modalităţile de calcul al venitului, respectiv
procentului mediu), constatăm că ar fi foarte utilă izolarea implementărilor specifice ale
algoritmului (în cazul nostru, scrierea unei funcţii în fiecare clasă). Acest lucru conduce, însă,
la introducerea unei metode noi în fiecare din clasele antrenate in prelucrări, de fiecare dată
cand vrem să punem la dispoziţie o nouă operaţie. Obţinem următoarele dezavantaje:

 în cazul unui număr mare de operaţii, interfeţele claselor se aglomerează excesiv şi


se ascunde funcţionalitatea de bază a acestora

 codul din interiorul clasei (care servea functionalităţii primare a acesteia) va fi


amestecat cu cel necesar algoritmilor de prelucrare, devenind mai greu de parcurs şi
întreţinut

 în cazul în care nu avem acces la codul claselor, singura modalitate de adăugare de


funcţionalitate este extinderea

În final, tragem concluzia că este de dorit să izolăm algoritmii de clasele pe care le


prelucrează. O primă idee se referă la utilizarea metodelor statice. Dezavantajul acestora
este că nu pot reţine, într-un mod elegant, informaţie de stare din timpul prelucrării. De
exemplu, dacă structura noastră ar fi arborescentă (recursivă), în sensul că o instanţă
Manager ar putea ţine referinţe la alte instanţe Manager, ce reprezintă şefii ierarhic inferiori,
o funcţie de prelucrare ar trebui să menţină o informaţie parţială de stare (precum suma
procentelor calculate până într-un anumit moment) sub forma unor parametri furnizaţi
apelului recursiv:

class Manager extends Employee {


...
public float getPercentage(float sum, int n) {
float f = bonus / getTotalRevenue();
if (f > 0)
return inferiorManager.getPercentage(sum + f, n +
1); // trimite mai departe cererea catre nivelul inferior
return inferiorManager.getPercentage(sum, n);
}
}

O abordare mai bună ar fi:

 conceperea claselor cu posibilitatea de primire/ataşare a unor obiecte-algoritm,


care definesc operaţiile dorite
 definirea unor clase algoritm care vor vizita structura noastră de date, vor efectua
prelucrările specifice fiecărei clase, având, totodată, posibilitatea de încapsulare a
unor informaţii de stare (cum sunt suma şi numărul din exemplul anterior)

After

Conform observațiilor precedente, structura programului Employee-Manager devine:

Test.java
interface Visitor {
public void visit(Employee employee);
public void visit(Manager manager);
}
interface Visitable {
public void accept(Visitor v);
}
class Employee implements Visitable {
...
public void accept(Visitor v) {
v.visit(this);
}
}
class Manager extends Employee {
...
public void accept(Visitor v) {
v.visit(this);
}
}
public class Test {
public static void main(String[] args) {
...
Visitor v = new SomeVisitor(); // creeaza un
obiect-vizitator concret
for (Employee e : employees)
e.accept(v);
}
}

Iată cum poate arăta un vizitator ce determină venitul total al fiecărui angajat şi îl afişează:

RevenueVisitor.java
public class RevenueVisitor implements Visitor {
public void visit(Employee employee) {
System.out.println(employee.getName() + " " +
employee.getSalary());
}
public void visit(Manager manager) {
System.out.println(manager.getName() + " " +
(manager.getSalary() + manager.getBonus()));
}
}

Secvenţele de cod de mai sus definesc:


 o interfaţă, Visitor, ce reprezintă un algoritm oarecare, ce va putea vizita orice clasă.
Observaţi definirea câte unei metode visit(…) pentru fiecare clasă ce va putea fi
vizitată

 o interfaţă, Visitable, a carei metodă accept(Visitor) permite rularea unui algoritm


pe structura curentă.

 implementări ale metodei accept(Visitor), în cele două clase, care, pur şi simplu,
solicită vizitarea instanţei curente de către vizitator.

 o implementare a unei operații aplicabilă pe obiectele de tip Visitable

În exemplul de mai sus, putem identifica :

 Element - Visitable

 ConcreteElement - Employee, Manager

Double-dispatch

Mecanismul din spatele pattern-ului Visitor poartă numele de double-dispatch. Acesta este
un concept raspândit, şi se referă la faptul că metoda apelată este determinată la runtime de
doi factori. În exemplul Employee-Manager, efectul vizitarii, solicitate prin apelul
e.accept(v), depinde de:

 tipul elementului vizitat, e (Employee sau Manager), pe care se invocă metoda

 tipul vizitatorului, v (RevenueVisitor), care conţine implementările metodelor visit

Acest lucru contrastează cu un simplu apel e.getTotalRevenue(), pentru care efectul este
hotărât doar de tipul anagajatului. Acesta este un exemplu de single-dispatch.

Tutorialul de double-dispatch oferă mai multe detalii legate de acest mecanism.

Aplicabilitate

Pattern-ul Visitor este util când:

 se doreşte prelucrarea unei structuri complexe, ce cuprinde mai multe obiecte de


tipuri diferite

 se doreşte definirea de operaţii distincte pe aceeaşi structură, pentru a preveni


poluarea interfeţelor claselor implicate, cu multe detalii aparţinând unor algoritmi
diferiţi. În acest fel, se centralizează aspectele legate de acelaşi algoritm într-un
singur loc, dar, în acelaşi timp, se separă detaliile ce ţin de algoritmi diferiţi. Acest
lucru conduce la simplificarea atât a claselor prelucrate, cât şi a vizitatorilor. Orice
date specifice algoritmului rezidă în vizitator.

 clasele ce se doresc prelucrate se modifică rar, în timp ce operaţiile de prelucrare


se definesc des. Dacă însă sunt introduse multe clase visitabile, după crearea
obiectelor Visitor, atunci este necesară modificarea acestora din urmă, pentru
adăugarea de metode visit pentru noile clase.

Avantaje:

 Decuplarea datelor de operațiile aplicate pe acestea

 Ușurează adăugarea unor noi operații/algortimi. Se creează o implementare a unui


obiect de tip Visitor și nu se schimbă nimic în obiecte vizitate.

 Spre deosebire de Iterator poate gestiona elemente de tipuri diferite

 Poate menține informații de stare pe măsură ce vizitează obiectele

Dezavantaje:

 Depinde de stabilitatea ierarhiei de obiecte vizitate. Adăugarea de obiecte vizitabile


rezultă în schimbarea implementării obiectelor Visitor.

o obiecte de noi tipuri adăugate des + multe operații aplicabile = NU folosiți


Visitor

 Expune metode publice care folosesc informații de stare ale obiectelor. Nu se pot
accesa membrii privați ai claselor, necesitatea expunerii acestor informaţii (in forma
publică) ar putea conduce la ruperea încapsulării

Exemple din API-uri

Visitor este de obicei utilizat pentru structuri arborescente de obiecte:

 Parcurgerea arborilor de parsare

o ASTVisitor din Eclipse JDT. Folosind ASTParser se creează arborele de


parsare al codului dat ca intrare, iar ASTVisitor parcurge arborele, oferind
metode (preVisit, postVisit, visit) pentru multe tipuri de noduri
(MethodDeclaration, Assignment, IfStatement etc.)

 Parcurgerea și vizitarea ierarhiei de directoare și fișiere

o Java Nio - FileVisitor

 FileVisitor - interfața cu metode de vizitare

 trebuie apelat Files.walkFileTree transmițându-i ca parametru un


obiect care implementează FileVisitor

 un tutorial

Summary
Supraîncărcarea (overloading) - mai multe metode cu același nume dar cu listă diferită de
argumente

 metoda care va fi executată este stabilită la compilare, pe baza tipului referinței

 metoda supraîncărcată are neapărat o listă diferită de argumente și poate, opțional,


avea:

o alți modificatori de acces

o alt tip de return

o alte excepții

 constructorii pot fi supraîncărcati

 metodele moștenite pot fi supraîncărcate

Suprascrierea (overriding) - redefinirea metodelor moștenite

 metoda care va fi executată este stabilită la runtime, pe baza tipului obiectului

 metoda suprascrisă are același tip de return și semnătură ca metoda inițială

 putem avea un tip de return diferit de cel al metodei inițiale, atâta timp cât este un tip
ce moștenește tipul de return al metodei inițiale

 specificatorul de access al metodei suprascrise nu poate fi mai restrictiv decât cel al


metodei inițiale

 nu poate arunca mai multe excepții sau excepții mai generale, poate însă arunca mai
puține sau mai particulare sau excepții unchecked (de runtime)

 metodele de tip static și final nu pot fi suprascrise

 constructorii nu pot fi suprascriși

Visitor - pattern pt modelarea comportamentului claselor

 util în situații în care:

o avem mai multe obiecte și operații pentru acestea

o dorim schimbarea/adăugarea operațiilor fără a modifica clasele

 indicat de utilizat pentru operații pe colecții și parcurgerea de structuri arborescente

 conceptul de double dispatch


Exerciţii
Task 1 [8p]

Dorim să prelucrăm bucăți de text pe care să le convertim în diferite formate, momentan


dokuwiki și markdown. Pentru un design decuplat între obiectele prelucrate și tipurile de
formate dorite, implementați conversia folosind patternul Visitor.

 Fișierul README din scheletul de cod cuprinde informațiile necesare designului


dorit.

o implementați structura de clase din diagrama din README

o implementați TODO-urile din scheletul de cod

 Pentru simplitatea testării scheletul oferă clasa Test care oferă bucățile de text pe care
să le prelucrați.

o dacă folosiți IntelliJ creați proiect din scheletul de laborator: File → New
Project → select Java → select the skel folder

 În implementare va trebui sa folositi clasa StringBuilder. Aceasta este o clasă


mutabilă (mutable), spre deosebire de String, care e imutabilă (immutable). Vă
recomandăm acest link pentru un exemplu si explicații despre diferențele dintre ele.

 Tips for faster coding:

o atunci cand creati o clasa care implementeaza o interfata sau o clasa cu metode
abstracte, nu scrieti de mana antetul fiecarei metode, ci folositi-va de IDE.

 In Intellij va aparea cu rosu imediat dupa ce scrieti


extends…/implements… Dati alt-enter sau option-enter (pe mac), si vi
se vor genera metodele pe care trebuie sa le implementati, voi
completand apoi continutul lor.

o generati constructorii folosind IDE-ul

Task 2 - Utilizare API implementat folosind Visitor [2p]

Afișați folosind java.nio informații despre fișierele cu extensia “.class” sau “.java” dintr-un
director.

 Implementați un FileVisitor, extinzând SimpleFileVisitor în care suprascrieți metoda


de vizitare a fișierelor

 Un exemplu similar găsiți în acest tutorial

Colecții
Obiective
Pe parcursul laboratoarelor și temelor ați folosit structuri de date oferite de API-ul Java. În
cadrul acestui laborator le vom aprofunda.

 lucrul cu cele trei tipuri principale de colecții din Java: List, Set, Map
 cunoașterea diferențelor dintre implementările colecțiilor (eficiență, sortare, ordonare
etc)
 compararea elementelor unor colecții
 contractul equals-hashcode

Collections Framework
În pachetul java.util (pachet standard din JRE) existǎ o serie de clase pe care le veti găsi
folositoare. Collections Framework este o arhitectură unificată pentru reprezentarea şi
manipularea colecţiilor. Ea conţine:

 interfeţe: permit colecţiilor sǎ fie folosite independent de implementǎrile lor


 implementǎri
 algoritmi metode de prelucrare (căutare, sortare) pe colecţii de obiecte oarecare.
Algoritmii sunt polimorfici: un astfel de algoritm poate fi folosit pe implementări
diferite de colecţii, deoarece le abordeazǎ la nivel de interfaţǎ.

Colecţiile oferǎ implementǎri pentru urmǎtoarele tipuri:

 Set (elemente neduplicate)


 List (o mulțime de elemente)
 Map (perechi cheie-valoare)

Existǎ o interfaţǎ, numitǎ Collection, pe care o implementeazǎ majoritatea claselor ce


desemneazǎ colecţii din java.util. Explicaţii suplimentare gǎsiţi pe Java Tutorials -
Collection

Exemplul de mai jos construieşte o listǎ populatǎ cu nume de studenţi:

Collection names = new ArrayList();


names.add("Andrei");
names.add("Matei");

Parcurgerea colecţiilor
Colecţiile pot fi parcurse (element cu element) folosind:

 iteratori
 o construcţie for specialǎ (cunoscutǎ sub numele de for-each)

Iteratori
Un iterator este un obiect care permite traversarea unei colecţii şi modificarea acesteia (ex:
ştergere de elemente) în mod selectiv. Puteţi obţine un iterator pentru o colecţie, apelând
metoda sa iterator(). Interfaţa Iterator este urmǎtoarea:

public interface Iterator<E> {


boolean hasNext();
E next();
void remove(); // optional
}

Exemplu de folosire a unui iterator:

Collection<Double> col = new ArrayList<Double>();


Iterator<Double> it = col.iterator();
 
while (it.hasNext()) {
Double backup = it.next();
// apelul it.next() trebuie realizat înainte de apelul it.remove()
if (backup < 5.0) {
it.remove();
}
}

Apelul metodei remove() a unui iterator face posibilă eliminarea elementului din colecţie care
a fost întors la ultimul apel al metodei next() din acelaşi iterator. În exemplul anterior, toate
elementele din colecţie mai mici decât 5 for fi şterse la ieşirea din bucla while.

For-each
Aceastǎ construcţie permite (într-o manierǎ expeditivǎ) traversarea unei colecţii. for-each
este foarte similar cu for. Urmǎtorul exemplu parcurge elementele unei colecţii şi le afişeazǎ.

Collection collection = new ArrayList();


for (Object o : collection)
System.out.println(o);

Construcţia for-each se bazeazǎ, în spate, pe un iterator, pe care îl ascunde. Prin urmare nu


putem şterge elemente în timpul iterǎrii. În aceastǎ manierǎ pot fi parcurşi şi vectori oarecare.
De exemplu, collection ar fi putut fi definit ca Object[].

Genericitate
Fie urmǎtoarea porţiune de cod:

c.add("Test");
 
Iterator it = c.iterator();
 
while (it.hasNext()) {
String s = it.next(); // ERROR: next() returns an Object and
it's needed an explicit cast to String
String s = (String)it.next(); // OK
}
Am definit o colecţie c, de tipul ArrayList (pe care îl vom examina într-o secţiune
urmǎtoare). Apoi, am adǎugat în colecţie un element de tipul String. Am realizat o
parcurgere folosind un iterator, şi am încercat obţinerea elementului nostru folosind apelul:
String s = it.next();. Funcţia next însǎ întoarce un obiect de tip Object. Prin urmare
apelul va eşua. Varianta corectǎ este String s = (String)it.next();. Am fi putut
preciza, din start, ce tipuri de date dorim într-o colecţie:

Collection<String> c = new ArrayList<String>();


c.add("Test");
c.add(2); // ERROR!
Iterator<String> it = c.iterator();
 
while (it.hasNext()) {
String s = it.next();
}

Mai multe detalii despre acest subiect găsiți in laboratorul următor: Genericitate

Interfaţa List
O listǎ este o colecţie ordonatǎ. Listele pot conţine elemente duplicate. Pe langǎ operaţiile
moştenite de la Collection, interfaţa List contine operaţii bazate pe pozitie (index), de
exemplu: set, get, add la un index, remove de la un index.

List<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Orange",


"Grape"));
fruits.add("Apple"); // metodă moștenită din Collection
fruits.add(2, "Pear"); // [Apple, Orange, Pear, Grape, Apple]
System.out.println(fruits.get(3)); // Grape
fruits.set(1, "Cherry"); // [Apple, Cherry, Pear, Grape, Apple]
fruits.remove(2);
System.out.println(fruits); // [Apple, Cherry, Grape, Apple]

Alǎturi de List, este definitǎ interfaţa ListIterator, ce extinde interfaţa Iterator cu metode
de parcurgere în ordine inversǎ. List posedǎ douǎ implementǎri standard:

 ArrayList - implementare sub formǎ de vector. Accesul la elemente se face în timp


constant: O(1)
 LinkedList - implementare sub formǎ de listǎ dublu înlǎnţuitǎ. Prin urmare, accesul
la un element nu se face în timp constant, fiind necesarǎ o parcurgere a listei: O(n).

Printre algoritmii implementaţi se numără:

 sort - realizeazǎ sortarea unei liste


 binarySearch - realizaeazǎ o cǎutare binarǎ a unei valori într-o listǎ sortatǎ

În general, algoritmii pe colecţii sunt implementaţi ca metode statice în clasa Collections.

Atenţie: Nu confundaţi interfaţa Collection cu clasa Collections. Spre deosebire de


prima, a doua este o clasǎ ce conţine exclusiv metode statice. Aici sunt implementate diverse
operaţii asupra colecţiilor.
Iatǎ un exemplu de folosire a sortǎrii:

List<Integer> l = new ArrayList<Integer>();


l.add(5);
l.add(7);
l.add(9);
l.add(2);
l.add(4);
 
Collections.sort(l);
System.out.println(l);

Mai multe detalii despre algoritmi pe colecţii gǎsiţi pe Java Tutorials - Algoritmi pe liste

Compararea elementelor
Rularea exemplului de sortare ilustrat mai sus aratǎ cǎ elementele din ArrayList se sorteazǎ
crescator. Ce se întâmplǎ când dorim sǎ realizǎm o sortare particularǎ pentru un tip de date
complex? Spre exemplu, dorim sǎ sortǎm o listǎ ArrayList<Student> dupǎ media anilor. Sǎ
presupunem cǎ Student este o clasǎ ce conţine printre membrii sǎi o variabilǎ ce reţine
media anilor. Acest lucru poate fi realizat folosind interfeţele:

 Comparable
 Comparator

Comparable Comparator
Logica de sortare trebuie sa fie în
Logica de sortare se află într-o clasă
Logica de clasa ale cărei obiecte sunt sortate.
separată. Astfel, putem defini mai
sortare Din acest motiv, această metodă se
multe metode de sortare, bazate pe
numeşte sortare naturală. diverse câmpuri ale obiectelor de sortat.
Clasa ale cărei instanţe se doresc a fi
Clasa ale cărei instanţe se doresc a
sortate nu trebuie să implementeze
fi sortate trebuie sa implementeze
Implementare această interfaţă. Este nevoie de o alta
această interfaţă şi, evident, să
clasă (poate fi şi internă) care să
suprascrie metoda compareTo().
implementeze interfaţa Comparator.
int compareTo(Object o1)
Această metodă compară obiectul int compare(Object o1,Object o2)
curent (this) cu obiectul o1 şi
întoarce un întreg. Valoarea întoarsă Această metodă compară obiectele o1
Metoda de este interpretată astfel: and o2 şi întoarce un întreg. Valoarea
comparare 1. pozitiv – obiectul este mai mare întoarsă este interpretată astfel:
decât o1 1. pozitiv – o2 este mai mare decât o1
2. zero – obiectul este egal cu o1 2. zero – o2 este egal cu o1
3. negativ – obiectul este mai mic 3. negativ – o2 este mai mic decât o1
decât o1
Collections.sort(List,
Collections.sort(List)
Metoda de Comparator)
Aici obiectele sunt sortate pe baza Aici obiectele sunt sortate pe baza
sortare metodei compareTo(). metodei compare() din Comparator.
Pachet Java.lang.Comparable  Java.util.Comparator
Interfaţa Set
Un Set (mulţime) este o colecţie ce nu poate conţine elemente duplicate. Interfaţa Set conţine
doar metodele moştenite din Collection, la care adaugǎ restricţii astfel încât elementele
duplicate sǎ nu poatǎ fi adǎugate. Avem trei implementǎri utile pentru Set:

 HashSet: memoreazǎ elementele sale într-o tabelǎ de dispersie (hash table); este
implementarea cea mai performantǎ, însǎ nu avem garanţii asupra ordinii de
parcurgere. Doi iteratori diferiţi pot parcurge elementele mulţimii în ordine diferitǎ.
 TreeSet: memoreazǎ elementele sale sub formǎ de arbore roşu-negru; elementele sunt
ordonate pe baza valorilor sale. Implementarea este mai lentǎ decat HashSet.
 LinkedHashSet: este implementat ca o tabelǎ de dispersie. Diferenţa faţǎ de HashSet
este cǎ LinkedHashSet menţine o listǎ dublu-înlǎnţuitǎ peste toate elementele sale.
Prin urmare (şi spre deosebire de HashSet), elementele rǎmân în ordinea în care au
fost inserate. O parcurgere a LinkedHashSet va gǎsi elementele mereu în aceastǎ
ordine.

Atenţie: Implementarea HashSet, care se bazeazǎ pe o tabelǎ de dispersie, calculeazǎ codul


de dispersie al elementelor pe baza metodei hashCode, definitǎ în clasa Object. De aceea,
douǎ obiecte egale, conform funcţiei equals, trebuie sǎ întoarcǎ acelaşi rezultat din
hashCode.

Explicaţii suplimentare gǎsiti pe Java Tutorials - Set.

Interfaţa Map
Un Map este un obiect care mapeazǎ chei pe valori. Într-o astfel de structurǎ nu pot exista
chei duplicate. Fiecare cheie este mapatǎ la exact o valoare. Map reprezintǎ o modelare a
conceptului de funcţie: primeşte o entitate ca parametru (cheia), şi întoarce o altǎ entitate
(valoarea). Cele trei implementǎri pentru Map sunt:

 HashMap
 TreeMap
 LinkedHashMap

Particularitǎţile de implementare corespund celor de la Set. Exemplu de folosire:

class Student {
String name;
float avg;
 
public Student(String name, float avg) {
this.name = name;
this.avg = avg;
}
 
public String toString() {
return "[" + name + ", " + avg + "]";
}
}
 
public class Test {
public static void main(String[] args) {
 
Map<String,Student> students = new HashMap<String, Student>();
 
students.put("Matei", new Student("Matei", 4.90F));
students.put("Andrei", new Student("Andrei", 6.80F));
students.put("Mihai", new Student("Mihai", 9.90F));
 
System.out.println(students.get("Mihai"));
 
// adaugăm un element cu aceeași cheie
System.out.println(students.put("Andrei", new Student("", 0.0F)));
// put(...) întoarce elementul vechi
 
// si îl suprascrie
System.out.println(students.get("Andrei"));
 
// remove(...) returnează elementul șters
System.out.println(students.remove("Matei"));
 
// afișăm structura de date
System.out.println(students);
}
}

Interfaţa Map.Entry desemneazǎ o pereche (cheie, valoare) din map. Metodele caracteristice
sunt:

 getKey: întoarce cheia


 getValue: întoarce valoarea
 setValue: permite stabilirea valorii asociatǎ cu aceastǎ cheie

O iterare obişnuitǎ pe un map se va face în felul urmǎtor:

for (Map.Entry<String, Student> entry : students.entrySet())


System.out.println(entry.getKey() + " has the following average grade:
" + entry.getValue().getAverage());

În bucla for-each de mai sus se ascunde, de fapt, iteratorul mulţimii de perechi, întoarse de
entrySet. Explicaţii suplimentare gǎsiţi pe Java Tutorials - Map.

Alte interfeţe
Queue defineşte operaţii specifice pentru cozi:

 inserţia unui element


 ştergerea unui element
 operaţii de “inspecţie” a cozii

Implementǎri utilizate frecvente pentru Queue:

 LinkedList: pe lângǎ List, LinkedList implementeazǎ şi Queue


 PriorityQueue;
Explicaţii suplimentare gǎsiţi pe Java Tutorials - Queue

TL;DR
 Pachetul java.util oferă implementări ale unor stucturi de date și algoritmi pentru
manipularea lor: ierarhiile Collection și Map și clasa cu metode statice Collections.
 Parcurgerea colecţiilor se face în două moduri:
o folosind iteratori (obiecte ce permit traversarea unei colecţii şi modificarea
acesteia)
o folosind construcţia speciala for each (care nu permite modificarea colecţiei in
timpul parcurgerii sale)
 Interfaţa List - colecţie ordonată ce poate conţine elemente duplicate.
 Interfaţa Set - colecţie ce nu poate conţine elemente duplicate. Există trei
implementǎri utile pentru Set: HashSet (neordonat, nesortat), TreeSet (set sortat) și
LinkedHashSet (set ordonat)
 Interfaţa Map - colecţie care mapează chei pe valori. Într-o astfel de structurǎ nu pot
exista chei duplicate. Cele trei implementǎri pentru Map sunt HashMap (neordonat,
nesortat), TreeMap (map sortat) și LinkedHashMap (map ordonat)
 Contractul equals - hashcode: dacă obj1 equals obj2 atunci hashcode obj1 ==
hascode obj2. Dacă implementați equals implementați și hashcode daca doriți să
folosiți acele obiecte în colecții bazate pe hashuri (e.g. HashMap, HashSet).

Exerciţii
1. (1p) Instanţiati o colecţie care sǎ nu permitǎ introducerea elementelor duplicate,
folosind o implementare corespunzǎtoare din bibliotecă. La introducerea unui element
existent, semnalaţi eroare. Colecţia va reţine String-uri şi va fi parametrizatǎ.
2. (2p) Creaţi o clasǎ Student.
1. Adǎugaţi urmǎtorii membri:
 câmpurile nume (de tip String) şi medie (de tip float)
 un constructor care îi iniţializeazǎ
 metoda toString.
2. Folosiţi codul de la exerciţiul anterior şi modificaţi-l astfel încât colecţia
aleasǎ de voi sǎ reţinǎ obiecte de tip Student. Testaţi prin adǎugare de
elemente duplicate, având aceleaşi valori pentru toate câmpurile, instanţiindu-
le, de fiecare datǎ, cu new. Ce observaţi?
3. Prelucraţi implementarea de mai sus astfel încât colecţia sǎ reprezinte o tabelǎ
de dispersie, care calculează codul de dispersie al elementelor dupǎ un criteriu
ales de voi (puteţi suprascrie funcţia hashCode).
 În Student suprascrieți metoda equals astfel încât să se ţină cont de
câmpurile clasei, şi încercaţi din nou. Ce observaţi?

1. Hint: Set.add, Object.equals, Object.hashCode


o (2p) Plecând de la implementarea exerciţiului anterior, realizaţi urmǎtoarele
modificǎri:
1. Supraîncǎrcaţi, în clasa Student, metoda equals, cu o variantǎ care primeşte
un parametru Student, şi care întoarce întotdeauna false.
2. Testaţi comportamentul prin crearea unei colecţii ce conţine instanţe de
Student şi iteraţi prin această colecţie, afişând la fiecare pas
element.equals(element) şi ((Object)element).equals(element)
(unde element este numele de variabilă ales pentru fiecare element al
colecţiei). Cum explicaţi comportamentul observat? Dacă folosiţi un iterator,
acesta va fi şi el parametrizat.
o (3p) Creați clasa Gradebook, de tip Map, pentru reţinerea studenţilor dupǎ
medie: cheile sunt mediile și valorile sunt liste de studenți. Gradebook va
menţine cheile ordonate descrescǎtor. Extindeţi o implementare potrivitǎ a
interfeţei Map, care sǎ permitǎ acest lucru.

1. Caracteristicile clasei definite sunt:

 Cheile pot avea valori de la 0 la 10 (corespunzǎtoare mediilor


posibile). Verificați acest lucru la adăugare.
 Valoarea asociată fiecǎrei chei va fi o listǎ (List) care va reţine toţi
studenţii cu media rotunjitǎ egalǎ cu cheia. Considerǎm cǎ un student
are media rotunjitǎ 8 dacǎ media sa este în intervalul [7.50, 8.49].
2. Implementați un Comparator pentru stabilirea ordinii cheilor. Gradebook va
primi un parametru de tip Comparator în constructor și îl va da mai departe
constructorului clasei moștenite.
3. Definiţi în clasǎ metoda add(Student), ce va adǎuga un student în lista
corespunzǎtoare mediei lui. Dacǎ, în prealabil, nu mai existǎ niciun student cu
media respectivǎ (rotunjitǎ), atunci lista va fi creatǎ la cerere.
4. Testați clasa:
 instanțiați un obiect Gradebook și adăugați in el câţiva studenţi.
 iteraţi pe Gradebook şi sortaţi alfabetic fiecare listǎ de studenţi pentru
fiecare notă. Pentru a sorta, se va folosi metoda Collections.sort, iar
clasa Student va implementa o interfață care specifică modul în care
sunt comparate elementele.
 clasa Student va implementa interfaţa Comparable,
suprascriind metoda compareTo.
2. (2p) Creaţi o clasǎ care moşteneşte HashSet<Integer>.

1. Definiţi în aceastǎ clasǎ o variabilǎ membru care reţine numǎrul total de


elemente adǎugate. Pentru a contoriza acest lucru, suprascrieți metodele add şi
addAll. Pentru adǎugarea efectivǎ a elementelor, folosiţi implementǎrile din
clasa pǎrinte (HashSet).
2. Testaţi, folosind atât add cât şi addAll. Ce observaţi? Corectaţi dacǎ este
cazul.
3. Modificaţi implementarea astfel încât clasa voastrǎ sǎ moşteneascǎ
LinkedList<Integer>. Ce observaţi? Ce concluzii trageţi?
4. Hint: Collection.add, Collection.addAll.
5. Hint: implementarea addAll din sursele pentru HashSet şi LinkedList.
Genericitate
Obiective
Scopul acestui laborator este prezentarea conceptului de genericitate și modalitățile de creare
și folosire a claselor, metodelor și interfețelor generice în Java.

Aspectele urmărite sunt:

 prezentarea structurilor generice simple


 conceptele de wildcard și bounded wildcards
 utilitatea genericității în design-ul unui sistem

Introducere
Să urmărim exemplul de mai jos:

List myIntList = new LinkedList();


myIntList.add(new Integer(0));
Integer x = (Integer) myIntList.iterator().next();

Se observă necesitatea operației de cast pentru a identifica corect variabila obținută din listă.
Această situație are mai multe dezavantaje:

 Este îngreunată citirea codului


 Apare posibilitatea unor erori la execuție, în momentul în care în listă se introduce un
obiect care nu este de tipul Integer.

Genericitatea intervine tocmai pentru a elimina aceste probleme. Concret, să urmărim


secvența de cod de mai jos:

List<Integer> myIntList = new LinkedList<Integer>();


myIntList.add(new Integer(0));
Integer x = myIntList.iterator().next();

În această situație, lista nu mai conține obiecte oarecare, ci poate conține doar obiecte de tipul
Integer. În plus, observăm că a dispărut și cast-ul. De această dată, verificarea tipurilor
este efectuată de compilator, ceea ce elimină potențialele erori de execuție cauzate de cast-
uri incorecte. La modul general, beneficiile dobândite prin utilizarea genericității constau în:

 îmbunătățirea lizibilității codului


 creșterea gradului de robusteţe

Definirea unor structuri generice simple


Să urmărim câteva elemente din definiția oferită de Java pentru tipurile List și Iterator.

public interface List<E> {


void add(E x);
Iterator<E> iterator();
}
 
public interface Iterator<E> {
E next();
boolean hasNext();
void remove();
}

Sintaxa <E> (poate fi folosită orice literă) este folosită pentru a defini tipuri formale în cadrul
interfețelor. Aceste tipuri pot fi folosite în mod asemănător cu tipurile uzuale, cu anumite
restricții totuși. În momentul în care invocăm o structură generică ele vor fi înlocuite cu
tipurile efective utilizate în invocare. Concret, fie un apel de forma:

ArrayList<Integer> myList = new ArrayList<Integer>();


Iterator<Integer> it = myList.iterator();

În această situație, tipul formal E a fost înlocuit (la compilare) cu tipul efectiv Integer.

Genericitatea în subtipuri
Să considerăm următoarea situaţie:

List<String> stringList = new ArrayList<String>(); // 1


List<Object> objectList = stringList; // 2

Operația 1 este evident corectă, însă este corectă și operația 2? Presupunând că ar fi, am putea
introduce în objectList orice fel de obiect, nu doar obiecte de tip String, fapt ce ar
conduce la potențiale erori de execuție, astfel:

objectList.add(new Object());
String s = stringList.get(0); // Aceasta operaţie ar fi ilegală

Din acest motiv, operația 2 nu va fi permisă de către compilator!

Dacă ChildType este un subtip (clasă descendentă sau subinterfață) al lui ParentType, atunci
o structură generică GenericStructure<ChildType> nu este un subtip al lui
GenericStructure<ParentType>. Atenție la acest concept, întrucât el nu este intuitiv!

Wildcards
Wildcard-urile sunt utilizate atunci când dorim să întrebuințăm o structură generică drept
parametru într-o funcție și nu dorim să limităm tipul de date din colecția respectivă.

void printCollection(Collection<Object> c) {
for (Object e : c)
System.out.println(e);
}

De exemplu, o situație precum cea de mai sus ne-ar restricționa să folosim la apelul funcției
doar o colecţie cu elemente de tip Object, care nu poate fi convertită la o colecție de un alt
tip, după cum am văzut mai sus. Această restricție este eliminată de folosirea wildcard-urilor,
după cum se poate vedea:

void printCollection(Collection<?> c) {
for (Object e : c)
System.out.println(e);
}

O limitare care intervine însă este că nu putem adǎuga elemente arbitrare într-o colecție cu
wildcard-uri:

Collection<?> c = new ArrayList<String>(); // Operaţie permisă


c.add(new Object()); // Eroare la compilare

Eroarea apare deoarece nu putem adăuga într-o colecţie generică decât elemente de un
anumit tip, iar wildcard-ul nu indică un tip anume.

Aceasta înseamnă că nu putem adăuga nici măcar elemente de tip String. Singurul element
care poate fi adăugat este însă null, întrucât acesta este membru al oricărui tip referință. Pe
de altă parte, operațiile de tip getter sunt posibile, întrucât rezultatul acestora poate fi mereu
interpretat drept Object:
List<?> someList = new ArrayList<String>();
((ArrayList<String>)someList).add("Some String");
Object item = someList.get(0);

Bounded Wildcards
În anumite situații, faptul că un wildcard poate fi înlocuit cu orice tip se poate dovedi un
inconvenient. Mecanismul bazat pe Bounded Wildcards permite introducerea unor restricţii
asupra tipurilor ce pot înlocui un wildcard, obligându-le să se afle într-o relație ierarhică (de
descendență) față de un tip fix specificat.

Exemplificăm acest mecanism:

class Pizza {
protected String name = "Pizza";
 
public String getName() {
return name;
}
}
 
class HamPizza extends Pizza {
public HamPizza() {
name = "HamPizza";
}
}
 
class CheesePizza extends Pizza {
public CheesePizza() {
name = "CheesePizza";
}
}
 
class MyApplication {
// Aici folosim "bounded wildcards"
public static void listPizza(List<? extends Pizza> pizzaList) {
for(Pizza item : pizzaList)
System.out.println(item.getName());
}
 
public static void main(String[] args) {
List<Pizza> pList = new ArrayList<Pizza>();
 
pList.add(new HamPizza());
pList.add(new CheesePizza());
pList.add(new Pizza());
 
MyApplication.listPizza(pList);
// Se va afişa: "HamPizza", "CheesePizza", "Pizza"
}
}

Sintaxa List<? extends Pizza> (Upper Bounded Wildcards) impune ca tipul elementelor
listei să fie Pizza sau o subclasă a acesteia. Astfel, pList ar fi putut avea, la fel de bine, tipul
List<HamPizza> sau List<CheesePizza>. În mod similar, putem imprima constrângerea ca
tipul elementelor listei să fie Pizza sau o superclasă a acesteia, utilizând sintaxa List<?
super Pizza> (Lower Bounded Wildcards).

Utilizarea bounded wildcards se manifestă în următoarele 2 situații :

 lower bounded wildcards se folosesc atunci când vrem să modificăm o colecție


generică
 upper bounded wildcards se folosesc atunci când vrem să parcurgem fără să
modificăm o colecție generică

Type Erasure
Type Erasure este un mecanism prin care compilatorul Java înlocuieşte la compile time
parametrii de genericitate ai unei clase generice cu prima lor apariţie (ţinând cont de restricţii
în cazul Bounded Wildcards) sau cu Object dacǎ parametrii nu apar (Raw Type). De
exemplu, următorul cod:

List<String> list = new ArrayList<String>();


list.add("foo");
String x = list.get(0);

se va transforma dupǎ acest pas al compilării în:

List list = new ArrayList();


list.add("foo");
String x = (String) list.get(0);

Sǎ urmǎrim urmǎtorul fragment de cod:

class GenericClass <T> {


void genericFunction(List<String> stringList) {
stringList.add("foo");
}
// {...}
public static void main(String[] args) {
GenericClass genericClass = new GenericClass();
List<Integer> integerList = new ArrayList<Integer>();
 
integerList.add(100);
genericClass.genericFunction(integerList);
 
System.out.println(integerList.get(0)); // 100
System.out.println(integerList.get(1)); // foo
}
}

Observăm că în main se instanţiază clasa GenericClass cu Raw Type, apoi se trimite ca


argument metodei genericFunction un ArrayList<Integer>. Codul nu va genera erori şi
va afişa 100, apoi foo. Acest lucru se întâmplă tot din cauza mecanismului de Type Erasure.
Să urmărim ce se întâmplă: la instanţierea clasei GenericClass nu se specifică tipul generic
al acesteia iar compilatorul va înlocui în corpul clasei peste tot T cu Object şi va dezactiva
verificarea de tip. Așadar, obiectul genericClass va aparţine unei clase de forma:

class GenericClass {
void genericFunction(List stringList) {
stringList.add("foo");
}
// {...}
}
Modelul de mai sus este bad practice tocmai pentru că are un comportament nedeterminat și
poate conduce la erori. De aceea nu e recomandat să folosiți Raw Types, ci să specificați
întotdeauna tipul obiectelor în cazul instanțierii claselor generice!

Metode generice
Java ne oferă posibilitatea scrierii de metode generice (deci având un tip-parametru) pentru a
facilita prelucrarea unor structuri generice. Să exemplificăm acest fapt. Observăm în
continuare 2 căi de implementare ale unei metode ce copiază elementele unui vector intrinsec
într-o colecție:

// Metoda corectă
static <T> void correctCopy(T[] a, Collection<T> c) {
for (T o : a)
c.add(o); // Operaţia va fi permisă
}
 
// Metoda incorectă
static void incorrectCopy(Object[] a, Collection<?> c) {
for (Object o : a)
c.add(o); // Operatie incorectă, semnalată ca eroare de către
compilator
}

Trebuie remarcat faptul că correctCopy() este o metodă validă, care se execută corect, însă
incorrectCopy() nu este, din cauza limitării pe care o cunoaştem deja, referitoare la
adăugarea elementelor într-o colecție generică cu tip specificat. Putem remarca, de asemenea,
că, și în acest caz, putem folosi wildcards sau bounded wildcards. Astfel, următoarele
declaraţii de metode sunt corecte:
// Copiază elementele dintr-o listă în altă listă
public static <T> void copy(List<T> dest, List<? extends T> src) { ... }
 
// Adaugă elemente dintr-o colecţie în alta, cu restricţionarea tipului
generic
public <T extends E> boolean addAll(Collection<T> c);

Exerciții
1. (6p) Implementați o tabelă de dispersie generică care va permite să stocaţi perechi de
tip cheie-valoare.
o (2p) Scrieţi antetul clasei MyHashMap şi prototipul funcţiilor put şi get. Aveţi
grijă la parametrizarea tipurilor.
o (2p) Implementaţi metoda put. Vă puteți crea o clasă internă cu rol de entry şi
puteţi stoca entry-urile într-o colecţie generică existentă în Java.
o (1p) Implementaţi metoda get.
o (1p) Testaţi implementarea voastră folosind o clasă definită de voi, care
suprascrie metoda hashCode din Object.
o (Bonus 2p) Implementați tabela de dispersie ca iterabilă, compatibilă cu
syntactic-sugar-ul for-each
 Trebuie să implementați interfața Iterable. Atenție, și ea este
generică.
 Creați-vă iteratorul, parametrizat, ca o clasă internă care să rețină
datele necesare.
 Nu este necesar să implementați metoda remove din Iterator.
 Afișați-vă rezultatele folosind for-each pe tabela de dispersie.
2. (4p) Să considerăm interfața Sumabil, ce conține metoda void addValue(Sumabil
value). Această metodă adună la valoarea curentă (stocată în instanța ce apelează
metoda) o altă valoare, aflată într-o instanță cu același tip. Pornind de la această
interfață, va trebui să:
o Definiți clasele MyVector3 și MyMatrix (ce reprezintă un vector cu 3
coordonate și o matrice de dimensiune 4 x 4), ce implementează Sumabil
o Scrieți o metodă generică ce primește o colecție generică cu elemente de tipul
Sumabil și returnează suma tuturor elementelor din colecție. Trebuie să
utilizați bounded types. Care trebuie să fie, deci, antetul metodei?
Excepții
Obiective
 înţelegerea conceptului de excepţie şi utilizarea corectă a mecanismelor de generare
şi tratare a excepţiilor puse la dispoziţie de limbajul / maşina virtuală Java

Introducere
În esenţă, o excepţie este un eveniment care se produce în timpul execuţiei unui program şi
care perturbă fluxul normal al instrucţiunilor acestuia.

De exemplu, în cadrul unui program care copiază un fişier, astfel de evenimente excepţionale
pot fi:

 absenţa fişierului pe care vrem să-l copiem

 imposibilitatea de a-l citi din cauza permisiunilor insuficiente sau din cauza unei zone
invalide de pe hard-disk

 probleme cauzate de accesul concurent la fişier

Utilitatea conceptului de excepţie

O abordare foarte des intâlnită, ce precedă apariţia conceptului de excepţie, este întoarcerea
unor valori speciale din funcţii care să desemneze situaţia apărută. De exemplu, în C, funcţia
fopen întoarce NULL dacă deschiderea fişierului a eşuat. Această abordare are două
dezavantaje principale:

 câteodată, toate valorile tipului de retur ale funcţiei pot constitui rezultate valide. De
exemplu, dacă definim o funcţie care întoarce succesorul unui numar întreg, nu putem
întoarce o valoare specială în cazul în care se depăşeşte valoarea maximă
reprezentabilă (Integer.MAX_VALUE). O valoare specială, să zicem -1, ar putea fi
interpretată ca numărul întreg -1.

 nu se poate separa secvenţa de instrucţiuni corespunzătoare execuţiei normale a


programului de secvenţele care trateaza erorile. Firesc ar fi ca fiecare apel de funcţie
să fie urmat de verificarea rezultatului întors, pentru tratarea corespunzătoare a
posibilelor erori. Această modalitate poate conduce la un cod foarte imbricat şi greu
de citit, de forma:

int openResult = open();


 
if (openResult == FILE_NOT_FOUND) {
// handle error
} else if (openResult == INUFFICIENT_PERMISSIONS) {
// handle error
} else {// SUCCESS
int readResult = read();
if (readResult == DISK_ERROR) {
// handle error
} else {
// SUCCESS
...
}
}

Mecanismul bazat pe excepţii înlătură ambele neajunsuri menţionate mai sus. Codul ar arăta
aşa:

try {
open();
read();
...
} catch (FILE_NOT_FOUND) {
// handle error
} catch (INUFFICIENT_PERMISSIONS) {
// handle error
} catch (DISK_ERROR) {
// handle error
}

Se observă includerea instrucţiunilor ce aparţin fluxului normal de execuţie într-un bloc try şi
precizarea condiţiilor excepţionale posibile la sfârşit, în câte un bloc catch. Logica este
următoarea: se execută instrucţiune cu instrucţiune secvenţa din blocul try şi, la apariţia unei
situaţii excepţionale semnalate de o instrucţiune, se abandonează restul instrucţiunilor
rămase neexecutate şi se sare direct la blocul catch corespunzător.

Excepţii în Java

Când o eroare se produce într-o funcţie, aceasta creează un obiect excepţie şi îl pasează către
runtime system. Un astfel de obiect conţine informaţii despre situaţia apărută:

 tipul de excepţie

 stiva de apeluri (stack trace): punctul din program unde a intervenit excepţia,
reprezentat sub forma lanţului de metode (obţinut prin invocarea succesivă a
metodelor din alte metode) în care programul se află în acel moment

Pasarea menţionată mai sus poartă numele de aruncarea (throwing) unei excepţii.

Aruncarea excepţiilor

Exemplu de aruncare a unei excepţii:

List<String> l = getArrayListObject();
if (null == l)
throw new Exception("The list is empty");

În acest exemplu, încercăm să obţinem un obiect de tip ArrayList; dacă funcţia


getArrayListObject întoarce null, aruncăm o excepţie.
Pe exemplul de mai sus putem face următoarele observaţii:

 un obiect-excepţie este un obiect ca oricare altul, şi se instanţiază la fel (folosind new)

 aruncarea excepţiei se face folosind cuvântul cheie throw

 există clasa Exception care desemnează comportamentul specific pentru excepţii.

În realitate, clasa Exception este părintele majorităţii claselor excepţie din Java. Enumerăm
câteva excepţii standard:

 IndexOutOfBoundsException: este aruncată când un index asociat unei liste sau unui
vector depăşeşte dimensiunea colecţiei respective.

 NullPointerException: este aruncată când se accesează un obiect neinstanţiat (null).

 NoSuchElementException: este aruncată când se apelează next pe un Iterator care


nu mai conţine un element următor.

În momentul în care se instanţiază un obiect-excepţie, în acesta se reţine întregul lanţ de


apeluri de funcţii prin care s-a ajuns la instrucţiunea curentă. Această succesiune se numeşte
stack trace şi se poate afişa prin apelul e.printStackTrace(), unde e este obiectul excepţie.

Prinderea excepţiilor

Când o excepţie a fost aruncată, runtime system încearcă să o trateze (prindă). Tratarea
unei excepţii este făcută de o porţiune de cod specială.

 Cum definim o astfel de porţiune de cod specială?

 Cum specificăm faptul că o porţiune de cod specială tratează o anumită excepţie?

Să observăm următorul exemplu:

public void f() throws Exception {


List<String> l = null;
 
if (null == l)
throw new Exception();
}
 
public void catchFunction() {
try {
f();
} catch (Exception e) {
System.out.println("Exception found!");
}
}

Se observă că dacă o funcţie aruncă o excepţie şi nu o prinde trebuie, în general, să adauge


clauza throws în antet.
Funcţia f va arunca întotdeauna o excepţie (din cauza că l este mereu null). Observaţi cu
atenţie funcţia catchFunction:

 în interiorul său a fost definit un bloc try, în interiorul căruia se apelează f. De


obicei, pentru a prinde o excepţie, trebuie să specificăm o zonă în care aşteptăm ca
excepţia să se producă (guarded region). Această zonă este introdusă prin try.

 în continuare, avem blocul catch (Exception e). La producerea excepţiei, blocul


catch corespunzător va fi executat. În cazul nostru se va afişa mesajul “S-a generat
o excepţie”.

Observaţi un alt exemplu:

public void f() throws MyException, WeirdException {


List<String> l = null;
 
if (null == l)
throw new MyException();
 
throw new WeirdException("This exception never gets thrown");
}
 
public void catchFunction() {
try {
f();
} catch (MyException e) {
System.out.println("Null Pointer Exception found!");
} catch (WeirdException e) {
System.out.println("WeirdException found!");
}
}

În acest exemplu funcţia f a fost modificată astfel încât să arunce MyException. Observaţi
faptul că în catchFunction avem două blocuri catch. Cum excepţia aruncată de f este de tip
MyException, numai primul bloc catch se va executa.

Prin urmare:

 putem specifica porţiuni de cod pentru tratarea excepţiilor folosind blocurile try şi
catch

 putem defini mai multe blocuri catch pentru a implementa o tratare preferenţială a
excepţiilor, în funcţie de tipul acestora

Nivelul la care o excepţie este tratată depinde de logica aplicaţiei. Acesta nu trebuie să fie neaparat
nivelul imediat următor ce invocă secţiunea generatoare de excepţii. Desigur, propagarea de-a
lungul mai multor nivele (metode) presupune utilizarea clauzei throws.

Dacă o excepţie nu este tratată nici în main, aceasta va conduce la încheierea execuţiei
programului!
Blocuri try-catch imbricate

În general, vom dispune în acelaşi bloc try-catch instrucţiunile care pot fi privite ca
înfăptuind un acelaşi scop. Astfel, dacă o operaţie din secvenţa esuează, se renunţă la
instrucţiunile rămase şi se sare la un bloc catch.

Putem specifica operaţii opţionale, al căror eşec să nu influenţeze întreaga secvenţă. Pentru
aceasta folosim blocuri try-catch imbricate:

try {
op1();
 
try {
op2();
op3();
} catch (Exception e) { ... }
 
op4();
op5();
} catch (Exception e) { ... }

Dacă apelul op2 eşuează, se renunţă la apelul op3, se execută blocul catch interior, după care
se continuă cu apelul op4.

Blocul finally

Presupunem că în secvenţa de mai sus, care deschide şi citeşte un fişier, avem nevoie să
închidem fişierul deschis, atât în cazul normal, cât şi în eventualitatea apariţiei unei erori. În
aceste condiţii se poate ataşa un bloc finally după ultimul bloc catch, care se va executa în
ambele cazuri menţionate.

Secvenţa de cod următoare conţine o structură try-catch-finally:

try {
open();
read();
...
} catch (FILE_NOT_FOUND) {
// handle error
} catch (INUFFICIENT_PERMISSIONS) {
// handle error
} catch (DISK_ERROR) {
// handle error
} finally {
// close file
}

Blocul finally se dovedeşte foarte util când în blocurile try-catch se găsesc instrucţiuni
return. El se va executa şi în acest caz, exact înainte de execuţia instrucţiunii return, aceasta
fiind executată ulterior.
Tipuri de excepţii

Nu toate excepţiile trebuie prinse cu try-catch. Pentru a înțelege de ce, să analizăm


clasificarea excepţiilor:

Checked exceptions, ce corespund clasei Exception:

 Acestea sunt excepţii pe care o aplicaţie bine scrisă ar trebui să le prindă, şi să


permită continuarea rulării programului.

 Să luăm ca exemplu un program care cere utilizatorului un nume de fişier (pentru a-l
deschide). În mod normal, utilizatorul va introduce un nume de fişier care există şi
care poate fi deschis. Există insă posibilitatea ca utilizatorul să greşească, caz în care
se va arunca o excepţie FileNotFoundException.

 Un program bine scris va prinde această excepţie, va afişa utilizatorului un mesaj de


eroare, şi îi va permite acestuia (eventual) să reintroducă un nou nume de fişier.

Errors, ce corespund clasei Error:

 Acestea definesc situaţii excepţionale declanşate de factori externi aplicaţiei, pe care


aceasta nu le poate anticipa şi nu-şi poate reveni, dacă se produc.

 Spre exemplu, tentativa de a citi un fişier care nu poate fi deschis din cauza unei
defecţiuni hardware (sau eroare OS), va arunca IOError.
 Aplicaţia poate încerca să prindă această excepţie, pentru a anunţa utilizatorul despre
problema apărută; după această însă, programul va eşua (afişând eventual stack
trace).

Runtime Exceptions, ce corespund clasei RuntimeException:

 Ca şi erorile, acestea sunt condiţii excepţionale, însă spre deosebire de erori, ele sunt
declanşate de factori interni aplicaţiei. Aplicaţia nu poate anticipa, şi nu îşi poate
reveni dacă acestea sunt aruncate.

 Runtime exceptions sunt produse de diverse bug-uri de programare (erori de logică


în aplicaţie, folosire necorespunzătoare a unui API, etc).

 Spre exemplu, a realiza apeluri de metode sau membri pe un obiect null va produce
NullPointerException. Fireşte, putem prinde excepţia. Mai natural însă ar fi să
eliminăm din program un astfel de bug care ar produce excepţia.

Excepţiile checked sunt cele prinse de blocurile try-catch. Toate excepţiile sunt checked cu
excepţia celor de tip Error, RuntimeException şi subclasele acestora, adica cele de tip unchecked.

Excepţiile error nu trebuie (în mod obligatoriu) prinse folosind try-catch. Opţional,
programatorul poate alege să le prindă.

Excepţiile runtime nu trebuie (în mod obligatoriu) prinse folosind try-catch. Ele sunt de tip
RuntimeException. Aţi întâlnit deja exemple de excepţii runtime, în urma diferitelor
neatenţii de programare: NullPointerException, ArrayIndexOutOfBoundsException, etc.

Putem arunca RuntimeException fără să o menţionăm în clauza throws din antet:

public void f(Object o) {


if (o == null)
throw new NullPointerException("o is null");
}
Definirea de excepţii noi

Când aveţi o situaţie în care alegerea unei excepţii (de aruncat) nu este evidentă, puteţi opta
pentru a scrie propria voastră excepţie, care să extindă Exception, RuntimeException sau
Error.

Exemplu:

class TemperatureException extends Exception {}


 
class TooColdException extends TemperatureException {}
 
class TooHotException extends TemperatureException {}

În aceste condiţii, trebuie acordată atenţie ordinii în care se vor defini blocurile catch.
Acestea trebuie precizate de la clasa excepţie cea mai particulară, până la cea mai generală
(în sensul moştenirii). De exemplu, pentru a întrebuinţa excepţiile de mai sus, blocul try-
catch ar trebui să arate ca mai jos:
try {
...
} catch (TooColdException e) {
...
} catch (TemperatureException e) {
...
} catch (Exception e) {
...
}

Afirmaţia de mai sus este motivată de faptul că întotdeauna se alege primul bloc catch care
se potriveşte cu tipul excepţiei apărute. Un bloc catch referitor la o clasă excepţie părinte, ca
TemperatureException prinde şi excepţii de tipul claselor copil, ca TooColdException.
Poziţionarea unui bloc mai general înaintea unuia mai particular ar conduce la ignorarea
blocului particular.

Din Java 7 se pot prinde mai multe excepţii în acelaşi catch. Sintaxa este:
try {
...
} catch(IOException | FileNotFoundException ex) {
...
}
Excepţiile în contextul moştenirii

Metodele suprascrise (overriden) pot arunca numai excepţiile specificate de metoda din clasa
de bază sau excepţii derivate din acestea.

Chain-of-responsibility Pattern
În proiectarea orientată pe obiect, pattern-ul “Chain-of-responsibility” (lanț de
responsabilitate) este un model de design constând dintr-o sursă de obiecte de comandă și o
serie de obiecte de procesare. Fiecare obiect de procesare conține logică care definește
tipurile de obiecte de comandă pe care le poate gestiona; restul sunt transferate către
următorul obiect de procesare din lanț. De asemenea, există un mecanism pentru adăugarea
de noi obiecte de procesare la sfârșitul acestui lanț. Astfel, lanțul de responsabilitate este o
versiune orientată pe obiecte a if … else if … else if …… else … endif, cu avantajul
că blocurile condiție-acțiune pot fi dinamic rearanjate și reconfigurate la timpul de execuție.
Într-o variantă a modelului standard al lanțului de responsabilitate, un handler poate acționa
ca un dispatcher, capabil să trimită comenzi în diverse direcții, formând un tree de
responsabilități. În unele cazuri, acest lucru poate apărea recursiv, cu procesarea obiectelor
care apelează obiecte de procesare de nivel superior cu comenzi care încearcă să rezolve o
parte mai mică a problemei; în acest caz, recurența continuă până când comanda este
procesată, sau întregul arbore a fost explorat. Un interpretor XML ar putea funcționa în acest
mod.

Modelul lanțului de responsabilitate este aproape identic cu modelul decoratorului, diferența


fiind că pentru decorator, toate clasele se ocupă de cerere, iar pentru lanțul de
responsabilitate, exact una dintre clasele din lanț se ocupă de cerere.

Exerciţii
1. (2p) Scrieţi o metodă (scurtă) care să genereze OutOfMemoryError şi o alta care să
genereze StackOverflowError. Verificaţi posibilitatea de a continua rularea după
interceptarea acestei erori. Comparaţi răspunsul cu posibilitatea de a realiza acelaşi
lucru într-un limbaj compilat, ce rulează direct pe platforma gazdă (ca C).

2. (2p) Definiţi o clasă care să implementeze operaţii pe numere întregi. Operaţiile vor
arunca excepţii. Scrieţi clasa Calculator, ce conţine trei metode:

o add: primeşte doi întregi şi întoarce un întreg

o divide: primeşte doi întregi şi întoarce un întreg

o average: primeşte o colecţie ce conţine obiecte Integer, şi întoarce media


acestora ca un numar de tip întreg. Pentru calculul mediei, sunt folosite
operaţiile add şi divide.

o Definiţi următoarele excepţii şi îmbogăţiţi corespunzător definiţia metodei


add:

o OverflowException: este aruncată dacă suma celor doua numere depăşeşte


Integer.MAX_VALUE

o UnderflowException: este aruncată dacă suma celor doua numere este mai
mică decat Integer.MIN_VALUE

o Care este alegerea firească: excepţii checked sau unchecked? De ce?


Consideraţi că, pentru un utilizator care doreşte efectuarea de operaţii
aritmetice, singurul mecanism disponibil este cel oferit de clasa Calculator.

o Evidenţiaţi prin teste toate cazurile posibile care generează excepţii.

3. (2p) Demonstraţi într-un program execuţia blocului finally chiar şi în cazul unui
return din metoda.
4. (4p) Dorim să implementăm un Logger pe baza pattern-ului Chain-of-responsibility,
definit mai sus, pe care îl vom folosi să păstram un jurnal de evenimente a unui
program (vezi adaptarea în Referințe):

o (1p) Creați enumerația LogLevel, ce va acționa ca un bitwise flag, care va


conține valorile - None, Info, Debug, Warning, Error,
FunctionalMessage, FunctionalError. Această enumerație va expune și o
metodă statică All() care va întoarce o colecție de EnumSet<LogLevel> în
care vor fi toate valorile de mai sus. Exemplu practic de folosire.

o (1p) Creați o clasă abstractă LoggerBase:

1. va primi pe constructor un obiect de tip EnumSet<LogLevel> care va


defini pentru ce LogLevel va afisa mesajul

2. va expune o metodă publică setNext ce va primi un LoggerBase, va


întoarce LoggerBase și va seta următorul delegat din lista de
responsabilitate

3. va defini o metodă abstractă protected writeMessage ce va primi


mesajul care trebuie afișat

4. va expune o metodă publică message ce va primi mesajul care trebuie


afișat și o severitate de tip LogLevel. Dacă instanța de logger trebuie
să afișeze mesajul pe baza colecției primite pe constructor, atunci se va
apela metoda writeMessage apoi se va pasa mesajul și severitatea
către următorul delegat din lista de responsabilitate (setat prin apelul
setNext)

o (2p) Definiți clasele de mai jos care vor extinde LoggerBase și implementa
metoda writeMessage:

1. ConsoleLogger - care va scrie toate tipurile de LogLevel și va prefixa


mesajele cu [Console]

2. EmailLogger - care va scrie doar tipurile FunctionalMessage și


FunctionalError și va prefixa mesajele cu [Email]

3. FileLogger - care va scrie doar tipurile Warning și Error și va prefixa


mesajele cu [File]

4. instanțiați obiectele în clasa Test și explicați de ce obțineți rezultatul


din schelet.

Design patterns - Singleton, Factory,


Observer
Obiective
Scopul acestui laborator este familiarizarea cu folosirea unor pattern-uri des întâlnite în
design-ul atât al aplicațiilor, cât și al API-urilor - Singleton, Factory și Observer.

Introducere
Design pattern-urile reprezintă soluții generale și reutilizabile ale unei probleme comune în
design-ul software. Un design pattern este o descriere a soluției sau un template ce poate fi
aplicat pentru rezolvarea problemei, nu o bucata de cod ce poate fi aplicata direct. În general
pattern-urile orientate pe obiect arată relațiile și interacțiunile dintre clase sau obiecte, fără a
specifica însă forma finală a claselor sau a obiectelor implicate.

Se consideră că există aproximativ 2000 de design patterns [2], iar principalul mod de a le
clasifica este următorul:

 “Gang of Four” patterns

 Concurrency patterns

 Architectural patterns - sunt folosite la un nivel mai inalt decat design patterns,
stabilesc nivele și componente ale sistemelor/aplicațiilor, interacțiuni între acestea
(e.g. Model View Controller şi derivatele sale). Acestea descriu structura întregului
sistem, iar multe framework-uri vin cu ele deja încoporate, sau faciliteaza aplicarea
lor (e.g. Java Spring). În cadrul laboratoarelor nu ne vom lega de acestea.

O carte de referință pentru design patterns este “Design Patterns: Elements of Reusable
Object-Oriented Software” [1], denumită și “Gang of Four” (GoF). Aceasta definește 23 de
design patterns, foarte cunoscute și utilizate în prezent. Aplicațiile pot încorpora mai multe
pattern-uri pentru a reprezenta legături dintre diverse componente (clase, module). În afară de
GoF, și alți autori au adus în discuție pattern-uri orientate în special pentru aplicațiile
enterprise și cele distribuite.

Pattern-urile GoF sunt clasificate în felul următor:

 Creational Patterns - definesc mecanisme de creare a obiectelor

o Singleton, Factory etc.

 Structural Patterns - definesc relații între entități

o Decorator, Adapter, Facade, Composite, Proxy etc.

 Behavioural Patterns - definesc comunicarea între entități

o Visitor, Observer, Command, Mediator, Strategy etc.

Design pattern-urile nu trebuie privite drept niște rețete care pot fi aplicate direct pentru a rezolva o
problemă din design-ul aplicației, pentru că de multe ori pot complica inutil arhitectura. Trebuie întâi
înțeles dacă este cazul să fie aplicat un anumit pattern, si de-abia apoi adaptat pentru situația
respectivă. Este foarte probabil chiar să folosiți un pattern (sau o abordare foarte similară acestuia)
fără să vă dați seama sau să îl numiți explicit. Ce e important de reținut după studierea acestor
pattern-uri este un mod de a aborda o problemă de design.

În laboratorul Visitor Pattern au fost introduse design pattern-urile și aplicabilitatea Visitor-


ului. Acesta este un pattern comportamental, și după cum ați observat oferă avantaje în
anumite situații, în timp ce pentru altele nu este potrivit. Pattern-urile comportamentale
modelează interacțiunile dintre clasele și componentele unei aplicații, fiind folosite în
cazurile în care vrem sa facem un design mai clar și ușor de adaptat și extins.

Singleton Pattern
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.

Utilizari

Pattern-ul Singleton este util în următoarele cazuri:

 ca un subansamblu al altor pattern-uri:

o impreună cu pattern-urile Abstract Factory, Builder, Prototype etc. De


exemplu, în aplicație dorim un singur obiect factory pentru a crea obiecte de
un anumit tip.

 în locul variabilelor globale. Singleton este preferat variabilelor globale deoarece,


printre altele, nu poluează namespace-ul global cu variabile care nu sunt necesare.

Singleton este utilizat des în situații în care avem obiecte care trebuie accesate din mai multe
locuri ale aplicației:

 obiecte de tip logger

 obiecte care reprezintă resurse partajate (conexiuni, sockets etc.)

 obiecte ce conțin configurații pentru aplicație

 pentru obiecte de tip Factory.

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.

Încercați să nu folosiți în exces metode statice (să le utilizați mai mult pt funcții “utility”) și
componente Singleton.

Implementare

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;
}
...
}

 Instanța instance este private

 Constructorul este privat ca sa nu poata fi apelat decat din clasa respectivă

 Instanța este inițial nulă

 Instanța este creată la prima rulare a getInstance()

De 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).

Factory
Patternurile de tip Factory sunt folosite pentru obiecte care generează instanțe de clase
înrudite (implementează aceeași interfață, moștenesc aceeași clasă abstractă). Acestea sunt
utilizate atunci când dorim să izolăm obiectul care are nevoie de o instanță de un anumit tip,
de creearea efectivă acesteia. În plus clasa care va folosi instanța nici nu are nevoie să
specifice exact subclasa obiectului ce urmează a fi creat, deci nu trebuie să cunoască toate
implementările acelui tip, ci doar ce caracteristici trebuie să aibă obiectul creat. Din acest
motiv, Factory face parte din categoria Creational Patterns, deoarece oferă o soluție legată de
creearea obiectelor.

Aplicabilitate:

 în biblioteci/API-uri, utilizatorul este separat de implementarea efectivă a tipului și


trebuie sa folosească metode factory pentru a obține anumite obiecte. Clase care oferă
o astfel de funcționalitate puteți găsi și in core api-ul de Java, in api-ul java.nio (e.g.
clasa FileSystems), în Android SDK (e.g. clasa SocketFactory) etc.

 atunci când crearea obiectelor este mai complexă (trebuie realizate mai multe etape
etc.), este mai util să separăm logica necesară instanțierii subtipului de clasa care are
nevoie de acea instanță.
Abstract Factory Pattern

Fig. 2: Diagrama de clase pentru Abstract Factory

Codul următor corespunde diagramei din figure 2. În acest caz folosim interfețe pentru
factory și pentru tip, însă în alte situații putem să avem direct SpecializedFooFactory, fără a
implementa interfața FooFactory.

public interface Foo {


public void bar();
}
public interface FooFactory {
public Foo createFoo();
}
public class SpecializedFoo implements Foo {
...
}
public class SpecializedFooFactory implements FooFactory {
public Foo createFoo() {
return new SpecializedFoo();
}
}
Factory Method Pattern

Folosind pattern-ul Factory Method se poate defini o interfață pentru crearea unui obiect.
Clientul care apelează metoda factory nu știe/nu îl interesează de ce subtip va fi la runtime
instanța primită.

Spre deosebire de Abstract Factory, Factory Method ascunde construcția unui obiect, nu a
unei familii de obiecte “inrudite”, care extind un anumit tip. Clasele care implementează
Abstract Factory conțin de obicei mai multe metode factory.
Fig. 3: Diagrama de clase pentru Factory Method

Exemplu

Situația cea mai întâlnită în care se potrivește acest pattern este aceea când trebuie instanțiate
multe clase care implementează o anumită interfață sau extind o altă clasă (eventual
abstractă), ca în exemplul de mai jos. Clasa care folosește aceste subclase nu trebuie să “știe”
tipul lor concret ci doar pe al părintelui. Implementarea de mai jos corespunde pattern-ului
Abstract Factory pentru clasa PizzaFactory, și foloseste factory method pentru metoda
createPizza.

PizzaLover.java
abstract class Pizza {
public abstract double getPrice();
}
class HamAndMushroomPizza extends Pizza {
public double getPrice() {
return 8.5;
}
}
class DeluxePizza extends Pizza {
public double getPrice() {
return 10.5;
}
}
class HawaiianPizza extends Pizza {
public double getPrice() {
return 11.5;
}
}
 
class PizzaFactory {
public enum PizzaType {
HamMushroom, Deluxe, Hawaiian
}
public static Pizza createPizza(PizzaType pizzaType) {
switch (pizzaType) {
case HamMushroom: return new HamAndMushroomPizza();
case Deluxe: return new DeluxePizza();
case Hawaiian: return new HawaiianPizza();
}
throw new IllegalArgumentException("The pizza type " +
pizzaType + " is not recognized.");
}
}
public class PizzaLover {
public static void main (String args[]) {
for (PizzaFactory.PizzaType pizzaType :
PizzaFactory.PizzaType.values()) {
System.out.println("Price of " + pizzaType + " is " +
PizzaFactory.createPizza(pizzaType).getPrice());
}
}
}
Output:
Price of HamMushroom is 8.5
Price of Deluxe is 10.5
Price of Hawaiian is 11.5
Singleton Factory

De obicei avem nevoie ca o clasă factory să fie utilizată din mai multe componente ale
aplicației. Ca să economisim memorie este suficient să avem o singură instanță a factory-ului
și să o folosim pe aceasta. Folosind pattern-ul Singleton putem face clasa factory un
singleton, și astfel din mai multe clase putem obține instanță acesteia.

Un exemplu ar fi Java Abstract Window Toolkit (AWT) ce oferă clasa abstractă


java.awt.Toolkit care face legătura dintre componentele AWT și implementările native din
toolkit. Clasa Toolkit are o metodă factory Toolkit.getDefaultToolkit() ce întoarce
subclasa de Toolkit specifică platformei. Obiectul Toolkit este un Singleton deoarece AWT
are nevoie de un singur obiect pentru a efectua legăturile și deoarece un astfel de obiect este
destul de costisitor de creat. Metodele trebuie implementate în interiorul obiectului și nu pot
fi declarate statice deoarece implementarea specifică nu este cunoscută de componentele
independente de platformă.

Observer Pattern
Design Pattern-ul Observer definește o relație de dependență 1 la n între obiecte astfel încât
când un obiect își schimbă starea, toți dependenții lui sunt notificați și actualizați automat.
Folosirea acestui pattern implică existența unui obiect cu rolul de subiect, care are asociată o
listă de obiecte dependente, cu rolul de observatori, pe care le apelează automat de fiecare
dată când se întâmplă o acțiune.

Acest pattern este de tip Behavioral (comportamental), deorece facilitează o organizare mai
bună a comunicației dintre clase în funcție de rolurile/comportamentul acestora.

Observer se folosește în cazul în care mai multe clase(observatori) depind de


comportamentul unei alte clase(subiect), în situații de tipul:
 o clasă implementează/reprezintă logica, componenta de bază, iar alte clase doar
folosesc rezultate ale acesteia (monitorizare).

 o clasă efectuează acțiuni care apoi pot fi reprezentate în mai multe feluri de către alte
clase (view-uri ca în figură de mai jos).

Practic în toate aceste situații clasele Observer observă modificările/acțiunile clasei Subject.
Observarea se implementează prin notificări inițiate din metodele clasei Subject.

Structură

Pentru aplicarea acestui pattern, clasele aplicației trebuie să fie structurate după anumite
roluri, și în funcție de acestea se stabilește comunicarea dintre ele. În exemplul din figure 4,
avem două tipuri de componente, Subiect și Observator, iar Observator poate fi o interfață
sau o clasă abstractă ce este extinsă cu diverse implementări, pentru fiecare tip de
monitorizare asupra obiectelor Subiect.

 observatorii folosesc datele subiectului

 observatorii sunt notificați automat de schimbări ale subiectului

 subiectul cunoaște toți observatorii

 subiectul poate adăuga noi observatori

Fig. 4: Diagrama de clase pentru Observer Pattern

Subiect

 nu trebuie să știe ce fac observatorii, trebuie doar să mențină referințe către obiecte de
acest tip

 nu știe ce fac observatorii cu datele


 oferă o metodă de adăugare a unui Observator, eventual și o metodă prin care se pot
deinregistra observatori

 menține o listă de referințe cu observatori

 când apar modificări (e.g. se schimbă starea sa, valorile unor variabile etc) notifică
toți observatorii

Observator

 definește o interfață notificare despre schimbări în subiect

 ca implementare:

o toți observatorii pentru un anumit subiect trebuie să implementeze această


interfață

o oferă una sau mai multe metode care să poată fi invocate de către Subiect
pentru a notifica o schimbare. Ca argumente se poate primi chiar instanța
subiectului sau obiecte speciale care reprezintă evenimentul ce a provocat
schimbarea.

View/ObservatorDerivat

 implementează interfața Observator

Aceasta schemă se poate extinde, în funcție de aplicație, observatorii pot ține referințe catre
subiect sau putem adauga clase speciale pentru reprezentarea evenimentelor, notificarilor. Un
alt exemplu îl puteți găsi aici.

Implementare

Un exemplu de implementare este exercițiul 2 de la laboratorul 5 (Clase interne). Observați


diagrama de clase asociată acestuia.

Tookit-urile GUI, cum este și Swing folosesc acest design pattern, de exemplu apăsarea unui
buton generează un eveniment ce poate fi transmis mai multor listeners înregistrați acestuia
(exemplu).

API-ul Java oferă clasele Observer și Observable care pot fi subclasate pentru a implementa
propriile tipuri de obiecte ce trebuie monitorizate și observatorii acestora.

Pentru cod complex, concurent, cu evenimente asincrone, recomandăm RxJava, care


folosește Observer pattern: github, exemplu.

Exerciții
Acest laborator și următorul au ca temă comună a exercițiilor realizarea unui joc controlat din
consolă. Jocul constă dintr-o lume (aka hartă) în care se plimbă eroi de trei tipuri, colectează
comori și se bat cu monștri. În acestă săptămână trebuie să implementați o parte din
funcționalitățile jocului folosind patternurile Singleton, Factory și Observer, urmând ca la
laboratorul următor să terminați implementarea folosind pattern-urile studiate atunci.

Detalii joc:

 Harta

o reprezentată printr-o matrice. Fiecare element din matrice reprezintă o zonă


care poate fi liberă, poate conține obstacole sau poate conține o comoară (în
laboratorul următor poate conține și monștrii).

o este menținută în clasa World.

 Eroii

o sunt reprezentați prin clase de tip Hero și sunt de trei tipuri: Mage, Warrior,
Priest.

o puteți adăuga oricâți eroi doriți pe hartă (cât vă permite memoria :))

o într-o zonă pot fi mai mulți eroi

o acțiunile pe care le pot face:

 move - se mută într-o zonă învecinată

 attack (de implementat în laboratorul următor)

 collect - eroul ia comoara găsită în zona în care se află

 Entry-point-ul în joc îl consitituie clasa Main.

 (4p) Folosiți design pattern-ul Factory pentru crearea obiectelor.

o Creati clase care mostenesc Hero pentru fiecare tip de erou.

 suprascrieti metoda toString din Object pentru fiecare erou

 metoda attack - deocamdată nu va omorî pe nimeni - puteți afișa ceva


la consolă

o Uitați-vă la clasele TreasureFactory și HeroFactory. Trebuie să


implementăm două metode: createTreasure în TreasureFactory și o
metodă de creare de eroi în HeroFactory, fie ea createHero.

 Puteți pune orice date doriți în comori, respectiv eroi.


 La HeroFactory.createHero, pasați ca parametru un Hero.Type și
un String cu numele eroului și întoarceți un subtip de Hero potrivit
pentru tipul de erou.

2. După ce ați creat factory-urile, folosiți-le:

 Completați metoda populateTreasures din World. Folosiți-vă de


membrii map și treasures din World. Trebuie să marcați pe hartă că
aveți o comoară și să adăugați obiectul-comoară în lista de comori.

 Uitați-vă apoi la cazul add din metoda main. Trebuie să adăugați eroi
acolo. Folosiți HeroFactory.createHero.

2. (2p) Folosiți design pattern-ul Singleton pentru elementele din joc care trebuie să aibă
doar o instanță.

1. Ce clase vor avea doar o singură instanță?

o (4p) Folosiți design pattern-ul Observer pentru a monitoriza ceea ce se


întâmplă în joc. Scheletul de cod vă sugerează două tipuri de observatori,
pentru bonus puteți adăuga și alții.

1. veți folosi interfața Observer și clasa Observable din java.util.

 Înainte să vă apucați să scrieți, citiți comentariile din cod (e.g.


TreasureDiscoverer) să vă faceți o idee despre ce vrem să facem în
clasele observatoare.

 Asigurati-va ca implementati corect functionalitatea din Observable.


Mare atentie la faptul ca metoda notifyObservers nu va face nimic
daca nu este apelata mai intai metoda setChanged.

2. Care sunt elementele observatoare și care sunt observabile? Uitați-vă și în


comentariile din cod.

3. Implementați interfețele Observer și Observable în clasele potrivite.

4. Înregistrați observatori la World. Cazul start din metoda main.

5. Notificați observatorii lui World când eroii execută o acțiune. Aveți două
TODO-uri în clasa Hero.

2. Începeți rezolvarea prin implementarea claselor pentru eroi și implementarea


design pattern-ului factory pentru crearea lor. Pentru a putea vizualiza harta
trebuie să implementați partea de observare a stării jocului. World trebuie să
fie observabilă și să notifice pe observatorii săi atunci când a început jocul și
când se schimbă ceva (e.g. s-a mutat un erou).
1. Urmăriți todo-urile din scheletul de cod (pentru a le vizualiza mai ușor pe
toate puteți să folosiți view-ul pt ele din IDE, de exemplu în eclipse aveți
Window → Show View → Tasks)

Fig. 5 : Componentele jocului

Design patterns - Command, Strategy


 Responsabil: Theodor Stoican

 Data ultimei modificări: 12.12.2017

Obiective
Scopul acestui laborator este familiarizarea cu folosirea unor pattern-uri des întâlnite în
design-ul atât al aplicațiilor, cât și al API-urilor - Command și Strategy.

Introducere
Design pattern-urile reprezintă soluții generale și reutilizabile ale unei probleme comune în
design-ul software. Un design pattern este o descriere a soluției sau un template ce poate fi
aplicat pentru rezolvarea problemei, nu o bucata de cod ce poate fi aplicata direct. În general
pattern-urile orientate pe obiect arată relațiile și interacțiunile dintre clase sau obiecte, fără a
specifica însă forma finală a claselor sau a obiectelor implicate.

Se consideră că există aproximativ 2000 de design patterns [2], iar principalul mod de a le
clasifica este următorul:

 “Gang of Four” patterns

 Concurrency patterns

 Architectural patterns - sunt folosite la un nivel mai inalt decat design patterns,
stabilesc nivele și componente ale sistemelor/aplicațiilor, interacțiuni între acestea
(e.g. Model View Controller şi derivatele sale). Acestea descriu structura întregului
sistem, iar multe framework-uri vin cu ele deja încoporate, sau faciliteaza aplicarea
lor (e.g. Java Spring). În cadrul laboratoarelor nu ne vom lega de acestea.

O carte de referință pentru design patterns este “Design Patterns: Elements of Reusable
Object-Oriented Software” [1], denumită și “Gang of Four” (GoF). Aceasta definește 23 de
design patterns, foarte cunoscute și utilizate în prezent. Aplicațiile pot încorpora mai multe
pattern-uri pentru a reprezenta legături dintre diverse componente (clase, module). În afară de
GoF, și alți autori au adus în discuție pattern-uri orientate în special pentru aplicațiile
enterprise și cele distribuite.

Pattern-urile GoF sunt clasificate în felul următor:

 Creational Patterns - definesc mecanisme de creare a obiectelor

o Singleton, Factory etc.

 Structural Patterns - definesc relații între entități

o Decorator, Adapter, Facade, Composite, Proxy etc.

 Behavioural Patterns - definesc comunicarea între entități

o Visitor, Observer, Command, Mediator, Strategy etc.

Design pattern-urile nu trebuie privite drept niște rețete care pot fi aplicate direct pentru a rezolva o
problemă din design-ul aplicației, pentru că de multe ori pot complica inutil arhitectura. Trebuie întâi
înțeles dacă este cazul să fie aplicat un anumit pattern, si de-abia apoi adaptat pentru situația
respectivă. Este foarte probabil chiar să folosiți un pattern (sau o abordare foarte similară acestuia)
fără să vă dați seama sau să îl numiți explicit. Ce e important de reținut după studierea acestor
pattern-uri este un mod de a aborda o problemă de design.

În laboratorul Visitor Pattern au fost introduse design pattern-urile și aplicabilitatea Visitor-


ului. Acesta este un pattern comportamental, și după cum ați observat oferă avantaje în
anumite situații, în timp ce pentru altele nu este potrivit. Pattern-urile comportamentale
modelează interacțiunile dintre clasele și componentele unei aplicații, fiind folosite în
cazurile în care vrem sa facem un design mai clar și ușor de adaptat și extins. /*În afară de
acest tip de pattern-uri, mai se folosesc și cele structural și creational, prezentate în
clasificarea următoare:

 Creational Patterns - mecanisme de creare a obiectelor

o Singleton, Factory etc

 Structural Patterns - definesc relații între entități

o Decorator, Adapter, Facade, Composite, Proxy etc.

 Behavioural Patterns - definesc comunicarea între entități


o Visitor, Observer, Command, Mediator, Strategy etc.

Command Pattern
Design pattern-ul Command încapsulează un apel cu tot cu parametri într-o clasă cu interfață
generică. Acesta este Behavioral pentru ca modifică interacțiunea dintre componente, mai
exact felul în care se efectuează apelurile.

Acest pattern este recomandat în următoarele cazuri:

 pentru a ușura crearea de structuri de delegare, de callback, de apelare întârziată

 pentru a reține lista de comenzi efectuate asupra obiectelor

o accounting

o liste de Undo, Rollback pentru tranzacții - suport pentru operații reversibile


(undoable operations)

Exemple de utilizare:

 sisteme de logging, accounting pentru tranzacții

 sisteme de undo (ex. editare imagini)

 mecanism ordonat pentru delegare, apel întârziat, callback

Functionare si necesitate

In esenta, Command pattern (asa cum v-ati obisnuit si lucrand cu celelate Pattern-uri pe larg
cunoscute) presupune incapsularea unei informatii referitoare la actiuni/comenzi folosind un
wrapper pentru a “tine minte aceasta informatie” si pentru a o folosi ulterior. Astfel, un astfel
de wrapper va detine informatii referitoare la tipul actiunii respective (in general un asemenea
wrapper va expunde o metoda execute(), care va descrie comportamentul pentru actiunea
respectiva).

Mai mult inca, cand vorbim de Command Pattern, in terminologia OOP o sa intalniti deseori
si notiunea de Invoker. Invoker-ul este un middleware ca functionalitate care realizeaza
managementul comenzilor. Practic, un Client, care vrea sa faca anumite actiune, va instantia
clase care implementeaza o interfata Command. Ar fi incomod ca, in cazul in care aceste
instantieri de comenzi provin din mai multe locuri, acest management de comenzi sa se face
local, in fiecare parte (din ratiuni de economie, nu vrem sa duplicam cod). Invoker-ul apare
ca o necesitate de a centraliza acest proces si de a realiza intern management-ul comenzilor
(le tine intr-o lista, tine cont de eventuale dependinte intre ele, totul in functie de context).

Si nu in cele din urma, un client (generic spus, un loc de unde se lanseaza comenzi)
instantiaza comenzile si le paseaza Invoker-ului. Din acest motiv Invoker-ul este un
middleware intre client si receiver, fiindca acesta va apela execute pe fiecare Command, in
functie de logica sa interna.
Recomandare: La Referinte aveti un link catre un post pe StackOverflow, pentru a intelege
mai bine de ce aveti nevoie de Pattern-ul Command si de ce nu lansati comenzi pur si simplu.

Structura

Ideea principală este de a crea un obiect de tip Command care va reține parametrii pentru
comandă. Comandantul reține o referință la comandă și nu la componenta comandată.
Comanda propriu-zisă este anunțată obiectului Command (de către comandant) prin execuția
unei metode specificate asupra lui. Obiectul Command este apoi responsabil de trimiterea
(dispatch) comenzii către obiectele care o îndeplinesc (comandați).

Fig. 1: Diagrama de stări pentru CommandPattern

Tipuri de componente (roluri):

 Invoker - comandantul

o apelează acțiuni pe comenzi (invocă metode oferite de obiectele de tip


Command)

o poate menține, dacă e cazul, o listă a tutoror comenzilor aplicate pe obiectul


(obiectele) comandate. Este necesară reținerea acestei liste de comenzi atunci
când implementăm un comportament de undo/redo al comenzilor.

o primește clase Command pe care să le invoce

 Receiver - comandatul

o este clasa asupra căreia se face apelul

o conține implementarea efectivă a ceea ce se dorește executat


 Command - obiectele pentru reprezentarea comenzilor implementează această
interfață/o extind dacă este clasă abstractă

o concrete command - ne referim la implementări/subclasele acesteia

o de obicei conțin metode cu nume sugestiv pentru executarea acțiunii comenzii


(e.g. execute()). Implementările acestora conțin apelul către clasa Receiver.

o în cazul implementării unor acțiuni undoable adăugăm metode pentru undo


și/sau redo.

o țin referințe către comandați (receivers) pentru a aplica/invoca acțiunea ce


reprezintă acea comandă

În Java, se pot folosi atât interfețe cât și clase abstracte, pentru Command, depinzând de
situație (e.g. clasă abstractă dacă știm sigur ca obiectele de tip Command nu mai au nevoie să
extindă și alte clase).

În diagrama din figure 1, comandantul este clasa Invoker care conține o referință la o instanță
(command) a clasei (Command). Invoker va apela metoda abstractă execute() pentru a cere
îndeplinirea comenzii. ConcreteCommand reprezintă o implementare a interfeței Command,
iar în metoda execute() va apela metoda din Receiver corespunzătoare acelei
acțiuni/comenzi.

Implementare

Diagrama de secvență din figure 2 prezintă apelurile în cadrul unei aplicație de editare a
imaginilor, ce este structurată folosind pattern-ul Command. În cadrul acesteia, Receiver-ul
este Image, iar comenzile BlurCommand și CropCommand modifică starea acesteia.
Structurând aplicația în felul acesta, este foarte ușor de implementat un mecanism de
undo/redo, fiind suficient să menținem în Invoker o listă cu obiectele de tip Command
aplicate imaginii.
Fig. 2: Diagrama de
secvență pentru comenzile de prelucrare a imaginilor

Pe wikipedia puteți analiza exemplul PressSwitch. Flow-ul pentru acesta este ilustrat în

figure 3 Fig. 3:
Diagrama de secvență pentru comenzile de aprindere/stingere a switch-ului

Strategy Pattern
Design pattern-ul Strategy încapsulează algoritmii în clase ce oferă o anumită interfață de
folosire, și pot fi selecționați la runtime. Ca și Command, acest pattern este behavioral pentru
ca permite decuplarea unor clase ce oferă un anumit comportament și folosirea lor
independentă în funcție de situația de la runtime.
Acest pattern este recomandat în cazul în care avem nevoie de un tip de algoritm (strategie)
cu mai multe implementări posibile si dorim să alegem dinamic care algoritm îl folosim, fără
a face sistemul prea strâns cuplat.

Exemple de utilizare:

 sisteme de tip Layout Managers din API-urile pentru UI

 selectarea in mod dinamic la runtime a unor algoritmi de sortare, compresie, criptare


etc.

Structură:

 trebuie să definiți o interfață comună pentru strategiile pe care le implementați (fie


ca o «interface» sau ca o clasa abstractă)

 implementați strategiile respectând interfața comună

 clasa care are nevoie să folosească strategiile va ști doar despre interfața lor, nu va
fi legată de implementările concrete

Denumirile uzuale în exemplele acestui pattern sunt: Strategy (pt interfață sau clasa
abstractă), ConcreteStrategy pentru implementare, Context, clasa care folosește/execută
strategiile.

Recomandare: Urmariti link-ul de la referinte catre postul de pe Stack Overflow care descrie
necesitatea pattern-ului Strategy. Pe langa motivul evident de incapsulare a
prelucrarilor/algoritmilor (care reprezinta strategiile efective), se prefera o anumita abordare:
la runtime se verifica mai multe conditii si se decide asupra strategiei. Concret, folosind
mecanismul de polimorfism dinamic, se foloseste o anumita instanta a tipului de strategie (ex.
Strategy str = new CustomStrategy), care se paseaza in toate locurile unde este nevoie de
Strategy. Practic, in acest fel, utilizatorii unei anumite strategii vor deveni agnostici in raport
cu strategia utilizata, ea fiind instantiata intr-un loc anterior si putand fi gata utilizata.
Ganditi-va la browserele care trebuie sa detecteze daca device-ul este PC, smartphone, tableta
sau altceva si in functie de acest lucru sa randeze in mod diferit. Fiecare randare poate fi
implementata ca o strategie, iar instantierea strategiei se va face intr-un punct, fiind mai apoi
pasata in toate locurile unde ar trebui sa se tina cont de aceasta strategie.

Exerciții
Acest laborator și cel precedent au ca temă comună a exercițiilor realizarea unui joc controlat
din consolă. Jocul constă dintr-o lume (aka hartă) în care se plimbă eroi de trei tipuri,
colectează comori și se bat cu monștrii. În acestă săptămână terminam jocul inceput in
laboratorul precedent folosind pattern-urile studiate (Strategy, Command).

Detalii joc:

 Harta
o reprezentată printr-o matrice. Fiecare element din matrice reprezintă o zonă
care poate fi liberă, poate conține obstacole sau poate conține o comoară (în
laboratorul următor poate conține și monștrii).

o este menținută în clasa GameState.

 Eroii

o sunt reprezentați prin clase de tip Hero și sunt de trei tipuri: Mage, Warrior,
Priest.

o puteți adăuga oricâți eroi doriți pe hartă (cât vă permite memoria :))

o într-o zonă pot fi mai mulți eroi

o acțiunile pe care le pot face:

 move - se mută într-o zonă învecinată

 attack - ataca un monstru cand se afla pe aceeasi pozitie cu el

 collect - eroul ia comoara găsită în zona în care se află

 Entry-point-ul în joc îl consitituie clasa Main.

 (5p) Folosiți design pattern-ul Command pentru a implementa functionalitatea de


undo si redo la comanda move.

o Momentan, aveti erori de compilare in clasele Main si GameState. Dupa ce


veti implementa acest exercitiu, se vor rezolva, nu modificati in mod direct
acolo.

o Va trebui sa completati clasa MoveCommand care implementeaza interfata


Command. Urmariti TODO-urile din aceasta clasa.

o Hint: Pentru Undo, de exemplu, daca v-ati deplasat la dreapta, ar trebui sa va


deplasati la stanga. Creati-va o metoda ajutatoare care trateaza astfel de cazuri.

o Precum si clasa CommandManager care va tine evidenta comenzilor si ordinea


lor.

o Hint: Amintiti-va de la cursul de Structuri de Date cum se implementeaza


operatiile Redo si Undo. Folositi doua stive.

 (5p) Folosiți design pattern-ul Strategy pentru a implementa logica de atac a unui
monstru.

o Pentru acest exercitiu va trebui sa implementati 2 strategii: AttackStrategy si


DefenseStrategy. Ambele vor implementa Strategy si metodele aferente.
Fiecare din aceste Strategy, va retine o referinta interna la un Hero. Abordarea
este urmatoarea:

 Exista 3 clase Hero, fiecare cu cate un DamageType aferent: Warrior -


Blunt, Mage - Magic, Priest - Poison

 Fiecare monstru are un weakness (slabiciune la atacurile de tip Blunt,


Magic sau Poison)

 AttackStrategy: In metoda attack(), veti itera prin inventory-ul eroului


si veti verifica daca exista un obiect Treasure care are DamageType-ul
identic cu cel al eroului. Daca da, atunci damage-ul pe care il veti da
unui obiect Monster este 3 x damage-ul treasure-ului. Daca nu aveti
un astfel de Treasure, atunci cautati un Treasure cu un DamageType
identic cu cel al mob-ului (mobul poate fi vulnerabil la Magic, de ex.).
Daca gasiti un astfel de Treasure, damage-ul pe care il veti imprima
mob-ului este 2 x damage-ul treasure-ului. Daca nu, veti scade din
HP-ul mob-ului rezultatul apelului metodei getBaseDamage() asupra
Hero-ului.

 DefenseStrategy: In metoda attack(), veti itera prin inventory-ul


eroului si veti verifica, la fel, daca exista un obiect Treasure care are
DamageType-ul identic cu cel al eroului. Daca da, atunci veti da un
boost de HP egal cu treasure.getBoostHp() + getBaseHpBoost()
eroului (getBaseHpBoost este implementata in clasa Hero). Daca nu,
veti da doar un boost egal cu ce va intoarce getBaseHpBoost().
Damage-ul imprimat mobului va fi, de asemenea, egal cu ce va
intoarce getBaseDamage(). Adaugati log-uri in clasa DefenseStrategy,
pentru a va asigura ca i se mareste viata eroului.

 Urmariti si TODO-urile din cele doua clase

o Implementati metoda attack din clasa Hero astfel incat, daca eroul are mai
mult de 50HP, folositi strategia AttackStrategy. Altfel, folositi
DefenseStrategy. Urmariti TODO-urile din cod.

 (Bonus 2p) Implementați coliziunile cu obstacolele de pe harta

o Va trebui sa creati un nou obiect Obstacle precum si un ObstacleObserver

o Cand eroul ajunge pe un obstacol se va printa un mesaj Can't move there !


si se va apela automat undo pe ultima comanda de move pentru a reveni in
pozitia anterioara coliziunii. Acest feature de wall collision va fi implementat
in ObstacleObserver
Recapitulare
Obiective
Scopul acestui laborator îl constă recapitularea principalelor noțiuni de OOP și Java
prezentate în cadrul laboratoarelor.

Testul final va verifica aceste concepte prin întrebări grilă similare celor date ca exemplu în
acest laborator (nu toate exercțiile din laborator sunt însă conforme cu formatul testului).

Exerciții
1) Se consideră următoarea declarație de clasă. Ce se va întâmpla la compilarea și executarea
ei?

public class Core


{
private void privateMethod()
{
System.out.println("This is a private method");
}
 
public static void staticMethod()
{
System.out.println("This is a static method");
 
this.privateMethod();
}
 
// Application Entry Point
public static void main(String[] Params)
{
Core Instance = new Core();
Instance.staticMethod();
Core.staticMethod();
}
}

 A. Se va afișa de 2 ori mesajul “This is a static method” și de două ori “This


is a private method”

 B. Se va genera o eroare de compilare la ultima linie din main()


(Core.staticMethod())

 C. Se va genera o eroare de compilare la linia this.privateMethod()

 D. Se va arunca o excepție la executarea liniei Instance.staticMethod()

2) Se consideră următoarea situație. Câte versiuni distincte ale variabilei x sunt accesibile în
(*)?

class Outer
{
int x;
 
class Inner extends Outer
{
int x;
 
void f(int x)
{
(*)
}
}
}

 A. Una singură

 B. Doua

 C. Trei

 D. Patru

3) Se consideră următoarea structură de clase. La rularea metodei printAll(), ce valori se


vor afișa și în ce ordine (ignorați faptul că acestea apar pe linii diferite)?

class Outer
{
int x = 10;
 
class Inner extends Outer
{
int x = 15;
 
void printFirst(int x)
{
System.out.println(this.x);
System.out.println(x);
System.out.println(super.x);
}
 
void printSecond()
{
System.out.println(this.x);
System.out.println(x);
System.out.println(super.x);
}
 
void printAll()
{
printFirst(20);
printSecond();
}
}
}

 A. 20, 15, 15, 10, 15, 20

 B. 10, 15, 15, 20, 20, 10

 C. 15, 20, 10, 15, 20, 10

 D. 15, 20, 10, 15, 15, 10

4) În următorul exemplu, ce se va afișa (ignorați faptul că mesajele apar pe linii diferite)?

public class Core


{
private void someFunction(int x)
{
try
{
System.out.println("1");
if (x == 1)
return;
x /= x;
}
catch (Exception error)
{
System.out.println("2");
}
finally
{
System.out.println("3");
}
}
 
// Application Entry Point
public static void main(String[] Params)
{
Core instance = new Core();
instance.someFunction(1);
instance.someFunction(0);
}
}

 A. Nici un raspuns din cele de mai jos

 B. 1, 2, 3, 1, 3

 C. 1, 1, 3

 D. 1, 1, 2, 3

5) În fișierul “LuxuryCar.java” există următoarele declarații:

class Car {
private String name = "Some Car";
}
 
public class LuxuryCar extends Car {
public LuxuryCar() {
name = "Luxury Car";
}
}
 
class Main {
// Application Entry Point
public static void main(String[] params) {
LuxuryCar instance = new Car();
}
}

Ce se întâmplă în momentul în care încercăm să compilăm conținutul acestui fișier?

 A. Compilarea decurge fără probleme

 B. Compilarea eșuează cu 3 erori

 C. Compilarea eșuează cu 1 eroare

 D. Compilarea eșuează cu 2 erori

6) Fie următoarea secvență de cod. Ce se întâmplă la execuția ei?

import java.util.*;
 
public class Main
{
private void process()
{
ArrayList<Integer> myList = new ArrayList<Integer>();
 
try
{
for(int I = 0; I < 5; ++I)
myList.add(I + 1);
 
int result = myList.get(5);
System.out.println(result);
}
catch(StackOverflowError error)
{
System.out.println("! Stack overflow !");
}
}
 
// Application Entry Point
public static void main(String[] Params)
{
new Main().process();
}
}

 A. Se va afișa “! Stack overflow !”

 B. Se va afișa 5

 C. Aplicația se va încheia normal (fără excepții), fără a afișa nici un mesaj

 D. Se va arunca o excepție de tipul IndexOutOfBoundsException, care nu va fi


prinsă

7) Ce se întâmplă la compilarea și executarea următoarei aplicații Java?

public class Main


{
private static Main Instance = new Main();
 
public static Main getInstance()
{
return Instance;
}
 
public void print()
{
System.out.println(this.getClass());
}
 
// Application Entry Point
public static void main(String[] Params)
{
Main.getInstance().print();
new Main().print();
}
}

 A. Nu apar erori sau excepții, nici la compilare nici la execuție

 B. Apare o eroare la compilare, deoarece nu se poate crea o instanță a clasei Main prin
new Main()
 C. Apare o eroare la compilare, deoarece linia new Main().print(); nu este corectă

 D. Apare o eroare la compilare, deoarece variabila Instance nu este accesibilă la


nivelul funcției getInstance()

8) Ce va afișa instrucțiunea new Child(2)?

class Parent {
public Parent() {
System.out.println("Parent 0");
}
 
public Parent(int x) {
System.out.println("Parent 1");
}
}
 
class Child extends Parent {
public Child(int x) {
System.out.println("Child 1");
}
}

9) Fie următoarea secvență:

class C {}
 
class D extends C {}
 
class A {
void f(C c) {...}
void f(D d) {...}
}
 
class B extends A {
void f(C c) {...}
void f(D d) {...}
}

Care implementare a metodei f va fi utilizată în cadrul invocării de mai jos:

A a = new B();
C c = new D();
a.f(c);

10) Adăugați un cuvânt cheie la următorul antet de clasă, astfel încât declarația să devină
contradictorie:

abstract class C
 
11) Întrebarea de mai sus, aplicată în cazul metodei:
abstract void f()

S-ar putea să vă placă și