Laboratorul 7: I/O și Gestionarea Excepțiilor

Obiective

  • înţelegerea conceptului de excepţie şi utilizarea corectă a mecanismelor de generare şi tratare a excepţiilor puse la dispoziţie de limbajul / maşina virtuală Java
  • familiarizarea cu modalitățile de scriere și citire în Java și gestionarea resurselor asociate acestor operații de tip I/O (ex.: stream-uri de date, fișiere)

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

💥 Excepții

În esenţă, o excepţie este un eveniment care se produce în timpul execuţiei unui program şi care perturbă fluxul normal al instrucţiunilor acestuia.

De exemplu, în cadrul unui program care copiază un fişier, astfel de evenimente excepţionale pot fi:

  • absenţa fişierului pe care vrem să-l copiem
  • imposibilitatea de a-l citi din cauza permisiunilor insuficiente
  • probleme cauzate de accesul concurent la fişier

Utilitatea conceptului de excepţie

O abordare des întâlnită înainte de introducerea excepțiilor era întoarcerea unor valori speciale din funcții pentru a semnala erori. De exemplu, în C, fopen returnează NULL dacă deschiderea fișierului eșuează. Această metodă are două dezavantaje majore:

  • Uneori, toate valorile de retur pot fi valide. De exemplu, o funcție care întoarce succesorul unui int nu are ce valoare specială să folosească dacă se depășește Integer.MAX_VALUE, iar o valoare precum -1 ar fi interpretată ca rezultat valid.
  • Nu există separare clară între logica normală și logica de tratare a erorilor. Fiecare apel de funcție trebuie urmat de verificări, ceea ce duce la cod imbricat, greu de citit, de forma:
Main.c
int openResult = open();
 
if (openResult == FILE_NOT_FOUND) {
    // handle error
} else if (openResult == INSUFFICIENT_PERMISSIONS) {
    // handle error
} else {// SUCCESS
    int readResult = read();
    if (readResult == DISK_ERROR) {
        // handle error
    } else {
        // SUCCESS
        ...
    }
}

Mecanismul bazat pe excepţii înlătură ambele neajunsuri menţionate mai sus. Codul ar arăta aşa:

Main.java
try {
    open();
    read();
    ...
} catch (FILE_NOT_FOUND) {
    // handle error
} catch (INSUFFICIENT_PERMISSIONS) {
    // handle error
} catch (DISK_ERROR) {
    // handle error
}

Instrucțiunile din fluxul normal de execuție sunt incluse într-un bloc try, iar situațiile excepționale sunt tratate în blocuri catch.

Logica este următoarea: codul din try se execută secvențial, iar când apare o excepție, restul instrucțiunilor se abandonează și execuția sare direct în blocul catch corespunzător.

Excepţii în Java

Când o eroare se produce într-o funcţie, aceasta creează un obiect excepţie şi îl pasează către runtime system. Un astfel de obiect conţine informaţii despre situaţia apărută:

  • tipul de excepţie
  • stiva de apeluri (stack trace): punctul din program unde a intervenit excepţia, reprezentat sub forma lanţului de metode în care programul se află în acel moment

Pasarea menţionată mai sus poartă numele de aruncarea (throwing) unei excepţii.

Aruncarea excepţiilor

Exemplu de aruncare a unei excepţii:

List<String> l = getArrayListObject();
if (null == l)
    throw new Exception("The list is empty");

În acest exemplu, încercăm să obţinem un obiect de tip ArrayList; dacă funcţia getArrayListObject întoarce null, aruncăm o excepţie.

Pe exemplul de mai sus putem face următoarele observaţii:

  • un obiect-excepţie este un obiect ca oricare altul, şi se instanţiază la fel (folosind new);
  • aruncarea excepţiei se face folosind cuvântul cheie throw;
  • există clasa Exception care desemnează comportamentul specific pentru excepţii.

În realitate, clasa Exception este părintele majorităţii claselor excepţie din Java. Enumerăm câteva excepţii standard:

  • IndexOutOfBoundsException: este aruncată când un index asociat unei liste sau unui vector depăşeşte dimensiunea colecţiei respective.
  • NullPointerException: este aruncată când se accesează un obiect neinstanţiat (null).
  • NoSuchElementException: este aruncată când se apelează next pe un Iterator care nu mai conţine un element următor.

Când se creează un obiect-excepție, acesta reține lanțul apelurilor de funcții care au dus la instrucțiunea curentă. Această succesiune se numește stack trace și vom discuta despre ea mai jos.

Prinderea excepţiilor

Când o excepţie a fost aruncată, runtime system încearcă să o trateze (prindă). Tratarea unei excepţii este făcută de o porţiune de cod specială.

public void f() throws Exception {
    List<String> l = null;
 
    if (null == l)
        throw new Exception();
}
 
public void catchFunction() {
    try {
        f();
    } catch (Exception e) {
        System.out.println("Exception found!");
    }
}

Se observă că dacă o metodă aruncă o excepție și nu o tratează, trebuie în general să includă clauza throws în semnătură.

Metoda f aruncă mereu o excepție (pentru că l este mereu null). În catchFunction observăm:

  • În interior există un bloc try care apelează f. Pentru a prinde o excepție, trebuie să definim o zonă în care o așteptăm (guarded region), introdusă prin try.
  • în continuare, avem blocul catch (Exception e). În momentul aruncării excepției, acest bloc este executat și afișează “Exception found!”, după care programul continuă normal.

Putem amâna tratarea unei excepții și o putem gestiona într-o metodă superioară din lanțul de apeluri. Vom vorbi mai multe despre acest comportament mai jos la propagarea excepțiilor.

Prinderea mai multor tipuri de excepții

Observaţi un alt exemplu:

public class FinallyExample {
    public static void main(String[] args) {
        try {
            System.out.println("În try");
            int a = 5 / 0; // va arunca ArithmeticException
        } catch (NullPointerException e) {
            // primul catch verificat, dar tipul excepției nu corespunde
            System.out.println("În catch NullPointerException");
        } catch (ArithmeticException e) { 
            // al doilea catch verificat, care corespunde și se va executa
            System.out.println("În catch ArithmeticException");
        }
    }
}

Output

Output

În try
În catch ArithmeticException

În exemplu, excepția ArithmeticException este prinsă de primul catch corespunzător tipului său, iar blocul finally se execută înainte de return.

Prin urmare:

  • Putem defini mai multe blocuri catch după un try pentru a trata diferite tipuri de excepții.
  • Blocul catch care va fi executat va fi ales pe baza ordinii (de la primul definit, la ultimul) și pe baza tipului excepției verificate.

