Table of Contents

Laboratorul 6: Clase interne

Video introductiv: link

Slides din video: link

Obiective

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.

Clasele interne sunt de mai multe tipuri:

  • O clasă internă se comportă ca un membru al clasei în care a fost declarată
  • O clasă internă are acces la toți membrii clasei în care a fost declarată, inclusiv cei private
  • O clasă internă poate fi public, final, abstract dar și private, protected și static, însumând modificatorii claselor obișnuite și cei permiși metodelor și variabilelor

Clase interne "normale"

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ă.

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;

Modificatorii de acces pentru clase interne

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, dacă modificăm clasa OttoEngine pentru a fi privată apar erori de compilare.

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).

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:

Limitări

Clasele interne anonime declarate în metode pot folosi variabilele declarate în metoda respectivă și parametrii metodei dacă aceștia sunt final sau effectively final. Dacă o variabilă nu e declarată final dar nu se modifică schimbă după inițializare, atunci este effectively final.

Variabilele si parametrii metodelor se află pe segmentul de stivă din 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.

Până la Java 8 nu exista conceptul de effectively final si toate aceste variabile trebuiau declarate final

Expresii Lambda

Când veți scrie o clasă anonimă într-un IDE cum e IntelliJ, veți primi un warning care vă recomandă să o transformați într-o lambda. Aceasta recomandare este valabilă doar pentru implementarea claselor/interfețelor cu o singură metoda.

Lambda este un concept din programarea funcțională (o să îl învățați în semestrul 2 la Paradigme de Programare) și reprezintă o funcție anonimă. Majoritatea limbajelor de nivel înalt au introdus suport pentru acest concept în ultimii 15 ani, inclusiv Java, din versiunea 8.

În exemplul următor am transformat clasa anonimă din secțiunea precedentă. Atenție! IDE-ul nu ar fi recomandat pentru cazul din acel exemplu transformarea în lambda pentru că aveam definită o variabilă private int fuelCapacity = 11, deci codul nu era echivalent unei simple implementări de funcții. Eliminănd acea variabilă constantă și punând-o în metodă am putut face transformarea.

class Car {
    public Engine getEngine() {
        return () -> 11; // expresie lambda
    }
}

În codul de mai sus declararea clasei anonime și suprascrierea metodei din getFuelCapacity() a fost înlocuită cu o expresie lambda. O altă situație des intalnită de folosire a lambda-urilor este pentru transmiterea de funcții ca parametru iar api-uri precum cel de filtrare al colecțiilor le utilizează intens (exemplu).

În exemplul de mai jos aveți niște exemple simple de folosire lambda ca paremetru pentru metode. Parametrii primiți în lambda pot fi zero (() ca în exemplul cu fuelCapacity) sau mai mulți (param1, param2, …).

    public static void main(String[] args) {
 
        ArrayList<Integer> values = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
 
        values.removeIf((v) -> v % 2 == 0);           // parametru o lambda care intoarce daca numărul primit este par sau nu
 
        values.forEach((v) -> System.out.println(v)); // parametru o lambda care afiseaza argumentul primit
        values.forEach(System.out::println);          // referinta catre metoda println
    }
}

Operatorul :: este folosit pentru referințierea metodelor.

O să mai oferim exemple și detalii despre lambdas și în laboratoarele următoare, de exemplu cel cu Colecții sau cel pentru concepte din Java 8.

Clase interne statice

În secțiunile precedente, s-a discutat despre clase interne ale caror instanțe există doar în contextul unei instanțe a clasei exterioare, astfel că pot accesa membrii obiectului exterior direct.

Clasele interne le vedem ca pe membri ai claselor exterioare, așa cum sunt câmpurile și metodele. De accea, ele pot lua toți modificatorii disponibili membrilor, printre ei aflându-se modificatori pe care clasele exterioare nu le pot avea (e.g. private, static).

Așa cum pentru a accesa metodele și variabilele statice ale unei clase nu este nevoie de o instanță a acesteia, putem obține o referință către o clasă internă fără a avea nevoie de o instanță a clasei exterioare.

