Paola Magillo, Univestita' di Genova, Corso di Programmazione II per SMID, a.a. 2007-2008.

Lezione 10:

INPUT E OUTPUT SU FILE

Nota: le classi per input / output si trovano nel package java.io, percio' in testa ai file sorgenti bisogna importare tale package:
import java.io.*;

Le due classi Java InputStream e OutputStream rappresentano un generico canale di input e di output.
Le due sotto-classi FileInputStream e FileOutputStream rappresentano un canale di input e di output che consiste in un file.
I costruttori di queste due classi prendono come argomento il nome del file da aprire in lettura e in scrittura (a seconda del caso):

Scrittura su file

Pero' le funzioni di scrittura fornite da OutputStream sono troppo di basso livello, scrivono un byte alla volta.
E' piu' comodo usare, per scrivere su file, la classe PrintStream.

La classe Java PrintStream rappresenta un canale di output che puo' scrivere stringhe, con le funzioni:

Come mi procuro un oggetto di classe PrintStream?

Lettura da file

Anche le funzioni di lettura fornite da InputStream sono di basso livello, leggono un byte (= un carattere) alla volta ritornando il suo codice ASCII come numero intero.
Purtroppo, per leggere su file, non esiste in Java un equivalente della classe PrintStream usata per scrivere.

Allora definiamo noi una nostra classe che permetta di leggere agevolmente i vari tipi base: stringhe, interi, double...

Classi per le stringhe

Abbiamo due classi Java per le stringhe:

  1. String = stringa non modificabile, gia' usata in lezioni scorse
  2. StringBuffer = stringa modificabile, per esempio ha la funzione "append" che concatena un'altra stringa in fondo a questa stringa
Uno string buffer si puo' costruire a partire da una stringa e viceversa:
  1. String s = new String(sb); dove sb e' uno string buffer
  2. StringBuffer sb = new StringBuffer(s); dove s e' una stringa

Per leggere una stringa usiamo un oggetto di classe StringBuffer, inizialmente vuoto, e a questo concateniamo in fondo, uno alla volta, i caratteri letti da file.
Ogni carattere viene restituito dalla funzione "read" della classe InputStream come un numero intero (il suo codice ASCII), che convertiamo forzatamente a carattere:
int cod = fd.read();
char c = (char)cod;
dove fd e' un oggetto di tipo InputStream.

Ci fermiamo quando incontiamo uno spazio bianco, oppure la fine del file. La funzione Java Character.isWhitespace(char c) controlla se un carattere c e' uno spazio bianco. Il carattere marchio di fine file ha codice ASCII = -1. Inoltre bisogna saltare gli eventuali spazi bianchi prima dell'inizio della stringa.
Questo legge una stringa e la ritorna:

  StringBuffer s = new StringBuffer(""); // stringa modificabile vuota
  int cod = skipWhitespace(); // salta spazi (ved. dopo)
  while ( (cod!=-1) && !Character.isWhitespace( (char)cod ))
  {
    s.append( (char)cod );    // concatena carattere letto
    cod = fd.read();          // legge prossimo carattere
  }
  return new String(s);       // stringa finale non modificabile
dove skipWhitespace e' la nostra funzione che salta gli spazi bianchi e ritorna il codice del primo carattere non bianco, implementata cosi':
  int cod = (int)(' ');       // inizializzo con spazio bianco
  while (Character.isWhitespace( (char)cod )) // finche' bianco
  {
    cod = fd.read();          // va avanti a leggere carattere
    if (cod == -1) return -1; // trovata fine file
  } 
  return cod;                 // codice del primo non bianco

Classe MyReader

Definiamo la nostra classe MyReader che contiene al suo interno un oggetto di classe InputStream da cui leggere, e ha le seguenti funzioni pubbliche:

Tutte queste funzioni, tranne isKeyword, possono sollevare eccezione in caso di input non disponibile o non corretto sintatticamente.

Vedere sorgente Java della classe MyReader.

Esercizi

Usare la classe MyReader per leggere da input le coordinate di due punti, calcolare e stampare la distanza tra i due punti.

Usate la classe PrintStream per scrivere su file i numeri primi trovati dal crivello di Eratostene.

Leggere e scrivere oggetti

Quando devo leggere / scrivere non valori di base ma oggetti di una classe, ho due alternative:

  1. Decidere una sintassi con cui scrivere gli oggetti della mia classe in termini di valori di base.
    In lettura fare il "parsing" (controllo sintattico) del contenuto del file secondo la stessa sintassi.

  2. Rendere la classe persistente e lasciare che Java scriva l'oggetto, e poi lo rilegga, secondo la sua propria sintassi.

