Documente Academic
Documente Profesional
Documente Cultură
Obiective
Scopul acestui laborator este familiarizarea studenților cu noțiunile de bază ale programării în
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.
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:
Un alt exemplu de clasă predefinita este clasa String. Ea se poate instanția astfel (nu este
necesară utilizarea new):
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;
}
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;
}
}
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
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):
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:
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!
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ă.
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)
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
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:
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)
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.
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.
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:
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());
}
}
public SomeClass() {
... // variables initialization
}
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;
}
}
Student st;
Crearea unui obiect Student se face obligatoriu prin apel la unul din cei 3 constructori de
mai sus:
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.
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.
Un fapt ce merită discutat este semnificația atribuirii de referințe. În exemplul de mai jos:
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.
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.
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.
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;
}
}
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
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).
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.
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:
Agregare:
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;
}
}
î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.
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)
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.
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
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.
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
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).
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.
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.
Prima metodă este o supraîncărcare a metodei equals iar a doua metodă este suprascrierea
metodei equals.
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.
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.
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:
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
realizată automat
Downcasting
Suprascrierea
Supraincarcarea
în interiorul clasei pot exista mai multe metode cu acelasi nume, cu condiția ca
semnătura (tipul, argumentele) să fie diferită
super
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
Î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:
Task 3 [1p]
Hint: Puteti genera automat metoda, cu ajutorul IDE. Selectați câmpurile considerate și analizați în
ce fel va fi suprascrisă metoda equals.
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:
Tot aici, parcurgeți array-ul, apeland metoda toString() pentru elementele sale.
In final, modificați cum considerati programul anterior astfel încât să nu mai aveți
nevoie de instanceof.
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";
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).
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.
Î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.
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.
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:
Î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ță.
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
}
}
Î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
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.
Implementare
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).
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:
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.
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
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ă
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:
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:
Deoarece o clasă abstractă este incompletă (există metode care nu sunt definite), crearea unui
obiect de tipul clasei este împiedicată de compilator.
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:
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()");
}
}
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.
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ă.
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++.
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
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:
(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.
Clase interne
Obiective
prezentarea tipurilor de clase 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.
O clasă internă are acces la toți membrii clasei în care a fost declarată, inclusiv cei
private
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.
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.
new Engine("Otto") {
// ...
}
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ă.
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)
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.
î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.
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();
}
}
[...]
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.
Î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.
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();
}
}
Exerciții
Task 1 - Meta (4p)
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.
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:
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).
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:
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.
numele metodei
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
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.
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
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)
Î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
@Override
public void purr() {
super.purr();
System.out.println("NO!");
}
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.
Elemente heterogene - tipuri diferite de obiecte pe care se aplică mai multe operații
Visitor - o interfață pentru operația aplicată Visitable - o interfață pentru obiecte pe care pot
fi aplicate operațiile (în diagramă este numită Element)
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.
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)
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ă:
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:
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:
After
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()));
}
}
implementări ale metodei accept(Visitor), în cele două clase, care, pur şi simplu,
solicită vizitarea instanţei curente de către vizitator.
Element - Visitable
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:
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.
Aplicabilitate
Avantaje:
Dezavantaje:
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
un tutorial
Summary
Supraîncărcarea (overloading) - mai multe metode cu același nume dar cu listă diferită de
argumente
o alte excepții
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
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)
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
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.
Afișați folosind java.nio informații despre fișierele cu extensia “.class” sau “.java” dintr-un
director.
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:
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:
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ǎ.
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:
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.
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:
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.
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
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:
Î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:
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?
Introducere
Să urmărim exemplul de mai jos:
Se observă necesitatea operației de cast pentru a identifica corect variabila obținută din listă.
Această situație are mai multe dezavantaje:
Î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:
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:
Î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:
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ă
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:
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.
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).
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:
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:
imposibilitatea de a-l citi din cauza permisiunilor insuficiente sau din cauza unei zone
invalide de pe hard-disk
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.
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
List<String> l = getArrayListObject();
if (null == l)
throw new Exception("The list is empty");
Î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.
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ă.
Î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.
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
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.
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).
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.
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.
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:
Î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.
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 UnderflowException: este aruncată dacă suma celor doua numere este mai
mică decat Integer.MIN_VALUE
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 (2p) Definiți clasele de mai jos care vor extinde LoggerBase și implementa
metoda writeMessage:
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:
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.
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.
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
Singleton este utilizat des în situații în care avem obiecte care trebuie accesate din mai multe
locuri ale aplicației:
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
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).
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:
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
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.
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.
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.
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.
Subiect
nu trebuie să știe ce fac observatorii, trebuie doar să mențină referințe către obiecte de
acest tip
când apar modificări (e.g. se schimbă starea sa, valorile unor variabile etc) notifică
toți observatorii
Observator
ca implementare:
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
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
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.
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
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 :))
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ță.
5. Notificați observatorii lui World când eroii execută o acțiune. Aveți două
TODO-uri în clasa Hero.
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:
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.
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.
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.
o accounting
Exemple de utilizare:
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).
Invoker - comandantul
Receiver - comandatul
Î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:
Structură:
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).
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 :))
(5p) Folosiți design pattern-ul Strategy pentru a implementa logica de atac a unui
monstru.
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.
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?
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
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();
}
}
}
B. 1, 2, 3, 1, 3
C. 1, 1, 3
D. 1, 1, 2, 3
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();
}
}
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();
}
}
B. Se va afișa 5
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ă
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");
}
}
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) {...}
}
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()