This is an old revision of the document!


Laboratorul 4: Abstractizare, Clase Speciale și Restricții

Obiective

Scopul acestui laborator este introducerea studenților în concepte mai avansate privind proiectarea claselor în Java, precum și familiarizarea acestora cu mecanismele de agregare, moștenire și polimorfism, alături de modul în care acestea pot fi utilizate pentru refolosirea și extinderea cu ușurință a codului existent.

Aspectele urmărite sunt:

  • înțelegerea abstractizării și a modului în care ascunde detaliile interne.
  • utilizarea corectă a claselor abstracte și interfețelor pentru refolosirea și organizarea codului.
  • înțelegerea diferenței dintre clase normale, abstracte și interfețe.
  • exersarea implementării metodelor abstracte și default.
  • înțelegerea și utilizarea polimorfismului în Java.
  • folosirea obiectelor imutabile pentru cod sigur și clar.
  • aplicarea constantelor cu final pentru valori fixe.

Aspectele bonus urmărite sunt:

  • Problema diamantului pentru metode default în interfețe.
  • clase de tip Record.
  • clase de tip Enum.

  • În acest laborator există mai multe secțiuni marcate [Optional]. Aceste secțiuni cuprind informații bonus care vă pot fi prezentate în timpul laboratorului sau pe care le puteți aprofunda în afara acestuia, ele nefiind necesare pentru laboratoarele viitoare sau pentru teme.
  • De asemenea, veți întâlni câteva secțiuni marcate [Nice to know]. Vă recomandăm ca acestea să aibă prioritate în parcurgerea secțiunilor de tip [Optional], deoarece vă pot oferi informații bonus care să fie și foarte probabil utile pentru teme sau laboratoare viitoare.

🌀 Abstractizare

Abstractizarea este unul dintre cele 4 principii POO de bază (Abstractizare, Încapsulare, Moștenire, Polimorfism).

Acest principiu este folosit pentru a ascunde detaliile interne ale obiectelor și evidențiază doar comportamentele esențiale. Ea oferă un șablon comun pentru o categorie de obiecte, fără a specifica implementarea fiecăruia. În Java, abstractizarea este realizată prin clase abstracte și interfețe.

Clase și metode abstracte

Programarea orientată pe obiecte permite modelarea unor concepte generale care pot avea comportamente variate în funcție de implementare. Clasele și metodele abstracte oferă un mecanism esențial pentru proiectarea ierarhiilor de clase flexibile și extensibile.

Ce este o metodă abstractă?

O metodă abstractă este o metodă fără implementare, declarată doar pentru a defini ce ar trebui să facă o metodă, fără a spune cum o face. Rolul ei este să oblige clasele derivate să furnizeze propria implementare.

Marcarea unei metode abstracte se face folosind keyword-ul abstract.

public abstract void startEngine(); 

Caracteristici:

  • Nu are corp (doar semnătură, terminată cu ;).
  • Este destinată suprascrierii în clasele copil.
  • Nu poate fi apelată direct
  • Poate exista doar într-o clasă abstractă.

Clase abstracte

O clasă abstractă este o clasă care poate conține:

  • metode abstracte
  • metode obișnuite cu implementare
  • variabile membru
  • constructori

Pentru a marca o clasă ca fiind abstractă folosim keyword-ul abstract în declararea clasei.

public abstract class Vehicle { 
    private String name;
 
    public Vehicle(String name) {
        this.name = name;
    }
 
    public abstract void startEngine();
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    } 
}

Este important să înțelegem că dacă avem cel puțin o metodă abstractă, trebuie să declarăm și clasa acesteia ca fiind abstractă.

Clasele abstracte au modificatorii de acces public sau default.

Implementarea metodelor abstracte în subclase

O clasă abstractă, nu poate fi instanțiată direct, trebuie moștenită de o altă clasă folosind keyword-ul extends.