La prima alternativa mi fa scrivere piu' codice, ma produce un file intelleggibile che posso editare a mano se voglio.

La seconda alternativa e' quasi gratis, ma produce un file non unanamente leggibile. Inoltre se cambio qualcosa nella classe cambia anche la sintassi che Java usa per gli oggetti, e Java non e' piu' in grado di rileggere oggetti scritti precedentemente.

Prima alternativa: sintassi mia

Esempio: rettangolo che sa scriversi su standard output e leggersi da standard input.

Sintassi che ho deciso per i rettangoli:

class Rectangle
{
  int length, width;

  int area()
  { return length * width; }

  void print()
  { System.out.println("Rectangle " + length + " x " + width); }

  void read()
  {
    try // le funzioni del reader possono sollevare eccezione
    {
      MyReader rd = new MyReader(System.in);
      String s = rd.readString();
      if (!rd.isKeyword(s,"Rectangle")) return; // non fa nulla
      length = rd.readInteger();
      s = rd.readString();
      if (!rd.isKeyword(s,"x")) return; // non fa nulla
      width = rd.readInteger();
    }
    catch(Exception exc) {} // se errore non fa nulla
  }

  public static void main(String[] args)
  {
    Rectangle r = new Rectangle();
    r.read();
    r.print();
  } 
}

Il main chiede un rettangolo da standard input (secondo la sintassi decisa) e poi lo stampa su standard output. Esempio di uso, digitare:

  java Rectangle
  Rectangle 33 x 24

Seconda alternativa: Persistenza

Oggetti persistenti = oggetti che sono capaci a scriversi su file e rileggersi da file. Servono per "ricordare" lo stato di un oggetto tra una esecuzione e l'altra del programma.

Classi persistenti implementano l'interfaccia Serializable, che non ha metodi, ma occorre dichiarare che una classe implementa Serializable per poterla scrivere e rileggere attraverso i metodi di ObjectOutputStream e ObjectInputStream.

ObjectOutputStream

Classe le cui istanze rappresentano file e sono in grado di scrivere oggetti appartenenti a classi che implementano l'interfaccia Serializable.

Costruzione di un'istanza di ObjectOutputStream, per es. sia "pippo.txt" il nome del file, e scrittura di un oggetto sul file:

 FileOutputStream fos = new FileOutputStream("pippo.txt");
 ObjectOutputStream oos = new ObjectOutputStream(fos);
 oos.writeObject(...oggetto da scrivere...);
 oos.close();
 fos.close();
Nota: tutte queste operazioni possono sollevare eccezioni di classe IOException.

ObjectInputStream

Classe le cui istanze rappresentano file e sono in grado di leggere oggetti appartenenti a classi che implementano l'interfaccia Serializable.

Costruzione di un'istanza di ObjectInputStream, per es. sia "pippo.txt" il nome del file, e lettura di un oggetto dal file:

 FileInputStream fis = new FileInputStream("pippo.txt");
 ObjectInputStream ois = new ObjectInputStream(fis);
 ...oggetto da leggere... = (...classe dell'oggetto...)ois.readObject();
 ois.close();
 fis.close();
Note:
L'oggetto viene restituito dalla funzione read come oggetto di classe generica Object, bisogna convertirlo forzatamente alla sua classe effettiva (fare un cast).
Tutte queste operazioni possono sollevare eccezioni di classe IOException.

Esempio: rettangolo persistente

class Rectangle implements Serializable
{
  int length, width;

  int area()
  { return length * width; }

  // Nota: non devo scrivere funzioni per lettura / scrittura

  public static void main(String[] args)
  {
    Rectangle r = new Rectangle(); r.length = 30; r.width = 77;
    try
    {
      FileOutputStream fos = new FileOutputStream("pippo.txt");
      ObjectOutputStream oos = new ObjectOutputStream(fos);
      oos.writeObject(r);
      oos.close();
      fos.close();
      FileInputStream fis = new FileInputStream("pippo.txt");
      ObjectInputStream ois = new ObjectInputStream(fis);
      r = (PRectangle)ois.readObject();
      ois.close();
      fis.close();
      System.out.println("r.length riletta = " + r.length); 
      System.out.println("r.width riletta = " + r.width); 
    }
    catch(Exception exc) {}
  } 
}