Administrativ
Laboratoare
Tema
Teste
Resurse utile
Alte resurse
Arhiva Teme
Administrativ
Laboratoare
Tema
Teste
Resurse utile
Alte resurse
Arhiva Teme
This is an old revision of the document!
Scopul acestui laborator este prezentarea conceptului de clasă internă și modalitățile de creare și folosire a claselor interne în Java.
Aspectele urmărite sunt:
Clasele declarate în interiorul unei alte clase se numesc clase interne (nested classes) și reprezintă o funcționalitate importantă deoarece permit gruparea claselor care sunt legate logic și controlul vizibilității uneia din cadrul celorlalte.
Clasele interne sunt de mai multe tipuri, în funcție de modul de a le instanția și de relația lor cu clasa exterioră:
private
. În plus, aceasta poate avea modificatorii permiși metodelor și variabilelelor claselor. Astfel, o clasa internă poate fi nu numai public
, final
, abstract
dar și private
, protected
și static
.
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ă.
class Car { class Engine { private int fuelCapacity; public Engine(int fuelCapacity) { this.fuelCapacity = fuelCapacity; } public int getFuelCapacity() { return fuelCapacity; } } public Engine getEngine() { Engine engine = new Engine(11); return engine; } } public class Test { public static void main(String[] args) { Car car = new Car(); Car.Engine firstEngine = car.getEngine(); Car.Engine secondEngine = car.new Engine(10); System.out.println(firstEngine.value()); System.out.println(secondEngine.value()); } }
În exemplul de mai sus, o dată ce avem o instanță a clasei Car, sunt folosite două modalități de a obține o instanță a clasei Engine
(definită în interiorul clasei Car
):
getEngine
, care creează și întoarce o astfel de instanță;Engine
; observați cu atentie sintaxa folosita! Pentru a instanția Engine
, avem nevoie de o instanta Car
: car.new Engine(10);
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.
interface Engine { public int getFuelCapacity(); } class Car { private class FerrariEngine implements Hidden { private int fuelCapacity; public FerrariEngine(int fuelCapacity) { this.fuelCapacity = fuelCapacity; } public int getFuelCapacity() { return fuelCapacity; } } public Engine getEngine() { FerrariEngine engine = new FerrariEngine(11); return engine; } } public class Car { public static void main(String[] args) { Car car = new Car(); Car.FerrariEngine firstEngine = car.getEngine(); // va genera eroare, deoarece // tipul Car.FerrariEngine nu este vizibil Car.FerrariEngine secondEngine = new Car().new FerrariEngine(10); // din nou eroare Engine thirdEngine = car.getEngine(); // acces corect la o instanta FerrariEngine System.out.println(thirdEngine.getFuelCapacity()); } }
Observați definirea interfeței Engine
. Ea este utilă pentru a putea asocia clasei FerrariEngine
un tip
, care să ne permita folosirea instanțelor acesteia, altfel tipul ei nu ar fi fost vizibil pentru ca a fost declarată private
. Observați, de asemenea, încercarile eronate de a instanția FerrariEngine
. Cum clasa internă a fost declarată private
, acest tip nu mai este vizibil in exteriorul clasei Car
.
Exista multe situații în care o clasă internă este instanțiată într-un singur loc (și este folosită prin upcasting
la o clasă de bază sau interfață), ceea ce face ca numele clasei să nu mai fie important, iar tipul ei poate fi un subtip al unei clase sau o implementare a unei interfețe. Singurele metode care pot fi apelate pe o clasa anonimă sunt cele ale tipului pe care îl extinde sau implementează.
În Java putem crea clase interne anonime (fără nume) ca în exemplul următor:
interface Engine { public int getFuelCapacity(); } class Car { public Engine getEngine(int fuelCapacity) { return new Engine () { private int fuelCapacity = 11; public int getFuelCapacity() { return fuelCapacity; } }; } } public class Test { public static void main(String[] args) { Car car = new Car(); Engine engine = car.getEngine(11); System.out.println(engine.getFuelCapacity()); } }
Observați modalitatea de declarare a clasei anonime. Sintaxa return new Engine() { … }
reprezintă urmatoarele:
Engine
return
, folosind new
(referința întoarsă de new
va fi upcast
la clasa de baza: Engine
)tipul
Engine
, prin urmare, va implementa metoda/metodele din interfață(cum e metoda value
). Corpul clasei urmeaza imediat instanțierii.
Construcția return new Engine() { … }
este echivalentă cu a spune: creează un obiect al unei clase anonime ce implementeaza Engine
.
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 Student("Andrei") { // ... }
În acest exemplu, am instanțiat o clasa anonimă, ce extinde clasa Student
, apelând constructorul clasei de bază cu parametrul “Andrei”
.
Î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:
class Student { public int grade = 9; class Gradebook { private int i = 1; public int updateGrade() { return i + Student.this.grade; // OK, putem accesa un membru al clasei exterioare } } static class EvilGradebook { public int k = 99; public int updateGrade() { k += grade; // EROARE, nu putem accesa un membru nestatic al clasei exterioare return k; } } } public class Test { public static void main(String[] args) { Student student = new Student(); Student.Gradebook out = student.new Gradebook(); // instanțiere CORECTĂ pentru o clasă nestatică Student.EvilGradebook badStudent = student.new EvilGradebook(); // instanțiere INCORECTĂ a clasei statice Student.EvilGradebook smartStudent = new Student.EvilGradebook(); // instanțiere CORECTĂ a clasei statice } }
În exemplul de mai sus se observă că folosirea membrului nestatic grade
în clasa statică EvilGradebook
este incorectă. De asemenea, se observă modalitățile diferite de instanțiere a celor două tipuri de clase interne (statice și nestatice):
student
(ca și în exemplele anterioare) pentru a instanția o clasă nestatică. student
este incorectă. 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ă).
interface Engine { public int getFuelCapacity(); } class Car { public Engine getEngine() { class FerrariEngine implements Engine { private int fuelCapacity = 11; public int getFuelCapacity() { return fuelCapacity; } } return new FerrariEngine(); } } public class Test { public static void main(String[] args) { Car car = new Car(); Car.FerrariEngine badEngine = car.getEngine(); // EROARE: clasa FerrariEngine nu este vizibila Engine goodEngine = car.getEngine(); System.out.println(goodEngine.getFuelCapacity()); } }
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.
public void f() { final Student s = new Student(); // s trebuie declarat final ca sa poata fi accesat din AlterStudent class AlterStudent { public void alterStudent() { s.name = "Andrei" // OK s = new Student(); // GRESIT! } } }
Exemplu de clasa internă declarata într-un bloc:
interface Engine { public int getFuelCapacity(); } class Car { public Engine getEngine(int fuelCapacity) { if (fuelCapacity == 11) { class FerrariEngine implements Engine { private int fuelCapacity = 11; public int getFuelCapacity() { return fuelCapacity; } } return new FerrariEngine(); } return null; } }
În acest exemplu, clasa internă FerrariEngine
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 FerrariEngine extends Car.Engine { FerrariEngine() { } // EROARE, avem nevoie de o legatura la obiectul clasei exterioare FerrariEngine(Car car) { // OK car.super(); } } public class Test { public static void main(String[] args) { Car car = new Car(); FerrariEngine ferrariEngine = new FerrariEngine(wi); ferrariEngine.getFuelCapacity(); } }
Observăm ca FerrariEngine
moșteneste doar Car.Engine
însa sunt necesare:
FerrariEngine
trebuie sa fie de tipul clasei externă (Car
)FerrariEngine
: car.super()
.Clasele 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++; } });
Implementați un terminal bash simplu pornind de la scheletul de cod. Comenzile pe care va știi să le execute sunt: echo, cd, ls și history. Bash-ul va citi comenzi de la tastatură până când va primi comanda exit când se va închide (programul se termină).
1. (1.5p) Din clasa Bash vom publica comenzile prin interfața CommandPublisher. În acest sens în clasa Bash vom crea o clasă internă BashCommandPublisher ce implementează interfața CommandPublisher.
2. (1p) Un obiect de tipul Bash va avea:
3. (2p) În metoda start din Bash vom citi de la tastatură comenzi pe câte o linie folosind Scanner.
Thread t = new Thread() { public void run() { // Do some work } }; t.start();
4. (1p) Implementați comanda echo ca o clasă internă în BashUtils.
5. (1.5p) Implementați comanda cd care schimbă directorul curent.
6. (2p) Implementați comanda ls care afișează conținutul directorului curent currentDirectory
File folder = currentDirectory.toFile();
7. (1p) Implementați comanda history care afișează la consolă conținutul membrului StringBuffer history din Bash.
>history History is: ls | | cd .idea | ls | history |