Video introductiv: link
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:
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.
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.
Folosim o clasă abstractă atunci când vrem:
Folosim o interfață atunci când vrem:
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ă:
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.
Pentru a exprima faptul că o metodă este abstractă (adică se declară doar interfaţa ei, nu și implementarea), Java folosește cuvântul cheie abstract
:
abstract void f();
O clasă care conţine metode abstracte este numită clasă abstractă. Dacă o clasă are una sau mai multe metode abstracte atunci ea trebuie să conţină în definiţie cuvântul abstract
.
Exemplu:
abstract class Instrument { ... }
Deoarece o clasă abstractă este incompletă (există metode care nu sunt definite), crearea unui obiect de tipul clasei este împiedicată de compilator.
O clasă care moștenește o clasă abstractă este ea însăși abstractă daca nu implementează toate metodele abstracte ale clasei de bază. Putem defini clase abstracte care moștenesc alte clase abstracte ș.a.m.d. O clasă care poate fi instanţiată (adică nu este abstractă) și care moștenește o clasă abstractă trebuie să implementeze (definească) toate metodele abstracte pe lanţul moștenirii (ale tuturor claselor abstracte care îi sunt “părinţi”). Este posibil să declarăm o clasă abstractă fără ca ea să aibă metode abstracte. Acest lucru este folositor când declarăm o clasă pentru care nu dorim instanţe (nu este corect conceptual să avem obiecte de tipul acelei clase, chiar dacă definiţia ei este completă).
Iată cum putem să modificăm exemplul instrument cu metode abstracte:
Interfeţele duc conceptul abstract un pas mai departe. Se poate considera că o interfaţă este o clasă abstractă pură: permite programatorului să stabilească o “formă” pentru o clasă (numele metodelor, lista de argumente, valori întoarse), dar fară nicio implementare. O interfaţă poate conţine câmpuri dar acestea sunt în mod implicit static și final. Metodele declarate în interfață sunt în mod implicit public.
Interfaţa este folosită pentru a descrie un contract între clase: o clasă care implementează o interfaţă va implementa metodele definite în interfaţă. Astfel orice cod care folosește o anumită interfaţă știe ce metode pot fi apelate pentru aceasta.
Pentru a crea o interfaţă folosim cuvântul cheie interface în loc de class. La fel ca în cazul claselor, interfaţa poate fi declarată public
doar dacă este definită într-un fișier cu același nume ca cel pe care îl dăm acesteia. Dacă o interfaţă nu este declarată public
atunci specificatorul ei de acces este package-private
.
Pentru a defini o clasă care este conformă cu o interfaţă anume folosim cuvântul cheie implements. Această relaţie este asemănătoare cu moștenirea, cu diferenţa că nu se moștenește comportament, ci doar “interfaţa”.
Pentru a defini o interfaţă care moștenește altă interfaţă folosim cuvântul cheie extends.
Dupa ce o interfaţă a fost implementată, acea implementare devine o clasă obișnuită care poate fi extinsă prin moștenire.
Iata exemplul dat mai sus, modificat pentru a folosi interfeţe:
Codul arată astfel:
interface Instrument { // Compile-time constant: int FIELD = 5; // static & final // Cannot have method definitions: void play(); // Automatically public String what(); void adjust(); } class WindInstrument implements Instrument { public void play() { System.out.println("WindInstrument.play()"); } public String what() { return "WindInstrument"; } public void adjust() { } } class Trumpet extends WindInstrument { public void play() { System.out.println("Trumpet.play()"); } public void adjust() { System.out.println("Trumpet.adjust()"); } }
Un exemplu pentru o interfață care extinde mai multe interfețe:
interface A{ void a1(); void a2(); } interface B { int x = 0; void b(); } interface C extends A, B { // this interface will expose // * all the methods declared in A and B (a1, a2 and b) // * all the fields declared in A and B (x) }
Implicit, specificatorul de acces pentru membrii unei interfeţe este public. Atunci când implementăm o interfaţă trebuie să specificăm că funcţiile sunt public chiar dacă în interfaţă ele nu au fost specificate explicit astfel. Acest lucru este necesar deoarece specificatorul de acces implicit în clase este package-private
, care este mai restrictiv decât public
.
Interfaţa nu este doar o formă “pură” a unei clase abstracte, ci are un scop mult mai înalt. Deoarece o interfaţă nu specifică niciun fel de implementare (nu există nici un fel de spaţiu de stocare pentru o interfaţă) este normal să “combinăm” mai multe interfeţe. Acest lucru este folositor atunci când dorim să afirmăm că “X este un A, un B si un C”. Acest deziderat este moștenirea multiplă și, datorită faptului ca o singură entitate (A, B sau C) are implementare, nu apar problemele moștenirii multiple din C++.
interface CanFight { void fight(); } interface CanSwim { void swim(); } interface CanFly { void fly(); } class ActionCharacter { public void fight() { } } class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly { public void swim() { } public void fly() { } } public class Adventure { static void t(CanFight x) { x.fight(); } static void u(CanSwim x) { x.swim(); } static void v(CanFly x) { x.fly(); } static void w(ActionCharacter x) { x.fight(); } public static void main(final String[] args) { Hero hero = new Hero(); t(hero); // Treat it as a CanFight u(hero); // Treat it as a CanSwim v(hero); // Treat it as a CanFly w(hero); // Treat it as an ActionCharacter } }
Se observă că Hero
combină clasa ActionCharacter
cu interfeţele CanSwim
, 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ă.
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
).
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++.
Î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ă.
1. (2p) Implementaţi interfaţa Task
(din pachetul lab5.task1
) în cele 3 moduri de mai jos.
OutTask.java
)RandomOutTask.java
)CounterOutTask.java
).
2. (3p) Interfaţa Container
(din pachetul lab5.task2
) 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ă:
Stack
- care implementează o strategie de tip LIFOQueue
- care implementează o strategie de tip FIFOBonus: Incercați să evitaţi codul similar în locuri diferite!
ArrayList<Task> list = new ArrayList<Task>();
3. (2p) În pachetul lab5.task3
creați 4 interfețe: Minus
, Plus
, Mult
, Div
care conțin câte o metodă aferentă numelui ce are ca argument un numar de tipul float. Scrieți clasa Operation
care să implementeze toate aceste interfețe și care are un câmp de tipul float ce va fi modificat de fiecare metodă implementată de voi.
4. (3p) În pachetul lab5.task4
scrieți clasa Song
ce are ca atribute: name
(String), id
(int), composer
(String) și clasa abstractă Album
care are o listă de cântece (puteți folosi ArrayList) metoda abstractă addSong
și metodele neabstracte removeSong
, toString
. Apoi, creați clasele DangerousAlbum
, ThrillerAlbum
, BadAlbum
care să moștenescă clasa Album
. 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 iar clasa BadAlbum
conține doar melodii care au nume de 3 litere și id număr palindrom.