Clasele declarate în interiorul unei alte clase se numesc clase interne (nested classes). Acestea permit gruparea claselor care sunt legate logic și controlul vizibilității uneia din cadrul celorlalte.
Clasele interne sunt de mai multe tipuri:
public
, final
, abstract
dar și private
, protected
și static
, însumând modificatorii claselor obișnuite și cei permiși metodelor și variabilelor
O clasă internă este definită în interiorul unei clase și poate fi accesată doar la runtime printr-o instanță a clasei externe (la fel ca metodele și variabilele ne-statice). Compilatorul creează fișiere .class separate pentru fiecare clasă internă, în exemplul de mai jos generând fișierele Outer.class
și Outer$Inner.class
, însă execuția fișierului Outer$Inner.class
nu este permisă.
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;
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ă.
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.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
.Î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; } }; } } [...]
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:
return
, folosind new
(referința întoarsă de new
va fi upcast
la clasa de bază: Engine
)tipul
Engine
, prin urmare, va implementa metoda/metodele din interfață(cum e metoda getFuelCapacity
). Corpul clasei urmeaza imediat instanțierii.Clasele anonime nu pot avea constructori din cauză că nu au nume (nu am ști cum să numim constructorii). Această restricție asupra claselor anonime ridică o problemă: în mod implicit, clasă de bază este creată cu constructorul default.
Ce se întâmplă dacă dorim să invocăm un alt constructor al clasei de bază? În clasele normale acest lucru era posibil prin apelarea explicită, în prima linie din constructor a constructorului clasei de bază cu parametrii doriți, folosind super
. În clasele interne acest lucru se obține prin transmiterea parametrilor către constructorul clasei de bază direct la crearea obiectului de tip clasă anonimă:
new Engine("Otto") { // ... }
În acest exemplu, am instanțiat o clasa anonimă, ce implementează interfața Engine
, apelând constructorul clasei de bază cu parametrul “Otto”
.
În secțiunile precedente, s-a discutat doar despre clase interne ale caror instanțe există doar în contextul unei instanțe a clasei exterioare, astfel că poate accesa membrii obiectului exterior direct. De asemenea, am menționat că fiind membri ai claselor exterioare, clasele interne pot avea modificatorii disponibili pentru metode și variabile, dintre care și static
(clasele exterioare nu pot fi statice!). Așa cum pentru a accesa metodele și variabilele statice ale unei clase nu este nevoie de o instanță a aceteia, putem obține o referință către o clasă internă fără a avea nevoie de o instanță a clasei exterioare.
Pentru a înțelege diferența dintre clasele interne statice și cele nestatice trebuie să reținem următorul aspect: clasele nestatice țin legătura cu obiectul exterior în vreme ce clasele statice nu păstrează această legătură.
Pentru clasele interne statice:
OttoEngine
pentru a fi statică.new Car.OttoEngine()
.Car
, putem instanția clasa internă statică folosind doar new OttoEngine()
, datorită contextului.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.
În exemplul următor, clasa internă a fost declarată în interiorul funcției getInnerInstance
. În acest mod, vizibilitatea ei a fost redusă pentru ca nu poate fi instanțiată decât în această funcție.
Singurii modificatori care pot fi aplicați acestor clase sunt abstract
și final
(binențeles, nu amândoi deodată).
[...] class Car { public Engine getEngine() { class OttoEngine implements Engine { private int fuelCapacity = 11; public int getFuelCapacity() { return fuelCapacity; } } return new OttoEngine(); } } [...]
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.
Exemplu de clasa internă declarata într-un bloc:
[...] class Car { public Engine getEngine(int fuelCapacity) { if (fuelCapacity == 11) { class OttoEngine implements Engine { private int fuelCapacity = 11; public int getFuelCapacity() { return fuelCapacity; } } return new OttoEngine(); } return null; } } [...]
Î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ă.
Deoarece constructorul clasei interne trebuie sa se atașeze de un obiect al clasei exterioare, moștenirea unei clase interne este puțin mai complicată decât cea obișnuită. Problema rezidă în nevoia de a inițializa legătura (ascunsă) cu clasa exterioară, în contextul în care în clasa derivată nu mai există un obiect default pentru acest lucru.
class Car { class Engine { public void getFuelCapacity() { System.out.println("I am a generic Engine"); } } } class OttoEngine extends Car.Engine { OttoEngine() { } // EROARE, avem nevoie de o legatura la obiectul clasei exterioare OttoEngine(Car car) { // OK car.super(); } } public class Test { public static void main(String[] args) { Car car = new Car(); OttoEngine ottoEngine = new OttoEngine(car); ottoEngine.getFuelCapacity(); } }
Observăm ca OttoEngine
moșteneste doar Car.Engine
însa sunt necesare:
OttoEngine
trebuie sa fie de tipul clasei externă (Car
)OttoEngine
: car.super()
.OttoEngine()
, rulați din nou codulClasele interne pot părea un mecanism greoi și uneori artificial. Ele sunt însă foarte utile în următoarele situații:
button.addActionListener(new ActionListener() { //interfata implementata e ActionListener public void actionPerformed(ActionEvent e) { numClicks++; } });
Task 1 - Meta (4p)
Îndrumarul este o componentă importantă a laboratorului de POO, iar citirea și înțelegerea acestuia nu trebuie neglijate. În îndrumarul din acest laborator există mai multe căsuțe de tip note, de forma celei de mai jos:
Identificați toate căsuțele de tip note din laborator și urmați instrucțiunile din acestea. Puteți folosi scheletul pus la dispoziție. Pentru a ușura procesul de evaluare, creați fișiere separate pentru fiecare task din note.
Task 2 - Car Dealership: The Beginning (2p)
Scheletul de cod conține implementarea unui dealership de mașini. Acesta vinde autoturisme doar la cerere, conform cu tipul de mașină cerut de client. Totuși, patronul este nemulțumit. El ar dori ca:
Hint: aceasta este o decizie de business teribilă din partea patronului, dar acesta v-a promis că veți putea închiria orice mașină de la el, dar doar după ce îl ajutați.
Task 3 - Car Dealership: The Crisis (2p)
Timpul a trecut peste dealership, iar vânzările au fost chiar bune, cu o singură excepție, mașinile de tip RACING
. Aceste tipuri de mașini sunt făcute pe comandă, iar cererea a fost atât de mică, încât nici după un an nu s-au vândut cele din primul lot. Astfel, dealership-ul riscă să dea faliment, din cauza datoriilor generate (mașinile de tip RACING
sunt și cele mai scumpe).
Din fericire, patronul a făcut o înțelegere cu Ferrari
, de unde a și cumpărat mașinile. Aceștia îl vor scăpa complet de datorii dacă acesta va elimina orice asociere dintre Dealership
și Ferrari
, lăsând totuși posibilitatea pentru clienți de a-și ridica mașinile de la Dealership
. Patronul este disperat și vă cere din nou ajutorul. Va trebui să eliminați orice asociere dintre cele două entități, păstrând funcționalitatea aplicației.
Hint: Clienții vor mai putea să își ridice mașinile Ferrari, dar acestea nu vor mai fi administrate de către Dealership.
Task 4 - Car Dealership: The New Age (2p)
Business-ul merge bine, astfel că patronul nostru s-a hotărât să se extindă. El vrea să aducă în Dealership
mașini de tip ELECTRIC
, Tesla
, și a vorbit chiar cu Elon Musk în acest sens. Această extindere este una importantă, astfel că patronul nostru vrea o performanță cât mai ridicată. Patronul vă roagă să îl ajutați cu o comparație între:
Tesla
folosind clase anonimeTesla
folosind funcții lambda
Un tutorial despre folosirea funcțiilor lambda îl găsiți în laborator, la secțiunea Clase anonime. (opțional) Pentru o mică prezentare în discuția Anonymous vs. Lambda
, puteți verifica secțiunea de Referințe
a laboratorului.