Input/Output

Progettazione e Sviluppo del Software

C.D.L. Tecnologie dei Sistemi Informatici

Gianluca Aguzzi — gianluca.aguzzi@unibo.it

Angelo Filaseta — angelo.filaseta@unibo.it

versione stampabile

Riconoscimenti

  • Questo materiale è ampiamente basato su quello realizzato dai Prof. Mirko Viroli e Roberto Casadei, che ringrazio.

  • Ogni errore riscontratovi è esclusiva responsabilità degli autori di questo documento.

Outline

Goal della lezione

  • Illustrare le API fornite da Java per l’I/O
  • Descrivere alcune scelte progettuali e pattern
  • Mostrare esempi di applicazione

Argomenti

  • Classi per gestire file
  • Classi per gestire Stream (di input/output)
  • Classi per gestire file di testo
  • Pattern Decorator

Il problema dell’Input/Output

Uno dei problemi fondamentali per un sistema operativo

  • Gestire le comunicazioni fra CPU e dispositivi affacciati sul BUS
  • Console, tastiera, mouse, dischi, rete, sensori, schermo
  • Vi sono varie modalità di interazione possibili
    • sequenziale, random-access, buffered, per carattere/linea/byte/oggetto
  • I sistemi operativi offrono vari meccanismi
    • file, I/O control interface, socket per il networking, video driver

La libreria java.io.*

  • Fornisce i concetti di File e Stream di dati
  • Consente una gestione flessibile dei vari aspetti
  • È estesa con la libreria java.nio
  • È la base di librerie avanzate (networking,..), anche di terze parti (Jackson, Gson, …)
  • I/O con l’utente in ambiente a finestre è realizzato con le GUI

File e loro proprietà

I File

File system

  • Il file system è un modulo del S.O. che gestisce la memoria secondaria
  • Maschera le diversità di dispositivi fisici (HD, CD, DVD, BR, SSD,..)
  • Maschera le diversità di contenuti informativi (testi, filmati, archivi,..)
  • Fornisce meccanismi per fornire prestazioni, concorrenza, robustezza

File

  • Un file system contiene un insieme di file
  • Un file ha un contenuto informativo che si esprime come sequenza di byte
    • interpretabili in vario modo (testi, programmi, strutture dati)
    • potrebbe essere un file virtuale, che mappa un dispositivo
    • un caso particolare è la directory (ossia una tabella di ID di file)
    • può avere specifici diritti di accesso (sola lettura, lettura/scrittura, eseguibilità…)
  • Le directory consentono un’organizzazione gerarchica (ogni file ha un percorso o path)

La classe java.io.File

Usi

  • Serve a identificare un preciso file su file systems
  • Permette di ottenere informazioni varie sul file
  • Permette di effettuare alcune operazioni sull’intero file
    • cancellazione
    • spostamento (rinominazione)
  • Permette di impostare alcune proprietà (se eseguibile, se scrivibile)
  • Permette di ottenere informazioni generali sul file system
  • Permette di creare cartelle
  • $\Rightarrow$ non include operazioni per accedere al suo contenuto, ma vi si potrà agganciare uno stream

