MokaByte 57 - 9mbre 2001 
Operazioni elementari di I/O
Primi passi nello sviluppo di
una classe per la gestione dell’IO
di
Alessio
Saltarin
Contrariamente a quanto accade in altri linguaggi di programmazione ad alto livello, per esempio Basic o Python, in Java non esiste la possibilità di invocare un comando univoco per il caricamento e il salvataggio di un file. Sarebbe bello, per esempio, disporre di una classe statica con due metodi, salva() e carica(), che fanno tutto quello che serve. In realtà le Java API che gestiscono l’I/O sono molto potenti e permettono allo sviluppatore di definire con un grande livello di dettaglio il modo con cui le operazioni input/output (I/O) devono essere svolte. 

Una classe statica per l’I/O
Il nostro obiettivo è quello di scrivere lo scheletro di una classe da usare un po’ come il prezzemolo, ogni volta che dobbiamo leggere o salvare un file e non vogliamo perderci troppo nei dettagli. La classe sarà statica per semplicità e perché non esiste alcun motivo di farne più di un’istanza.  Supponiamo di chiamarla “FileManager”: avrà di base due metodi:

FileManager.openFile(String fileName)

che ritorna una String col contenuto del file, e

FileManager.saveFile(String fileName, String fileString)

che potrà ritornare, ad esempio, un valore booleano (sì, ho salvato, oppure no, ho avuto qualche problema). 
Chiaramente la classe si dovrà preoccupare di gestire in qualche modo gli errori, poiché è intrinseca in qualsiasi operazione di I/O la possibilità che qualcosa vada storto (il file non esiste, il supporto non è pronto, la memoria sul supporto è esaurita e via dicendo).
Anche qui sceglieremo una strada semplice, lasciando al futuro e alla nostra buona volontà l’implementazione di un sistema più evoluto di gestione degli errori. In pratica registreremo in una variabile di stato della classe la descrizione dell’errore, fornendo un metodo che ne ritorni il valore.
L’idea è quella di permettere al programma di continuare l’esecuzione (senza sollevare eccezioni, quindi) anche in presenza di errori. Ad esempio, se lo spazio su disco è esaurito, il metodo saveFile ritornerà false. Se ciò dovesse accadere, l’utente della classe potrà richiedere una descrizione dell’errore alla classe stessa attraverso un metodo getErrorMsg(). Scegliendo quest’approccio, ahimé, rinunciamo alla staticità della classe – una classe statica non può infatti per definizione contenere variabili di stato. Poco male, il lettore esperto potrà facilmente trasformarla in un Singleton (una classe di cui esiste sempre e soltanto una sola istanza) avendo cura di assicurarsi che chi chiama il metodo getErrorMsg() sia lo stesso che ha generato l’errore (non è una cosa banale, ma è certamente fattibile).
 
 
 

Leggere un file
Per semplicità, supporremo che il file che vogliamo leggere sia un file di testo, e che il suo contenuto possa essere riversato in una stringa. Utilizzeremo principalmente la classe della Java API 1.1 (quindi presente nel JDK 1.1 e successivi) 
 
 
 

java.io.FileReader
Questa classe non contiene però alcun metodo. Questo non significa che non sia utilizzabile. Le Java API sono costruite seguendo attentamente i dettami della programmazione orientata agli oggetti, per cui capita molto spesso di imbattersi in classi all’apparenza inutili. In realtà, per usarle, occorre utilizzare i concetti di incapsulamento, ereditarietà e polimorfismo. Nel caso in esame il problema è specificare al sistema in che modo vogliamo leggere il file: carattere per carattere? Forse è meglio specificare un buffer e leggere a quantità discrete di byte. C’è esattamente una classe che fa al caso nostro: si chiama BufferedReader, e scopriamo che tra i suoi costruttori ce n’è uno che prende un argomento di tipo “Reader”, che guarda caso è la classe genitore di “FileReader”.  Ecco applicato il concetto di ereditarietà e polimorfismo: a seconda dello specifico tipo di Reader che passerò a BufferedReader potrò bufferizzare in memoria, su disco o quant’altro. Questa classe contiene un metodo readLine che legge linea per linea un file di testo.
Ecco dunque come implementeremo il metodo openFile:

public String openFile(String fileName)
{
  String ilFile = new String();

  try
  {
   BufferedReader in=
    new BufferedReader(new FileReader(fileName));

   String linea = new String();

   while ((linea=in.readLine())!= null )
   {
    ilFile+=linea;
ilFile+=System.getProperty(“line.separator”);
   }

   in.close();
  }
  catch (IOException e)
  {
   ilFile=null;
   errorMsg=”Oper error:”+e.getMessage();
  }

  return ilFile;

}

