1. Introduzione
Iniziamo dalla cosa più semplice: in cosa un file binario è diverso da un file di testo? Un file di testo è un file che si può aprire in un normale blocco note e vedere lettere, numeri, spazi e altri simboli. Per esempio, my_notes.txt oppure poem.txt.
Un file binario è un file che non contiene testo, ma byte arbitrari. Può essere un’immagine (.jpg, .png), musica (.mp3), un archivio (.zip), un eseguibile (.exe), un video (.mp4), un file di database e così via. Se aprite un file del genere in un blocco note, vedrete qualcosa come ÿØÿà o un muro di simboli incomprensibili. È normale! Il computer «capisce» solo i byte — per lui testo, immagini e video sono solo una sequenza di byte. Nei file di testo questi byte possono essere interpretati come caratteri, mentre nei file binari sono dati «grezzi», non destinati alla lettura umana.
Classi principali per lavorare con i file binari
In Java si usano i flussi di byte per lavorare con i file binari:
- InputStream — classe base per leggere byte.
- OutputStream — classe base per scrivere byte.
Per i file esistono implementazioni concrete:
- FileInputStream — legge i byte da un file.
- FileOutputStream — scrive i byte su un file.
Se avete sentito parlare di FileReader e FileWriter, sappiate che lavorano con i caratteri e sono adatti solo al testo. Per i file binari usate esclusivamente InputStream/OutputStream e le loro sottoclassi.
2. Lettura di file binari
Lettura byte per byte
Il modo più semplice è leggere il file un byte alla volta. È didattico, ma molto lento.
try (FileInputStream in = new FileInputStream("image.jpg")) {
int b;
while ((b = in.read()) != -1) {
// b è un numero da 0 a 255 (byte), -1 indica la fine del file
// Si può elaborare il byte, ad esempio calcolare la somma di tutti i byte
}
}
Il metodo read() restituisce il byte successivo come int (da 0 a 255) e quando il file è finito restituisce -1. Di solito si legge byte per byte solo quando serve qualcosa di molto specifico (per esempio, analizzare la struttura del file).
Lettura a blocchi (con buffer)
Leggere un byte alla volta è come andare al negozio per ogni mela separatamente. Molto più efficiente è prendere subito un intero sacchetto! In Java c’è il metodo read(byte[] buffer), che riempie un array con i byte dal file.
try (FileInputStream in = new FileInputStream("image.jpg")) {
byte[] buffer = new byte[4096]; // buffer da 4 KB
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
// buffer contiene bytesRead byte dal file
// Si possono elaborare questi byte, ad esempio salvarli altrove
}
}
Il metodo read(buffer) restituisce quanti byte sono stati effettivamente letti (può essere inferiore alla dimensione del buffer, specialmente all’ultima lettura). Questo approccio è molto più veloce perché ci sono meno accessi al disco.
Esempio: copia di un file
Scriviamo un semplice programma che copia qualsiasi file binario (per esempio, un’immagine) da un percorso a un altro.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BinaryCopyExample {
public static void main(String[] args) {
String source = "cat.jpg";
String dest = "cat_copy.jpg";
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 8 KB — dimensione ottimale per la maggior parte dei casi
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("Copia completata!");
} catch (IOException e) {
System.out.println("Errore durante la copia: " + e.getMessage());
}
}
}
È semplice: leggiamo blocchi dal file sorgente e li scriviamo subito nel nuovo file. Questo metodo funziona con qualsiasi file: immagini, archivi, video.
3. Scrittura di file binari
Scrittura di un array di byte
Se avete un array di byte (per esempio ottenuto dalla rete o generato nel programma), potete scriverlo su file così:
byte[] data = new byte[] {1, 2, 3, 4, 5}; // esempio di array
try (FileOutputStream out = new FileOutputStream("data.bin")) {
out.write(data); // scrive l'intero array su file
}
Il metodo write(byte[]) scrive tutti i byte dell’array. Si può anche scrivere solo una parte dell’array: out.write(data, offset, length).
Scrittura del file a pezzi (ad esempio durante la copia)
Come per la lettura, si usa di solito un buffer:
try (FileInputStream in = new FileInputStream("source.bin");
FileOutputStream out = new FileOutputStream("dest.bin")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
Qui tutto ciò che leggiamo dal file lo scriviamo immediatamente in un altro file. Questo codice si vede spesso in programmi di archiviazione, downloader, elaboratori di immagini, ecc.
4. Dettagli utili
Perché non si possono usare Reader/Writer per i file binari?
Reader e Writer lavorano con i caratteri (char), non con i byte. Convertono automaticamente i byte in caratteri secondo la codifica (per esempio UTF-8). È comodo per il testo, ma per i file binari — pericolosissimo!
Se provate a scrivere un’immagine tramite FileWriter, otterrete un file corrotto, impossibile da aprire. Ricordate: per qualunque file non testuale usate solo InputStream/OutputStream!
Differenze importanti e particolarità nel lavoro con i file binari
- Dimensione del buffer: Un buffer troppo piccolo rallenta (troppi accessi al disco), uno troppo grande occupa memoria inutile. 4–16 KB è in genere ottimale.
- Gestione degli errori: Gestite sempre IOException — il file potrebbe non esistere, essere bloccato oppure lo spazio su disco potrebbe esaurirsi.
- Chiusura dei flussi: Usate try-with-resources — garantisce la chiusura dei file anche in caso di errori.
- Sovrascrittura del file: Se aprite un file con new FileOutputStream("file.bin"), verrà sovrascritto. Per aggiungere in coda, usate il costruttore con parametro append = true.
- Permessi di accesso: Se il programma non riesce ad aprire il file, controllate i permessi di lettura/scrittura.
- readAllBytes(): Consente di leggere l’intero file in un array di byte con una sola chiamata. Per file grandi non usatelo, per non «consumare» tutta la memoria!
5. Errori tipici nel lavoro con i file binari
Errore n. 1: usare FileReader/FileWriter per file binari. Questo porta alla corruzione dei dati, perché queste classi convertono i byte in caratteri e viceversa, cosa disastrosa per immagini, archivi, ecc.
Errore n. 2: ignorare il valore restituito da read(). Il metodo read(byte[]) può leggere meno byte di quelli richiesti, specialmente nell’ultimo blocco. Usate sempre il valore restituito per sapere quanti byte sono stati effettivamente elaborati.
Errore n. 3: dimenticare di chiudere il flusso. Se non si chiude il file, può rimanere bloccato e i dati potrebbero non essere completamente scritti (soprattutto in scrittura!). Usate try-with-resources.
Errore n. 4: leggere l’intero file in memoria senza considerare la dimensione. Per file grandi questo porta a OutOfMemoryError. Usate un buffer e leggete a pezzi.
Errore n. 5: non gestire le eccezioni. Il lavoro con i file può sempre generare errori: file non trovato, permessi insufficienti, disco pieno. Non dimenticate di gestire IOException.
GO TO FULL VERSION