Mereu se va executa doar un bloc catch pe baza criteriilor de mai sus, deci este important să definim blocurile de la cel mai restrictiv la cel mai general ca să evităm următoarele situații:

public class CatchOrderExample {
    public static void main(String[] args) {
        try {
            int a = 5 / 0; // va arunca ArithmeticException
        } catch (Exception e) { 
            // primul catch, prinde orice excepție datorită upcasting-ului
            System.out.println("Prinsă de Exception: " + e);
        } catch (ArithmeticException e) { 
            // nu va fi niciodată executat
            System.out.println("Prinsă de ArithmeticException");
        }
    }
}

Din Java 7 se pot prinde mai multe excepţii în acelaşi catch. Sintaxa este:

try { 
  ...
} catch(IOException | FileNotFoundException e) { 
  ...
}

Blocuri try-catch imbricate

În general, vom dispune în acelaşi bloc try-catch instrucţiunile care pot fi privite ca înfăptuind un acelaşi scop. Astfel, dacă o operaţie din secvență eșuează, se renunţă la instrucţiunile rămase şi se sare la un bloc catch.

Putem specifica operaţii opţionale, al căror eşec să nu influenţeze întreaga secvenţă. Pentru aceasta folosim blocuri try-catch imbricate:

try {
    op1();
 
    try {
        op2();
        op3();
    } catch (Exception e) { ... }
 
    op4();
    op5();
} catch (Exception e) { ... }

Dacă apelul op2 eşuează, se renunţă la apelul op3, se execută blocul catch interior, după care se continuă cu apelul op4.

Blocul finally

Presupunem că în secvenţa de mai sus, care deschide şi citeşte un fişier, avem nevoie să închidem fişierul deschis, atât în cazul normal, cât şi în eventualitatea apariţiei unei erori. În aceste condiţii se poate ataşa un bloc finally după ultimul bloc catch, care se va executa în ambele cazuri menţionate.

Secvenţa de cod următoare conţine o structură try-catch-finally:

try {
    open();
    read();
    ...
} catch (FILE_NOT_FOUND) {
    // handle error
} catch (INUFFICIENT_PERMISSIONS) {
    // handle error
} catch (DISK_ERROR) {
    // handle error
} finally {
    // close file
}

Blocul finally se execută chiar și când există un return în try sau catch, rulând înainte ca metoda să returneze.

public class FinallyExample {
    public static int test() {
        try {
            System.out.println("În try"); // se execută
            return 1; // se amână și se execută finally
        } catch (Exception e) {
            System.out.println("În catch");
            return 2;
        } finally {
            System.out.println("În finally"); // se execută
 
            // se revine la return-ul din try
        }
    }
 
    public static void main(String[] args) {
        int result = test();
        System.out.println("Rezultat: " + result);
        // Output: "Rezultat: 1"
    }
}

Tipuri de excepţii

Nu toate excepţiile trebuie prinse cu try-catch. Pentru a înțelege de ce, să analizăm clasificarea excepţiilor:

Tipuri de excepţii

Clasa Throwable:

  • Superclasa tuturor erorilor și excepțiilor din Java.
  • Doar obiectele ce extind această clasă pot fi aruncate de către JVM sau prin instrucțiunea throw.
  • Numai această clasă sau una dintre subclasele sale pot fi tipul de argument într-o clauză catch.

Checked exceptions, ce corespund clasei Exception:

  • O aplicaţie bine scrisă ar trebui să le prindă şi să permită continuarea rulării programului.
  • Să luăm ca exemplu un program care cere utilizatorului un nume de fişier (pentru a-l deschide). În mod normal, utilizatorul va introduce un nume de fişier care există şi care poate fi deschis. Există insă posibilitatea ca utilizatorul să greşească, caz în care se va arunca o excepţie FileNotFoundException.
  • Un program bine scris va prinde această excepţie, va afişa utilizatorului un mesaj de eroare, şi îi va permite eventual să reintroducă un nou nume de fişier.

Errors, ce corespund clasei Error:

  • Acestea definesc situaţii excepţionale declanşate de factori externi aplicaţiei, pe care aceasta nu le poate anticipa şi nu-şi poate reveni, dacă se produc.
  • Spre exemplu, alocarea unui obiect foarte mare (un vector cu milioane de elemente), poate arunca OutOfMemoryError.
  • Aplicaţia poate încerca să prindă această eroare, pentru a anunţa utilizatorul despre problema apărută; după aceasta însă, programul va eşua (afişând eventual stack trace).

Runtime Exceptions, ce corespund clasei RuntimeException:

  • Ca şi erorile, acestea sunt condiţii excepţionale, însă, spre deosebire de erori, ele sunt declanşate de factori interni aplicaţiei. Aplicaţia nu poate anticipa şi nu îşi poate reveni dacă acestea sunt aruncate.
  • Runtime exceptions sunt produse de diverse bug-uri de programare (erori de logică în aplicaţie, folosire necorespunzătoare a unui API, etc).
  • Spre exemplu, a realiza apeluri de metode sau membri pe un obiect null va produce NullPointerException. Fireşte, putem prinde excepţia. Mai natural însă ar fi să eliminăm din program un astfel de bug care ar produce excepţia.

  • Excepţiile checked sunt cele prinse de blocurile try-catch. Toate excepţiile sunt checked, mai puțin cele de tip Error, RuntimeException şi subclasele acestora, adică cele de tip unchecked.
  • Nu este indicată prinderea excepţiilor unchecked (de tip Error sau RuntimeException) cu try-catch.

Putem arunca RuntimeException fără să o menţionăm în clauza throws din semnătură:

public void f(Object o) {
    if (o == null)
        throw new NullPointerException("o is null");
}

Definirea de excepţii noi

Când aveţi o situaţie în care alegerea unei excepţii (de aruncat) nu este evidentă, puteţi opta pentru a scrie propria voastră excepţie, care să extindă Exception, RuntimeException sau Error.

class TemperatureException extends Exception {}
 
class TooColdException extends TemperatureException {}
 
class TooHotException extends TemperatureException {}

Totodată, reamintim că trebuie acordată atenţie ordinii în care se vor defini blocurile catch. De exemplu, pentru a întrebuinţa excepţiile de mai sus, blocul try-catch ar trebui să urmeze sensul moștenirilor:

try {
    ...
} catch (TooColdException e) {
    ...
} catch (TemperatureException e) {
    ...
} catch (Exception e) {
    ...
}

Propagarea excepțiilor