Se il fileName specifica un path inesistente, o si verifica un errore di supporto non pronto, il metodo ritorna semplicemente un valore nullo. Vedremo poi come utilizzare questo valore per comunicare all’utente il tipo di errore verificatosi.
 
 
 

Salvare un file
L’operazione di salvataggio di un file è leggermente più complessa della precedente, perché oltre che specificare un’operazione con un buffer, vorremmo che il file venga formattato come un file di testo e non come un semplice flusso di byte – siccome Java è indipendente dalla piattaforma questa è un’informazione tutt’altro che irrilevante: sul sistema in uso come si descrive un ritorno carrello? Un file di testo è formattato con uno speciale carattere di controllo? Occorre sempre ricordare, quando si programma in Java, di non specificare direttamente i formati, che possono differire da piattaforma a piattaforma e, soprattutto, possono cambiare in futuro. 
La classe che fa per noi è PrintWriter. Ancora una volta, utilizzando il polimorfismo, specificheremo che intendiamo utilizzare un buffer attraverso la classe BufferedOutputStream.
Ecco quindi una possibile implementazione:

public boolean saveFile(String fileName, String fileString)
{
  try
  {
   PrintWriter out = new PrintWriter(
new BufferedOutputStream(
new FileOutputStream(fileName)));
  }
catch (FileNotFoundException e)
  {
   errorMsg="Save File Error: "+e.getMessage();
   return false;
  }

  out.print(fileString);
out.close();

if (out.checkError())

 errorMsg=”Out of memory.”;
 return false;
  }

return true;
}
 

Se il fileName specifica un path inesistente, il metodo ritorna false. Poiché PrintWriter non solleva eccezioni (proprio come la nostra FileManager!) esiste il metodo checkError per verificare se tutto è andato nel modo giusto. 
 
 
 

Altre utilità di I/O
Naturalmente potremo estendere a piacimento la nostra classe con metodi che salvano e leggono direttamente in byte, oppure che serializzano oggetti che utilizziamo. Ma senza arrivare a un tale livello di complessità, esistono due metodi che sicuramente vorremo aggiungere fin da adesso. Uno che salva in modalità append e uno che cancelli un file. Per entrambi avremo bisogno di una routine che controlla che un file esista. Cominciamo da quest’ultima:

public boolean existFile(String fileName)
{
     boolean exist;

     try
     {
         FileReader fr = new FileReader(fileName);
         exist=true;
     }
     catch (FileNotFoundException fex)
     {
         exist=false;
     }

     return exist;
}

sfruttando cioè il fatto che il costruttore di FileReader innalza un’eccezione nel caso il file non esista. Per noi non è affatto un’eccezione, ma talvolta può essere comodo utilizzare try/catch in questo modo.
Per la prima funzionalità la soluzione è molto semplice, dal momento che esiste un costruttore di BufferedOutputStream che prende come argomento un booleano che specifica se il salvataggio deve avvenire in modalità append. Siccome l’utente della nostra classe può non saperlo, non sarà male aggiungere un metodo sovraccaricato (overloaded) che accetta come parametri il nome del file, il contenuto e questo booleano. Per semplificare quindi ulteriormente le cose aggiungeremo:

public boolean appendToFile(String fileName, String fileString)
{
 if (this.existFile(fileName))
  return this.saveFile(fileName, fileString, true);
 else
errorMsg=”File to append to does not exist.”;

return false;
}

Per la seconda, invece, sfrutteremo il metodo delete della classe File:

public boolean deleteFile(String fileName)
{

        boolean deleted = false;

        if (this.existFile(fileName))
        {
            File fileToKill = new File(fileName);
            if (fileToKill.delete())
                deleted = true;
            else
                errorMsg="Can't delete "+fileName;
        }
        else
            errorMsg="Can't delete: file does not exist.";

        return deleted;
}
 
 

Usare la classe FileManager
Possiamo testare la classe in questo modo:

FileManager fm = new FileManager();
if (fm.saveFile("Prova.txt", "Prova"))
{
       System.out.print(fm.openFile("Prova.txt"));
   fm.deleteFile("Prova.txt");
}
else
       System.out.print(fm.getErrorMsg());

