Java I/O Streams

Cuprins

Introducere în I/O Streams

În Java, un stream (flux) reprezintă o secvență de date care circulă între o sursă și o destinație. I/O Streams (Input/Output Streams) furnizează mecanisme pentru citirea și scrierea datelor, facilitând interacțiunea programelor Java cu diferite surse și destinații de date cum ar fi fișiere, rețea, memorie sau dispozitive.

Concepte fundamentale

  • Stream (Flux): O succesiune ordonată de date disponibile în timp

  • Input Stream: Citește date dintr-o sursă

  • Output Stream: Scrie date într-o destinație

  • Byte Stream: Procesează date la nivel de octet (8 biți)

  • Character Stream: Procesează date la nivel de caracter (conform standardului Unicode)

Caracteristicile Streams

  • Unidirecționale: Un stream este fie de intrare, fie de ieșire, niciodată ambele

  • Secvențiale: Datele sunt procesate în ordinea în care sunt primite

  • Blocante: Operațiile de citire sau scriere pot bloca execuția până când datele sunt disponibile sau pot fi scrise

  • Închidere manuală: Majoritatea stream-urilor trebuie închise explicit pentru a elibera resursele

Avantajele utilizării Streams

  • Abstracție consistentă indiferent de sursa sau destinația datelor

  • Ierarhie flexibilă care permite extindere și personalizare

  • Capacitatea de a înlănțui stream-uri pentru funcționalități avansate (pattern Decorator)

  • Mecanism standard pentru I/O în întregul ecosistem Java

Ierarhia claselor de Streams

Java I/O este organizat într-o ierarhie de clase și interfețe care oferă funcționalitate specializată pentru diferite scenarii.

Clase de bază pentru Byte Streams

InputStream (abstract)
 ├── FileInputStream
 ├── ByteArrayInputStream
 ├── PipedInputStream
 ├── FilterInputStream
 │    ├── BufferedInputStream
 │    ├── DataInputStream
 │    └── ...
 └── ObjectInputStream
 
OutputStream (abstract)
 ├── FileOutputStream
 ├── ByteArrayOutputStream
 ├── PipedOutputStream
 ├── FilterOutputStream
 │    ├── BufferedOutputStream
 │    ├── DataOutputStream
 │    └── ...
 └── ObjectOutputStream

Clase de bază pentru Character Streams

Reader (abstract)
 ├── InputStreamReader
 │    └── FileReader
 ├── StringReader
 ├── CharArrayReader
 ├── PipedReader
 └── BufferedReader
 
Writer (abstract)
 ├── OutputStreamWriter
 │    └── FileWriter
 ├── StringWriter
 ├── CharArrayWriter
 ├── PipedWriter
 └── BufferedWriter
 └── PrintWriter

Relația între Byte și Character Streams

Character Streams sunt adesea construite pe baza Byte Streams:

// InputStreamReader convertește un InputStream (bytes) într-un Reader (caractere)
Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);

// OutputStreamWriter convertește caractere în bytes pentru un OutputStream
Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);

Byte Streams - Fluxuri de Octeți

Byte Streams procesează datele la cel mai jos nivel - fluxuri de octeți (8 biți). Aceste stream-uri sunt potrivite pentru toate tipurile de date binare.

InputStream

Clasa abstractă InputStream este baza tuturor stream-urilor de intrare care operează pe bytes. Metodele sale principale includ:

public abstract int read() throws IOException;    // Citește un singur byte
public int read(byte[] b) throws IOException;     // Citește bytes într-un array
public int read(byte[] b, int off, int len) throws IOException;  // Citește bytes într-o porțiune de array
public long skip(long n) throws IOException;      // Sare peste n bytes
public int available() throws IOException;        // Estimează bytes disponibili
public void close() throws IOException;           // Închide stream-ul și eliberează resursele
public synchronized void mark(int readlimit);     // Marchează poziția curentă (dacă este suportat)
public synchronized void reset() throws IOException;  // Resetează la ultima marcă
public boolean markSupported();                   // Verifică dacă mark() și reset() sunt suportate

OutputStream

Clasa abstractă OutputStream este baza tuturor stream-urilor de ieșire care operează pe bytes. Metodele sale principale includ:

public abstract void write(int b) throws IOException;    // Scrie un singur byte
public void write(byte[] b) throws IOException;          // Scrie un array de bytes
public void write(byte[] b, int off, int len) throws IOException;  // Scrie o porțiune din array
public void flush() throws IOException;                  // Forțează scrierea datelor tampon
public void close() throws IOException;                  // Închide stream-ul și eliberează resursele

Exemple de Byte Streams

FileInputStream și FileOutputStream

