This shows you the differences between two versions of the page.
— |
poo-ca-cd:arhiva:laboratoare:2024:clase-abstracte-interfete [2025/09/27 10:41] (current) florian_luis.micu created |
||
---|---|---|---|
Line 1: | Line 1: | ||
+ | ===== Laboratorul 5: Abstractizare ===== | ||
+ | |||
+ | **Video introductiv:** [[https://www.youtube.com/watch?v=R9KO-GHbNUA|link]] | ||
+ | |||
+ | ==== Obiective ==== | ||
+ | Scopul acestui laborator este de a înțelege principiul **abstractizării**, prin prezentarea conceptelor de **interfață** și de **clasă abstractă** și utilizarea lor în limbajul Java. | ||
+ | |||
+ | Conceptele de metode și clase abstracte și de interfețe sunt prezente și în alte limbaje OOP, fiecare cu particularitățile lor de sintaxă. Este important ca în urma acestui laborator să înțelegeți ce reprezintă și situațiile în care să le folosiți. | ||
+ | |||
+ | Aspectele urmărite sunt: | ||
+ | * Prezentarea interfețelor și claselor abstracte | ||
+ | * Moștenirea în cazul interfețelor si claselor abstracte | ||
+ | * Diferențele dintre interfețe și clase abstracte | ||
+ | |||
+ | ==== Introducere ==== | ||
+ | |||
+ | Fie următorul exemplu (Thinking in Java) care propune o ierarhie de clase pentru a descrie o suită de instrumente muzicale, cu scopul demonstrării polimorfismului: | ||
+ | |||
+ | {{:poo-ca-cd:laboratoare:clase-abstracte-interfete:instrument.jpg}} | ||
+ | |||
+ | 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. | ||
+ | |||
+ | ==== Abstractizare ==== | ||
+ | |||
+ | Abstractizarea este unul dintre cele 4 principii POO de bază (Abstractizare, Încapsulare, Moștenire, Polimorfism). Abstractizarea nu permite ca anumite caracteristici să fie vizibile în exterior. Acest lucru se referă la construirea unei interfețe comune, a unui șablon general pe care se bazează o anumită categorie de obiecte, fără a descrie explicit caracteristicile fiecaruia. Obiectele cunosc interfața comună pe care o au dar nu și cum este ea interpretată de fiecare obiect in parte. Acest lucru este realizat prin utilizarea claselor abstracte și a interfețelor. | ||
+ | |||
+ | ==== Clase abstracte vs Interfețe ==== | ||
+ | |||
+ | Folosim o **clasă abstractă** atunci când vrem: | ||
+ | * să implementăm doar unele dintre metodele din clasă | ||
+ | * ca respectiva clasă să nu poată fi instanțiată | ||
+ | |||
+ | Folosim o **interfață** atunci când vrem: | ||
+ | * să avem doar o descriere a structurii, fără implementări | ||
+ | * ca interfața în cauză să fie folosită împreună cu alte interfețe în contextul moștenirii | ||
+ | |||
+ | == Clase abstracte == | ||
+ | |||
+ | Dorim să stabilim interfaţa comună pentru a putea crea funcţionalitate diferită pentru fiecare subtip și pentru a ști ce anume au clasele derivate în comun. | ||
+ | O clasă cu caracteristicile enumerate mai sus se numește **abstractă**. Creăm o clasă abstractă atunci când dorim să: | ||
+ | * manipulăm un set de clase printr-o **interfaţă comună** | ||
+ | * **reutilizăm** o serie metode si membrii din această clasă in clasele derivate. | ||
+ | Metodele suprascrise în clasele derivate vor fi apelate folosind [[http://en.wikipedia.org/wiki/Dynamic_binding_%28computer_science%29|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'': | ||
+ | <code java>abstract void f();</code> | ||
+ | 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: | ||
+ | <code java>abstract class Instrument { | ||
+ | ... | ||
+ | }</code> | ||
+ | Deoarece o clasă abstractă este incompletă (există metode care nu sunt definite), crearea unui obiect de tipul clasei este împiedicată de compilator. | ||
+ | |||
+ | ==== Clase abstracte în contextul moștenirii ==== | ||
+ | |||
+ | O clasă care moștenește o clasă abstractă este ea însăși abstractă daca nu implementează **toate** metodele abstracte ale clasei de bază. Putem defini clase abstracte care moștenesc alte clase abstracte ș.a.m.d. O clasă care poate fi instanţiată (adică nu este abstractă) și care moștenește o clasă abstractă trebuie să implementeze (definească) toate metodele abstracte pe lanţul moștenirii (ale tuturor claselor abstracte care îi sunt "părinţi"). | ||
+ | Este posibil să declarăm o **clasă abstractă fără** ca ea să aibă **metode abstracte**. Acest lucru este folositor când declarăm o clasă pentru care nu dorim instanţe (nu este corect conceptual să avem obiecte de tipul acelei clase, chiar dacă definiţia ei este completă). | ||
+ | |||
+ | Iată cum putem să modificăm exemplul instrument cu metode abstracte: | ||
+ | |||
+ | {{:poo-ca-cd:laboratoare:clase-abstracte-interfete:claseabstracteincontextlmostenirii.jpg|}} | ||
+ | == Interfeţe == | ||
+ | Interfeţele duc conceptul abstract un pas mai departe. Se poate considera că o interfaţă este o **clasă abstractă pură**: permite programatorului să stabilească o "formă" pentru o clasă (numele metodelor, lista de argumente, valori întoarse), dar fară **nicio implementare**. O interfaţă poate conţine câmpuri dar acestea sunt în mod implicit **static** și **final**. Metodele declarate în interfață sunt în mod implicit **public**. | ||
+ | |||
+ | Interfaţa este folosită pentru a descrie un **contract** între clase: o clasă care implementează o interfaţă va implementa metodele definite în interfaţă. Astfel orice cod care folosește o anumită interfaţă știe ce metode pot fi apelate pentru aceasta. | ||
+ | |||
+ | Pentru a crea o interfaţă folosim cuvântul cheie **interface** în loc de **class**. La fel ca în cazul claselor, interfaţa poate fi declarată ''public'' doar dacă este definită într-un fișier cu același nume ca cel pe care îl dăm acesteia. Dacă o interfaţă nu este declarată ''public'' atunci specificatorul ei de acces este ''package-private''. | ||
+ | Pentru a defini o **clasă** care este conformă cu o interfaţă anume folosim cuvântul cheie **implements**. Această relaţie este asemănătoare cu moștenirea, cu diferenţa că nu se moștenește comportament, ci doar "interfaţa". | ||
+ | Pentru a defini o **interfaţă** care moștenește altă interfaţă folosim cuvântul cheie **extends**. | ||
+ | Dupa ce o interfaţă a fost implementată, acea implementare devine o clasă obișnuită care poate fi extinsă prin moștenire. | ||
+ | |||
+ | Iata exemplul dat mai sus, modificat pentru a folosi interfeţe: | ||
+ | |||
+ | {{:poo-ca-cd:laboratoare:clase-abstracte-interfete:interfete.jpg|}} | ||
+ | |||
+ | Codul arată astfel: | ||
+ | |||
+ | <code java> | ||
+ | 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()"); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | Un exemplu pentru o interfață care extinde mai multe interfețe: | ||
+ | |||
+ | <code java> | ||
+ | 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) | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | Implicit, specificatorul de acces pentru membrii unei interfeţe este **public**. Atunci când implementăm o interfaţă trebuie să specificăm că funcţiile sunt public chiar dacă în interfaţă ele nu au fost specificate explicit astfel. Acest lucru este necesar deoarece specificatorul de acces implicit în clase este ''package-private'', care este **mai restrictiv** decât ''public''. | ||
+ | ==== Moștenire multiplă în Java ==== | ||
+ | Interfaţa nu este doar o formă "pură" a unei clase abstracte, ci are un scop mult mai înalt. Deoarece o interfaţă nu specifică niciun fel de implementare (nu există nici un fel de spaţiu de stocare pentru o interfaţă) este normal să "combinăm" mai multe interfeţe. Acest lucru este folositor atunci când dorim să afirmăm că "X este un A, un B si un C". Acest deziderat este moștenirea multiplă și, datorită faptului ca o singură entitate (A, B sau C) are implementare, nu apar problemele moștenirii multiple din C++. | ||
+ | |||
+ | {{:poo-ca-cd:laboratoare:clase-abstracte-interfete:mosteniremultipla.jpg|}} | ||
+ | |||
+ | <code java> | ||
+ | interface CanFight { | ||
+ | void fight(); | ||
+ | } | ||
+ | |||
+ | interface CanSwim { | ||
+ | void swim(); | ||
+ | } | ||
+ | |||
+ | interface CanFly { | ||
+ | void fly(); | ||
+ | } | ||
+ | |||
+ | class ActionCharacter { | ||
+ | public void fight() { | ||
+ | } | ||
+ | } | ||
+ | |||
+ | class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly { | ||
+ | |||
+ | public void swim() { | ||
+ | } | ||
+ | |||
+ | public void fly() { | ||
+ | } | ||
+ | } | ||
+ | |||
+ | public class Adventure { | ||
+ | |||
+ | static void t(CanFight x) { | ||
+ | x.fight(); | ||
+ | } | ||
+ | |||
+ | static void u(CanSwim x) { | ||
+ | x.swim(); | ||
+ | } | ||
+ | |||
+ | static void v(CanFly x) { | ||
+ | x.fly(); | ||
+ | } | ||
+ | |||
+ | static void w(ActionCharacter x) { | ||
+ | x.fight(); | ||
+ | } | ||
+ | |||
+ | public static void main(final String[] args) { | ||
+ | Hero hero = new Hero(); | ||
+ | |||
+ | t(hero); // Treat it as a CanFight | ||
+ | |||
+ | u(hero); // Treat it as a CanSwim | ||
+ | |||
+ | v(hero); // Treat it as a CanFly | ||
+ | |||
+ | w(hero); // Treat it as an ActionCharacter | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | Se observă că ''Hero'' combină clasa ''ActionCharacter'' cu interfeţele ''CanSwim'', ''CanFight'', ''CanFly''. 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'', ''CanFight'', ''CanFly'' si clasa ''ActionCharacter''. La fiecare apel de metodă din ''Adventure'' se face **upcast** de la obiectul ''Hero'' la clasa sau interfaţa dorită de metoda respectivă. | ||
+ | |||
+ | ==== Coliziuni de nume la combinarea interfeţelor ==== | ||
+ | Combinarea unor interfețe care conţin o metodă cu același nume este posibilă doar dacă metodele nu au tipuri întoarse diferite și aceeași listă de argumente. Totuși este preferabil ca în interfețe diferite care trebuie combinate să nu existe metode cu același nume deoarece acest lucru poate duce la confuzii evidente (sunt amestecate în acest mod 3 concepte: ''overloading'', ''overriding'' și ''implementation''). | ||
+ | |||
+ | == Extinderea interfeţelor == | ||
+ | Se pot adăuga cu ușurinţă metode noi unei interfeţe prin extinderea ei într-o altă interfaţă: | ||
+ | |||
+ | Exemplu: | ||
+ | <code java> | ||
+ | interface Monster { | ||
+ | void menace(); | ||
+ | } | ||
+ | |||
+ | interface DangerousMonster extends Monster { | ||
+ | void destroy(); | ||
+ | } | ||
+ | </code> | ||
+ | Deoarece câmpurile unei interfeţe sunt automat **static** și **final**, interfeţele sunt un mod convenabil de a crea grupuri de constante, asemănătoare cu ''enum-urile'' din C , C++. | ||
+ | |||
+ | ==== Iniţializarea câmpurilor în interfeţe ==== | ||
+ | În interfeţe **nu** pot exista **blank finals** (câmpuri ''final'' neiniţializate) însă pot fi iniţializate cu **valori neconstante**. Câmpurile fiind statice, ele vor fi iniţializate prima oară când clasa este iniţializată. | ||
+ | |||
+ | ==== Exerciţii ==== | ||
+ | <note important> | ||
+ | Pentru a încărca soluția, va trebui să accesați link-ul https://code.devmind.ro/login, să intrați pe tab-ul Contests, unde veți găsi laboratorul grupei voastre. | ||
+ | |||
+ | </note> | ||
+ | |||
+ | 1. (**2 puncte**) Implementaţi interfaţa ''Task'' în cele 3 moduri de mai jos: | ||
+ | * Un task (''OutTask.java'') care să afișeze un mesaj la output. Mesajul este specificat în contructorul clasei. | ||
+ | * Un task (''RandomOutTask.java'') care genereaza un număr aleator de tip ''int'' și afișeaza un mesaj cu numărul generat la output. Generarea se va realiza în constructor utilizandu-se o instanță globală a unui obiect de tip [[https://docs.oracle.com/javase/8/docs/api/java/util/Random.html#Random-long-|Random]] care a fost inițializat cu seed-ul **12345**. | ||
+ | * Un task (''CounterOutTask.java'') care incrementeaza un contor global și afișează valoarea contorului după fiecare incrementare. | ||
+ | |||
+ | <note>**Notă**: Acesta este un exemplu simplu pentru [[http://en.wikipedia.org/wiki/Command_pattern|Command Pattern]]</note> | ||
+ | |||
+ | {{:poo-ca-cd:laboratoare:clase-abstracte-interfete:ex1.png?600|}} | ||
+ | |||
+ | 2. (**3 puncte**) Interfaţa ''Container'' specifică interfaţa comună pentru colecţii de obiecte Task, în care se pot adăuga și din care se pot elimina elemente. | ||
+ | |||
+ | Interfața conține metodele: | ||
+ | * ''pop():Task'' | ||
+ | * ''push(Task):void'' | ||
+ | * ''size():int'' | ||
+ | * ''isEmpty():boolean'' | ||
+ | * ''transferFrom(Container):void'' | ||
+ | |||
+ | Creaţi două tipuri de containere care implementează această clasă: | ||
+ | * ''Stack'' - care implementează o strategie de tip [[https://en.wikipedia.org/wiki/Stack_(abstract_data_type)|LIFO]] | ||
+ | * ''Queue'' - care implementează o strategie de tip [[https://en.wikipedia.org/wiki/Queue_(abstract_data_type)|FIFO]] | ||
+ | |||
+ | **Bonus**: Incercați să evitaţi codul similar în locuri diferite! | ||
+ | |||
+ | {{:poo-ca-cd:laboratoare:clase-abstracte-interfete:ex2.png?600|}} | ||
+ | |||
+ | 3. (**2 puncte**) Creați 4 interfețe: ''Minus'', ''Plus'', ''Mult'', ''Div'' care conțin câte o metodă aferentă numelui ce are ca argument un număr de tipul float. | ||
+ | |||
+ | Spre exemplu interfața ''Minus'' va declara metoda: | ||
+ | <code java> | ||
+ | void minus(float value); | ||
+ | </code> | ||
+ | Scrieți clasa ''Operation'' care să implementeze toate aceste interfețe. Pe lânga metodele interfețelor implementate aceasta va conține: | ||
+ | * un câmp ''number'' de tipul ''float'' asupra căruia se vor efectua operațiile implementate | ||
+ | * o metodata getter ''getNumber'' care va returna valoarea câmpului ''value'' | ||
+ | * un constructor care primește ca parametru valoarea inițială pentru campul ''value'' | ||
+ | |||
+ | Pentru cazul în care metoda div este apelată cu valoare 0 operația nu va fi efectuată și se va afișa mesajul "Division by 0 is not possible". | ||
+ | |||
+ | 4. (**3 puncte**) Implementaţi clasa ''Song'' și clasa abstracta ''Album''. | ||
+ | |||
+ | Song: | ||
+ | * stochează atributele ''name'' de tip ''String''. ''id'' de tip ''int'' si ''composer'' de tip ''String'' | ||
+ | * are un constructor care va inițializa atributele specificate anterior | ||
+ | * implementează metodele de tip getter și setter pentru fiecare atribut | ||
+ | * suprascrie metoda ''toString()'' care va returna un ''String'' de forma "Song{name=''name'', id=''id'', composer=''composer''}" | ||
+ | |||
+ | Album: | ||
+ | * stochează o listă de cântece | ||
+ | * declară metoda abstractă numită addSong care primește un Song și nu returnează nimic | ||
+ | * implementează metoda removeSong care primește un song și nu returnează nimic | ||
+ | * suprascrie metoda ''toString()'' care va returna un ''String'' de forma "Album{songs=[Song, Song, ...]}" | ||
+ | |||
+ | După implementarea claselor ''Song'' și ''Album'', creați clasele ''DangerousAlbum'', ''ThrillerAlbum'' și ''BadAlbum'', care vor moșteni clasa ''Album'' și vor implementa metoda ''addSong'' după următoarele reguli: | ||
+ | * ''DangerousAlbum'' conține doar melodii cu id-ul număr prim | ||
+ | * ''ThrillerAlbum'' conține melodii scrise doar de ''Michael Jackson'' și au id-ul număr par | ||
+ | * ''BadAlbum'' conține doar melodii care au nume de 3 litere și id număr palindrom | ||
+ | |||
+ | În cazul în care criteriul de adăugare specific unui album nu este respectat, melodia nu va fi adaugată în acesta. | ||