Laboratorul 5: Clase Interne și Strings

  • Data publicării: 03.11.2025
  • Data ultimei modificări: 06.11.2025
    • ștergerea notiței legată de contest (ambele observații au fost rezolvate).
    • refrazări pentru favorizarea unei lecturi clare și rapide.
    • indicarea că o clasă internă poate fi record, enum, interfață sau clasă abstractă.
    • modificări pentru secțiunea clase anonime
      • adăugarea unui exemplu mai detaliat pentru clase anonime în GUI.
      • precizarea că o clasă internă are acces la membrii privați ai clasei externe.
      • adăugarea unor secțiuni legate de accesul la metode și variabile în mai multe contexte.

Obiective

  • înțelegerea conceptului de clase interne și a subtipurilor.
  • familiarizarea cu clasele interne normale și instanțierea lor.
  • înțelegerea accesului la membrii clasei exterioare dintr-o clasă internă.
  • aplicarea modificatorilor de acces la clasele interne.
  • folosirea claselor anonime pentru implementări rapide și restrânse.
  • înlocuirea claselor anonime cu expresii lambda când este posibil.
  • înțelegerea claselor interne statice și diferențele față de cele normale.
  • declararea și utilizarea claselor interne în metode și blocuri.
  • înțelegerea moștenirii claselor interne și inițializarea corectă a constructorilor.
  • aplicarea claselor interne pentru încapsulare, ascunderea implementării și manipularea evenimentelor.
  • înțelegerea imutabilității String-urilor și metode de creare și manipulare.
  • compararea corectă a String-urilor și utilizarea String Pool.
  • conversia între tipuri primitive și String folosind String.valueOf() și parsing.

Aspectele bonus urmărite sunt:

  • tokenizarea și căutarea avansată în String-uri.
  • metode ale clasei String.
  • alte metode specifice de creare a unui String.

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

🪆 Clase interne

Clasele declarate în interiorul unei alte clase se numesc clase interne (nested classes). Ele sunt folosite pentru a grupa logic clase care lucrează împreună și pentru a controla mai bine accesul între ele.

Clasele interne sunt de mai multe tipuri:

  • clase interne normale (regular inner classes)
  • clase anonime (anonymous inner classes)
  • clase interne statice (static nested classes)
  • clase interne metodelor (method-local inner classes) sau blocurilor

Clase interne "normale"

O clasă internă este o clasă definită în interiorul altei clase și poate fi accesată doar printr-o instanță a clasei externe, la fel ca variabilele și metodele non-statice.

La compilare, Java creează fișiere separate pentru fiecare clasă internă, denumite după modelul: ClasaExterioară$ClasaInternă.class.

În exemplul de mai jos, compilarea va genera fișierele Car.class și Car$OttoEngine.class. Totuși, fișierul Car$OttoEngine.class nu poate fi executat direct, deoarece este dependent de clasa sa exterioară.

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());
    }
}
$ javac Test.java
$ ls
Car.class Car$OttoEngine.class Engine.class Test.class Test.java

Din interiorul unei clase interne, putem accesa referința la instanța clasei externe folosind numele acesteia urmat de keyword-ul this:

class Car {
    private String model = "Tesla";
 
    class OttoEngine {
        private int fuelCapacity;
 
        public OttoEngine(int fuelCapacity) {
            this.fuelCapacity = fuelCapacity;
        }
 
        public void printCarModel() {
            // Accesăm instanța clasei exterioare folosind Car.this
            System.out.println("Car model: " + Car.this.model);
        }
    }
 
    public OttoEngine getEngine() {
        return new OttoEngine(11);
    }
}
 
public class TestThis {
    public static void main(String[] args) {
        Car car = new Car();
        Car.OttoEngine engine = car.getEngine();
        engine.printCarModel(); // Va afișa: Car model: Tesla
    }
}

  • Țineți cont de faptul că o clasă internă se comportă ca un membru al clasei, concret nu puteți folosi un membru al clasei dacă nu inițializați mai întâi clasa.
  • Chiar dacă model este un câmp privat, clasa internă îl poate accesa, deoarece și clasa internă este un membru al clasei.
  • O clasă internă nu trebuie neapărat să fie o clasă obișnuită; puteți declara ca membri interni și records, enums, interfețe sau clase abstracte.

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.

Spre deosebire de clasele top-level, care pot fi doar public sau package-private (denumit și default), o clasă internă poate fi și private sau protected.

Această flexibilitate permite ascunderea detaliilor de implementare și controlul strict al accesului.

Tip clasă Modificatori de acces permiși Scop principal
Clasă top-level public, package-private Definirea tipurilor de bază accesibile global
Clasă internă public, protected, private, package-private Gruparea logică și ascunderea implementării
Exemplu: ascunderea unei implementări interne

Folosind exemplul clasei Car care conține o clasă internă OttoEngine, dacă marcăm această clasă internă ca private, apar erori de compilare, deoarece tipul Car.OttoEngine nu mai este accesibil din exterior.

class Car {
    private class OttoEngine implements Engine {
        public void start() {
            System.out.println("Engine started.");
        }
    }
 
    public Engine getEngine() {
        return new OttoEngine(); // upcasting la Engine
    }
}
 
public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        Engine e = car.getEngine(); // acces permis doar prin interfață
        e.start();
    }
}

  1. Clasa Car.OttoEngine este privată, deci nu poate fi instanțiată direct din exterior.
    • new Car.OttoEngine() va genera o eroare de compilare.
  2. Toți constructorii unei clase interne private sunt implicit privați.
    • Obiectele pot fi create doar din interiorul clasei Car.
  3. Ascunderea implementării se face prin upcasting la interfața Engine.
    • Astfel, codul extern folosește doar interfața, fără a cunoaște detalii interne.

Avantajul acestui mecanism

  • se întărește încapsularea (detaliile implementării nu pot fi abuzate din exterior);
  • se clarifică relația logică dintre clase (motorul aparține mașinii);
  • se simplifică API-ul public al clasei (exteriorul vede doar Engine, nu OttoEngine).

Clase anonime

În dezvoltarea software există situații în care o componentă are o funcționalitate bine definită, dar este utilizată doar într-un context restrâns.

Definirea unei clase separate ar adăuga complexitate inutilă. În aceste cazuri, putem folosi clase interne anonime, adică clase fără nume, definite și instanțiate simultan.

O clasă anonimă extinde o altă clasă sau implementează o interfață și suprascrie metodele acesteia direct în momentul creării obiectului.

Exemplu de utilizare

Presupunem că avem o interfață Engine și o clasă Car care o folosește. Putem rescrie metoda getEngine() astfel încât să întoarcă o clasă anonimă:

class Car {
    public Engine getEngine(int fuelCapacity) {
        return new Engine() {
            private int fuelCapacity = 11;
 
            public int getFuelCapacity() {
                return fuelCapacity;
            }
        };
    }
}

Sintaxa return new Engine() { … } poate fi citită astfel:

„Creează o clasă care implementează interfața Engine conform implementării următoare.”

Această tehnică reduce volumul codului și îmbunătățește lizibilitatea atunci când nu avem nevoie de o clasă separată reutilizabilă.

  • Obiectul este creat direct după cuvântul return, folosind new.
    • Referința întoarsă este upcastată automat la tipul de bază (Engine).
  • Clasa este anonimă, adică nu are nume, dar implementează tipul Engine, deci poate defini metodele interfeței.
  • Corpul clasei apare imediat după instanțiere, între { … }.

Clasa moștenită poate să fie o interfață, o clasă abstractă sau o clasă normală.

Limitări

Limitare Explicație
Nu pot avea constructori Deoarece nu au nume, nu se pot defini constructori expliciți. Se folosește implicit constructorul clasei de bază.
Pot extinde doar o singură clasă sau implementa o singură interfață Nu se pot combina moștenirea și implementarea multiplă în același timp.
Durata de viață Obiectele anonime sunt create și folosite pe loc, deci potrivite pentru scopuri scurte.

Accesul la variabile și metode

Față de clasele interne normale, clasele anonime au câteva restricții în ceea ce privește accesul la variabile și metode în funcție de locația acestora.

Accesul la câmpurile clasei externe

Câmpurile clasei externe pot fi accesate din interiorul clasei anonime fără să fie nevoie să se impună anumite restricții.

public class Outer {
    private String secret = "Private info";
 
    interface Greeter {
        void greet();
    }
 
    public void createAnonymous() {
        Greeter g = new Greeter() {
            @Override
            public void greet() {
                // acces direct la câmpul privat al clasei externe
                System.out.println("Secretul este: " + secret);
            }
        };
 
        g.greet(); // -> "Secretul este: Private info"
    }
 
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.createAnonymous();
    }
}
Accesul la câmpuri moștenite de clasa anonimă

Dacă clasa anonimă extinde o clasă normală sau abstractă, ea primește toate câmpurile (public/protected/package-private sau private prin getteri) ale clasei părinte:

abstract class Animal {
    protected int age = 5;  // câmp moștenit
 
    public abstract void showDetails();
}
 
public class Test {
    public static void main(String[] args) {
        Animal a = new Animal() {
            @Override
            public void showDetails() {
                System.out.println("Animal has age: " + age); // folosește câmpul moștenit
            }
        };
 
        a.showDetails(); // Output: "Animal has age 5"
    }
}

Dacă clasa anonimă implementează o interfață, interfața nu are câmpuri ale instanței, doar constante (declarate public static final) care pot fi accesate direct din clasa anonimă.

interface Greeter {
    String NAME = "Ana"; // constantă
 
    void greet();
}
 
public class Main {
    public static void main(String[] args) {
        Greeter g = new Greeter() {
            @Override
            public void greet() {
                // Accesăm direct constanta din interfața implementată
                System.out.println("Salut, " + NAME);
            }
        };
 
        g.greet(); // Output: Salut, Ana
    }
}
Accesul la metode
  1. Metode moștenite din clasa părinte
    • Clasele anonime pot folosi și suprascrie metodele clasei părinte.
    • Se pot apela aceste metode direct din corpul clasei anonime.
    • Metodele abstracte trebuie implementate obligatoriu în clasa anonimă
      class Animal {
          protected int age = 5;
       
          public void speak() {
              System.out.println("Animal speaks");
          }
       
          public abstract void showDetails();
      }
       
      public class Test {
          public static void main(String[] args) {
              Animal a = new Animal() {
                  // Implementăm metoda abstractă
                  @Override
                  public void showDetails() {
                      speak(); // folosește metoda moștenită
                      System.out.println("Animal has age: " + age);
                  }
              };
       
              a.showDetails();
              a.speak(); // o putem apela și direct pentru că referința este de tip Animal
          }
      }
  2. Metode noi definite în clasa anonimă
    • Se pot adăuga metode proprii, dar nu se pot accesa prin referința superclasei/interfeței.
    • Sunt vizibile doar în interiorul clasei anonime, deci pot fi considerate metode private sau helper.
      class Animal {
          protected int age = 5;
       
          public void speak() {
              System.out.println("Animal speaks");
          }
      }
       
      public class Test {
          public static void main(String[] args) {
              Animal a = new Animal() {
                  // Declarăm o metodă proprie
                  public void showDetails() {
                      System.out.println("Age: " + age);
                  }
              };
       
              a.showDetails(); // nu va funcționa pentru că referința este de tip Animal
          }
      }
  3. Metode statice
    • Nu putem defini metode statice într-o clasă anonimă, deoarece acestea nu au nume, deci nu am avea cum să le apelăm.

  • Atunci când suprascriem o metodă, metoda apelată la run-time este determinată de instanța efectivă, nu de tipul referinței. În exemplele cu clase anonime, referința pare de tip Animal, dar JVM creează pentru fiecare clasa anonimă un nume intern unic, astfel încât suprascrierea să funcționeze corect.
  • Metodele definite exclusiv în clasele anonime nu pot fi apelate din afara acestora, deoarece compilatorul verifică tipul referinței pentru a determina ce metode există. Deși JVM alocă fiecărei clase anonime un ID intern la run-time, acesta nu este cunoscut la compilare, deci nu putem declara o referință care să aibă tipul clasei anonime.

Accesul la variabile locale metodei

O clasă internă anonimă declarată într-o metodă poate folosi variabilele locale și parametrii acelei metode doar dacă aceștia sunt:

  • declarați final, sau
  • sunt effectively final, adică nu își schimbă valoarea după inițializare.
class Car {
    interface Engine { int getFuelCapacity(); }
 
    public Engine getEngine(int fuelCapacityParam) {
        int baseCapacity = fuelCapacityParam; // effectively final
 
        return new Engine() {
            public int getFuelCapacity() {
                return baseCapacity + 5; // poate fi folosit
            }
        };
    }
 
    public static void main(String[] args) {
        Car car = new Car();
        System.out.println(car.getEngine(10).getFuelCapacity()); // 15
    }
}

Variabilele locale sunt stocate pe stivă, deci dispar după terminarea metodei. Pentru a fi accesibile în clasa internă, Java creează o copie a acestor variabile și o stochează ca atribut intern al clasei anonime.

Q: Ce s-ar întâmpla dacă nu am folosi variabile final?

A: Variabila s-ar copia inițial în clasa anonimă cu valoarea de la acel moment de timp. Dacă ulterior variabila din metodă își schimbă valoarea, această schimbare nu va fi reflectată și în clasa anonimă, deci vom ajunge să lucrăm cu două valori diferite simultan, ceea ce crează bug-uri ascunse.