Acest mecanism funcționează și pe lanțuri de moștenire între clase abstracte: o clasă abstractă poate moșteni o altă clasă abstractă și poate adăuga metode abstracte noi. Toate metodele abstracte din lanț trebuie să fie implementate în cele din urmă într-o clasă concretă pentru a putea crea obiecte.

Vehicle.java
public abstract class Vehicle { 
    private String name;
 
    public Vehicle(String name) {
        this.name = name;
    }
 
    public abstract void startEngine();
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    } 
}
Car.java
public abstract class Car extends Vehicle { 
    public abstract void openTrunk();
 
    public Car(String name) {
        super(name);
    }
 
    @Override
    public void startEngine() {
        System.out.println("Car engine started!");
    }
}
Sedan.java
public class Sedan extends Car {
    public Sedan(String name) {
        super(name);
    }
 
    @Override
    public void openTrunk() {
        System.out.println("Trunk opened in the back of the car!");
    }
}

După implementare, pot fi create obiecte de tipul clasei Sedan:

Vehicle vehicle = new Sedan();
vehicle.startEngine(); // Output: Car engine started!
vehicle.openTrunk(); // Output: Trunk opened in the back of the car!

  • Clasa Vehicle și clasa Car nu pot fi instanțiate, deoarece nu implementează toate metodele abstracte moștenite și proprii.
  • Dacă dorim să nu se poată instanția clasa Animal de mai sus, am putea să o declarăm ca fiind abstractă chiar dacă aceasta nu ar avea metode abstracte:
    public abstract class Animal { // valid
        private double weight;
     
        public void setWeight(double weight) {
            this.weight = weight;
        }
     
        public double getWeight() {
            return weight;
        }
    }

  • O clasă abstractă nu poate fi instanțiată, deoarece o clasă abstractă poate conține metode abstracte care nu au încă o implementare. Chiar și dacă clasa nu conține metode abstracte această restricție este păstrată pentru a putea exista scenariul de mai sus.
  • O clasă abstractă poate avea constructori chiar dacă nu poate fi instanțiată direct, deoarece acei constructori pot fi folosiți la inițializarea stării interne a clasei, ca în exemplul de mai sus.

De ce folosim clase abstracte?

AvantajDescriere
Șablon de codDefinește structura și comportamentele esențiale pentru un grup de clase
Reutilizare de codPermite împărtășirea metodelor și a atributelor comune între subclase
PolimorfismPermite folosirea unei referințe la clasa de bază pentru obiecte diferite
Control asupra implementăriiForțează subclasele să implementeze metodele abstracte

Clase abstracte vs. Clase normale

Folosește o clasă abstractă atunci când:

  • Ai nevoie să definești un comportament comun pentru mai multe clase
    • Exemplu: Instrument oferă doar metodele abstracte play() și stop(), iar clasele care o moștenesc vor avea aceste metode de bază în comun (Saxophone, Guitar etc.).
  • Vrei să implementezi parțial o clasă, lăsând detalii subclaselor
    • Exemplu: Vehicle oferă metoda comună start(), dar fiecare subclasă (Car, ElectricScooter) definește propriul tip de combustibil prin metoda fuelType().
  • Există metode care nu au sens la nivel general, dar sunt obligatorii în subclase
    • Exemplu: Document are metoda abstractă print(), pentru că doar documentele concrete (PDFDocument, WordDocument) pot defini cum se tipăresc.
  • Vrei să eviți instanțierea unei clase de bază
    • Exemplu: Shape nu poate fi instanțiată direct; doar formele concrete (Circle, Square) pot fi folosite pentru a calcula aria.

Interfețe

Interfețele oferă un mecanism prin care putem defini comportamente fără a forța clasele să moștenească implementarea. Ele stabilesc un contract pe care orice clasă îl poate îndeplini, indiferent din ce parte a ierarhiei de clase provine.

Ce este o interfață?

O interfață este un tip de referință în Java ce conține doar semnături de metode (fără implementare). Ea definește un set de capabilități pe care o clasă trebuie să le ofere, astfel poate fi considerată o clasă abstractă pură, deoarece conține doar metode abstracte.

  • Putem privi interfețele ca pe niște ecusoane de competență, dacă o clasă implementează interfața, înseamnă că știe să execute comportamentele definite acolo.
  • Interfețele au modificatorii de acces public sau default.

