Î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:
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:
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.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:
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.
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ă:
Pasarea menţionată mai sus poartă numele de aruncarea (throwing) unei excepţii.
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:
new);
În realitate, clasa Exception este părintele majorităţii claselor excepţie din Java. Enumerăm câteva excepţii standard:
null).Iterator care nu mai conţine un element următor.
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:
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.catch (Exception e). În momentul aruncării excepției, acest bloc este executat și afișează “Exception found!”, după care programul continuă normal.
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"); } } }
Î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:
catch după un try pentru a trata diferite tipuri de excepții.catch care va fi executat va fi ales pe baza ordinii (de la primul definit, la ultimul) și pe baza tipului excepției verificate.
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"); } } }
catch. Sintaxa este:
try { ... } catch(IOException | FileNotFoundException e) { ... }
Î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.
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" } }
Nu toate excepţiile trebuie prinse cu try-catch. Pentru a înțelege de ce, să analizăm clasificarea excepţiilor:
Clasa Throwable:
throw.catch.Checked exceptions, ce corespund clasei Exception:
FileNotFoundException.Errors, ce corespund clasei Error:
OutOfMemoryError.stack trace).Runtime Exceptions, ce corespund clasei RuntimeException:
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.
try-catch. Toate excepţiile sunt checked, mai puțin cele de tip Error, RuntimeException şi subclasele acestora, adică cele de tip unchecked.
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"); }
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 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:
main().main() nu o tratează, programul se încheie cu:
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.
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:
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..."); } }
class SubClass extends SuperClass { @Override public void readData() { // OK: fără excepții System.out.println("Reading safely..."); } }
class SubClass extends SuperClass { @Override public void readData() throws SQLException { // EROARE: SQLException nu este permis System.out.println("Reading..."); } }
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.
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.
try (PrintWriter writer = new PrintWriter(file)) { writer.println("Hello World"); } catch (FileNotFoundException e) { System.err.println("File could not be opened: " + e.getMessage()); }
Excepțiile se pot propaga prin mai multe niveluri înainte de a fi tratate, așa că este important să știm:
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); }
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() + ")"); } }
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?
try nu adaugă costuri suplimentare la execuția programului.try/catch corespunzător și să efectueze operații suplimentare consumatoare de timp la rulare cum ar fi:Recomandare:
// 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 }
În Java, scrierea și citirea din fișiere se realizează prin două pachete principale:
Aceste pachete oferă:
Ele constituie fundamentul pentru toate comunicațiile cu fișiere, resurse media și rețea.
În Java, lucrăm cu două mari tipuri de fluxuri:
FileInputStream / FileOutputStreamFileReader / FileWriter, InputStreamReader / OutputStreamWriterPeste acestea, putem adăuga buffering și funcționalități suplimentare (Scanner, BufferedReader, PrintWriter etc.).
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:
System.in)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();
System.in, deoarece închide fluxul System.in și nu mai poate fi reutilizat.
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:
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(); }
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:
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(); }
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:
new BufferedReader(new FileReader(…)))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(); }
BufferedInputStream și FileInputStream.
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:
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(); }
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:
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(); }
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:
BufferedWriter sau PrintWriter pentru performanță și funcționalitatetry (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(); }
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:
OutputStreamWritertry (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(); }
BufferedWriterStream și FileWriterStream.
FileWriter / BufferedWriter / OutputStreamWriter / PrintWriter:
append = true.
| 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”))) |
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.
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ă.
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:
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
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ă.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);
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.
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!"); }
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:
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.
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.
java.io.File oferă un mod de a accesa informații despre un fișier sau un director din sistemul de fișiere.
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. 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");
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ă."); }
'/'.C:\Users\student\file.txt.
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.
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(); }
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:
FileInputStream într-un InputStreamReaderFileReadertry (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(); }
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(); }
FileInputStream / FileOutputStream → pentru octeți (date binare)FileReader / FileWriter → pentru caractere (text)try-with-resources pentru a elibera resursele implicate în procesele I/OBufferedReader sau BufferedWriterPrintWriter
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(); }
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.EOFException).seek() pentru a naviga în fișier și getFilePointer() pentru a verifica poziția curentă.
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 doubledivide: primeşte două numere şi întoarce un doubleaverage: 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 !! .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.main din clasa MainEx2, evidențiind prin teste toate cazurile posibile care generează excepţii.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ă.
Book care are parametrii title, author, genre și price.NotEnoughMoneyException, care e aruncată atunci când utilizatorul nu are bani suficienți pentru a cumpăra o carteNoSuchBookException, care e aruncată atunci când cartea dorită nu se găsește în librărie.OnlineLibrary care are:addBook - primește o carte și o adaugă în librărie dacă utilizatorul are fonduri suficientegetBook - returnează cartea dorită, dacă aceasta se află în librărie.Main să se realizeze TODO-urile:TODO1 - adaugă lista de cărți în librărieTODO2 - ia cartea book4 din librărie. Dacă nu există, adaug-o.
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:
LogLevel, ce va acționa ca un bitwise flag, care va conține:Info, Debug, Warning, Error, FunctionalMessage, FunctionalError.all() care va întoarce o colecție de EnumSet<LogLevel> în care vor fi toate valorile de mai sus (Hint: EnumSet.allOf()).LoggerBase care:setNext ce va primi un LoggerBase și va seta următorul delegat din lista de responsabilitatewriteMessage ce va primi mesajul care trebuie afișat și afișează mesajul în cauză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)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]EnumSet.of() pentru constructori)