Până la Java 8, toate aceste variabile trebuiau declarate explicit final. De la Java 8 încolo, compilatorul tratează automat variabilele nemodificate ca effectively final.

Prescurtarea folosind expresii Lambda

Începând cu Java 8, clasele anonime care implementează interfețe cu o singură metodă abstractă (așa-numitele Functional Interfaces) pot fi înlocuite cu expresii lambda.

Lambda este o funcție anonimă, o formă mai scurtă de a exprima același comportament. Formatul unei funcții lambda este:

(parametri) -> expresie

De exemplu:

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

Dar putem avea și o logică mai complexă:

(parametri) -> {
    // bloc de cod
    return valoare;
}

  • Lambda () → 11 înlocuiește declarația clasei anonime.
  • Codul este echivalent, atâta timp cât nu avem câmpuri. Exemplul următor nu poate fi convertit la o expresie lambda pentru că are un câmp declarat:
    return new Engine() {
        private int fuelCapacity = 11;
     
        public int getFuelCapacity() {
            return fuelCapacity;
        }
    };

  • IDE-uri precum IntelliJ IDEA recomandă automat transformarea claselor anonime în expresii lambda atunci când este posibil.
  • Vom studia expresiile lambda în detaliu în laboratoarele dedicate colecțiilor și stream-urilor.

Clase interne statice

În secțiunile precedente, am discutat despre clase interne obișnuite, ale căror instanțe există doar în contextul unei instanțe a clasei exterioare. Acestea pot accesa direct membrii obiectului exterior.

Clasele interne pot fi privite ca membri ai clasei exterioare (la fel ca metodele sau câmpurile), deci pot avea aceiași modificatori de acces, inclusiv private și static, pe care clasele top-level nu le pot avea.

O clasă internă statică funcționează similar cu un câmp sau o metodă statică:

  • nu are nevoie de o instanță a clasei exterioare pentru a fi utilizată,
  • dar, în același timp, nu poate accesa membrii nestatici ai clasei exterioare (deoarece nu există o instanță a acesteia).

Exemplu de utilizare

class Car {
    static class OttoEngine {
        private int fuelCapacity;
 
        public OttoEngine(int fuelCapacity) {
            this.fuelCapacity = fuelCapacity;
        }
 
        public int getFuelCapacity() {
            return fuelCapacity;
        }
    }
 
    public OttoEngine getEngine() {
        return new OttoEngine(11); // contextul permite apelul direct
    }
}
 
public class Test {
    public static void main(String[] args) {
        Car.OttoEngine engine = new Car.OttoEngine(10); // fără instanță Car
        System.out.println(engine.getFuelCapacity());
    }
}

Observații

Situație Consecință
Instanțiere din interiorul clasei exterioare Se poate folosi direct new OttoEngine()
Instanțiere din afara clasei exterioare Se folosește new Car.OttoEngine()
Acces la câmpuri/membri nestatici ai clasei exterioare Nu este permis

Când folosim o clasă internă statică

Clasele interne statice sunt utile atunci când dorim organizare logică a codului, dar nu avem nevoie de o legătură directă cu o instanță a clasei exterioare.

Ele combină avantajele grupării (aparțin clasei exterioare) cu cele ale independenței (pot fi folosite separat, fără instanța exterioară).

Situație Explicație
Clasa internă este utilizată doar de clasa exterioară Oferă o organizare mai clară a codului, evitând “aglomerarea” pachetului cu multe clase top-level.
Clasa internă nu are nevoie să acceseze câmpuri sau metode non-statice ale clasei exterioare O putem face statică pentru a evita referințele inutile către instanța exterioară.
Clasa internă reprezintă un concept auxiliar al clasei exterioare De exemplu, Car.Engine, Database.Connection, Map.Entry.
Vrem eficiență — instanța clasei interne nu mai păstrează o referință către cea exterioară Economisim memorie și evităm scurgerile (memory leaks).

Când nu e recomandată:

  • Când clasa internă trebuie să aceseze datele instanței exterioare (câmpuri, metode nestatice).
  • Când există riscul să devină prea mare sau prea complexă, pentru acest caz ar trebui mutată într-o clasă top-level separată.

  • Pe scurt: O clasă internă statică descrie o relație “aparține de”, dar nu “depinde de” clasa exterioară.
  • Deși Java folosește Garbage Collector, pot exista memory leak-uri. Un memory leak apare atunci când un obiect din memorie nu mai este folosit, dar nici nu poate fi eliberat (garbage collected), pentru că încă există o referință către el. Acest caz este plauzibil când clasa internă păstrează o referință la cea exterioară.

Clase interne în metode și blocuri

Primele exemple ilustrează cele mai frecvente moduri de utilizare a claselor interne.

Totuși, designul acestora este destul de flexibil, iar Java permite și forme mai puțin întâlnite. Concret, o clasă internă poate fi declarată de asemenea și în:

  • interiorul unei metode;
  • într-un bloc de cod.

Aceste variante sunt mai rare, dar oferă un control mai fin asupra vizibilității și duratei de viață a clasei interne.

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 metodei getEngine. În acest mod, vizibilitatea ei a fost redusă pentru că nu poate fi instanțiată decât în această metodă.

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

În mod obișnuit, moștenirea unei clase presupune doar apelarea constructorului clasei părinte. Însă, pentru clasele interne, lucrurile sunt mai complexe: constructorul clasei interne trebuie să se „atașeze” întotdeauna de un obiect al clasei exterioare.

Acest lucru înseamnă că atunci când derivăm o clasă internă, constructorul clasei derivate trebuie să știe de ce instanță a clasei exterioare aparține, deoarece nu există un obiect „default” al clasei exterioare la care să se lege automat.

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

Observații:

Aspect Detalii
Parametru constructor Constructorul clasei derivate trebuie să primească o referință la instanța clasei exterioare (Car car)
Inițializare super Linia car.super(); creează legătura între clasa internă părinte (Engine) și obiectul exterior (car)
Efect Moștenirea clasei interne se realizează corect fără erori de compilare

Utilizarea claselor interne

Clasele interne pot părea complicate la prima vedere, dar devin foarte utile în următoarele situații:

  1. Încapsularea logicii care nu trebuie să fie accesibilă din exterior
    • Creăm o clasă pentru rezolvarea unei probleme specifice, dar nu dorim ca aceasta să fie vizibilă sau utilizată în alte părți ale programului.
  2. Ascunderea implementării unei interfețe
    • Dorim să întoarcem o referință la o interfață fără a expune implementarea concretă.
    • Exemplu: clasele interne anonime sau lambda pentru implementarea interfețelor cu o singură metodă.
  3. Moștenirea multiplă „simulată”
    • În Java nu putem extinde decât o singură clasă.
    • Folosind clase interioare, putem crea structuri care extind o clasă și în același timp accesează membri ai clasei exterioare, obținând efecte similare moștenirii multiple.
  4. Sisteme bazate pe evenimente (GUI)
    • Clasele interne sunt frecvent folosite în arhitecturi de control pentru tratarea evenimentelor.
    • Exemple: Swing, AWT, SWT.
    • Se pot atașa clase interne anonime pentru a gestiona evenimente specifice, fără a defini clase separate:
// Codul tău cu clasa anonimă
public class MyGUI {
 
    public void closeWindow() {
        // Clasa anonimă implementează ActionListener
        button.addActionListener(new ActionListener() { 
            @Override
            public void actionPerformed(ActionEvent e) {            
                numClicks++;
            }
        });
    }
}
 
// Cum ar putea arăta metoda addActionListener intern
class JButton {
    ...
 
    public void addActionListener(ActionListener listener) {
        // apelează metoda suprascrisă din clasa anonimă
        listener.actionPerformed(this.event);
    }
 
    ...
}

  • În acest exemplu, clasa anonimă implementează ActionListener și suprascrie metoda actionPerformed, iar implementarea rămâne ascunsă de restul aplicației.
  • Exemplul de mai sus folosește o versiune simplificată a clasei JButton pentru a ilustra exact de ce este nevoie să pasăm o clasă ca argument și cum putem scurta codul folosind o clasă anonimă pentru acest caz.

[Optional] Nested vs. Inner classes

[Optional] Nested vs. Inner classes

Acum că am aflat mai multe despre clasele interne, le putem clasifica în Inner classes și Nested classes.

Nested class

  • O clasă definită în interiorul altei clase.
  • Poate fi static sau non-static.
  • Dacă este static, se numește static nested class și nu poate accesa direct membrii instanței clasei exterioare.
  • Utilitate: organizare cod, grupare logică a claselor.

Inner class

  • O clasă non-static definită în interiorul altei clase.
  • Poate accesa direct membrii instanței clasei exterioare (inclusiv private).
  • Tipuri:
    • Member inner class: definită în corpul clasei exterioare.
    • Local inner class – definită în interiorul unei metode.
    • Anonymous inner class – fără nume, folosită de obicei pentru interfețe sau clase abstracte.

Diferențe cheie:

Caracteristică Nested static class Inner class (non-static)
Acces la membri instanță Nu Da
Necesită instanță outer? Nu Da
Poate fi instanțiată direct Da Nu

🧶 Strings (deep dive)

Un obiect de tip String reprezintă o secvență de caractere Unicode. Intern, aceste caractere sunt stocate într-un array Java. Accesul la acest array este strict controlat: poate fi manipulat doar prin metodele puse la dispoziție de obiectul String.

Deși unele operații par să modifice conținutul sau lungimea șirului, în realitate obiectul original rămâne neschimbat. Aceste operații creează un nou obiect String, care poate fie să copieze, fie să facă referire internă la caracterele relevante. Acest comportament se datorează imutabilității obiectelor String: odată creat, conținutul unui șir nu poate fi modificat.

Construirea String-urilor

În Java, obiectele de tip String pot fi construite în mai multe moduri, în funcție de sursa datelor și de scopul aplicației.

String literal

Un string literal este definit direct în cod între ghilimele duble (). Java creează automat obiectul și îl atribuie variabilei.

String quote = "To be or not to be";
  • Lungimea șirului se obține cu metoda length():
int length = quote.length(); // 18
  • Verificarea șirului gol se face cu metoda isEmpty():
boolean empty = quote.isEmpty(); // false
  • Concatenarea șirurilor se poate face folosind singurul operator overloaded din Java, adică + sau folosind metoda concat():
String name = "John " + "Smith";
String name2 = "John ".concat("Smith");

În Java, String-urile își gestionează propria lungime și prin urmare ele nu necesită terminatori speciali cum ar fi '\0' în limbajul C.

Text block

Pentru texte mai lungi, Java 13 a introdus text blocks (”””).

Acestea permit scrierea de șiruri pe mai multe linii și păstrează indentarea. Sunt utile pentru HTML, SQL sau mesaje lungi.

String poem = """
    Twas brillig, and the slithy toves
        Did gyre and gimble in the wabe:
    All mimsy were the borogoves,
        And the mome raths outgrabe.
    """;
System.out.print(poem); // Try this out, see how it works!

Cum funcționează:

  • Primul caracter non-spațiu din partea stângă definește „marginea stângă”.
  • Spațiile din stânga acelei margini sunt ignorate.
  • Spațiile după margine sunt păstrate.

Încorporarea de texte lungi direct în codul sursă nu este, în general, recomandată. Pentru texte mai mari de câteva linii, este mai eficient să le încarci din fișiere externe dedicate.

[Optional] Alte modalități de creare a unui obiect String

[Optional] Alte modalități de creare a unui obiect String

Pe lângă utilizarea expresiilor de tip string literal, un obiect String în Java pot fi construit și din alte surse, cum ar fi array-uri de caractere sau array-uri de octeți (bytes).

Această metodă este utilă atunci când caracterele sunt generate sau procesate individual, iar la final se dorește formarea unui șir complet.

char[] data = new char[] { 'L', 'e', 'm', 'm', 'i', 'n', 'g' };
String lemming = new String(data);

Se poate construi un String și dintr-un array de octeți, specificând opțional și schema de codificare a caracterelor. În acest exemplu, octeții sunt interpretați conform schemei de codificare ISO8859_1. Dacă nu se specifică o codificare, se va folosi codificarea implicită a sistemului.

byte[] data = new byte[] { (byte)97, (byte)98, (byte)99 }; // a, b, c
String abc = new String(data, "ISO8859_1");

Accesarea individuală a fiecărui caracter

Metoda charAt(int index) permite accesul la caracterele dintr-un șir într-un mod similar cu accesul într-un array.

String s = "Newton";
for (int i = 0; i < s.length(); i++) {
    System.out.println(s.charAt(i));
}

Output

Output

N
e
w
t
o
n

Clasa String implementează interfața java.lang.CharSequence, astfel:

  • se definesc metodele length() și charAt(int index).
  • se permite obținerea de subseturi și manipularea caracterelor individuale.

Strings from Things

În Java, orice obiect sau tip primitiv poate fi transformat într-un String, adică o reprezentare textuală.

  • Pentru tipurile primitive (int, float, boolean etc.), șirul rezultat este evident.
  • Pentru obiecte, reprezentarea textuală depinde de metoda toString() definită de obiectul respectiv.

Folosirea String.valueOf()

Metoda statică String.valueOf() convertește orice element într-un String. Există mai multe suprascrieri care acceptă toate tipurile primitive:

String one = String.valueOf(1);         // întreg → "1"
String two = String.valueOf(2.384f);    // float → "2.384"
String notTrue = String.valueOf(false); // boolean → "false"

Reprezentarea obiectelor

Toate obiectele moștenesc metoda toString() din Object.

  • Unele clase suprascriu metoda și oferă o reprezentare utilă:
    Date date = new Date(); // suprascrie metoda toString() din Object
    String d1 = String.valueOf(date);
    String d2 = date.toString();
    // Ex: "Fri Dec 19 05:45:34 CST 1969"
  • Dacă metoda toString() nu este definită, se afișează un identificator unic:
    System.out.println(new Student("Andrei", 22)); 
    // Student@<cod_hex_id>

Diferența între valueOf() și toString()

Atunci când este apelată metoda String.valueOf(), ea invocă de fapt metoda toString() a acelui obiect și returnează rezultatul.

Singura diferență reală în utilizarea acestei metode este că dacă îi transmiți o referință null, ea returnează șirul “null” în loc să genereze o excepție de tip NullPointerException:

date = null;
d1 = String.valueOf(date); // "null"
d2 = date.toString(); // NullPointerException!

[Optional] Folosirea String.valueOf() pentru concatenare

[Optional] Folosirea String.valueOf() pentru concatenare

Concatenarea string-urilor folosește intern metoda valueOf(), așadar, dacă se face append cu un obiect sau o valoare primitivă folosind operatorul + se obține:

String today = "Today's date is: " + LocalDate.now(); // Today's date is: 2025-10-31

Un artificiu pentru obținerea valorii textuale a unui obiect este folosirea unui șir gol (””) împreună cu operatorul + ca o prescurtare.

// practic se apelează String.valueOf() pe cele două obiecte de tip float și Date
String two = "" + 2.384f; // "2.384"
String today = "" + new Date(); // "Fri Oct 31 17:54:01 EET 2025"

Things from Strings

Limbajul Java oferă API-uri puternice pentru a interpreta (parsa) și formata textul, fie că este vorba de numere, date, ore sau valori monetare.

Parsarea valorilor primitive

În Java, tipurile primitive (precum int, double, boolean) nu sunt obiecte. Totuși, fiecare dintre ele are o clasă wrapper (de exemplu, Integer, Double, Boolean) care oferă metode utile pentru conversii între text și valori numerice.

byte b = Byte.parseByte("16");
int n = Integer.parseInt("42");
long l = Long.parseLong("99999999999");
float f = Float.parseFloat("4.2");
double d = Double.parseDouble("99.9999");
boolean flag = Boolean.parseBoolean("true");

Aceste metode parsează un String și returnează valoarea primitivă corespunzătoare.

[Optional] Radix (baza numerică)

[Optional] Radix (baza numerică)

Metodele Integer.parseInt() și Long.parseLong() acceptă un parametru opțional numit radix, adică baza în care este reprezentat numărul:

int hex = Integer.parseInt("A", 16);   // 10 în baza 10
int oct = Integer.parseInt("10", 8);   // 8 în baza 10

Aceasta este utilă pentru lucrul cu numere hexazecimale, octale sau alte formate, des întâlnite în domenii precum criptografie sau prelucrarea fișierelor binare.

Vom discuta mai multe despre clasele Wrapper în următoarele laboratoare, momentan este suficient să știm că le putem folosi pentru a converti String-uri în tipuri de date primitive.

Compararea String-urilor

În Java, compararea string-urilor se face cu metoda equals(), care verifică dacă două șiruri conțin exact aceleași caractere în aceeași ordine.

Totodată, pentru comparare insensibilă la majuscule, se folosește equalsIgnoreCase():

String one = "FOO";
String two = "foo";
 
one.equals(two); // false
one.equalsIgnoreCase(two); // true

Nu folosiți operatorul == pentru compararea String-urilor, deoarece acesta verifică identitatea obiectelor, adică dacă referințele indică către același obiect.

String foo1 = "foo";
String foo2 = String.valueOf(new char [] { 'f','o','o' });
foo1 == foo2 // false
foo1.equals(foo2) // true

String Pool

Compararea șirurilor poate fi înșelătoare dacă nu înțelegem cum funcționează memoria și optimizările interne, iar greșeala de a folosi == apare mai ales când lucrăm cu String literals.

Când mai multe șiruri identice apar în aceeași aplicație, Java le stochează într-un pool comun numit Java String Pool. Drept urmare, toate referințele către un șir din pool indică către același obiect.

Avantajele acestei optimizări sunt:

  • reducerea consumului de memorie.
  • permiterea comparațiilor rapide folosind doar referința ==.

  • Această optimizare este posibilă, deoarece String-urile sunt imutabile, deci nu își pot schimba valoarea.
  • String Pool-ul se află în aceeași zonă de memorie cu Constant Pool descris în laboratoarele trecute.

Pentru cazul când folosim String literal:

  • Valoarea este stocată în String Pool.
  • Dacă există deja un șir identic, referința indică același obiect.
  • Nu se creează un obiect nou.
String str1 = "Java";

Pentru cazul când folosim String cu constructor:

  • Se creează un obiect nou în Heap.
  • Chiar dacă valoarea există deja în pool, new forțează instanțierea unui obiect separat.
String str4 = new String("Java");

Datorită optimizării prin String Pool, se preferă mereu să creăm String-uri folosind String literals.

Egalitatea String-urilor în contextul String Pool

Pe baza exemplului de mai sus, expresiile urmǎtoare sunt evaluate astfel:

Operatorul == verifică dacă cele două referințe indică același obiect în memorie.

  • str1 == str2true, deoarece ambele referințe indică același obiect din String Pool.
  • str3 == str4false, deoarece str4 a fost creat cu new String(), deci este un obiect diferit în Heap.

Metoda equals() verifică dacă conținutul este identic, indiferent de locația în memorie.

  • str1.equals(str2)true, conținutul este “Java”.
  • str1.equals(str3)false, conținutul lor diferǎ
  • str3.equals(str4)true, conținutul este “C++”.

[Nice to know] Compararea valorilor lexicale

[Nice to know] Compararea valorilor lexicale

Metoda compareTo() compară valoarea lexicală a două String-uri, folosind specificația Unicode pentru a determina poziția relativă a celor două șiruri în „alfabet” (mai exact, în tabelul Unicode).

Metoda returnează un număr întreg conform următoarelor reguli:

  • < 0 dacă primul șir este „mai mic” decât al doilea
  • = 0 dacă șirurile sunt egale
  • > 0 dacă primul șir este „mai mare” decât al doilea
String abc = "abc";
String def = "def";
String num = "123";
 
if (abc.compareTo(def) < 0) { ... } // true
if (abc.compareTo(abc) == 0) { ... } // true
if (abc.compareTo(num) > 0) { ... } // true

