archiviareOggetti

leggere e scrivere informazioni su memoria permanente

Il problema

La nostra squadra di atletica ha bisogno tener conto dei tempi migliori degli atleti che compongono la squadra di staffetta 4x400 e di poter calcolare il tempo totale. L'allenatore vuole che i tempi vengano memorizzati da una sesione di allenamento all'altra.

I programmi che abbiamo scritto finora perdono tutte le informazioni una volta chiusi, vorremmo adesso invece imparare a registrare le informazioni.

Percorsi

Per registrare e leggere informazioni è necessario interagire con in file system: dobbiamo specificare la posizione del file nel sistema di archiviazione.

Sistemi operativi diversi descrivono in maniera diversa la posizione di un file: Windows per separare le cartelle che compongono il percorso usa "\" mentre Unix (ad esempio Linux e MacOS) usa "/", il primo specifica il disco usando una lettera seguita da due punti (es: "C:") mentre il secondo indica un percorso assoluto usando un "/" all'inizio del percorso.

Oltre a questo se trattiamo percorsi assoluti (cioè quelli che iniziano con una lettera su Windows o con uno slash su Unix) che contengono il nome della cartella utente questi cambiano per ciascuno di noi.

Prendiamo per esempio il percorso del file saluti.txt che sta nella cartella documenti di un utente.
per l'utente Matteo su Windows è C:\Users\matteo\Documents\saluti.txt
per l'utente Luca su Linux è /home/luca/Documenti/saluti.txt

Attenzione a due cose:

Stream

Java ci mette a disposizione molti oggetti nel pacchetto java.io per poter svolgere queste operazioni, inizieremo utilizzando gli Stream che sono oggetti che permettono di lavorare con dei flussi di byte, ne esistono molti e si possono distinguere dal loro nome due grandi categorie: gli InputStream (che servono per leggere) e gli OutputStream (che servono per scrivere).
Noi useremo due oggetti per scrivere:

FileOutputStream
invia un flusso di dati direttamente su un disco, può essere creato fornendogli il nome del file
ObjectOutputStream
è in grado di scrivere oggetti e i tipi base su qualsiasi OutputStream

In effetti non possiamo usare un ObjectOutputStream per scrivere su disco direttamente ma dobbiamo creare una specie di catena: L'ObjectOutputStream scrive i dati su un FileOutputStream che scrive i dati su disco.