Definirea unei interfețe

Interfețele se definesc cu keyword-ul interface în locul keyword-ului class. O interfață poate conține:

  • constante
  • metode

Metodele dintr-o interfață:

  • sunt implicit public și abstract
  • nu au corp (doar semnătură)
  • pot fi implementate de orice clasă
public interface Driveable {
    boolean startEngine();
    void stopEngine();
    float accelerate(float acc);
    boolean turn(Direction dir);
}

Interfețele nu pot avea constructori, deoarece ele nu au o stare proprie, au doar metode și constante.

Implementarea unei interfețe

O clasă implementează o interfață folosind keyword-ul implements, furnizând implementarea tuturor metodelor:

public class Automobile implements Driveable {
    public boolean startEngine() {
        engineRunning = true;
        return true;
    }
 
    public void stopEngine() {
        engineRunning = false;
    }
 
    public float accelerate(float acc) {
        // logică accelerare
        return acc;
    }
 
    public boolean turn(Direction dir) {
        // logică pentru viraj
        return true;
    }
}

Ca și în cazul moștenirii clasice, putem avea o altă clasă care implementează aceeași interfață fără a avea legătură prin moștenire:

public class Lawnmower implements Driveable {
    // implementări proprii pentru metodele din interfață
}

Când implementăm o metodă dintr-o interfață trebuie neapărat să folosim specificatorul public, deoarece toate metodele dintr-o interfață sunt publice implicit.

Iniţializarea câmpurilor în interfeţe

În interfețe toate câmpurile sunt implicit public static final. Nu pot exista blank finals (câmpuri finale neinițializate), dar pot exista constante non-primitive dacă sunt inițializate la declarație.

public interface MathConstants {
    double PI = 3.14159; // valid
}

Interfețele ca tipuri

După ce definim o interfață, ea devine un tip de referință în Java, la fel ca o clasă. Asta înseamnă că putem:

  • Declara variabile de tipul interfeței
  • Folosi interfața ca tip pentru parametrii unor metode
  • Specifica interfața ca tip de return al unei metode

Astfel, orice obiect care implementează interfața poate fi atribuit unei variabile de acel tip, indiferent de clasa sa concretă (upcasting).

Driveable vehicle;
 
vehicle = new Automobile();
vehicle.startEngine();
vehicle.stopEngine();
 
vehicle = new Lawnmower();
vehicle.startEngine();
vehicle.stopEngine();

Acest mecanism permite polimorfismul, deoarece putem trata obiecte diferite (Automobile, Lawnmower etc.) ca fiind de același tip (Driveable), fără să ne bazăm pe moștenirea clasică.

Extinderea interfețelor

Interfețele pot extinde alte interfețe folosind keyword-ul extends:

public interface Monster {
    void actMenacing();
}
 
public interface DangerousMonster extends Monster {
    void destroyEverything();
}

Clasa care va moșteni interfața DangerousMonster va trebui să implementeze ambele metode actMenacing() și destroyEverything():

public class LochNessMonster implements DangerousMonster {
    public void actMenacing() {
        System.out.println("*Jump out of water*");
    }
 
    public void destroyEverything() {
        System.out.println("*Attack every pier and boat*");
    }
}

Moștenire multiplă

În Java, o interfață poate moșteni mai multe interfețe folosind keyword-ul extends. Acesta permite combinarea comportamentelor din mai multe surse fără a fi nevoie de moștenire multiplă de clase (care nu este permisă în Java).

interface Flyer {
    void fly();
}
 
interface Swimmer {
    void swim();
}
 
// Moștenire multiplă de interfețe
interface Duck extends Flyer, Swimmer {
    void quack();
}
 
class Mallard implements Duck {
    public void fly() { System.out.println("Flying!"); }
    public void swim() { System.out.println("Swimming!"); }
    public void quack() { System.out.println("Quack!"); }
}

