MokaByte Numero 20 - Giugno 1998
 

 
Uno splittatore di file

 
 
 
di
Marco Molinari
 

 



Questo articolo descrive un semplice programma con due funzioni: dividere un file in pezzi più piccoli e riunire dei pezzi di file in un file unico. La motivazione di questo programma è stata la necessità di trasferire alcuni file piuttosto grossi tra un PC e un Mac; pur sapendo che esistono programmi multipiattaforma che servono a questo scopo, si era sicuri solo della presenza di Java sul Mac e non si sapeva quali programmi si sarebbero potuti installare; si è preferito scrivere qualche riga di codice per evitare sorprese.


Il programma prevede due modalità di funzionamento: da linea di comando e grafica. Per questo la classe estende Frame (una finestra dotata di titolo e di bordo) ed è dotata di questi metodi:

Il programma non è un'applet perchè le limitazioni delle applet comprendono quella di non avere accesso ai file della macchina su cui girano.

Il cuore del programma è costituito dalle due procedure split() e join(), che rispettivamente dividono e uniscono il file. Se questi metodi non fossero dichiarati static non potrebbero essere chiamati senza istanziare la classe stessa, ovvero quando si utilizza il programma dalla linea di comando.

L'algoritmo (se di algoritmo si può parlare) di split() è semplice: in input si ha il nome di un file e la grandezza in kbytes che ogni file spezzato dovrà avere; in output si hanno i file spezzati, chiamati nomeoriginale.00, .01 ecc. Il funzionamento è elementare: si prende il pezzo successivo del file di partenza e lo si scrive nel file di output particolare fino a che si raggiunge la grandezza voluta del file di output; allora si chiude quel file di output e se ne apre un altro; si continua così fino a terminare il file di partenza.

In Java i file si gestiscono con le classi File, FileInputStream e FileOutputStream: File rappresenta un puntatore al file su cui si possono fare operazioni come vedere se esiste, cambiarne il nome e cancellarlo. FileInputStream e FileOutputStream prendono come parametro un File e rappresentano i flussi di dati da e verso quel File: i loro metodi fondamentali sono quelli ereditati da InputStream e OutputStream, ovvero letture e scritture di byte e array di byte. Alternativamente i due FileStream possono prendere direttamente una String; nel codice si è istanziato un File solo quando ce ne fosse effettivamente bisogno, cioè quando era necessario vedere se il file esisteva e quando bisognava sapere quanto era lungo il file. Nel codificare bisogna prestare attenzione, perchè si rischia di produrre un codice di una lentezza mortificante. Il pericolo è di usare i metodi che leggono o scrivono un byte alla volta; agendo così bisogna armarsi di molta pazienza, il codice funziona ma impiega parecchio tempo.

Ecco come sarebbe il codice (size è la grandezza del file di output in un certo momento, chunks è la grandezza finale di ogni pezzo; length è la lunghezza del file originale):
 

FileInputStream fis = new FileInputStream(source);
while (length > 0) {
    FileOutputStream fos = new FileOutputStream(source+"."+Integer.toString(i));
    int size = 0;

    while ( (chunks > size) && ((singoloByte = fis.read()) != -1) ){
        fos.write(singoloByte);
        size++;
    }

    length -= chunks;
    fos.close();
    i++;

}
fis.close();
 

Il metodo read() ritorna il prossimo byte letto, -1 quando il file di input è finito.
Il sistema di I/O di Java permette in modo molto semplice di aggiungere dei buffer ai due flussi di dati. E' sufficiente infatti "decorare" i due Stream rispettivamente con BufferedInputStream e BufferedOutputStream per vedere le prestazioni aumentare.
 
