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()
înBufferedReader
)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
șitransient
nu sunt serializateControlul versiunilor cu
serialVersionUID
este important pentru compatibilitateSerializarea are implicații de securitate - nu deserializa date din surse neîncredere
Mecanisme alternative: JSON, XML, Protocol Buffers, etc.
Last updated