Totodată, o clasă poate moșteni mai multe interfețe folosind keyword-ul implements:

// Interfețe separate
interface Flyer {
    void fly();
}
 
interface Swimmer {
    void swim();
}
 
// Clasă care implementează ambele interfețe
class Duck implements Flyer, Swimmer {
    @Override
    public void fly() {
        System.out.println("Flying high!");
    }
 
    @Override
    public void swim() {
        System.out.println("Swimming in the pond!");
    }
}

Coliziuni de nume la combinarea interfețelor

Dacă două interfețe au metode cu aceeași semnătură și același tip de return, nu există probleme, deoarece clasa care le implementează poate furniza o singură implementare.

Însă, dacă metodele au același nume și parametri, dar tipuri de return diferite, apare un conflict, deoarece Java nu știe ce tip de return să folosească.

// Exemplu valid: metode cu aceeasi semnatura si acelasi tip de return
interface A { int run(); }
interface B { int run(); }
 
class ABRunner implements A, B {
    @Override
    public int run() {
        return 42; // o singura implementare pentru ambele interfete
    }
}
 
// Exemplu care dă eroare: metode cu același nume și parametri, dar tipuri diferite
interface C { String run(); }
 
// Clasa de mai jos va genera eroare dacă încercăm să implementăm A și C simultan
class ACFail implements A, C {  
    // eroare: conflicte la tipul return
    @Override
    public ??? run() { ... } // Java nu știe dacă să returneze "int" sau "String"
}

Situația de mai sus nu se încadrează la Problema diamantului, deoarece chiar dacă se pot moșteni mai multe interfețe, acestea nu au un corp, deci implementarea este lăsată la latitudinea clasei care le va implementa.

[Optional] Metode default în interfețe și Problema diamantului

[Optional] Metode default în interfețe și Problema diamantului

Metode default în interfețe

În Java (de la Java 8), interfețele pot avea metode cu implementare folosind cuvântul cheie default.

Scopul lor:

  • să permită adăugarea de noi metode în interfețe fără să strice compatibilitatea cu clasele care le folosesc;
  • să ofere o implementare implicită, dar care poate fi suprascrisă de clasele care implementează interfața.

Exemplu:

interface Animal {
    default void eat() {
        System.out.println("Animal is eating");
    }
}

Problema diamantului în interfețe

Apare această problemă când o clasă implementează două interfețe care au aceeași metodă default cu aceeași semnătură.

interface A {
    default void hello() { System.out.println("Hello from A"); }
}
 
interface B {
    default void hello() { System.out.println("Hello from B"); }
}
 
class C implements A, B {
    // EROARE: conflict, Java nu știe ce versiune de hello() să folosească
}

Java nu știe dacă să o folosească pe cea din A sau B, ceea ce rezultă într-un conflict.

Soluția pentru Problema diamantului în Java

Clasa care implementează interfețele trebuie să rezolve conflictul suprascriind metoda:

class C implements A, B {
    @Override
    public void hello() {
        A.super.hello(); // sau B.super.hello();
    }
}

De ce folosim interfețe?

Avantaj Descriere clarificată
Contract clar Clasa care implementează interfața este obligată să furnizeze implementarea tuturor metodelor declarate în interfață.
Polimorfism flexibil Obiectele diferite care implementează aceeași interfață pot fi tratate uniform prin tipul interfeței, fără să conteze clasa lor concretă.
Independență O clasă poate implementa interfața fără să fie legată de o anumită ierarhie de clase; nu trebuie să extindă o clasă specifică.
Reutilizare Orice clasă poate implementa aceeași interfață, ceea ce permite refolosirea logicii comune în contexte diferite.
Alternativă la multiple inheritance Java nu permite moștenirea multiplă a claselor, dar o clasă poate implementa mai multe interfețe, obținând astfel comportamente multiple.

Clase abstracte vs. Interfețe