// Citirea unui fișier byte cu byte
try (FileInputStream fis = new FileInputStream("fisier.dat")) {
    int byteData;
    while ((byteData = fis.read()) != -1) {
        // Procesează fiecare byte
        System.out.print(byteData + " ");
    }
} catch (IOException e) {
    e.printStackTrace();
}

// Scrierea în fișier
try (FileOutputStream fos = new FileOutputStream("output.dat")) {
    byte[] data = {65, 66, 67, 68, 69}; // ABCDE în ASCII
    fos.write(data);
} catch (IOException e) {
    e.printStackTrace();
}

ByteArrayInputStream și ByteArrayOutputStream

// Citirea din un array de bytes
byte[] data = {1, 2, 3, 4, 5};
try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
    int byteValue;
    while ((byteValue = bais.read()) != -1) {
        System.out.println(byteValue);
    }
}

// Scrierea într-un ByteArrayOutputStream
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    baos.write(new byte[]{10, 20, 30});
    baos.write(40);
    
    byte[] result = baos.toByteArray();
    System.out.println(Arrays.toString(result)); // [10, 20, 30, 40]
}

Character Streams - Fluxuri de Caractere

Character Streams sunt proiectate pentru a procesa date text (Unicode). Acestea gestionează automat conversiile între bytes și caractere folosind codificări specifice.

Reader

Clasa abstractă Reader este baza tuturor stream-urilor de intrare care operează cu caractere. Metodele principale includ:

public int read() throws IOException;    // Citește un singur caracter
public int read(char[] cbuf) throws IOException;    // Citește caractere într-un array
public abstract int read(char[] cbuf, int off, int len) throws IOException;    // Citește într-o porțiune de array
public long skip(long n) throws IOException;    // Sare peste n caractere
public boolean ready() throws IOException;    // Verifică dacă stream-ul este pregătit pentru citire
public boolean markSupported();    // Verifică dacă mark() și reset() sunt suportate
public void mark(int readAheadLimit) throws IOException;    // Marchează poziția curentă
public void reset() throws IOException;    // Resetează la ultima marcă
public abstract void close() throws IOException;    // Închide stream-ul

Writer

Clasa abstractă Writer este baza tuturor stream-urilor de ieșire care operează cu caractere. Metodele principale includ:

public void write(int c) throws IOException;    // Scrie un singur caracter
public void write(char[] cbuf) throws IOException;    // Scrie un array de caractere
public abstract void write(char[] cbuf, int off, int len) throws IOException;    // Scrie o porțiune din array
public void write(String str) throws IOException;    // Scrie un String
public void write(String str, int off, int len) throws IOException;    // Scrie o porțiune din String
public abstract void flush() throws IOException;    // Forțează scrierea datelor tampon
public abstract void close() throws IOException;    // Închide stream-ul

Exemple de Character Streams

FileReader și FileWriter

// Citirea unui fișier text caracter cu caracter
try (FileReader fr = new FileReader("fisier.txt")) {
    int charData;
    while ((charData = fr.read()) != -1) {
        System.out.print((char) charData);
    }
} catch (IOException e) {
    e.printStackTrace();
}

// Scrierea textului în fișier
try (FileWriter fw = new FileWriter("output.txt")) {
    fw.write("Hello, Java I/O!");
} catch (IOException e) {
    e.printStackTrace();
}

InputStreamReader și OutputStreamWriter

// Convertirea unui InputStream în Reader cu codificare specifică
try (InputStreamReader isr = new InputStreamReader(
        new FileInputStream("data.txt"), StandardCharsets.UTF_8)) {
    char[] buffer = new char[1024];
    int charsRead;
    while ((charsRead = isr.read(buffer)) != -1) {
        System.out.print(new String(buffer, 0, charsRead));
    }
}

// Scrierea caracterelor cu codificare specifică
try (OutputStreamWriter osw = new OutputStreamWriter(
        new FileOutputStream("output.txt"), StandardCharsets.UTF_8)) {
    osw.write("Text cu caractere speciale: åäö");
}

StringReader și StringWriter

// Citirea din String ca sursă
try (StringReader sr = new StringReader("Hello, StringReader!")) {
    int charValue;
    while ((charValue = sr.read()) != -1) {
        System.out.print((char) charValue);
    }
}

// Scrierea în un StringWriter
try (StringWriter sw = new StringWriter()) {
    sw.write("Hello, ");
    sw.write("StringWriter!");
    
    String result = sw.toString();
    System.out.println(result); // Hello, StringWriter!
}

Buffered Streams - Fluxuri Tampon

Buffered Streams îmbunătățesc performanța prin reducerea numărului de operații I/O, acumulând date într-un buffer intern înainte de a le citi sau scrie efectiv.

BufferedInputStream și BufferedOutputStream