Classe java.io.File: pt1

 public class File implements Serializable, Comparable<File> {

    public File(String pathname) {...}
    public File(String parent, String child) {...}
    public File(File parent, String child) {...}
    
    /* -- Path-component accessors -- */
    public String getName() {...}
    public String getParent() {...}
    public File getParentFile() {...}
    public String getPath() {...}

    /* -- Path operations -- */
    public boolean isAbsolute() {...}
    public String getAbsolutePath() {...}
    public File getAbsoluteFile() {...}
    public String getCanonicalPath() throws IOException {...}
    public File getCanonicalFile() throws IOException {...}
    
    /* -- Attribute accessors -- */
    public boolean canRead() {...}
    public boolean canWrite() {...}
    public boolean exists() {...}
    public boolean isDirectory() {...}
    public boolean isFile() {...}
    public boolean isHidden() {...}
    public long lastModified() {...}
    public long length() {...}

Classe java.io.File: pt2

    /* -- File operations -- */

    public boolean createNewFile() throws IOException {...}
    public boolean delete() {...}
    public void deleteOnExit() {...}
    public String[] list() {...}
    public String[] list(FilenameFilter filter) {...}
    public boolean mkdir() {...}
    public boolean renameTo(File dest) {...}
    public boolean setLastModified(long time) {...}
    public boolean setReadOnly() {...}
    public boolean setWritable(boolean writable, boolean ownerOnly) {...}
    public boolean setWritable(boolean writable) {...}
    public boolean setReadable(boolean readable, boolean ownerOnly) {...}
    public boolean setReadable(boolean readable) {...}
    public boolean setExecutable(boolean executable, boolean ownerOnly) {...}
    public boolean setExecutable(boolean executable) {...}
    public boolean canExecute() {...}
    
    /* -- Disk usage -- */
    public long getTotalSpace() {...}
    public long getFreeSpace() {...}
    public long getUsableSpace() {...}
}

Esempio di utilizzo di File: cercare file con una certa enstensione dentro una directory

package it.unibo.io.files;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class FileUtil {

    private static final String USER_HOME = System.getProperty("user.home");

    public static List<File> findFilesInHomeDirectoryByExtension(final String ext) {
        final var userHome = new File(USER_HOME);
        final var allFiles = userHome.listFiles();
        if (!userHome.isDirectory()) {
            throw new IllegalStateException("The user home is not a directory!");
        }
        final var result = new ArrayList<File>();
        for (final var file: allFiles) {
            if (file.isFile() && file.getName().endsWith("." + ext)) {
                result.add(file);
            }
        }
        return result;
    }
}

Accedere al contenuto di un file

Come fare?

  • Un file ha un contenuto informativo (potenzialmente di grosse dimensioni)
  • Lo si potrebbe leggere (in vari modi)
  • Lo si potrebbe scrivere (in vari modi)
  • Il suo contenuto potrebbe essere interpretabile in vari modi

Alcuni di tali concetti sono condivisi con altri meccanismi

  • Risorse interne al classpath Java
  • Networking e file di rete
  • Archivi su database
  • Depositi di informazione in memoria

Il concetto di input/output-stream è usato come astrazione unificante

Input/OutputStream

Overview sugli InputStream e OutputStream in Java

InputStream e OutputStream

  • Stream = flusso (di dati)
  • Di base, gestiscono flussi binari (di byte) leggibili vs. scrivibili
  • Sono classi astratte (e non interfacce..)
  • Possono essere specializzate da “sottoclassi” e “decorazioni”, tra cui
    • Per diverse sorgenti e destinazioni di informazione
      • su file (File(In|Out)putStream)
      • su memoria (ByteArray(In|Out)putStream)
    • Per tipo di informazione:
      • valori primitivi (Data(In|Out)putStream)
      • interi oggetti Java (Object(In|Out)putStream)
      • archivi compressi (Zip(In|Out)putStream)

Tipicamente usati per alimentare altre classi

  • File di testo (Reader, Writer, e specializzazioni)
  • Librerie avanzate comunemente usate per l’accesso al file system tipicamente hanno metodi che accettano (In|Out)putStream

La classe java.io.InputStream

public abstract class InputStream implements Closeable {
    // Reads the next byte (0 to 255, -1 is end-of-stream)
    public abstract int read() throws IOException;

    public int read(byte b[]) throws IOException {...}

    public int read(byte b[], int off, int len) throws IOException {...}

    public long skip(long n) throws IOException {...}

    public int available() throws IOException {...}

    public void close() throws IOException {...}

    public synchronized void mark(int readlimit) {...}

    public synchronized void reset() throws IOException {...}

    public boolean markSupported() {...}
}

FileInputStream e ByteArrayInputStream