Propagarea excepțiilor este mecanismul prin care o excepție „urcă” prin lanțul de apeluri (call stack) până când este prinsă sau ajunge la nivelul cel mai înalt al programului. Dacă o metodă nu tratează excepția, execuția ei se oprește și excepția este transmisă metodei apelante.

Fluxul propagării:

  1. O metodă aruncă o excepție.
  2. Dacă metoda nu o tratează, excepția este transmisă metodei apelante.
  3. Procesul continuă până când excepția este prinsă sau ajunge în metoda main().
  4. Dacă nici main() nu o tratează, programul se încheie cu:
    • mesaj de eroare
    • stack trace

Pentru excepțiile checked, compilatorul impune declararea lor în semnătura metodei folosind clauza throws, astfel încât apelantul să fie obligat să le trateze sau să le redeclare.

Acest comportament permite tratarea erorilor la niveluri diferite ale aplicației, oferind flexibilitate în designul codului.

Excepţiile în contextul moştenirii

Metodele suprascrise (overriden) pot arunca numai excepţiile specificate de metoda din clasa de bază sau excepţii derivate din acestea.

Reguli pentru gestionarea excepțiilor la suprascrierea metodelor:

  • Subclasa poate arunca aceleași excepții sau excepții mai specifice (subclase) decât metoda din superclasă.
class SuperClass {
    public void readData() throws IOException {
        System.out.println("Reading...");
    }
}
 
class SubClass extends SuperClass {
    @Override
    public void readData() throws FileNotFoundException { // OK: subclasă a IOException
        System.out.println("Reading from file...");
    }
}
  • Subclasa poate alege să nu arunce nicio excepție.
class SubClass extends SuperClass {
    @Override
    public void readData() { // OK: fără excepții
        System.out.println("Reading safely...");
    }
}
  • Subclasa NU poate arunca excepții checked noi care nu apar în metoda din superclasă.
class SubClass extends SuperClass {
    @Override
    public void readData() throws SQLException { // EROARE: SQLException nu este permis
        System.out.println("Reading...");
    }
}

Pentru unchecked exceptions (ex. RuntimeException), nu există restricții și pot fi adăugate la liber. Dacă metoda din superclasă nu aruncă nicio excepție checked, metoda copil nu poate introduce excepții checked.

try-with-resources

Din Java 7, putem folosi construcția try-with-resources pentru a declara resurse într-un bloc try, asigurându-ne că acestea sunt închise automat după executarea blocului.

  • Resursele declarate trebuie să implementeze interfața AutoCloseable.
  • Dacă apare o excepție în timpul folosirii resursei, aceasta va fi închisă automat, similar cu fclose() în C.

try (PrintWriter writer = new PrintWriter(file)) {
    writer.println("Hello World");
} catch (FileNotFoundException e) {
    System.err.println("File could not be opened: " + e.getMessage());
}

Stack Traces

Excepțiile se pot propaga prin mai multe niveluri înainte de a fi tratate, așa că este important să știm:

  1. Unde a fost aruncată excepția
  2. Cum s-a ajuns acolo: ce metode au apelat alte metode până la punctul problematic

Pentru depanare, toate excepțiile pot genera un stack trace, o listă a apelurilor de metode care au condus la excepție, folosind metoda printStackTrace().

Acesta este foarte util pentru a identifica rapid sursa erorii și lanțul de apeluri care a dus la apariția ei.

try {
    // cod complex, diverse apeluri
} catch (Exception e) {
    // afișează informații despre locul unde a apărut excepția
    e.printStackTrace(System.err);
}

Exemplu de stack-trace și explicații

Exemplu de stack-trace și explicații

Acest stack trace indică faptul că linia 5 din metoda main() a apelat loadFile(), care la rândul ei a încercat să creeze un FileInputStream la linia 137, unde s-a produs excepția.

Odată ce stack trace-ul ajunge la clasele interne Java (ex. FileInputStream), numerele de linie pot fi pierdute, mai ales dacă codul a fost optimizat. De obicei, optimizarea poate fi dezactivată temporar pentru a găsi linia exactă, dar uneori sunt necesare alte tehnici de debugging.

java.io.FileNotFoundException: myfile.xml
    at java.io.FileInputStream.<init>(FileInputStream.java)
    at MyApplication.loadFile(MyApplication.java:137)
    at MyApplication.main(MyApplication.java:5)

Fiecare obiect de tip StackTraceElement oferă mai multe detalii prin expunerea unor metode precum: getFileName(), getClassName(), getMethodName(), getLineNumber().

...
catch (Exception e) {
    for (StackTraceElement el : e.getStackTrace()) {
        System.out.println(el.getClassName() + "." + el.getMethodName() +
                           " (line: " + el.getLineNumber() + ")");
    }
}

Probleme de performanță

Așa cum sugerează și numele, excepțiile trebuie utilizate doar pentru cazuri excepționale, nu pentru condiții așteptate sau frecvente, mai ales când performanța programului este una critică.

De ce?

  • Un bloc try nu adaugă costuri suplimentare la execuția programului.
  • Aruncarea unei excepții, însă, implică operații complexe: JVM trebuie să identifice blocul try/catch corespunzător și să efectueze operații suplimentare consumatoare de timp la rulare cum ar fi:
    • Crearea obiectului de tip Exception – instanțierea clasei, alocarea memoriei și capturarea mesajului.
    • Capturarea stack trace-ului – JVM parcurge stiva apelurilor pentru a construi lista completă a metodelor implicate.
    • Oprirea execuției normale – abandonează instrucțiunile rămase în metoda curentă.
    • Transferul controlului – mută execuția către blocul catch corespunzător sau, dacă nu există, finalizează programul.
    • Operații suplimentare – actualizarea registrelor interne, gestionarea referințelor etc.

Recomandare:

  • Evită folosirea excepțiilor pentru condiții previzibile (ex. EOF).
  • Folosește verificări simple pentru situații frecvente.
  • Aruncă excepții doar pentru erori rare și neprevăzute.

Pe de altă parte, dacă excepția apare extrem de rar, verificările suplimentare pot fi mai costisitoare decât aruncarea excepției în sine.

// Corect
while ((line = reader.readLine()) != null) {
    // procesează linia
}
 
// Greșit
try {
    while (true) {
        line = reader.readLine(); // va arunca excepție la EOF
    }
} catch (EOFException e) {
    // nu folosi excepția pentru program flow control
}

📥📤 File I/O

În Java, scrierea și citirea din fișiere se realizează prin două pachete principale:

  • java.io - pentru operații tradiționale de input/output
  • java.nio - pachetul „new I/O” pentru I/O de înaltă performanță