// Citire tamponată din fișier
try (BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("fisier.dat"), 8192)) {  // Buffer de 8KB
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = bis.read(buffer)) != -1) {
        // Procesează datele din buffer
    }
}

// Scriere tamponată în fișier
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("output.dat"))) {
    byte[] data = new byte[10000];
    // Umple data cu valori
    bos.write(data);
    // Nu este nevoie să apelăm flush() explicit - close() o va face
}

BufferedReader și BufferedWriter

Acestea sunt echivalentele tampon pentru Character Streams și oferă metode suplimentare utile:

// Citire tamponată linie cu linie
try (BufferedReader br = new BufferedReader(
        new FileReader("fisier.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {  // readLine() este o metodă foarte utilă!
        System.out.println(line);
    }
}

// Scriere tamponată cu suport pentru newline
try (BufferedWriter bw = new BufferedWriter(
        new FileWriter("output.txt"))) {
    bw.write("Prima linie");
    bw.newLine();  // Adaugă separator de linie specific platformei
    bw.write("A doua linie");
}

Avantajele folosirii Buffered Streams

  • Performanță semnificativ îmbunătățită, mai ales pentru operații frecvente cu volume mici de date

  • Reducerea apelurilor de sistem care sunt costisitoare în timp

  • Metode suplimentare utile (ex: readLine() în BufferedReader)

  • Operații de mark/reset mai eficiente (unde sunt suportate)

Data Streams și Object Streams

Aceste stream-uri permit citirea și scrierea tipurilor de date primitive și a obiectelor.

DataInputStream și DataOutputStream

Aceste clase permit citirea și scrierea tipurilor primitive de date Java în format binar:

// Scrierea valorilor primitive
try (DataOutputStream dos = new DataOutputStream(
        new FileOutputStream("data.bin"))) {
    dos.writeInt(42);
    dos.writeDouble(3.14);
    dos.writeUTF("Hello, DataStream!");  // UTF-8 encoded String
    dos.writeBoolean(true);
}

// Citirea valorilor primitive
try (DataInputStream dis = new DataInputStream(
        new FileInputStream("data.bin"))) {
    int intValue = dis.readInt();
    double doubleValue = dis.readDouble();
    String stringValue = dis.readUTF();
    boolean boolValue = dis.readBoolean();
    
    System.out.println(intValue + ", " + doubleValue + ", " + 
                      stringValue + ", " + boolValue);
}

Este important să citiți datele în exact aceeași ordine în care au fost scrise.

ObjectInputStream și ObjectOutputStream

Aceste clase permit serializarea și deserializarea obiectelor Java:

// Clasa care va fi serializată trebuie să implementeze Serializable
class Person implements Serializable {
    private static final long serialVersionUID = 1L; // Recomandat pentru controlul versiunilor
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

// Serializarea unui obiect
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("person.ser"))) {
    Person person = new Person("John Doe", 30);
    oos.writeObject(person);
}

// Deserializarea unui obiect
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("person.ser"))) {
    Person person = (Person) ois.readObject();
    System.out.println(person);
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

Citirea și Scrierea Fișierelor de Text

Java oferă mai multe modalități de a lucra cu fișiere text, de la cele tradiționale la cele moderne introduse în Java 7 și versiunile ulterioare.

Abordare Tradițională

// Citirea unui fișier text
try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
    StringBuilder content = new StringBuilder();
    String line;
    while ((line = br.readLine()) != null) {
        content.append(line).append(System.lineSeparator());
    }
    String fileContent = content.toString();
}

// Scrierea unui fișier text
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    bw.write("Prima linie a fișierului");
    bw.newLine();
    bw.write("A doua linie a fișierului");
}

Abordare Modernă (Java 7+)

Java 7 a introdus clasa Files care simplifică multe operații I/O:

import java.nio.file.*;

// Citirea întregului fișier într-un String
String content = Files.readString(Path.of("input.txt"));

// Citirea tuturor liniilor într-o listă
List<String> lines = Files.readAllLines(Path.of("input.txt"));

// Scrierea unui String într-un fișier
Files.writeString(Path.of("output.txt"), "Conținutul fișierului");

// Scrierea unei liste de linii
List<String> outputLines = Arrays.asList("Linia 1", "Linia 2", "Linia 3");
Files.write(Path.of("output.txt"), outputLines);

PrintWriter pentru Formatarea Ieșirii

PrintWriter oferă metode convenabile pentru formatarea datelor de ieșire:

try (PrintWriter pw = new PrintWriter(new FileWriter("raport.txt"))) {
    pw.println("Raport Zilnic");
    pw.println("-------------");
    pw.printf("Data: %tF%n", new Date());
    pw.printf("Vânzări: %.2f lei%n", 1234.56);
    pw.print("Status: ");
    pw.println("Complet");
}