InputStream
read() : int
read(byte b[]) : int
read(byte b[], int off, int len) : int
skip(long n) : long
available() : int
close() : void
FileInputStream
getChannel() : FileChannel
getFD() : FileDescriptor
ByteArrayInputStream
readAllBytes() : byte[]
readNBytes(byte[] b, int off, int len) : int

Uso di ByteArrayInputStream

ByteArrayInputStream

  • crea un InputStream a partire da un byte[]
package it.unibo.io.files;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

public class UseByteArrayStream {
    public static void main(String[] args) throws IOException {
        final byte[] b = new byte[] { 10, 20, -1, 40, -58 };
        final InputStream in = new ByteArrayInputStream(b);
        try {
            int c = in.read();
            while (c != -1) {
                System.out.println(c);
                c = in.read();
            }
            System.out.println("End of stream");
        } finally { // assicura la chiusura anche con eccezioni
            in.close(); // problema: può tirare eccezione!
        }
    }
}

Il costrutto try-with-resources

  • vuole la creazione di un java.lang.AutoCloseable come primo argomento
  • ne assicura la chiusura
  • si possono opzionalmente aggiungere delle catch di eccezioni
  • è il modo preferibile di utilizzare risorse in Java!
package it.unibo.io.files;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

public class UseTryWithResources {
    public static void main(String[] args) throws IOException {
        final byte[] b = new byte[] { 10, 20, 30, 40, 50 };
        try (final InputStream in = new ByteArrayInputStream(b)) {
            int c = in.read();
            while (c != -1) { // C-style
                System.out.println(c);
                c = in.read();
            }
            System.out.println("End of stream");
        }
    }
}

Esempio StreamDumper

package it.unibo.io.files;

import java.io.IOException;
import java.io.InputStream;

public class StreamDumper {
    // rendo inaccessibile il costruttore
    private StreamDumper() {}

    public static void dump(final InputStream input) throws IOException {
        for (int value = input.read(); value != -1; value = input.read()) {
            System.out.print(value + "\t");
        }
    }
}

UseStreamDumper – uso uniforme di vari InputStream

package it.unibo.io.files;

import ...

public class UseStreamDumper {
    public static void main(String[] args) throws IOException {
        final byte[] bytes = new byte[]{ 10, 20, 30 };
        // Cerco un file txt nella home folder dell'utente
        final var txtFiles = FileUtil.findFilesInHomeDirectoryByExtension("txt");
        if (txtFiles.isEmpty()) {
            throw new IllegalStateException("No txt files on the user home directory!");
        }
        final var file = txtFiles.get(0);
        try(
            final InputStream input = new ByteArrayInputStream(bytes);
            final InputStream input2 = new FileInputStream(file);
        ) {
            System.out.println(txtFiles.get(0).getAbsolutePath());
            System.out.println("First stream (from memory):");
            StreamDumper.dump(input);
            System.out.println("\nSecond stream (from file " + file.getPath() + ":");
            StreamDumper.dump(input2);
            System.out.println();
        }
    }
}

La classe java.io.OutputStream

public abstract class OutputStream implements Closeable, Flushable{
    /**
     * ...
     * The byte to be written is the eight low-order bits of the argument <code>b</code>.
     * The 24 high-order bits of <code>b</code> are ignored.
     *
     * 0x000000FF writes byte 0xFF (255)
     * 0xFFFFFFFF writes byte 0xFF (255)
     * 0x12345678 writes byte 0x78 (120)
     */
    public abstract void write(int b) throws IOException;

    public void write(byte b[]) throws IOException {...}

    public void write(byte b[], int off, int len) throws IOException {...}

    public void flush() throws IOException {...}

    public void close() throws IOException {...}
}

Stream di uscita – Duale all’InputStream

  • Esistono anche le analoghe specializzazioni ByteArrayOutputStream e FileOutputStream

UseOutputStream

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;