Pentru clasele interne statice:

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.

Avem o clasă internă MyOuterClass.MyInnerClass, de ce să o facem statică?

  • Dacă în interiorul clasei MyInnerClass nu avem nevoie de nimic specific instanței clasei externe MyOuterClass, deci nu avem nevoie de o instanță a acesteia o putem face 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.

Click aici pentru exemplu de clasă internă declarată în metodă

Click aici pentru exemplu de clasă internă declarată în metodă

În exemplul următor, clasa internă a fost declarată în interiorul funcției getEngine. Î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ă).

Pentru a accesa variabile declarate în metodă respectivă sau parametri ai acesteia, ele trebuie să fie final. Vedeți explicația în secțiunea despre clasele anonime.

Test.java
[...]
class Car {
    public Engine getEngine() {
        class OttoEngine implements Engine {
            private int fuelCapacity = 11;
 
            public int getFuelCapacity() {
                return fuelCapacity;
            }
        }
 
        return new OttoEngine();
    }
}
[...]

Click aici pentru exemplu de clasă internă declarată în bloc

Click aici pentru exemplu de clasă internă declarată în bloc

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ă.

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.

Moștenirea claselor interne

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ștenește doar Car.Engine însă sunt necesare:

Utilizarea claselor interne

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++;
	}
    });

Exerciții

Exercițiile din acest laborator, cu excepția taskului 0, au ca scop simularea obținerii prețului unei mașini de la un dealer. Construcția obiectelor necesare o veți face de la zero conform instrucțiunilor din taskuri.

Task 0 (1p)

Copiați clasele date ca exemplu în secțiunile Clase interne normale și Clase anonime în proiectul vostru și executați-le.

Task 1 - Structura de bază (2p)

Car

Creați clasa Car cu următoarele proprietăți: prețul, tipul și anul fabricației.

Tipul este reprezentat printr-un enum CarType declarat intern în Car. Acesta conține trei valori, fiind la latitudinea voastră ce mărci de mașini alegeți.

Prețul și anul pot fi integers.

Offer

Creați interfața Offer ce conține metoda: int getDiscount(Car car);.

Dealership

Creați clasa Dealership care se va ocupa cu aplicarea ofertelor pentru mașini.

Task 2 - Ofertele (2p)

În clasa Dealership creați trei clase interne private care implementează Offer.

Adăugați o metodă în clasa Dealership care oferă prețul mașinii după aplicarea discount-urilor din oferte: getFinalPrice(Car car)

Task 3 - Test (2p)

Creați clasa Test cu o metodă main în care să testați funcționalitatea.

Task 4 - Negocierea (2p)

Aăugați în clasa Dealership metoda void negotiate(Car car, Offer offer). Aceasta permite clientului să propună un discount. Dealershipul aplică oferta primită de la client sau nu (dat random de un randInt(2)).

În metoda main din Test apelați negotiate dând ca parametru oferta sub formă de clasă anonimă. Implementarea ofertei clientului poate fi un simplu return de o valoare aleasă de voi.

Exemplu output - aceasta este sugestia noastră, voi puteți afișa și în alt fel, contează însă să evidențiați aplicarea ofertelor și prețurile.

Initial price: 10000 euros
Applying Brand discount: 1000 euros
Applying Dealer discount: 100 euros
Applying Special discount: 957 euros
Final price: 7943
Applying Client discount: 600 euros
Final price after negotiation: 7343 euros

Ce fișiere .class sunt generate la compilare?

Task 5 - Lambda (1p)

Testați folosirea expresiilor lambda pe următorul caz: pe o listă de obiecte de tip Car cu prețuri variate, eliminați toate mașinile care au prețul peste o anumită sumă. Afișați lista înainte și după modificare.

Resurse

Referințe

  1. Kathy Sierra, Bert Bates. SCJP Sun Certified Programmer for Java™ 6 - Study Guide. Chapter 8 - Inner Classes (available online)