Aceste pachete oferă:

  • Funcții de bază de citire și scriere a datelor
  • Suport pentru operații avansate:
    • Compresia datelor
    • Serializarea obiectelor
    • Media streaming
    • I/O de înaltă performanță

Ele constituie fundamentul pentru toate comunicațiile cu fișiere, resurse media și rețea.

The Basics

În Java, lucrăm cu două mari tipuri de fluxuri:

  • Fluxuri de octeți (byte streams) – pentru date binare → FileInputStream / FileOutputStream
  • Fluxuri de caractere (character streams) – pentru text → FileReader / FileWriter, InputStreamReader / OutputStreamWriter

Peste acestea, putem adăuga buffering și funcționalități suplimentare (Scanner, BufferedReader, PrintWriter etc.).

Modalități de citire

Scanner

Folosit pentru fișiere text simple, când avem nevoie să citim token-uri: cuvinte, numere, simboluri.

Are metode integrate pentru parsing (nextInt(), nextDouble() etc.), ceea ce îl face ideal pentru input-uri structurate.

Este însă mai lent pentru fișiere mari, deoarece face parsing și tokenizare internă.

Poate fi folosit pentru:

  • citire de la tastatură (ex. System.in)
  • citire din fișier (ex. new File(“exemplu.txt”))
// Citire din fișier
try (Scanner sc = new Scanner(new File("exemplu.txt"))) {
    while (sc.hasNext()) {
        System.out.println(sc.next());
    }
}
// Citire de la tastatură cu conversie automată
Scanner sc = new Scanner(System.in)
 
double d = sc.nextDouble();

Nu este recomandat să închidem Scanner-ul asociat cu System.in, deoarece închide fluxul System.in și nu mai poate fi reutilizat.

FileReader

Folosit pentru citirea fișierelor text simple, când avem nevoie să lucrăm direct cu caractere.

Este un Reader de nivel jos, care extrage caractere dintr-un fișier folosind setul de caractere implicit al sistemului (ce poate varia între OS-uri). Nu oferă funcționalități avansate precum citirea linie cu linie sau parsarea datelor, fiind practic doar o „ușă” către fișier.

Este util când vrem să citim text într-un mod predictibil și liniar, fără procesări suplimentare.

Poate fi folosit pentru:

  • citire caracter cu caracter
  • sursă pentru clase mai performante, precum BufferedReader
  • citire simplă de fișiere în care nu ne interesează structura datelor
try (FileReader fr = new FileReader("exemplu.txt")) {
 
    int c;
    while ((c = fr.read()) != -1) { 
        // read() -> întoarce un caracter ca int sau -1 dacă am ajuns la final
        System.out.print((char) c);
    }
 
} catch (IOException e) {
    e.printStackTrace();
}
InputStreamReader

Reprezintă un adaptor între fluxuri de octeți (InputStream) și fluxuri de caractere (Reader). Este necesar în situațiile în care primim date brute sub formă de bytes (dintr-un socket, fișier binar sau System.in) și vrem să le convertim în caractere cu un anumit charset.

Permite specificarea explicită a encodării (ex. UTF-8), ceea ce îl face esențial în programe internaționale, pentru a evita problemele de afișare.

Poate fi folosit pentru:

  • citirea textului de la tastatură în mod corect (cu charset setat)
  • citirea de pe rețea (ex. socket TCP care trimite bytes)
  • sursă pentru un BufferedReader atunci când FileReader nu este potrivit (ex. fișiere cu encoding special)
// Citim text dintr-un InputStream, dar cu encoding UTF-8 specificat
try (InputStreamReader isr = new InputStreamReader(
        new FileInputStream("exemplu_utf8.txt"), StandardCharsets.UTF_8)) {
 
    int c;
    while ((c = isr.read()) != -1) {
        System.out.print((char) c);
    }
 
} catch (IOException e) {
    e.printStackTrace();
}

Rețineți că această clasă este utilă doar dacă vrem să facem conversia de la conținut binar la conținut text.

BufferedReader

Folosit pentru citirea eficientă a textului, deoarece încarcă în memorie blocuri mari de date, reducând numărul de apeluri la sistemul de fișiere. Oferă metoda readLine(), una dintre cele mai utile metode pentru prelucrarea fișierelor text pe linii.

Este ideal pentru fișiere mari sau situații în care performanța contează, cum ar fi procesarea de log-uri sau citirea continuă dintr-un stream.

Poate fi folosit pentru:

  • parcurgerea eficientă, linie cu linie, a unui fișier mare
  • citirea de la tastatură cu latență minimă
  • citire prin compunere (ex. new BufferedReader(new FileReader(…)))
  • situații în care vrem să evităm procesarea caracter-cu-caracter
try (BufferedReader br = new BufferedReader(new FileReader("exemplu.txt"))) {
 
    String line;
    while ((line = br.readLine()) != null) {
        // readLine() -> citește o linie întreagă, fără \n
        System.out.println("Linie citită: " + line);
    }
 
} catch (IOException e) {
    e.printStackTrace();
}

Pentru procesarea fișierelor binare eficient puteți folosi BufferedInputStream și FileInputStream.

Modalități de scriere

PrintWriter

Folosit pentru scrierea de text într-un mod ușor și prietenos, cu metode precum print(), println() și printf(). Este ideal când vrem claritate și simplitate în generarea de text.

Are opțiunea de auto-flush, utilă în aplicații interactive (ex. comunicare prin socket). Este însă mai lent decât BufferedWriter atunci când generăm output masiv, datorită funcțiilor suplimentare de formatare.

Poate fi folosit pentru:

  • generare de rapoarte text cu formatare
  • scriere în console, socket-uri sau fișiere
  • logging uman-citibil
  • combinații precum new PrintWriter(new BufferedWriter(new FileWriter(…)))
try (PrintWriter pw = new PrintWriter(new FileWriter("raport.txt"))) {
 
    pw.println("Raport generat automat:");
    pw.printf("Valoare X: %d, Valoare Y: %.2f\n", 10, 12.345);
    pw.println("Linie finală.");
 
    // Auto-flush dacă folosim constructorul: new PrintWriter(..., true)
 
} catch (IOException e) {
    e.printStackTrace();
}
FileWriter

Folosit pentru a scrie text simplu într-un fișier, caracter cu caracter. Este echivalentul lui FileReader, dar pentru output. Folosește implicit encoding-ul sistemului, motiv pentru care poate cauza incompatibilități dacă fișierul va fi folosit pe alte platforme.

Nu oferă bufere interne și este mai lent pentru volume mari, dar este o soluție minimă, directă și simplă pentru scrierea textului.

Poate fi folosit pentru:

  • scriere de fișiere mici
  • jurnalizare simplă
  • sursă pentru clase mai performante (ex. BufferedWriter)