public class UseOutputStream {
    public static void main(final String[] args) throws IOException {
        try (
            final OutputStream output = new FileOutputStream(UseFile.FILE_NAME);
        ) {
            // Aggiungo byte random
            final Random r = new Random();
            for (int i = 0; i < 100; i++) {
                output.write(r.nextInt(256));
            }
            // Aggiungo un array di byte
            final byte[] b = new byte[] { 10, 20, 30, 40 };
            for (int i = 0; i < 10; i++) {
                output.write(b);
            }
        }
    }
}

Solo byte?

  • Vorremmo poter leggere e scrivere formati dati diversi
    • Ad esempio testi
    • Oppure avere dei flussi di dati che vengono compressi

Il concetto di decoratore

Per ottenere questo risultato, si “circonda” l’(In|Out)putStream con un altro, che si occupa di aggiungere funzionalità:

  • tradurre un certo tipo di dati (ad esempio testo) da/a sequenza di byte
  • modificare il formato della sequenza di byte (ad esempio, (de)comprimendo il flusso in formato zip)
  • lasciando inalterato il flusso, modificarne la gestione (ad esempio, registrando statistiche o aggiungendo un buffer per migliorare le performance)

Ad sempio, si definisce Buffered(In|Out)putStream che estende (In|Out)putStream aggiungendo un buffer intermedio

  • Buffered(In|Out)putStream fa da wrapper per un altro (In|Out)putStream al quale delega le operazioni
  • Buffered(In|Out)putStream è quindi un decoratore per (In|Out)putStream
    • ne modifica il funzionamento senza modificarlo!

Con questa tecnica è possibile decorare qualunque (In|Out)putStream!

Decorazione, in generale

Decorazione, il caso di BufferedInputStream

Decoratori

Pro e contro

  • Sono un mix di polimorfismo e incapsulamento
  • Consentono di comporre funzionalità in modo piuttosto flessibile
  • Danno luogo a più flessibilità rispetto all’ereditarietà
  • Più complicati da usare e comprendere

Con gli (In|Out)putStream, è possibile comporre:

  • Uno stream di sorgente/destinazione dati
  • File(In|Out)putStream, ByteArray(In|Out)putStream
  • Uno (o più) stream di gestione interna: Buffer(In|Out)putStream, ..
  • Uno stream di presentazione dati:
  • PrintStream, formato stringhe testuali
  • DataInputStream, dati grezzi (non visto)
  • ObjectInputStream, interi oggetti (non visto)

Decoratori in azione

Scrittura di testo su file

Scrittura di testo su file in modo bufferizzato

Stesse chiamate lato cliente, basta costruire la catena di decoratori in modo opportuno!

File ed encoding

Il contenuto dei file

Supponiamo utilizziate un FileOutputStream per scrivere in sequenza i numeri da 0 a 20(escluso)

  • Che cosa avete realmente scritto?
  • avete scritto i byte da zero a 19
  • In esadecimale, 0x000102030405060708090A0B0C0D0E0F10111213
  • Non avete scritto il testo 012345678910111213141516171819

I file sono sequenze di byte

Per fare input/output occorre stabilire:

  • Una conversione dalla struttura dati che stiamo manipolando a sequenza di byte (encoding)
  • Una conversione da sequenza di byte a struttura dati (decoding)

Cosa succede se ignoro l’encoding?

public class ReadWriteWithoutEncoding {

    private static final String FILE_NAME = "test-output.txt";

    public static void main(String[] args) throws IOException {
        try (final var out = new FileOutputStream(FILE_NAME)) {
            System.out.println("Writing...");
            for (int b = 0; b < 1000; b++) {
                System.out.print(b + " ");
                out.write(b);
            }
            System.out.println("Done.");
        }
        try (final var in = new FileInputStream(FILE_NAME)) {
            System.out.println("Reading...");
            for (var read = in.read(); read != -1; read = in.read()) {
                System.out.print(read + " ");
            }
            System.out.println("Done.");
        }
        // Quick method to read the file as a text string
        System.out.println("Actual file content:");
        System.out.println(new String(Files.readAllBytes(Path.of(FILE_NAME))));
    }
}