FileInputStream fis = new FileInputStream(new BufferedInputStream(source));
while (length > 0) {
    FileOutputStream fos = new FileOutputStream(new BufferedOutputStream(source+"."+Integer.toString(i)));
    [...]
 
Questa idea è alla base di tutto il sistema di I/O di Java e permette di aggiungere solo le caratteristiche volute a una certa stream.

Purtroppo però la situazione migliora ma non di molto: la soluzione ottima è usare i metodi delle FileStreams che leggono e scrivono array di byte ogni volta. Decidiamo di leggere e scrivere 1024 byte alla volta e in input chiediamo il numero di kbyte che deve comporre ogni file di output invece del numero preciso di byte; per l'aumento di prestazione che si ricava è un buon compromesso.
 

FileInputStream fis = new FileInputStream(source);
int amount;         // contiene a ogni passaggio il numero di byte letti
byte[] b = new byte[bufSize];

while (length > 0) {
    FileOutputStream fos = new FileOutputStream(source+"."+Integer.toString(i));
    int size = 0;

    while ( (chunks > size) && ((amount = fis.read(b,0,bufSize)) != -1) ){
        fos.write(b,0,bufSize);
        size++;
    }

    length -= chunks*bufSize;
    fos.close();
    i++;

}
fis.close();
 

Il metodo utilizzato qui è read(byte[], int, int) che riempie il byte[] e ritorna il numero di byte effettivamente letti, -1 se il file è finito.
Così facendo le prestazioni migliorano di qualche ordine di grandezza; in effetti è interessante notare che l'aggiunta dei BufferedStreams non suscita miglioramenti sensibili. Questo non toglie però che in altri casi, dove non si possono leggere semplici array di byte, la bufferizzazione delle stream è tassativa.
L'operazione di join() è l'esatto inverso di split(); vuole il nome dei pezzi di file senza estensione ".n"; si aprono uno a uno i file di input e si riversano i loro contenuti nel file di output. Utilizza la lettura e la scrittura di array di byte come split() e non risolve il problema di sapere quanti file ci sono in input: quando non ci saranno più file di input viene generata un'eccezione che viene ignorata.
 

File outputFile = new File(destination);
FileOutputStream fos = new FileOutputStream(outputFile);
byte c[] = new byte[bufSize];

try {
    for (int i = 0; true; i++) {
        FileInputStream fis = new FileInputStream(destination+"."+i);
        int amount;

        while ((amount = fis.read(c, 0, bufSize)) != -1) {
            fos.write(c, 0, amount);
        }

        fis.close();
    }
} catch (FileNotFoundException e) {
}       // lo facciamo continuare finchè ci sono file da unire

fos.close();
 

In Java 1.1 sono state aggiunte nuove classi di I/O, facenti capo a Reader e Writer. Il motivo principale per questo cambiamento è stato l'internalizzazione (i vecchi Stream non supportano bene i caratteri Unicode a 16 bit); infatti Reader, Writer e derivati servono solo a gestire flussi di caratteri, quindi non servono al nostro scopo.

Per la gestione degli eventi si è restati sui vecchi e poco eleganti metodi di Java 1.0 per motivi di compatibilità. Il metodo action() e la sua fila di if si occupano di questo. handleEvent() serve invece solo alla chiusura della finestra.

Per quanto riguarda il costruttore, esso è chiamato solo nella "modalità grafica" e si occupa di istanziare i vari oggetti. Invece di rovinarsi la vita correndo dietro al troppo sfuggevole GridBagLayout, si è preferito usare quattro Panel messi nella finestra in un GridLayout di 4 righe e una colonna. I Panel hanno come layout standard il FlowLayout, che mette semplicemente i componenti in riga disegnadoli con la loro grandezza naturale, al contrario di quanto sarebbe accaduto mettendo i componenti direttamente nel GridLayout.
 

setLayout (new GridLayout(4,1));

Panel p1 = new Panel();
p1.add(browse);
p1.add(filename);
add(p1);

Panel p2 = new Panel();
p2.add(messages);
add(p2);

Panel p3 = new Panel();
p3.add(chunkSize);
p3.add(sizeOfChunks);
add(p3);

Panel p4 = new Panel();
p4.add(split);
p4.add(join);
add(p4);
 

Il programma ha fatto il suo dovere, dividendo il file sotto Windows 95 e ricomponendolo sotto Mac; inoltre sotto Linux non c'è neanche bisogno di caricare XWindows per farlo funzionare se ci si accontenta di usarlo dalla shell.
 
 
 
 
 
 
 

MokaByte Web  1998 - www.mokabyte.it 
MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it