Valoarea returnatǎ nu contează de fapt, deoarece poate avea valori multiple (poate fi -1, -5 sau -1000), deci ne interesează doar semnul rezultatului:

  • negativ → primul șir este „mai mic”
  • zero → șirurile sunt egale
  • pozitiv → primul șir este „mai mare”

Compararea se face strict pe baza poziției caracterelor în Unicode. Pentru comparații mai complexe (ex. suport internaționalizare), se recomandă clasa java.text.Collator.

String vs. StringBuilder

În Java, lucrul cu șiruri de caractere poate fi realizat folosind String sau StringBuilder. Alegerea între ele are impact asupra performanței și memoriei aplicației, mai ales când lucrăm cu texte mari sau modificări frecvente.

String

String este imutabil. Orice modificare a unui obiect String creează un nou obiect în memorie.

String s = "Hello";
s += " World"; // se creează un obiect nou
System.out.println(s); // Hello World
 
// Se creează 100 de obiecte noi (ineficient)
for (int i = 0; i < 100; i++) {
   s += " World";
   System.out.println(s);
}

Avantaje:

  • Simplu de folosit pentru texte fixe sau rar modificate.
  • Sigur în medii multi-threaded (thread-safe).

Dezavantaje:

  • Modificările frecvente duc la alocări repetate de memorie.
  • Crește consumul de memorie și poate încetini aplicația în bucle mari.
StringBuilder

StringBuilder este mutabil. Modificările se aplică direct obiectului existent, fără a crea noi obiecte.

StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // modifică același obiect
System.out.println(sb); // Hello World
 
// Se modifică același obiect de 100 de ori (eficient)
for (int i = 0; i < 100; i++) {
   sb.append(" World");
   System.out.println(sb);
}

Avantaje:

  • Ideal pentru concatenări frecvente sau modificări multiple de text.
  • Consumă mai puțină memorie și crește performanța în bucle.

Dezavantaje:

  • Nu este thread-safe, deci trebuie folosit cu atenție în aplicații multi-threaded.

Un obiect StringBuilder poate fi convertit în String folosind metoda toString() din clasa StringBuilder.

Dacă efectuați doar câteva concatenări, nu este necesar să folosiți StringBuilder. De exemplu, metodele toString() generate de IntelliJ utilizează direct string literals, deoarece concatenările sunt puține. În acest caz, crearea unui obiect StringBuilder, utilizarea lui și apoi convertirea în String ar fi mai puțin eficientă.

Aveți grijă să țineți cont de cazurile de utilizare dintre String și StringBuilder, deoarece folosirea incorectă a acestora vă poate impacta drastic performanța apliacației voastre.

Următoarele secțiuni marcate [Nice to know] vă pot ajuta la rezolvarea temelor.

[Nice to know] Căutarea în conținutul unui String

Clasa String oferă mai multe metode simple pentru a căuta subșiruri fixe într-un șir. Astfel, metodele startsWith() și endsWith() verifică dacă șirul începe sau se termină cu un anumit subșir:

String url = "http://foo.bar.com/";
if (url.startsWith("http:")) // true

Metoda indexOf() caută prima apariție a unui caracter sau subșir și returnează poziția de start (indexul) al acestuia, sau -1 dacă nu este găsit. În mod similar, metoda lastIndexOf() caută ultima apariție a unui caracter sau subșir, parcurgând șirul invers.

String abcs = "abcdefghijklmnopqrstuvwxyz";
int i = abcs.indexOf('p'); // 15
int i = abcs.indexOf("def"); // 3
int i = abcs.indexOf("Fang"); // -1

Metoda contains() se ocupă de sarcina foarte comună de a verifica dacă un anumit subșir se regǎsește în șirul țintă.

String log = "There is an emergency in sector 6!";
if (log.contains("emergency")) pageSomeone();
// equivalent to
if (log.indexOf("emergency") != -1) ...

Pentru căutări complexe în șiruri, Java oferă API-ul pentru expresii regulate (Regular Expressions), care utilizează sintaxa RegEx pentru definirea și identificarea tiparelor în text.

Metodele clasei String

Metodele clasei String

În Java, clasa String oferă o varietate de metode care permit manipularea și analiza șirurilor de caractere. Mai jos prezentăm câteva metode importante, dar pentru detalii suplimentare consultați documentația oficială Oracle.

Metoda Funcționalitate
charAt() Extrage din string caracterul de la indexul dat ca parametru
compareTo() Compară șirul cu un alt șir de caractere
concat() Concatenează șirul cu un alt șir de caractere
contains() Verifică dacă șirul conține un alt șir de caractere
copyValueOf() Returnează un șir echivalent cu un array de caractere specificat
endsWith() Verifică dacă șirul se termină cu un sufix specific
equals() Compară șirul cu un alt șir de caractere
equalsIgnoreCase() Compară șirul cu un alt șir, ignorând diferențele de majuscule/minuscule
getBytes() Copiază caracterele din șir într-un array de octeți
getChars() Copiază caracterele din șir într-un array de caractere
hashCode() Returnează codul hash al șirului
indexOf() Caută prima apariție a unui caracter sau subșir
intern() Obține o instanță unică a șirului dintr-un pool global de șiruri partajate
isBlank() Returnează true dacă șirul este gol sau conține doar spații albe
isEmpty() Returnează true dacă șirul este gol
lastIndexOf() Caută ultima apariție a unui caracter sau subșir
length() Returnează lungimea șirului
lines() Returnează un flux de linii separate prin terminatori de linie
matches() Verifică dacă întregul șir se potrivește cu un model de expresie regulată
regionMatches() Verifică dacă o regiune din șir se potrivește cu o regiune din alt șir
repeat() Returnează concatenarea șirului repetat de un număr specificat de ori
replace() Înlocuiește toate aparițiile unui caracter cu alt caracter
replaceAll() Înlocuiește toate aparițiile unui model de expresie regulată cu alt model
replaceFirst() Înlocuiește prima apariție a unui model de expresie regulată cu alt model
split() Împarte șirul într-un array de șiruri folosind o expresie regulată ca delimitator
startsWith() Verifică dacă șirul începe cu un prefix specific
strip() Elimină spațiile albe de la început și sfârșit, conform Character.isWhitespace()
stripLeading() Elimină spațiile albe de la început, similar cu strip()
stripTrailing() Elimină spațiile albe de la sfârșit, similar cu strip()
substring() Returnează un subșir din șirul original
toCharArray() Returnează un array cu caracterele din șir
toLowerCase() Convertește șirul în litere mici
toString() Returnează valoarea șirului ca obiect
toUpperCase() Convertește șirul în litere mari
trim() Elimină spațiile albe de la început și sfârșit (caractere cu cod Unicode ≤ 32)
valueOf() Returnează o reprezentare textuală a unei valori