Esecuzione

Writing...
0 1 2 3 4 ... 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 ... Done.
Reading...
0 1 2 3 4 ... 255 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ... Done.
Actual file content:
 
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ
[\]^_`abcdefghijklmnopqrstuvwxyz{|}~����������������������������������������������������
���������������������������������������������������������������������������� 
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ
[\]^_`abcdefghijklmnopqrstuvwxyz{|}~����������������������������������������������������
���������������������������������������������������������������������������� 
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ
[\]^_`abcdefghijklmnopqrstuvwxyz{|}~����������������������������������������������������
���������������������������������������������������������������������������� 
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ
[\]^_`abcdefghijklmnopqrstuvwxyz{|}~����������������������������������������������������
����������������������������������������������������

Non avete scritto testo!

Avete scritto il valore in byte della parte meno significativa degli interi che avete passato

  • Per questo in lettura “riparte” da 0 dopo 255 (massimo valore di un byte senza segno)
  • Per questo, se letto come stringa, vi trovate un testo non comprensibile!
    • Non avete scritto caratteri di testo, avete scritto bytes!
    • E nessuno ha spiegato come convertire da/a testo

Conversione da byte a testo

Per interpretare i byte come sequenza di caratteri, occorre usare una tabella di conversione (character encoding, Charset in Java)

Nota

  • si applica anche importante anche alla rappresentazione in memoria dei caratteri!
  • La memoria contiene byte, se vediamo delle stringhe di testo è perchè avviene un encoding

Formati notevoli di character encoding

  • ASCII (RFC 20)
    • l’encoding che usa il linguaggio C per i char
    • 1 byte per carattere (massimo 256 caratteri)
  • UTF-8 (RFC 3629)
    • Standard di fatto sul web e per il codice, encoding da usare per sorgenti Java
    • da 1 a 4 byte per carattere
    • Codifica 1.112.064 simboli
  • UTF-16 (RFC 2781)
    • Encoding in memoria delle String in Java
    • 2 o 4 byte per carattere
    • Codifica 1.112.064 simboli
  • ISO/IEC 8859-1:1998 o semplicemente ISO Latin
    • Encoding di default del testo in Windows

Stream di caratteri: Reader e Writer

(In|Out)putStreams sono pensati per gestire direttamente byte, non caratteri.

Molto spesso abbiamo però bisogno di leggere o scrivere stringhe di testo!

Reader e Writer sono pensati per gestire “stream di caratteri” invece che stream di byte

  • Noi ci interesseremo solo di Reader: per la scrittura in formato carattere possiamo infatti usare PrintStream
    • (che, purtroppo, non ha un omologo stream di input)

Esempio con InputStreamReader

public class UseBufferedPrintStream {

    private static final File FILE = new File("test-output.txt");

    public static void main(String[] args) throws IOException {
        // string -> buffer ->  compress -> file
        try (final PrintStream fileOut = new PrintStream(
            new BufferedOutputStream(new FileOutputStream(FILE)), // better performance
            false,
            StandardCharsets.UTF_8 // Always specify how to translate text into bytes and viceversa
        )) {
            fileOut.println("Hey, this is my first printed line in a text file!");
            fileOut.println("Here is another text line");
        }
        System.out.println("Writing completed!");
        // To read, we must reverse the operations:
        // file -> decompress -> string (optionally, with a buffer)
        try (
            final var inputReader = new BufferedReader(
                new InputStreamReader(new FileInputStream(FILE), StandardCharsets.UTF_8)
            )
        ) {
            for (String line = inputReader.readLine(); line != null; line = inputReader.readLine()) {
                System.out.println("Read line: " + line);
            }
        }
    }
}

Utility Files

Nella maggior parte dei casi, vogliamo salvare e caricare i nostri dati scrivendo e leggendo un intero file.

  • Scrivere una stringa di testo in un file
  • Leggere un file di testo riga per riga
  • Scrivere un binario che abbiamo convertito in byte[] (ad esempio un’immagine)