După cum se poate observa, atât clasele abstracte cât și interfețele oferă avantaje similare. Să observăm următorul tabel pentru o comparație directă:

Caracteristică Clasă abstractă Interfață
Instanțiere Nu poate fi instanțiată Nu poate fi instanțiată
Metode abstracte Poate conține metode abstracte și implementate Toate metodele sunt abstracte (Java <8); Java 8+ permite și metode default și static cu implementare
Variabile Poate avea variabile de instanță Toate variabilele sunt public static final (constante)
Constructor Poate avea constructor Nu poate avea constructor
Moștenire O clasă poate extinde o singură clasă abstractă O clasă poate implementa mai multe interfețe
Acces metode Poate avea orice modificator de acces (private, protected, public) Metodele sunt implicit public abstract; Metodele default (Java 8+) sunt public și metodele static pot fi public sau private (Java 8+)
Utilizare Când există cod comun de reutilizat și dorim un șablon pentru subclase Când vrem să definim un contract pentru comportament independent de ierarhia claselor
Polimorfism Permite polimorfism pe baza clasei de bază Permite polimorfism pe baza interfeței, indiferent de clasa concretă
Extindere / implementare O clasă abstractă poate extinde o altă clasă abstractă sau normală O interfață poate extinde mai multe interfețe

Exemplu ierarhie de clase

Proiectele voastre ar trebui să conțină o combinație între clase abstracte și interfețe.

Să presupunem următoarea ierarhie de clase.

Explicație:

  1. Living este abstractă pentru că vrem să forțăm implementarea metodei eat() în toate clasele care o extind, dar oferim și un comportament comun grow().
  2. Animal este abstractă pentru că nu are sens să instanțiem un animal generic; fiecare subclasă (Cat, Dog, Bird) trebuie să implementeze makeSound().
  3. Clasele normale (Cat, Dog, Bird) implementează metodele abstracte și pot fi instanțiate.
  4. Interfețele (Domesticable, Guardable, Flyable) definesc capabilități suplimentare, fără a impune moștenirea ierarhiei.
  5. Clasele pot implementa mai multe interfețe pentru a combina comportamente, exemplu: Dog poate fi Domesticable și Guardable.

🔒 Obiecte imutabile și constante

Constante folosind keyword-ul final

Reamintim din laboratoarele trecute cum final poate fi folosit pentru a crea câmpuri sau variabile locale constante:

public class Alien {
    private final String name; // camp constant
 
    public Alien(String name) {
        this.name = name;
    }
 
    public String sayHelloToHumans() {
        // Constante locale
        final String evilMessage = " prepare your world to be conquered!";
        final String goodMessage = " we want to taste shaorma!";
 
        if (name.equals("Good Boy Roy")) {
            return "My name is " + name + goodMessage;
        }
 
        return "My name is " + name + badMessage;
    }
 
    public String getName() {
       return name;
    }
}

De asemenea, putem marca parametrii unei metode ca fiind final pentru a indica faptul că aceștia nu pot fi schimbați în corpul unei metode:

public class Alien {
    private final String name; // camp constant
 
    public Alien(String name) {
        this.name = name;
    }
 
    public String sayHelloToHumans() {
        // Constante locale
        final String evilMessage = " prepare your world to be conquered!";
        final String goodMessage = " we want to taste shaorma!";
 
        if (name.equals("Good Boy Roy")) {
            return "My name is " + name + goodMessage;
        }
 
        return "My name is " + name + badMessage;
    }
 
    public void shootRayGun(final Human human) {
        human.decreaseHealthBy(70);
        System.out.println("Resistence is futile!");
 
        human = new Human(); // eroare, human este final
        human.job = "plumber"; // se poate, final se refera doar la referinta obiectului 
    }
 
    public String getName() {
       return name;
    }
}

Obiecte imutabile

Un obiect imutabil este un obiect a cărui stare internă nu poate fi modificată după ce a fost creat. Cu alte cuvinte, toate câmpurile sale sunt constante după inițializare, iar orice operație aparentă de “modificare” va genera de fapt un nou obiect.

