Problemi per le vacanze di Natale 2017
L'obiettivo di questa esercitazione è duplice: da un lato, acquisirete dimestichezza con alcune classi di I/O standard in Java;
dall'altro, vi troverete a dover gestire eccezioni (controllate) e dovrete ogni volta stabilire se farlo mediante il costrutto try/catch
o sollevando a vostra volta l'eccezione al chiamante (mediante la clausola throws).
- [Uso di stream di dati] L'obiettivo di questo esercizio è di scrivere su un file, in formato binario,
dei dati. Per farlo useremo le classi FileOutputStream e FileInputStream (che, rispettivamente,
servono per scrivere e leggere file visti come flussi di byte) e gli adattatori DataOutputStream
e DataInputStream (che permettono di scrivere e leggere tutti i tipi primitivi e anche gli oggetti
serializzabili).
Scrivete un programma (costituito dal solo metodo main) che operi come segue:
- Aprite un nuovo file (in scrittura) e avvolgetelo in un DataOutputStream. Abbiate cura di
fornire il pathname completo del file in modo da essere sicuri di dove il file verrà creato.
- Su questo DataOutputStream scrivete, ad esempio, in questo ordine: l'intero 5, il booleano true,
il numero double Math.PI, l'intero 14 e l'intero 27.
- Chiudete lo stream ed eseguite il programma. Potrete verificare che:
- il file è stato creato;
- la lunghezza in byte del file è quella che vi aspettate? (Ricordatevi che un intero occupa 4 byte, un booleano
1 byte e un double 8 byte);
- se provate ad aprire il file con un editor (o a fare un cat del file), che cosa vedete?
Ora scrivete un altro programma (costituito dal solo metodo main) che operi come segue:
- Aprite lo stesso file di prima (in lettura) e avvolgetelo in un DataInputStream.
- Leggete dall'input stream un intero, un booleano, un double e due interi.
- Stampate i dati appena letti (usando System.out.println) e verificate che siano gli stessi che avevate scritto.
- [Apertura in append] Un FileOutputStream si può aprire in append: ciò vuol dire che il
file, se esiste già, non viene cancellato ma i dati che scriverete verranno scritti in fondo a quelli già presenti.
Cercate il costruttore che vi permette di aprire un output stream in append; ora modificate il programma di scrittura
dell'esercizio precedente in modo che, dopo aver chiuso il flusso, lo riapra in append e scriva altri dati a vostro piacimento.
Modificate il programma che legge il file aggiungendo le letture dei dati in più.
- [Uso di un Writer] Scrivete un programma che apra in scrittura un FileOutputStream
e avvolgete l'output stream in un OutputStreamWriter che converte l'output stream (flusso di byte) in un flusso di
caratteri. Notate che il costruttore vuole, oltre all'output stream su cui scrivere, anche la codifica da usare per convertire i
caratteri in byte: usate la codifica "ISO-8859-1".
Ora, usando il metodo write(String s) del writer che avete creato, scrivete sul flusso delle stringhe a vostro
piacimento e chiudete il writer.
Da un terminale provate a fare un cat del file e verificare che cosa avete prodotto.
- [Uso di un PrintWriter] Modificate il programma precedente come segue: dopo aver creato
l'OutputStreamWriter, avvolgetelo in un PrintWriter.
Il PrintWriter ha dei metodi (di nome print e println) che consentono di stampare
qualunque cosa (tipi primitivi e oggetti; per questi ultimi verrà invocato il loro metodo toString)
su un writer. Inoltre c'è un metodo printf che funziona come la funzione omonima del C. Ecco un esempio d'uso
(pw è un PrintWriter):
pw.prinf("Se x=%.2f e y=%.2f allora si ottiene %.4f come risultato!\n", x, y, Math.sqrt(x*x+y*y));
Usate il PrintWriter per stampare sul flusso quel che volete. Verificate il risultato ottenuto facendo
un cat del file.
- [Lettura di un file di testo, riga per riga] Il nostro obiettivo è leggere il file che avete appena scritto,
una riga per volta. Per farlo, create un FileInputStream e avvolgetelo in un InputStreamReader (dovrete
ovviamente specificare la codifica, come prima). Ora avvolgete quest'ultimo in un BufferedReader: quest'ultimo
fornisce un metodo readLine() che legge e restituisce un'intera riga (fino all'a-capo), oppure
null
quando il flusso è finito. Scrivete un ciclo che legga una riga per volta il file fino alla fine (potete usare un ciclo infinito
in cui fate una readLine() e un break non appena quest'ultima restituisce null
), stampando ogni
volta sullo schermo la riga letta. Verificate che quel che ottenete sia quello che avevate visto con il cat.
- [Uso di Scanner] Create un file (come spiegato nell'esercizio sui PrintWriter) e scrivete
sul file, una linea per volta, due interi, un double e un altro intero a vostro piacimento. Controllate il contenuto del file.
Ora create un FileInputStream e avvolgetelo in un InputStreamReader e avvolgete quest'ultimo in uno Scanner: questo
fornisce metodi quali nextInt() e nextDouble() che leggono e restituiscono un intero o un double dal flusso.
Usate questi metodi per leggere due interi, un double e un ultimo intero, e verificate che i dati letti siano quelli che vi aspettate.
- [Comportamenti inattesi di Scanner] Quest'ultimo esercizio serve per mettervi in guardia da un comportamento
inatteso che accade quando usate uno scanner per leggere dati e linee. Modificate il programma dell'esercizio precedente in modo
che dopo il double e prima dell'intero venga stampata una linea contenente una stringa qualsiasi.
Ora modificate analogamente la lettura invocando, dopo il metodo nextDouble e prima dell'ultima chiamata a nextInt, il metodo nextLine
che dovrebbe leggere un'intera riga. Che cosa accade?
La spiegazione è che i metodi nextInt, nextDouble ecc. non consumano mai nessun carattere in più oltre a quelli previsti. Più precisamente,
sono in grado di saltare eventuali spazi prima dell'intero (o del double) da leggere, ma dopo aver letto tale intero/double/ecc. non consumano nessun
altro carattere. Quindi, dopo aver letto il double, il flusso rimasto disponibile inizia con un "a-capo" non consumato (quello che segue immediatamente
il double). Per risolvere il problema, aggiungete una chiamata a nextLine che consumi l'a-capo in più.
- [Curva di Koch (1)] Per questo problema, rispolverate la libreria per la turtlegraphics. Scrivete una classe
Koch che nel costruttore istanzi un campo e una tartaruga e che abbia un metodo disegna(int x) che si limita a disegnare un segmento di lunghezza
data. Scrivete una classe con un metodo main che istanzi la classe Koch e che poi disegni un segmento di lunghezza 10. Quando siete certi che
funzioni, modificate il metodo disegna in modo che invece di disegnare un segmento, disegni una curva come quella chiamata F1 nella figura.
- [Curva di Koch (2)] Ora modificate la classe trasformando il metodo disegna in uno di segnatura
disegna(int liv,int x) che, se liv==0 disegna un segmento di lunghezza x, altrimenti muove la tartaruga come nella figura F1 ma, ogni volta
che la tartaruga dovrebbe disegnare un segmento, invochi invece disegna(liv-1,x).