La classe Files contiene diversi metodi convenienti per leggere e scrivere file con poco codice:

  • Paths.get(String) ottiene un oggetto di tipo Path da un nome
    • simile al costruttore di File File(String)
  • Files.write(Path, byte[]) scrive un array di byte dentro il Path
  • Files.write(Path, Iterable<? extends Charsequence>, Charset) scrive una collezione di stringhe dentro il Path, traducendo le stringhe in byte usando il Charset fornito
  • Files.writeString(Path, String, Charset) scrive una collezione di stringhe dentro il Path, traducendo le stringhe in byte usando il Charset fornito
  • byte[] Files.readAllBytes(Path) legge il contenuto di Path e lo restituisce come byte[]
  • List<String> Files.readAllLines(Path, Charsets) legge il contenuto di Path e lo restituisce come List<String>, una stringa per ogni linea, interpretando i byte del file come testo usando il Charset fornito
  • String Files.readString(Path, Charsets) legge il contenuto di Path e lo restituisce come singola String, interpretando i byte del file come testo usando il Charset fornito

Esempio d’uso di Files

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;

public class UseFilesUtilities {
    public static void main(final String[] args) throws IOException {
        final var bytes = new byte[] { 1, 2, -128, 99 };
        final Path binary = Paths.get("binary-file.dat");
        Files.write(binary, bytes);
        System.out.println("Read bytes: " + Arrays.toString(Files.readAllBytes(binary)));
        final List<String> dante = List.of(
            "Tanto gentile e tanto onesta pare",
            "la donna mia, quand'ella altrui saluta",
            "ch'ogne lingua deven tremando muta"
        );
        final Path text = Paths.get("text-file.txt");
        Files.write(text, dante, StandardCharsets.UTF_8); // Always specify the Charset!
        System.out.println("Read all file lines:\n" + Files.readAllLines(text, StandardCharsets.UTF_8));
        System.out.println("Read the file as string:\n" + Files.readString(text, StandardCharsets.UTF_8));
        Files.writeString(text, "this is\na multiline string!", StandardCharsets.UTF_8);
        System.out.println("Read all file lines:\n" + Files.readAllLines(text, StandardCharsets.UTF_8));
        System.out.println("Read the file as string:\n" + Files.readString(text, StandardCharsets.UTF_8));
    }
}

Caricamento da classpath

Organizzazione di un progetto Gradle con risorse

├── src
│   ├── main
│   │   ├── java
│   │   └── resources
│   └── test
│       ├── java
│       └── resources
├── build.gradle.kts
└── settings.gradle.kts
  • Le cartelle src/[main|test]/resources contengono le risorse del progetto opportunamente organizzate
    • Per risorse si intendono icone, file di testo, video, immagini, modelli 3D e qualunque cosa sia necessaria al corretto funzionamento del programma ma non sia una libreria o un file sorgente.
  • Il contenuto di resources diventa parte del classpath!
    • viene anche copiato nel file Jar finale
    • $\Rightarrow$ dobbiamo imparare a caricare quelle risorse direttamente dal jar
    • Potremo così dare solo il nostro jar dell’applicazione, senza ulteriori risorse

Risorse caricate dal classpath

  • Abbiamo visto finora il classpath come l’insieme dei percorsi dove la virtual machine va a cercare le classi da caricare

    • Come abbiamo visto usando l’opzione -cp di java e javac, il classpath può contenere indifferentemente dei path o dei JAR (o anche degli zip)
  • Esso includerà tipicamente anche le risorse del progetto, i JAR delle dipendenze importate, etc.

  • Come possiamo accedere a queste risorse in modo uniforme?

    • Ossia caricarle sia che si trovino sul file system, sia che si trovino nel JAR eseguibile, sia che vengano incluse in un JAR di risorse separato.
  • Java fornisce un’utilità per caricare risorse dal classpath

    • Approccio location-independent: non importa dove il codice venga eseguito fin tanto che l’ambiente viene correttamente impostato per trovare le risorse.