[Nice to know] Tokenizare

În procesul de prelucrare a textului, apar frecvent situații în care este necesar să extragem și să lucrăm cu porțiuni individuale dintr-un String.

De exemplu, putem dori să analizăm fiecare cuvânt dintr-un paragraf sau să extragem câmpuri dintr-un rând de date. Este mult mai eficient să lucrăm cu segmente mai mici decât cu întregul șir.

  • Această împărțire a textului se realizează pe baza unor delimitatori, adică caractere sau secvențe care separă elementele între ele (cum ar fi spații, virgule sau punct și virgulă).
  • Rezultatul este o colecție de tokeni, adică unități logice extrase din text. Un token poate fi un cuvânt simplu, un nume de utilizator, o adresă de email, un număr sau orice altă informație relevantă.

Java oferă mai multe metode și clase pentru manipularea textului, uneori cu funcționalități suprapuse.

Una dintre cele mai utile metode pentru tokenizare este split(), disponibilă în clasa String. Această metodă acceptă ca argument o expresie regulată (RegEx) care descrie delimitatorul. Pe baza acestei expresii, metoda împarte șirul original într-un array de șiruri (String[]), fiecare reprezentând un token.

String text1 = "Now is the time for all good people";
String[] words = text1.split("\\s");

Expresia \\s este o expresie regulată care corespunde unui caracter de tip white-space (space, tab, newline). În exemplul de mai sus, rezultatul obținut este un array cu 8 elemente, fiecare conținând un cuvânt din propoziție.

String text2 = "4231,     Java Programming, 1000.00";
String[] fields = text2.split("\\s*,\\s*");

Expresia \\s*,\\s* corespunde unui separator de tipul: virgulǎ înconjuratǎ de zero sau mai multe caractere de tip white-space. Astfel, șirul este împărțit în trei câmpuri: “4231”, “Java Programming”, “1000.00”.

În Java, putem face căutări și separări mai complexe în text folosind pattern matching cu expresii regulate (RegEx), nu doar delimitatori simpli. Vom vedea mai jos câteva exemple.

[Nice to know] Expresii Regulate

În prelucrarea textului, apar frecvent situații în care trebuie să verificăm dacă un șir respectă un anumit format sau să căutăm pattern-uri specifice în interiorul lui.

De exemplu, putem dori să validăm o adresă de email, să identificăm toate numerele dintr-un paragraf sau să extragem linkurile dintr-un text. Pentru astfel de sarcini, expresiile regulate (RegEx) sunt instrumentul ideal.

  • Expresiile regulate sunt șiruri speciale de caractere care reprezintă un model (șablon de căutare) care descrie un set de reguli pentru potrivirea textului.
  • Spre deosebire de căutarea simplă, unde verificăm dacă un cuvânt există într-un șir, expresiile regulate ne permit să definim modele complexe pentru validarea unui String.

RegEx

Sintaxa regex-ului combină caractere obișnuite (litere, numere, sau caractere precum spatiu sau underscore) cu metacaractere speciale.

Metacaracterele sunt caractere care au un inteles special in cadrul unei expresii regulate. Ele sunt expandate in niste caractere obisnuite. Prin utilizarea metacaracterelor nu trebuie specificate toate combinatiile distincte de caractere pentru care vrei sa existe potrivire.

Câteva metacaractere:

  • . - potrivire cu orice caracter mai puțin newline
  • * - potrivire cu 0 sau mai multe aparitii ale caracterului anterior
  • + → una sau mai multe aparițiiale caracterului anterior
  • [chars] - potrivire cu unul din caracterele din secventa chars; se poate utiliza - pentru utilizarea unei plaje de caractere (a-z, 0-9); daca primul caracter este ^ atunci se potriveste cu orice caracter care nu este specificat in chars
  • ^ - potrivire cu inceputul liniei
  • $ - potrivire cu sfarsitul liniei
  • \ - trateaza caracterul ce urmeaza literal; este folosit pentru escaparea altor metacaractere (inclusiv \)
  • \d - înseamnă potrivire „orice cifră”
  • \s – potrivește orice caracter de tip white-space (spațiu, tab, newline)
  • \S – potrivește orice caracter care nu este white-space
  • \w – potrivește orice caracter care se aflǎ de regulǎ în cuvinte (litere upper-case/lower-case, cifre 0-9, underscore)
  • \W – potrivește orice caracter care nu aflǎ de regulǎ în cuvinte (oricare, mai puțin cele enunțate mai sus)
  • ? – zero sau una apariție a unui caracter, apariția sa este opționalǎ
  • | – permite alegerea între două (sau mai multe) expresii; practic, spune „potrivește fie partea din stânga, fie partea din dreapta”

Pentru o listă completă de simboluri și exemple cu explicații, puteți consulta acest cheat sheet.

Exemple de expresii
  • a.c va potrivi „abc”, „a1c” sau „a-c”, pentru că între „a” și „c” poate fi orice caracter.
  • \d+ va potrivi un număr format din una sau mai multe cifre, + cere cel puțin o cifră
  • \d* va potrivi un număr format din 0 sau mai multe cifre, va potrivi inclusiv un șir gol
  • [^@]+@[^@]+ va potrivi o adresǎ de email (exemplu minimalist)

Regex-ul este foarte util, dar nu garantează acuratețe în toate cazurile. Pentru validări robuste cum ar fi verificarea unui email, se recomandă librării dedicate, cum ar fi cele din librǎria apache.commons.

import org.apache.commons.validator.routines.EmailValidator;
 
String email = "myName@example.com";
boolean valid = EmailValidator.getInstance().isValid(email);

Pattern Matching

În Java, expresiile regulate sunt implementate prin pachetul java.util.regex, care oferă două clase principale: Pattern și Matcher. Clasa Pattern este responsabilă pentru compilarea expresiei regulate, iar Matcher aplică această expresie asupra unui text.

Pentru verificări rapide, putem folosi metoda statică Pattern.matches(regex, text), care întoarce true dacă textul se potrivește complet cu expresia regulată. De exemplu:

boolean match = Pattern.matches("\\d+\\.\\d+f?", "42.0f");

Aceasta verifică dacă șirul este un număr în stil Java, cum ar fi „42.0f”. Dacă vrem să căutăm apariții parțiale sau multiple într-un text, folosim Matcher. Mai întâi compilăm expresia regulată:

Pattern pattern = Pattern.compile("horse|course");
Matcher matcher = pattern.matcher("A horse is a horse, of course of course");

Apoi putem apela matcher.find() într-un loop pentru a găsi toate potrivirile:

while (matcher.find()) {
    System.out.println("Matched: '" + matcher.group() + "' at position " + matcher.start());
}