try (FileWriter fw = new FileWriter("output.txt")) {
 
    fw.write("Salut!\n");        // scrie caractere direct
    fw.write("Scriem text simplu.");
 
    // write(char[]), write(String), write(int) sunt disponibile
 
} catch (IOException e) {
    e.printStackTrace();
}
OutputStreamWriter

Este perechea lui InputStreamReader, convertind caracterele Java în octeți folosind un anumit charset. Este util atunci când vrem să controlăm encoding-ul sau când sursa finală a datelor este un OutputStream (socket, fișier binar etc.).

Este clasa de bază pentru output text cu encoding explicit, fiind preferabilă în aplicații care trebuie să scrie UTF-8 (majoritatea).

Poate fi folosit pentru:

  • scriere în fișiere cu encoding custom
  • scriere în socket-uri sau procese externe
  • compunere cu BufferedWriter sau PrintWriter pentru performanță și funcționalitate
try (OutputStreamWriter osw =
        new OutputStreamWriter(new FileOutputStream("output_utf8.txt"), "UTF-8")) {
 
    osw.write("Scriu UTF-8 cu diacritice: ăîșț!");
    osw.flush(); // forțăm scrierea datelor
 
} catch (IOException e) {
    e.printStackTrace();
}

Rețineți că această clasă este utilă doar dacă vrem să facem conversia de la conținut text la conținut binar.

BufferedWriter

Folosit pentru a scrie text eficient, deoarece stochează temporar caractere în buffer și le scrie în bloc, reducând accesările la disc. Oferă și metoda newLine(), care inserează un separator de linie independent de OS.

Este alegerea potrivită pentru generarea de fișiere mari sau pentru scriere intensivă în loop-uri mari.

Poate fi folosit pentru:

  • scriere rapidă în fișiere
  • generarea de loguri sau rapoarte
  • scriere formatată pe linii mari
  • împreună cu FileWriter sau OutputStreamWriter
try (BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"))) {
 
    bw.write("Prima linie");
    bw.newLine();                 // adaugă separatorul de linie corect
    bw.write("A doua linie");
    // buffer-ul este golit automat la închiderea resursei
 
} catch (IOException e) {
    e.printStackTrace();
}

Pentru scrierea fișierelor binare eficient puteți folosi BufferedWriterStream și FileWriterStream.

Pentru clasele FileWriter / BufferedWriter / OutputStreamWriter / PrintWriter:

  • Dacă fișierul nu există, Java îl creează automat.
  • Dacă fișierul există deja, conținutul va fi suprascris, cu excepția cazului când folosim constructorul cu append = true.

TL;DR - I/O Basics

Scenariu / Tip flux Cum citim Cum scriem
Fișier text mic FileReader(“fisier.txt”) FileWriter(“fisier.txt”)
Fișier text mare / linie cu linie new BufferedReader(new FileReader(“fisier.txt”)) new BufferedWriter(new FileWriter(“fisier.txt”))
Text cu encoding (UTF-8 etc.) new BufferedReader(new InputStreamReader(new FileInputStream(“fisier.txt”), StandardCharsets.UTF_8)) new BufferedWriter(new OutputStreamWriter(new FileOutputStream(“fisier.txt”), StandardCharsets.UTF_8))
Input de la tastatură / conversie Scanner(System.in)
Scriere text ușor formatat / raport PrintWriter(new BufferedWriter(new FileWriter(“fisier.txt”)))

[Nice to know] File I/O - Advanced

I/O Streams

Majoritatea operațiilor I/O în Java se bazează pe fluxuri (streams).

Conceptual, un stream este un flux continuu de date, cu un reader la un capăt și un writer la celălalt.

Atunci când lucrăm cu pachetul java.io pentru a efectua operații de input/output în terminal, pentru a citi sau scrie în fișiere sau pentru a comunica prin socket-uri de rețea, se folosesc de fapt diferite tipuri de fluxuri.

Fluxurile în Java sunt unidirecționale. Dacă aveți nevoie de comunicare bidirecțională (read + write), va trebui să folosiți câte un flux pentru fiecare direcție/operație.

[Optional] IO vs NIO

[Optional] IO vs NIO

În pachetul NIO întâlnim un concept similar numit channel. Diferența principală este că:

  • Streams sunt orientate pe bytes sau caractere.
  • Channels sunt orientate pe buffers, care conțin aceste date.

Un buffer este, în esență, o zonă rapidă de stocare temporară, care ajută la optimizarea transferului de date. Atât fluxurile, cât și canalele fac în practică același lucru, dar pachetul NIO este gândit pentru performanță ridicată.

InputStream și OutputStream

Clasele InputStream și OutputStream sunt abstracte și definesc interfața de bază pentru toate fluxurile de bytes în Java. Ele oferă metode pentru citirea și scrierea datelor nestructurate, la nivel de byte.

Fiind abstracte, nu pot fi instanțiate direct, ceea ce înseamnă că nu putem crea un flux generic de intrare sau ieșire fără a folosi o clasă derivată.

[Optional] Moștenirea claselor InputStream și OutputStream

[Optional] Moștenirea claselor InputStream și OutputStream

Deoarece toate fluxurile de bytes moștenesc structura claselor InputStream sau OutputStream, diferitele tipuri de fluxuri pot fi folosite în mod interschimbabil.

De exemplu, o metodă care primește un InputStream ca argument poate accepta orice subclasă a acestuia (ex. AudioInputStream, ByteArrayInputStream, FileInputStream, FilterInputStream etc).

Pe lângă fluxurile de bază, există tipuri specializate care pot fi wrapped peste fluxurile standard pentru a adăuga funcționalități suplimentare, cum ar fi:

  • buffering (pentru performanță),
  • filtrare,
  • compresie,
  • sau manipularea unor tipuri de date la nivel mai înalt.

Basic I/O

Input-ul standard pentru o aplicație în Java se bazează pe un obiect de tip InputStream, similar cu stdin din C sau cin din C++. Acesta reprezintă o sursă de date ce provine din environment-ul în care lucrăm:

  • input primit din terminal.
  • output-ul/rezultatul unei alte comenzi.

Fluxurile standard sunt gestionate prin clasa java.lang.System, care oferă trei variabile statice:

InputStream stdin = System.in;    // flux pentru date de intrare 
OutputStream stdout = System.out; // pentru ieșire, scriere de date 
OutputStream stderr = System.err; // pentru erori

  • Deși System.out și System.err sunt de fapt la bază obiecte de tip PrintStream, ele pot fi tratate ca OutputStream deoarece moștenesc această clasă.
  • Putem citi date folosind InputStream, dar aceste vor fi reprezentate sub tipul int.