Una volta che si sono completate le operazioni di output (e anche di input) questi oggetti vanno chiusi utilizzando il metodo close() sia per liberare risorse di sistema che per far si che eventuali operazioni non ancora completate vengano completate (ad esempio se l'oggetto usa dei buffer).

La classe chiamata ObjectInputStream serve a:

scrivere oggetti su discoin questo caso avrebbe avuto "Output" nel nome leggere oggetti da uno Stream

Qui sotto è riportato il programma (da completare) che salva le informazioni e successivamente le va a leggere.

public class SalvaCarica extends Application{
    TextField cNome1 = new TextField("");
    TextField cNome2 = new TextField("");
    TextField cTempo1 = new TextField("");
    TextField cTempo2 = new TextField("");
    TextField cTotale = new TextField("");
    Label eErrore = new Label();
    
    @Override
    public void start(Stage primaryStage){
        Label eNome = new Label("nome");
        Label eTempo = new Label("tempo");
        Label e1 = new Label("frazionista 1:");
        Label e2 = new Label("frazionista 2:");
        Button pCalcola = new Button("calcola");
        Button pSalva = new Button("salva");
        Button pCarica = new Button("carica");
        GridPane griglia = new GridPane();
        
        griglia.add(eNome,   1, 0);
        griglia.add(eTempo,  2, 0);
        griglia.add(e1,      0, 1);
        griglia.add(cNome1,  1, 1);
        griglia.add(cTempo1, 2, 1);
        griglia.add(e2,      0, 2);
        griglia.add(cNome2,  1, 2);
        griglia.add(cTempo2, 2, 2);
        griglia.add(pCalcola, 1, 5);
        griglia.add(cTotale, 2, 5);
        griglia.add(pSalva, 1, 6);
        griglia.add(pCarica, 1, 7);
        griglia.add(eErrore, 0, 8, 3, 1);
        
        Scene s = new Scene(griglia);
        primaryStage.setScene(s);
        primaryStage.setTitle("reticolato");
        primaryStage.show();
        
        pSalva.setOnAction(e->salva());
        pCarica.setOnAction(e->carica());
    }
    
    private void salva() {
        String nome;
        double tempo;
        try {
            FileOutputStream uscitaFile = new FileOutputStream("/Volumes/ramdisk/o.file");
            ObjectOutputStream uscita = new ObjectOutputStream(uscitaFile);
            nome = cNome1.getText();
            tempo = Double.parseDouble(cTempo1.getText());
            uscita.writeObject(nome);
            uscita.writeDouble(tempo);
            uscita.close();
            uscitaFile.close();
        } catch (FileNotFoundException e) {
            eErrore.setText("Non trovo il file ("+e.getMessage()+")");
        } catch (IOException e) {
            eErrore.setText("Problemi di input output ("+e.getMessage()+")");
        }
    }
    
    private void carica() {
        String nome;
        double tempo;
        try { 
            FileInputStream ingressoFile = new FileInputStream("/Volumes/ramdisk/o.file");
            ObjectInputStream ingresso = new ObjectInputStream(ingressoFile);
            nome = (String) ingresso.readObject();
            tempo = ingresso.readDouble();
            cNome1.setText(nome);
            cTempo1.setText(""+tempo);
            ingresso.close();
            ingressoFile.close();
        } catch (FileNotFoundException e) {
            eErrore.setText("Non trovo il file ("+e.getMessage()+")");
        } catch (IOException e) {
            eErrore.setText("Problemi di input output ("+e.getMessage()+")");
        } catch (ClassNotFoundException e) {
            eErrore.setText("Classe non trovata ("+e.getMessage()+")");
        }
    }

    public static void main(String x[]){ launch(x); }
}

Vediamo in dettaglio la parte di caricamento delle informazioni (il salvataggio è praticamente speculare)

try {
    FileInputStream ingressoFile = new FileInputStream("/Volumes/ramdisk/o.file");
    ObjectInputStream ingresso = new ObjectInputStream(ingressoFile);
    nome = (String) ingresso.readObject();
    tempo = ingresso.readDouble();
    cNome1.setText(nome);
    cTempo1.setText(""+tempo);
    ingresso.close();
    ingressoFile.close();
}[...]

Questo è uno schema piuttosto usuale: creo gli oggetti che fanno I/O, leggo (o scrivo) e poi chiudo, il tutto racchiuso in un blocco try-catch per intercettare eventuali eccezioni. È talmente usuale che possiamo usare una scorciatoia chiamata try with resources che ci consente di evitare di scrivere (o di dimenticare!) il close: all'uscita del try le risorse aperte nel try stesso verranno chiuse in automatico da java.

try ( 
    FileInputStream ingressoFile = new FileInputStream("/Volumes/ramdisk/o.file");
    ObjectInputStream ingresso = new ObjectInputStream(ingressoFile);
){
    nome = (String) ingresso.readObject();
    tempo = ingresso.readDouble();
    cNome1.setText(nome);
    cTempo1.setText(""+tempo);
}[...]

La riga nome = (String) ingresso.readObject(); legge un oggetto (Object) da disco e lo converte in una stringa (cioè fa un cast perché io sò di aver salvato un oggetto di tipo String), per i tipi base questo non serve perché ho diversi metodi (come readDouble()) che mi consentono di leggere direttamente il tipo che mi interessa.

Completare il programma in modo che salvi i dati di entrambe gli atleti.

In realtà una staffetta è composta da 4 atleti, completare il programma in modo da poter gestire 4 atleti.