Caracteristici ale Obiectelor Imutabile

Pentru ca o clasă să fie considerată imutabilă, trebuie să respecte următoarele reguli:

  • Toate câmpurile sale trebuie să fie private și finale.
  • Clasa trebuie declarată final pentru a preveni moștenirea.
  • Nu trebuie să existe setters sau alte metode care modifică starea internă.
  • Obiectele returnate de metode trebuie să fie copii, nu referințe directe către câmpurile interne (pentru a evita modificarea indirectă).

Avantaje

  • Siguranță în programe paralelizabile: Obiectele imutabile pot fi partajate între thread-uri fără sincronizare suplimentară.
  • Simplitate: Nu este nevoie de protecție împotriva modificărilor accidentale.
  • Fiabilitate: Pot fi folosite ca chei în colecții (HashMap, HashSet) fără riscul ca starea lor să se schimbe.
  • Cache și reutilizare: Obiectele pot fi memorate în cache fără teama că valorile se vor modifica ulterior.

Dezavantaje

  • Consum mai mare de memorie: Crearea de obiecte noi la fiecare modificare poate fi costisitoare.
  • Performanță scăzută în cazurile în care modificările frecvente sunt inevitabile.

Crearea unei clase imutabile

Exemplu de clasă imutabilă:

Adress.java
public final class Adress {
    private final String street;
    private final String city;
    private final int zipCode;
 
    public Adresa(String street, String city, int zipCode) {
        this.street = street;
        this.city = city;
        this.zipCode = zipCode;
    }
 
    public String getStreet() { return street; }
    public String getCity() { return city; }
    public int getZipCode() { return zipCode; }
 
    @Override
    public String toString() {
        return street + ", " + city + " (" + zipCode + ")";
    }
}
Main.java
public class Main {
    public static void main(String[] args) {
        Adresa adresa1 = new Adresa("Str. Florilor", "București", 12345);
        Adresa adresa2 = new Adresa("Str. Florilor", "București", 12345);
 
        // ambele obiecte sunt independente și nu pot fi modificate
        System.out.println(adresa1); // Str. Florilor, București (12345)
    }
} 

Exemple de clase imutabile din Java

Java oferă numeroase exemple de clase imutabile, printre care:

  • String
  • Wrapper-urile pentru tipurile primitive (Integer, Double, Boolean etc.).
  • LocalDate, LocalTime, LocalDateTime (din pachetul java.time).

Vom vorbi mai multe despre imutabilitate în contextul folosirii obiectelor de tip String și wrappers (Integer, Double etc.) în următoarele laboratoare.

[Nice to know] 🎛️ Clase speciale

Enums

Enums sunt tipuri speciale de clasă care definesc un set fix de constante. Ele oferă o modalitate sigură și lizibilă de a reprezenta valori finite și constante într-un program.

Exemplu simplu:

enum Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST
}

  • Valorile din interiorul unui Enum nu au un tip anume, deci NORTH nu este nici String, nici int. Este o instanță statică și constantă a enum-ului Direction.
  • Enum-ul stochează valorile sub forma static final ca și cum ați declara constante clasice în Java.

Caracteristici principale

  • Reprezintă un set fix de valori constante.
  • Sunt tipuri sigure, deci nu poți atribui o valoare invalidă.
  • Poți adăuga metode, constructori și câmpuri într-un enum.
  • Implicit, toate valorile enum sunt public static final.

Exemplu avansat

enum Planet {
    MERCURY(3.3e+23, 2.4e6),
    VENUS(4.87e+24, 6.1e6),
    EARTH(5.97e+24, 6.4e6);
 
    private final double mass;   // în kilograme
    private final double radius; // în metri
 
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
 
    public double getMass() {
        return mass;
    }
 
    public double getRadius() {
        return radius;
    }
 
    public double surfaceGravity() {
        final double G = 6.67300E-11;
        return G * mass / (radius * radius);
    }
}