Pentru a citi date din System.in, putem folosi metoda read() a clasei InputStream. Aceasta returnează un int și nu un byte, deoarece valoarea -1 indică sfârșitul fluxului. Valorile citite sunt în intervalul 0–255 și pot fi convertite la byte dacă este necesar.

try {
    int val;
    while ((val = System.in.read()) != -1) {
        System.out.println((byte) val);
    }
} catch (IOException e) {
    e.printStackTrace();
}

Metoda read() poate arunca o excepție IOException în cazul unei erori. Există și o altă modalitate, aceea ce a citi într-un buffer, un array de octeți:

byte[] buffer = new byte[1024];
int bytesRead = System.in.read(buffer);

Deși codul de mai sus este valid, putem face codul mai optim și scalabil pentru cazuri generale folosind Character Streams.

Character Streams

Pentru că ne dorim să lucrăm cu valori reprezentate ca text, în Java au fost introduse clasele Reader și Writer din pachetul java.io.

Aceste fluxuri sunt dedicate datelor de tip char pentru a lucra direct cu caractere și șiruri de caractere, fără a manipula direct octeți.

Există implementări directe ale acestor clase, mai exact InputStreamReader și OutputStreamWriter, ce crează o punte de legătură între stream-urile de octeți și stream-urile de caractere.

Conversia se face conform unei scheme de codificare, care poate fi specificată în constructor. Dacă nu este indicată explicit, se folosește codificarea implicită a sistemului (vezi un exemplu mai jos).

Astfel avem următoarele asocieri:

  • InputStreamReader transformă un InputStream într-un Reader.
  • OutputStreamWriter transformă un OutputStream într-un Writer.
try {
    InputStream in = System.in; // Standard Input
    InputStreamReader charsIn = new InputStreamReader(in, "UTF-8"); 
 
    // Citim caractere manual, până la Enter ('\n')
    StringBuilder sb = new StringBuilder();
    int c;
    while ((c = charsIn.read()) != -1 && c != '\n') {
        sb.append((char) c);
    }
 
    String line = sb.toString();
    int i = Integer.parseInt(line.trim()); // parsare directă în int
    System.out.println("Numărul introdus: " + i);            
} catch (IOException e) {
    e.printStackTrace();
} catch (NumberFormatException nfe) {
    System.out.println("Nu ați introdus un număr valid!");
}

Codul nostru este acum mai general și ușor de extins prin folosirea unei codificări și prin lucrul direct cu șiruri de caractere, dar îl putem face mai optim folosind buffers.

Stream Wrappers

Când dorim să facem mai mult decât citirea sau scrierea simplă a octeților sau caracterelor, putem folosi fluxuri de tip filtru.

Acestea sunt clase care extind InputStream, OutputStream, Reader sau Writer și adaugă funcționalități suplimentare, cum ar fi buffering, conversii de date sau compresie.

Un flux de tip filtru funcționează astfel:

  1. Ia ca argument alt flux în constructor.
  2. Efectuează procesări suplimentare asupra datelor.
  3. Deleagă apelurile către fluxul original, păstrând interfața de bază și comportamentul standard.

Astfel, filter streams permit combinarea și extinderea funcționalităților fluxurilor de bază într-un mod modular și flexibil.

De exemplu, putem face wrap la System.in într-un BufferedInputStream pentru a adăuga buffering:

InputStream bufferedIn = new BufferedInputStream(System.in);

BufferedInputStream citește anticipat și stochează date într-un buffer, ceea ce reduce accesările directe la sursa reală și îmbunătățește performanța.

Cum funcționează buffer-ul intern

Cum funcționează buffer-ul intern

La citire, buffer-ul este o zonă temporară de memorie unde sunt încărcate datele dintr-un flux, de exemplu un fișier. În loc să citim un caracter sau un byte la un moment dat, fluxul preia un bloc mare de date și îl stochează în buffer. Apelurile ulterioare la read() iau datele direct din buffer, fără a accesa imediat fișierul, ceea ce crește semnificativ performanța operațiilor de citire. Practic, bufferul reduce numărul de accesări ale sursei de date.

La scriere, datele sunt mai întâi adunate într-un buffer. În loc să fie scrise direct în fișier sau rețea caracter cu caracter, ele se acumulează și se trimit în blocuri mari atunci când bufferul se umple sau când apelăm explicit flush() sau close(). Această abordare minimizează numărul de operații I/O și face scrierea mai rapidă și mai eficientă.

În esență, buffer-ul servește ca un intermediar între program și sursa sau destinația de date, permițând citirea și scrierea în blocuri mari, ceea ce optimizează performanța.


Mai mult, această clasă ne oferă metode care ne pot simplifica codul. Să reluăm exemplul de cod anterior:

try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"))) {
    System.out.print("Introduceți un număr: ");
    String line = reader.readLine();          // citire linie completă
    int i = Integer.parseInt(line.trim());    // parsare directă în int
    System.out.println("Numărul introdus: " + i);
} catch (IOException e) {
    e.printStackTrace();
} catch (NumberFormatException nfe) {
    System.out.println("Nu ați introdus un număr valid!");
}

Observăm cum codul de mai sus este mult mai simplu. În mod similar, putem folosi DataInputStream pentru a citi tipuri de date complexe (primitive Java, șiruri) peste un flux de octeți.

Un avantaj important este că aceste fluxuri pot fi combinate:

DataInputStream din = new DataInputStream(new BufferedInputStream(System.in));
double d = dis.readDouble();
 
double d = 3.1415926;
DataOutputStream dos = new DataOutputStream(System.out);
dos.writeDouble(d);

DataOutputStream și DataInputStream lucrează cu date binare, nu cu text în format human-readable. De obicei, se folosește un DataInputStream pentru a citi conținutul care a fost produs anterior de un DataOutputStream. Aceste fluxuri sunt folosite pentru a lucra cu fișiere binare, cum ar fi imagini, fișiere PDF etc.

Codul nostru este acum optim, dar doar dacă lucrăm cu STDIN, STDERR și STDOUT. Dacă vrem să lucrăm cu fișiere avem nevoie de alte clase specializate.

Clasa File

java.io.File oferă un mod de a accesa informații despre un fișier sau un director din sistemul de fișiere.

  • Este important de reținut că un obiect File se ocupă doar de aceste operații “meta”, adică cele legate de existența și proprietățile fișierului, nu de conținutul lui.
  • Pentru citirea și scrierea efectivă a datelor din fișier, trebuie să folosim tot un stream.