Qui sotto trovate il listato completo. 
/**
 * FileManager.java
 * @author Alessio Saltarin 2001
 * @since 04/2001
 * @version 1.0
 *
 * Utility per il salvataggio/caricamento di file di testo
 *
 */
 

package name.alessiosaltarin.utils;

import java.io.*;

public final class FileManager
{

    /**
     * Se qualcosa è andato storto, questo metodo dice perché
     * @return messaggio d'errore
     */
    public String getErrorMsg()
    {
        return errorMsg;
    }

    /**
     * Il contenuto del file aperto
     * @param Path completo al file
     * @return rappresentazione dei contenuti del file
     */
 public String openFile(String fileName)
 {
  String ilFile = new String();

  try
  {
   BufferedReader in=
    new BufferedReader(new FileReader(fileName));

   String linea = new String();

   while ((linea=in.readLine())!= null )
    ilFile+=linea+NEWLINE;

   in.close();
  }
  catch (IOException e)
  {
   ilFile=null;
   errorMsg="Open File Error: "+e.getMessage();
  }

  return ilFile;

 }

 /**
     * Controlla se un file esiste
     * @param fileName path completo al file
     * @return true se il file esiste
     */
 public boolean existFile(String fileName)
 {
     boolean exist;

     try
     {
         FileReader fr = new FileReader(fileName);
         exist=true;
     }
     catch (FileNotFoundException fex)
     {
         exist=false;
     }

     return exist;
 }

    /**
     * Salva il contenuto di fileString in un file
     * @param fileName Path completo al file che si vuole salvare
     * @param fileString Contenuto del file
     * @return true se il file e' stato salvato correttamente
     */
 public boolean saveFile(String fileName, String fileString)
 {

     PrintWriter out;

        try
        {
      out = new PrintWriter(
       new BufferedOutputStream(new FileOutputStream(fileName)));
  }
  catch (FileNotFoundException e)
  {
       errorMsg="Save File Error: "+e.getMessage();
       return false;
  }

  out.print(fileString);
  out.close();

  if (out.checkError())
     {
       errorMsg="Out of memory"; 
       return false;
  }

  return true;
 }

 /**
     * Salva il contenuto di fileString in un file
     * @param fileName Path completo al file che si vuole salvare
     * @param fileString Contenuto del file
     * @param append se true, appende la stringa al file esistente
     * @return true se il file e' stato salvato correttamente
     */
 public boolean saveFile(String fileName, String fileString, boolean append)
 {

     PrintWriter out;

  try
  {
   out = new PrintWriter(
    new BufferedOutputStream(new FileOutputStream(fileName)), append);

  }
  catch (FileNotFoundException e)
  {
       errorMsg="Save File Error: "+e.getMessage();
       return false;
  }

  out.print(fileString);
  out.close();

  if (out.checkError())
     {
       errorMsg="Out of memory"; 
       return false;
  }

  return true;

 }
 

 /**
     * Salva il contenuto di fileString in un file
     * @param fileName Path completo al file che si vuole salvare in modalità append
     * @param fileString Contenuto del file
     * @return true se il file e' stato salvato correttamente
     */
 public boolean appendToFile(String fileName, String fileString)
 {
     if (this.existFile(fileName))
      return this.saveFile(fileName, fileString, true);
  else
   errorMsg="File to append to does not exist";

  return false;
 }

    /**
     * Cancella un file
     * @param fileName Path completo al file che si vuole cancellare
     * @return true se il file e' stato salvato cancellato
     */
    public boolean deleteFile(String fileName)
    {

        boolean deleted = false;

        if (this.existFile(fileName))
        {
            File fileToKill = new File(fileName);
            if (fileToKill.delete())
                deleted = true;
            else
                errorMsg="Can't delete "+fileName;
        }
        else
            errorMsg="Can't delete: file does not exist.";

        return deleted;
    }

    /**
     * Constructor
     */
    public FileManager()
    {
        errorMsg="OK";
    }
 

    /**
     * For test purposes, only
     * @deprecated
     */
    public static void main(String[] args)
    {
        FileManager fm = new FileManager();
        if (fm.saveFile("Prova.txt", "Prova"))
   {
            System.out.print(fm.openFile("Prova.txt"));
  fm.deleteFile("Prova.txt");
        }
        else
            System.out.print(fm.getErrorMsg());

    }

    private String errorMsg;
    private static final String NEWLINE = System.getProperty(“line.separator”);
 

}
 


 
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati 
E' vietata la riproduzione anche parziale 
Per comunicazioni inviare una mail a info@mokabyte.it