Constructorii în Enum-uri sunt folosiți pentru a inițializa câmpurile acestuia. În exemplul de mai sus când declarăm valoarea EARTH, trebuie să menționăm și valorile pentru mass și radius asociate acestuia prin folosirea constructorului (ex. EARTH(5.97e+24, 6.4e6)).

Exemplu utilizare

Pentru exemplul simplu de mai sus avem:

Direction dir = Direction.NORTH;
 
switch (dir) {
    case NORTH:
        System.out.println("Mergeți spre nord");
        break;
    case SOUTH:
        System.out.println("Mergeți spre sud");
        break;
    default:
        System.out.println("Altă direcție");
}

Pentru exemplul mai complex avem:

public class PlanetExample {
    public static void main(String[] args) {
        double massOnEarth = 70; // masa în kg (nu greutatea!)
 
        for (Planet planet : Planet.values()) {
            double weight = planet.surfaceGravity() * massOnEarth;
            System.out.printf("Greutatea ta pe %s este %.2f N%n", planet.name(), weight);
        }
    }
}

Output

Output

Greutatea ta pe MERCURY este 25.94 N
Greutatea ta pe VENUS este 61.47 N
Greutatea ta pe EARTH este 687.30 N

Metoda values() este o metodă generată automat de Java pentru orice Enum. Ea returnează un array cu toate constantele definite în acel enum, în ordinea în care au fost declarate (ex. MERCURY, VENUS, EARTH).

Avantaje ale folosirii Enum

  • Cod mai clar și mai lizibil comparativ cu constantele int sau String.
  • Siguranță la compilare: nu poți atribui valori invalide.
  • Poți adăuga metode și comportamente specifice fiecărei constante.
  • Se integrează bine cu switch/case pentru decizii pe valori finite.

Puteți folosi în continuare câmpuri de tipul static final într-o clasă normală care are rol de a ține constante, însă este de preferat să folosiți Enum-uri fiind o alternativă mai modernă.

Records

Tipul record este un tip special de clasă introdus în Java 16, creat pentru a reprezenta date imutabile într-un mod concis. Scopul său principal este să reducă codul boilerplate necesar pentru clasele care doar păstrează date (data carriers), cum ar fi DTO-uri sau simple modele.

Caracteristici principale

Un record:

  • Este implicit final (nu poate fi moștenit).
  • Are câmpuri imutabile (private final).
  • Generează automat:
    • Constructor,
    • equals() și hashCode(),
    • toString(),
    • Getteri (numiți exact ca atributele, fără get).

Exemplu simplu

public record Person(String name, int age) {}

Exemplul de mai sus este echivalent cu:

public final class Person {
    private final String name;
    private final int age;
 
    public Person(String name, int age) { 
        this.name = name; 
        this.age = age; 
    }
 
    public String name() { return name; }
    public int age() { return age; }
 
    @Override
    public boolean equals(Object o) { ... }
 
    @Override
    public int hashCode() { ... }
 
    @Override
    public String toString() { ... }
}

Accesarea datelor

Person p = new Person("Ana", 25);
System.out.println(p.name()); // Ana
System.out.println(p.age());  // 25

Observă că getteri există, dar nu se numesc getName(), ci exact ca proprietatea: name().

Records cu validare (constructor compact)

Constructorul compact ne permite să validăm datele fără să repetăm lista de parametri.

public record Product(String name, double price) {
    public Product {
        if (price < 0) {
            throw new IllegalArgumentException("Price must be positive");
        }
    }
}

Metode în Records

Pe lângă getteri și setteri putem adăuga de asemenea și metode.

public record Point(int x, int y) {
    public double distanceToOrigin() {
        return Math.sqrt(x * x + y * y);
    }
}

Moștenirea din Record

Records pot implementa interfețe, dar nu pot extinde clase.

public interface Printable {
    void print();
}
 
public record Book(String title, String author) implements Printable {
    @Override
    public void print() {
        System.out.println(title + " by " + author);
    }
}

Avantaje și dezavantaje