Clasa java.io.File oferă mai multe modalități de a crea un obiect care reprezintă un fișier sau un director. Cel mai simplu mod este să folosim un String care indică calea completă sau relativă către fișier:

// Cale absolută/completă
File fooFile = new File("/tmp/foo.txt");
File barDir = new File("/tmp/bar");
 
// Cale relativă, raportată la directorul curent al aplicației: 
File f = new File("foo");
// Pentru a afla care este directorul curent al JVM, putem citi proprietatea user.dir:
System.getProperty("user.dir"); // exemplu: "/Users/student/exercitii-poo"

Există și constructori suprascriși care permit specificarea directorului și a numelui fișierului separat, fie ca două String-uri, fie ca un obiect de tip File și un String:

File fooFile = new File("/tmp", "foo.txt");
File tmpDir = new File("/tmp");
File fooFile2 = new File(tmpDir, "foo.txt");

Este foarte important să reținem că niciunul dintre acești constructori nu creează efectiv fișierul sau directorul pe disc. Crearea unui obiect File nu garantează existența fișierului.

File este doar un „handle” care ne permite să accesăm informații despre acel fișier sau director și să efectuăm operații asupra lui, dacă există. Pentru a verifica dacă fișierul există, putem folosi metoda exists():

if (fooFile.exists()) {
    System.out.println("Fișierul există.");
} else {
    System.out.println("Fișierul nu există.");
}

  • În Java, o cale relativă este raportată la directorul curent al aplicației, în timp ce o cale absolută include întreaga adresă din sistemul de fișiere.
    • Unix/macOS: calea absolută începe cu '/'.
    • Windows: calea absolută include litera unității, de exemplu C:\Users\student\file.txt.
  • Pentru a scrie cod portabil, Java oferă constante precum File.separator și File.separatorChar
    <code java>
    String path = "docs" + File.separator + "exemplu.txt";
    File fisier = new File(path);

File Stream

Acum că ați înțeles ce este un fișier și cum îl puteți reprezenta cu clasa File, este momentul să învățați cum să citiți și să scrieți efectiv date. În Java, pentru lucrul cu fișiere există două fluxuri fundamentale:

  • FileInputStream – pentru citirea datelor din fișiere (la nivel de octet)
  • FileOutputStream – pentru scrierea datelor în fișiere (la nivel de octet)

Aceste fluxuri sunt orientate pe octeți și pot fi combinate cu fluxuri de tip filtru (Buffered, Data, etc.) pentru funcționalități suplimentare, așa cum am enunțat mai sus.

Cum citiți date dintr-un fișier?

Puteți crea un FileInputStream folosind o cale/path către fișier sau direct folosind un obiect File:

try (FileInputStream fin = new FileInputStream("exemplu.txt")) {
    int data; // citim octeți
    while ((data = fin.read()) != -1) {
        System.out.print((char) data);
    }
} catch (IOException e) {
    e.printStackTrace();
}

Observați folosirea blocului try-with-resources, care închide automat fluxul la final. Dacă fișierul nu există, se aruncă FileNotFoundException.

Dacă vreți să lucrați cu caractere, nu cu octeți, puteți:

  • face wrapping pentru FileInputStream într-un InputStreamReader
  • să folosiți direct FileReader