ClassLoader.getSystemResource(AsStream)(String)

public abstract class ClassLoader {
  public static ClassLoader getSystemClassLoader();
  public static URL getSystemResource(String name);
  public static InputStream getSystemResourceAsStream(String name);
  public URL getResource(String name);
  // ...
  • Un class loader (istanza di ClassLoader) è un’oggetto responsabile del caricamento di classi e risorse
    • ogni class loader ha un class loader padre, per sfruttare un meccanismo di delega
    • il parent di default è il system class loader che carica classi e risorse dal classpath
  • Una risorsa di sistema (system resource) è una risorsa “built-in” del sistema software, oppure disponibile nel sistema host (ad es. nel filesystem locale)
    • Per esempio, l’implementazione di base ricerca nel CLASSPATH
  • L’argomento di getSystemResource e getSystemResourceAsStream è il nome di una risorsa (non un percorso del filesystem!), che è una stringa separata da / che identifica la risorsa
    • L’interpretazione del nome della risorsa dipende dall’implementazione
    • Il system class loader usa il nome come un path per cercare la risorsa a partire dalle entry del classpath
  • ClassLoader.getSystemResource() equivale a ClassLoader.getSystemClassLoader().getResource()

Risorse caricate dal classpath – Esempi

Caricamento di File

final InputStream in = ClassLoader.getSystemResourceAsStream("/settings/settings");
final BufferedReader br = new BufferedReader(new InputStreamReader(in));
final String line = br.readLine();
in.close();

Caricamento di Immagini

final URL imgURL = ClassLoader.getSystemResource("/images/gandalf.jpg");
final ImageIcon icon = new ImageIcon(imgURL);
final JLabel lab1 = new JLabel(icon);

Progetto di esempio: https://github.com/unibo-oop/example-with-get-resources

Installazione delle impostazioni per-utente

Motivazione

Spesso un software ha necessità di caricare al primo avvio delle impostazioni di default, quindi lasciare l’utente libero di modificarle e, se avviato successivamente caricare quelle scelte dall’utente. In caso di sistema multiutente, le impostazioni saranno diverse per ciascuno.

Strategia

  • Si sceglie una cartella nella home folder dell’utente dove salvare le impostazioni.
    • È norma consolidata creare una cartella .nomeprogramma.
  • Al primo avvio, si verifica se tale cartella esista e se contenga i file di configurazione previsti.
    • Se non è presente, o se non sono presenti e leggibili alcuni i file, si procede a caricare nella cartella di destinazione i file di default dal JAR usando getResource().

Standard per la serializzazione di oggetti

Formati standard per la serializzazione di oggetti

Per scrivere oggetti (a parte le String), abbiamo bisogno di convertire gli oggetti in byte[] o in String (serializzare), ossia nei formati che sappiamo scrivere.

Allo stesso modo, sapendo leggere solo byte[] oppure String, dovremo avere modo di deserializzare questi flussi in oggetti

Esistono sistemi di conversione sia binaria (oggetto $\xrightarrow{\text{serializzazione}}$ byte[] $\xrightarrow{\text{deserializzazione}}$ oggetto)

  • ad esempio, ProtocolBuffers

che testuale (oggetto $\xrightarrow{\text{serializzazione}}$ String $\xrightarrow{\text{deserializzazione}}$ oggetto)

  • JavaScript Object Notation – JSON (RFC 7159)
    • Nato in seno a JavaScript (che non c’entra nulla con Java)
    • Molto usato in ambito web
    • Librerie Java Jackson e Guava
  • YAML Ain’t Markup Language – YAML
    • Superset di JSON dalla versione 1.2
    • Supporto per funzioni avanzate (e.g. anchoring)
    • Molto usato per file di configurazione complessi

Standard testuali sono più facilmente leggibili, e, laddove le prestazioni e lo spazio non siano stringenti, preferiti