Records sunt potrivite pentru:

  • DTO-uri (Data Transfer Objects)
  • Rezultate interne ale metodelor
  • Clase imutabile simple
  • Chei pentru mapări (hashCode generat automat)
  • Proiecții din baze de date (ex: Spring repository projections)

Limitări:

  • Nu pot extinde alte clase (extends interzis).
  • Sunt implicit immutabile (valorile nu pot fi schimbate).
  • Nu sunt potrivite pentru obiecte cu logică complexă sau stare mutabilă.

  • DTO (Data Transfer Object) este un obiect folosit pentru transportul datelor între straturi ale unei aplicații sau între aplicații diferite, fără logică de business. De exemplu, dacă am vrea să transmitem tot obiectul User dintr-o aplicație bancară am putea transmite inclusiv date confidențiale despre acesta (CNP, adresă etc.). Pentru a transmite doar datele necesare, putem crea o clasă UserDTO care conține doar câmpurile relevante pentru transfer.
  • Veți învăța despre Spring când veți face web back-end development.

Summary

Abstractizare

  • Permite ascunderea detaliilor interne și expunerea doar a comportamentului esențial.
  • Realizată prin clase abstracte și interfețe.
  • Favorizează polimorfismul și reutilizarea codului.

Clase abstracte

  • Pot conține metode abstracte și metode cu implementare.
  • Pot avea câmpuri și constructori.
  • Nu pot fi instanțiate direct.
  • Forțează subclasele să implementeze metodele abstracte.
  • Permite moștenire între clase abstracte.

Interfețe

  • Definește un contract de comportamente (metode abstracte).
  • Poate conține metode default (cu implementare) și static.
  • Permite moștenire multiplă fără conflicte de ierarhie (exceptând problema diamantului).
  • Polimorfism: o variabilă de tip interfață poate referi orice clasă care o implementează.
  • Nu poate avea constructori.
  • Toate câmpurile sunt implicit public static final.

Problema diamantului (în interfețe)

  • Apare când o clasă implementează două interfețe cu aceeași metodă default.
  • Soluția: clasa trebuie să suprascrie metoda și să aleagă implementarea.

Constante (final)

  • Variabilele final nu pot fi modificate după inițializare.
  • Folosite pentru constante globale, valoare fixă în clase sau enum-uri.
  • În interfețe, toate câmpurile sunt implicit public static final.

Obiecte imutabile

  • Obiectele nu își pot schimba starea după creare.
  • Se obțin prin câmpuri private final și fără setteri.
  • Oferă siguranță în programarea concurentă.

Enums

  • Tip special care reprezintă un set fix de constante.
  • Fiecare constantă este de fapt un obiect de tip enum.
  • Pot avea câmpuri, constructori și metode.
  • Metode utile: .values(), .ordinal(), .name().

Records

  • Introduse în Java 16, pentru tipuri de date imutabile.
  • Au câmpuri finale, constructor generat automat, equals(), hashCode() și toString().
  • Perfect pentru DTO-uri sau obiecte care transportă date fără logică suplimentară.

Exerciții

  • Exercițiile vor fi făcute pe platforma Devmind Code. Găsiți exercițiile din acest laborator în contestul aferent.
  • Vă recomandăm să copiați scheletul și să faceți exercițiile mai întâi în IntelliJ, deoarece acolo aveți acces la o serie de instrumente specifice unui IDE. După ce ați terminat exercițiile puteți să le copiați pe Devmind Code.

Task 1 (2p)

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

Notă: Acesta este un exemplu simplu pentru Command Pattern

Task 2 (3p)

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 LIFO
  • Queue - care implementează o strategie de tip FIFO

Bonus (nepunctat): Incercați să evitaţi codul similar în locuri diferite!

Task 3 (2p)

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:

void minus(float value);

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

Task 4 (3p)

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.

poo-ca-cd/laboratoare/abstractizare-clase-speciale-si-restrictii.1761520255.txt.gz · Last modified: 2025/10/27 01:10 by florian_luis.micu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0