Procesare I/O cu NIO și NIO.2

Java NIO (New I/O) și NIO.2 (Java 7+) oferă API-uri alternative pentru operații I/O, inclusiv I/O non-blocant și suport îmbunătățit pentru fișiere.

Lucrul cu Buffer și Channel (NIO)

// Citirea unui fișier folosind Channel și Buffer
try (FileChannel channel = FileChannel.open(
        Path.of("fisier.txt"), StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    while (channel.read(buffer) != -1) {
        buffer.flip();  // Pregătește buffer-ul pentru citire
        
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            // Procesează fiecare byte
        }
        
        buffer.clear();  // Pregătește buffer-ul pentru următoarea scriere
    }
}

// Scrierea într-un fișier folosind Channel și Buffer
try (FileChannel channel = FileChannel.open(
        Path.of("output.txt"), 
        StandardOpenOption.CREATE, 
        StandardOpenOption.WRITE)) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    buffer.put("Hello, Channel!".getBytes());
    buffer.flip();  // Pregătește buffer-ul pentru citire
    
    channel.write(buffer);
}

API-ul Path (NIO.2)

Java 7 a introdus API-ul Path pentru manipularea mai ușoară a căilor de fișiere:

// Crearea unei referințe Path
Path path = Paths.get("director/subdirector/fisier.txt");

// Informații despre path
System.out.println("Nume fișier: " + path.getFileName());
System.out.println("Părinte: " + path.getParent());
System.out.println("Număr componente: " + path.getNameCount());

// Manipularea path-urilor
Path absolut = path.toAbsolutePath();
Path normalizat = path.normalize();
Path rezolvat = Paths.get("director").resolve("fisier.txt");

Operații pe Fișiere și Directoare (NIO.2)

Clasa Files oferă metode statice pentru operații comune:

// Verificări
boolean exists = Files.exists(path);
boolean isDirectory = Files.isDirectory(path);
boolean isReadable = Files.isReadable(path);

// Creare
Files.createFile(path);
Files.createDirectories(path.getParent());

// Copiere și mutare
Files.copy(sursaPath, destinatiePath, StandardCopyOption.REPLACE_EXISTING);
Files.move(sursaPath, destinatiePath);

// Ștergere
Files.delete(path);
Files.deleteIfExists(path);

// Parcurgerea unui director
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dirPath)) {
    for (Path entry : stream) {
        System.out.println(entry.getFileName());
    }
}

// Parcurgerea unui arbore de directoare
Files.walkFileTree(dirPath, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("Fișier: " + file);
        return FileVisitResult.CONTINUE;
    }
    
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        System.out.println("Director: " + dir);
        return FileVisitResult.CONTINUE;
    }
});

Serializarea și Deserializarea

Serializarea este procesul de conversie a unui obiect într-un flux de bytes, care poate fi apoi stocat sau transmis. Deserializarea este procesul invers.

Serializarea de Bază

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    private transient String tempData; // câmpurile transient nu sunt serializate
    
    // constructori, getteri, setteri, etc.
}

// Serializare
Person person = new Person("Alice", 30);
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("person.ser"))) {
    oos.writeObject(person);
}

// Deserializare
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("person.ser"))) {
    Person deserializedPerson = (Person) ois.readObject();
    System.out.println(deserializedPerson);
}

Personalizarea Serializării

class CustomPerson implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient char[] password; // Nu dorim să serializăm parola direct
    
    // Metodă apelată la serializare
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject(); // Efectuează serializarea standard
        
        // Serializăm manual câmpuri sensibile (ex: o versiune criptată)
        if (password != null) {
            oos.writeInt(password.length);
            // Aici ar trebui să criptăm parola într-un scenariu real
            for (char c : password) {
                oos.writeChar(c + 1); // "Criptare" simplă pentru exemplu
            }
        } else {
            oos.writeInt(0);
        }
    }
    
    // Metodă apelată la deserializare
    private void readObject(ObjectInputStream ois) 
            throws IOException, ClassNotFoundException {
        ois.defaultReadObject(); // Efectuează deserializarea standard
        
        // Deserializăm manual câmpurile personalizate
        int len = ois.readInt();
        if (len > 0) {
            password = new char[len];
            for (int i = 0; i < len; i++) {
                password[i] = (char)(ois.readChar() - 1); // "Decriptare"
            }
        }
    }
}

Considerații pentru Serializare

  • Toate câmpurile non-transient trebuie să fie serializabile

  • static și transient nu sunt serializate

  • Controlul versiunilor cu serialVersionUID este important pentru compatibilitate

  • Serializarea are implicații de securitate - nu deserializa date din surse neîncredere

  • Mecanisme alternative: JSON, XML, Protocol Buffers, etc.

Last updated