Acest cod va afișa toate aparițiile cuvintelor „horse” și „course” din text, împreună cu poziția lor.

Regex-ul este util și pentru împărțirea textului în câmpuri. Metoda split() din clasa String acceptă o expresie regulată. De exemplu:

String text = "Foo, bar , blah";
String[] fields = text.split("\\s*,\\s*");

Rezultatul va fi un array cu elementele „Foo”, „bar” și „blah”, fără spații suplimentare.

Puteți și vă recomandăm să testați reguli de tip RegEx folosind acest validator online.

Summary

Clase interne

  • Clasele interne sunt declarate în interiorul altei clase.
  • Pot fi normale, statice, anonime sau locale metodelor/blocurilor.
  • Au acces la membrii clasei exterioare, inclusiv cei private.
  • Clasele interne pot fi public, protected, private sau static.
  • Clasele anonime nu au constructor și se folosesc pentru scopuri restrânse.
  • Clasele interne statice nu depind de o instanță a clasei exterioare.
  • Moștenirea claselor interne necesită referință la obiectul exterioar.
  • Folosirea claselor interne ajută la încapsulare, ascunderea implementării și organizarea logică a codului.

Strings

  • Obiectele String sunt imutabile; operațiile creează noi obiecte.
  • Se pot crea din literal, array de caractere sau array de octeți.
  • Concatenarea se face cu + sau concat().
  • Se accesează caractere individuale cu charAt().
  • Compararea se face cu equals() sau equalsIgnoreCase().
  • String Pool stochează literaluri identice pentru economisirea memoriei.
  • Conversia altor tipuri în String se face cu String.valueOf().
  • Parsarea stringurilor în primitive se face cu metode ca Integer.parseInt(), Boolean.parseBoolean().
  • Tokenizare: sparge un string în bucăți folosind split() sau StringTokenizer.
  • Regex (expresii regulate): căutare, validare sau înlocuire în stringuri folosind matches(), replaceAll(), Pattern și Matcher.

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.

Exercițiile din acest laborator 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 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 enum CarType declarat intern în Car. Acesta conține trei valori: Mercedes, Fiat și Skoda.
  • Prețul și anul vor fi de tipul integers.

Creați un constructor cu toti cei trei parametri, în ordinea din enunț și suprascrieți metoda toString() pentru afișare în felul următor: Car{price=20000, carType=SKODA, year=2019}

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 (4p)

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

  • BrandOffer - calculează un discount în funcție de tipul mașinii:
    1. Mercedes: discount 5%;
    2. Fiat: discount 10%;
    3. Skoda: discount 15%;
  • DealerOffer - calculează un discount în funcție de vechimea mașinii:
    1. Mercedes: discount 300 pentru fiecare an de vechime;
    2. Fiat: discount 100 pentru fiecare an de vechime;
    3. Skoda: discount 150 pentru fiecare an de vechime;
  • SpecialOffer - calculează un discount random, cu seed 20. Generarea se va realiza în constructor utilizându-se o instanța globală a unui obiect de tip Random care a fost inițializat cu seed-ul 20 și cu limita superioară (bound) 1000 Random.

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

  • aplicați pe obiectul car primit ca argument cele trei oferte in ordinea: BrandOffer, DealerOffer, SpecialOffer.
  • metoda va returna prețul final după aplicarea ofertelor

Testare oferte: Creati 2 obiecte Car pentru fiecare tip de mașină cu urmatoarele valori:.

  1. Mercedes:
    1. Pret: 20000, An: 2010;
    2. Pret: 35000, An: 2015;
  2. Fiat:
    1. Pret: 3500, An: 2008;
    2. Pret: 7000, An: 2010;
  3. Skoda:
    1. Pret: 12000, An: 2015;
    2. Pret: 25000, An: 2021;
  • Creati un obiect de tip Dealership.
  • Obțineți și afișați prețul oferit de Dealership(folosind metoda getFinalPrice) pentru fiecare obiect.
  • De fiecare data cand se aplica o oferta asupra unui obiect de tip Car se va afisa un mesaj de tipul: “Applying x discount: y euros”, unde:
    • x reprezinta oferta care a fost aplicata(Brand, Dealer, Special, Client)
    • y reprezinta discount-ul ofertei.

Task 3 - Negocierea (2p)

Aăugați în clasa Dealership metoda void negotiate(Car car, Offer offer). Aceasta permite clientului să propună un discount.

În metoda main apelați negotiate dând ca parametru oferta sub formă de clasă anonimă. Implementarea ofertei clientului reprezinta returnarea unui discount de 5%. Pentru testare folositi urmatorul obiect Car: -Pret: 20000 -Tip: Mercedes -An: 2019

Task 4 - Detectivul Stringson (2p)

Ai primit un mesaj misterios de la o inteligență artificială. Mesajul conține un cod secret și un număr de identificare. Pentru a rezolva misiunea detectivul trebuie să:

  1. Să curățe mesajul de spațiile de la început și sfârșit și să salveze rezultatul într-o variabilă numită codeMessage.
    • printați “Cod curatat: ”.
  2. Să înlocuiască POO din codeMessage cu un șir gol de caractere și să verifice dacă String-ul rezultat este gol.
    • printați “Codul este gol!” sau “Codul nu este gol” în funcție de caz.
  3. Să afișeze lungimea codului și primul caracter.
    • printați “Lungime cod: ” și “Primul caracter: ” în această ordine.
  4. Să concateneze codeNumber la codeSecret și să printeze valoarea.
    • printați “Cod complet: ”.
  5. Să transforme codeNumber în int, la care se adună numărul 5, iar apoi rezultatul va fi stocat într-un String numit modifiedCodeNumber.
    • printați “CodeNumber modificat: ”.
  6. Să compare codeNumber cu modifiedCodeNumber folosind metoda compareTo().
    • printați “Codul initial și modificat sunt identice” sau ”“Codul initial este mai mic decat codul modificat” sau “Codul initial este mai mare decat codul modificat” în funcție de caz.
  7. Să compare codeMessage cu message folosind == pentru a testa dacă se folosește aceeași referință.
    • printați “Acelasi obiect” sau “Obiecte diferite”.
  8. Să adauge extraCodes la începutul lui codeSecret de 5 ori și să salveze rezultatul în fullCodeMessage.
    • printați “Cod complet: ”
  9. Să verifice egalitatea lui fullCodeMessage cu password folosind equals() și equalsIgnoreCase().
    • printați “equals: ” și “equalsIgnoreCase: ” în această ordine.

Pentru acest exercițiu vă recomandăm să folosiți metodele din clasa String prezentate în laborator în acordeonul “Metodele clasei String” (ctrl + f).

poo-ca-cd/laboratoare/clase-interne-si-strings.txt · Last modified: 2025/11/06 14:02 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