try (BufferedReader br = new BufferedReader(new FileReader("exemplu.txt"))) {
    String line; // citim șiruri de caractere
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
Cum scrieți date într-un fișier?

Pentru scriere, folosiți FileOutputStream. Dacă fișierul nu există, acesta este creat automat. Dacă există, conținutul este suprascris, cu excepția cazului în care folosiți constructorul cu flag-ul append (care va face textul să fie scris în continuare):

try (FileOutputStream fout = new FileOutputStream("exemplu.txt", true)) {
    String text = "Salut!";
    fout.write(text.getBytes());
} catch (IOException e) {
    e.printStackTrace();
}

Pentru scrierea textului, este mai comod să folosiți FileWriter sau să îi faceți wrap fluxului într-un PrintWriter:

try (PrintWriter pw = new PrintWriter(new FileWriter("exemplu.txt"))) {
    pw.println("Aceasta este o linie nouă.");
} catch (IOException e) {
    e.printStackTrace();
}

Ce trebuie să rețineți?

  • FileInputStream / FileOutputStream → pentru octeți (date binare)
  • FileReader / FileWriter → pentru caractere (text)
  • Folosiți try-with-resources pentru a elibera resursele implicate în procesele I/O
  • Pentru performanță, faceți wrap fluxurilor în BufferedReader sau BufferedWriter
  • Pentru scriere formatată, folosiți PrintWriter

[Optional] RandomAccessFile

Până acum am discutat despre fluxuri care citesc sau scriu date secvențial, de la începutul fișierului până la sfârșit. Dar ce faceți dacă aveți nevoie să accesați direct o anumită poziție/index din fișier? Pentru asta există clasa java.io.RandomAccessFile, ce vă permite să citiți și să scrieți date oriunde în fișier. RandomAccessFile implementează interfețele DataInput și DataOutput, ceea ce înseamnă că puteți folosi metodele pentru citirea și scrierea tipurilor primitive Java și a șirurilor, la fel ca în DataInputStream și DataOutputStream. Totuși, nu este o subclasă a InputStream sau OutputStream, deoarece oferă acces aleatoriu, nu secvențial.

Constructorul primește două argumente: calea către fișier și modul de acces (permisiunile):

// "r" → doar citire
// "rw" → citire și scriere
try {
    RandomAccessFile users = new RandomAccessFile("Users", "rw");
} catch (IOException e) {
    e.printStackTrace();
}

Dacă deschideți fișierul în modul read-only, Java încearcă să deschidă fișierul existent și aruncă un IOException dacă nu îl găsește. În modul read/write, fișierul este creat dacă nu există.

Metoda seek(long position) vă permite să setați poziția curentă în fișier pentru citire sau scriere. Puteți afla poziția curentă cu getFilePointer() și lungimea fișierului cu length(). Dacă vreți să adăugați date la sfârșitul fișierului, apelați seek(length()).

users.seek(userNum * RECORDSIZE);
users.writeUTF(userName);
users.writeInt(userID);

În acest exemplu de mai sus, presupunem că fiecare înregistrare de tip “user” din fișierul nostru are o dimensiune fixă (RECORDSIZE), astfel încât putem calcula poziția exactă pentru datele utilizatorul dorit.

  • RandomAccessFile este ideal pentru fișiere structurate, cum ar fi baze de date simple sau fișiere binare cu înregistrări/intrări de dimensiuni fixe.
  • Puteți scrie sau citi oriunde în fișier, dar nu puteți citi dincolo de sfârșitul fișierului (în acest caz se aruncă EOFException).
  • Folosiți seek() pentru a naviga în fișier și getFilePointer() pentru a verifica poziția curentă.

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.

Deoarece lucrăm pe platforma Devmind Code care este de tip Sandbox, nu avem posibilitatea să creăm sau să lucrăm cu fișiere direct. Cunoștințele din acest laborator legate de I/O vor fi testate la teme.

Task 1 (4p)

Definiţi o clasă care să implementeze operaţii pe numere double. Operaţiile vor arunca excepţii. Clasa va trebui să implementeze interfața CalculatorBase, ce conţine trei metode:

  • add: primeşte două numere şi întoarce un double
  • divide: primeşte două numere şi întoarce un double
  • average: primeşte o colecţie ce conţine obiecte double, şi întoarce media acestora ca un numar de tip double. !! Pentru calculul mediei, sunt folosite metodele add şi divide !! .
  • Metodele pot arunca următoarele excepții (definite în interfața Calculator):
    • NullParameterException: este aruncată dacă vreunul din parametrii primiți este null;
    • OverflowException: este aruncată dacă suma a două numere e egală cu Double.POSITIVE_INFINITY;
    • UnderflowException: este aruncată dacă suma a două numere e egală cu Double.NEGATIVE_INFINITY.
  • Completați metoda main din clasa MainEx2, evidențiind prin teste toate cazurile posibile care generează excepţii.

Task 2 (4p)

Vom realiza o mini librărie online în care putem adăuga cărți cumpărându-le și din care putem să extragem o carte deja existentă.

  1. Definește clasa Book care are parametrii title, author, genre și price.
  2. Definește două noi excepții care extind clasa Exception:
    • NotEnoughMoneyException, care e aruncată atunci când utilizatorul nu are bani suficienți pentru a cumpăra o carte
    • NoSuchBookException, care e aruncată atunci când cartea dorită nu se găsește în librărie.
  3. Definește clasa OnlineLibrary care are:
    • doi parametrii: o listă de cărți și bugetul utilizatorului
    • un constructor care primește bugetul inițial al utilizatorului
    • metodele:
    • addBook - primește o carte și o adaugă în librărie dacă utilizatorul are fonduri suficiente
    • getBook - returnează cartea dorită, dacă aceasta se află în librărie.
  4. În metoda Main să se realizeze TODO-urile:
    • TODO1 - adaugă lista de cărți în librărie
    • TODO2 - ia cartea book4 din librărie. Dacă nu există, adaug-o.
  • Atenție la tratarea excepțiilor! (A se afișa un mesaj corespunzător fiecărui caz, ca în exemplu).

Task 3 (2p)

Dorim să implementăm un Logger pe baza pattern-ului Chain-of-responsibility, definit în laborator, pe care îl vom folosi să păstram un jurnal de evenimente al unui program:

  1. Creați enumerația LogLevel, ce va acționa ca un bitwise flag, care va conține:
    • valorile - Info, Debug, Warning, Error, FunctionalMessage, FunctionalError.
    • Această enumerație va expune și o metodă statică all() care va întoarce o colecție de EnumSet<LogLevel> în care vor fi toate valorile de mai sus (Hint: EnumSet.allOf()).
  2. Creați o clasă abstractă LoggerBase care:
    • va primi în constructor un obiect de tip EnumSet<LogLevel> care va defini pentru ce nivele de log se va afisa mesajul
    • va păstra o referință către următorul LoggerBase la care se trimite mesajul
    • va expune o metodă publică setNext ce va primi un LoggerBase și va seta următorul delegat din lista de responsabilitate
    • va defini o metodă abstractă protected writeMessage ce va primi mesajul care trebuie afișat și afișează mesajul în cauză
    • va expune o metodă publică message ce va primi mesajul care trebuie afișat și o severitate de tip LogLevel (adică Info, Debug, Warning, Error, FunctionalMessage sau FunctionalError). Dacă instanța de logger conține această severitate în colecția primită în constructor, atunci se va apela metoda writeMessage. Apoi se vor pasa mesajul și severitatea către următorul delegat din lista de responsabilitate (dacă există unul)
  3. Definiți clasele de mai jos care vor extinde LoggerBase și implementa metoda writeMessage:
    • ConsoleLogger - care va scrie toate tipurile de LogLevel (Hint: all()) și va prefixa mesajele cu [Console]
    • EmailLogger - care va scrie doar tipurile FunctionalMessage și FunctionalError și va prefixa mesajele cu [Email]
    • FileLogger - care va scrie doar tipurile Warning și Error și va prefixa mesajele cu [File]
  4. Completați cele 2 TODO-uri rămase în metoda main din clasa Main. (Hint: EnumSet.of() pentru constructori)

[Optional] Informații despre Chain of Responsability

[Optional] Informații despre Chain of Responsability

Chain of Responsability este un Design Pattern și veți învăța mai multe despre acestea în laboratoarele următoare. Vă recomandăm să păstrați întrebările despre acest DP pentru acele laboratoare sau să consultați această documentație Chain of Responsability - Digital Ocean.

În proiectarea orientată pe obiect, pattern-ul “Chain-of-responsibility” (lanț de responsabilitate) este un model de design constând dintr-o sursă de obiecte de comandă și o serie de obiecte de procesare.

Fiecare obiect de procesare conține logică care definește tipurile de obiecte de comandă pe care le poate gestiona; restul sunt transferate către următorul obiect de procesare din lanț. De asemenea, există un mecanism pentru adăugarea de noi obiecte de procesare la sfârșitul acestui lanț. Astfel, lanțul de responsabilitate este o versiune orientată pe obiecte a if … else if … else if …… else … endif, cu avantajul că blocurile condiție-acțiune pot fi dinamic rearanjate și reconfigurate la timpul de execuție.

Chain-of-responsibility

Într-o variantă a modelului standard al lanțului de responsabilitate, un handler poate acționa ca un dispatcher, capabil să trimită comenzi în diverse direcții, formând un arbore de responsabilități (tree of responsibility).

În unele cazuri, acest lucru poate apărea recursiv, cu procesarea obiectelor care apelează obiecte de procesare de nivel superior cu comenzi care încearcă să rezolve o parte mai mică a problemei; în acest caz, recurența continuă până când comanda este procesată, sau întregul arbore a fost explorat. Un interpretor XML ar putea funcționa în acest mod.

Resurse și Linkuri utile

poo-ca-cd/laboratoare/io-si-gestionarea-exceptiilor.txt · Last modified: 2025/11/19 14:12 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