Metodologie di Programmazione: Lezione 10

Riccardo Silvestri

Classi locali

A volte serve creare oggetti la cui classe non è importante ma ha importanza solamente l'interfaccia implementata o la classe estesa. Oppure l'oggetto è creato all'interno di un metodo e non è esportato all'esterno. In questi casi definire una classe (top level) o una classe annidata per creare tali oggetti è quasi uno spreco e comunque renderebbe troppo ampia la visibilità della classe. Per offrire una soluzione soddisfacente a queste situazioni Java permette di definire classi in un qualsiasi blocco di codice, come il corpo di un metodo o di un costruttore. Come vedremo questo favorisce anche la scrittura di codice più snello e leggibile.

Classi locali

Classi definite in un blocco di codice sono dette classi interne locali (inner local classes) o semplicemente classi locali. Come si evince dal loro nome, sono classi accessibili solamente all'interno del blocco in cui sono definite. Però le istanze di tali classi sono oggetti normali che possono essere passati come argomenti o ritornati da metodi. Le principali caratteristiche di una classe locale sono le seguenti.

I primi esempi che vedremo riguardano situazioni in cui una classe locale serve per creare un oggetto che è usato esclusivamente all'interno del metodo.

Classi locali: memoizzazione

Vogliamo definire un metodo per calcolare i coefficienti binomiali2. Un modo molto semplice di calcolare il coefficiente binomiale C(n, k) è tramite la formula ricorsiva:

C(n, k) = C(n − 1, k) + C(n − 1, k − 1)

La spiegazione è altrettanto semplice: il numero di k-sottoinsiemi di un insieme di n elementi si può ottenere sommando il numero di k-sottoinsiemi dell'insieme meno un elemento x (questo numero è uguale a C(n − 1, k)) e il numero di k-sottoinsiemi che contengono l'elemento x (che è uguale a C(n − 1, k − 1)). I casi base sono C(n, 0) = 1 e C(n, n) = 1. Definiamo quindi un metodo per effettuare tale calcolo in un classe mp.LocalClasses che useremo per esempi sulle classi locali:

package mp;

import java.math.BigInteger;

/** Una classe per fare esempi sulla classi locali */
public class LocalClasses {
    /** Ritorna il coefficiente binomiale n su k. Ovvero, il numero di
     * k-sottoinsiemi di un insieme di n elementi. Calcolo basato sulla formula
     * ricorsiva: C(n, k) = C(n-1, k) + C(n-1, k-1).
     * @param n,k  parametri del coefficiente binomiale
     * @return il coefficiente binomiale n su k */
    public static BigInteger binom(int n, int k) {
        if (k == 0 || k == n) return BigInteger.valueOf(1);
        else return binom(n - 1, k).add(binom(n - 1, k - 1));
    }
}

I coefficienti binomiali anche con parametri n e k relativamente piccoli hanno valori molto grandi, perciò abbiamo usato i BigInteger che rappresentano interi arbitrariamente grandi. Però questa implementazione è piuttosto inefficiente come si può vedere provandola con un metodo di test:

private static void test_binom() {
    Scanner input = new Scanner(System.in);
    while (true) {
        out.print("Test binom: digita n e k: ");
        int n = input.nextInt(), k = input.nextInt();
        if (k > n || k < 0) break;
        out.println(binom(n, k));
    }
}

Già per n = 30, k = 15 il nostro metodo arranca. La ragione è che il numero di chiamate ricorsive cresce esponenzialmente al crescere dei parametri n e k. Per n = 30, k = 15 è precisamente 310235039, cioè più di trecento milioni di chiamate ricorsive. Come accade molto spesso nei calcoli ricorsivi, la maggior parte delle chiamate ricorsive sono ripetute tantissime volte. Ad esempio, nel calcolo di C(30, 15) il calcolo di C(28, 14) è ripetuto due volte e quello di C(27, 14) e di C(27, 13) è ripetuto tre volte, e così via a crescere.

Per evitare la ripetizione di calcoli già fatti, si può usare la tecnica della memoizzazione3 che consiste nel salvare i risultati dei calcoli parziali per poterli usare in seguito senza doverli ricalcolare. Si tratta di usare una tabella c in cui salvare i valori di C(., .) che sono calcolati durante il calcolo di C(n, k). Potremmo usare un campo statico per mantenere la tabella ma non sarebbe una scelta soddisfacente perché tale campo, che è un dettaglio dell'implementazione del metodo binom, sarebbe visibile in tutta la classe. Possiamo invece usare una classe locale definita nel metodo binom:

/** Ritorna il coefficiente binomiale n su k. Ovvero, il numero di
 * k-sottoinsiemi di un insieme di n elementi. Calcolo basato sulla formula
 * ricorsiva: (n, k) = (n-1, k) + (n-1, k-1). La ricorsione è resa efficiente
 * dalla memoizzazione.
 * @param n,k  parametri del coefficiente binomiale
 * @return il coefficiente binomiale n su k */
public static BigInteger binomM(int n, int k) {
    class Binom { // Classe locale per mantenere la tabella di memoizzazione
        Binom(int n, int k) {           // Crea una tabella di memoizzazione
            c = new BigInteger[n+1][k+1];         // sufficientemente grande
        }

        BigInteger compute(int n, int k) {
            if (c[n][k] == null) {    // Se non è già stato calcolato...
                if (k == 0 || k == n) c[n][k] = BigInteger.valueOf(1);
                else c[n][k] = compute(n - 1, k).add(compute(n - 1, k - 1));
            }
            return c[n][k];
        }

        BigInteger[][] c;            // Tabella di memoizzazione
    }
    Binom b = new Binom(n, k);  // Prepara per la ricorsione memoizzata
    return b.compute(n, k);     // Esegue la ricorsione memoizzata
} 

Ora il calcolo di C(30, 15) è notevolmente più veloce, il numero di chiamate ricorsive da più di trecento milioni scende ad appena 451. Con la versione memoizzata possiamo calcolare ad esempio C(1000, 500) che con il metodo binom sarebbe praticamente impossibile (millenni di tempo di calcolo).

Grazie alle classi locali possiamo nascondere i dettagli implementativi del nostro metodo all'interno del metodo stesso. Possiamo anche dare una definizione un po' più semplice, che evita di definire anche il costruttore della classe locale:

/** Implementazione alternativa di {@link mp.LocalClasses#binomM(int,int)} */
public static BigInteger binomM2(int n, int k) {
    class Binom {
        BigInteger compute(int n, int k) {
            if (c[n][k] == null) {
                if (k == 0 || k == n) c[n][k] = BigInteger.valueOf(1);
                else c[n][k] = compute(n - 1, k).add(compute(n - 1, k - 1));
            }
            return c[n][k];
        }

        BigInteger[][] c = new BigInteger[n+1][k+1];
    }
    return new Binom().compute(n, k);
}

In quest'ultima implementazione nell'inizializzazione della tabella c si sono usati i valori dei parametri del metodo n e k. Ciò è ammissibile perché i parametri sono effettivamente final, cioè il loro valore non viene mai modificato. Vedremo fra poco che possiamo dare un'implementazione ancora più compatta di questo metodo.

Classi locali: visita di alberi

Vogliamo scrivere un metodo che prende in input il percorso di una directory e ritorna una stringa che rappresenta l'albero delle directory e file contenuti a qualsiasi livello nella directory di input. Possiamo usare il seguente metodo della classe Files:

Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
Visita l'albero di tutte le directory e file contenuti a qualsiasi livello nella directory start. La visita è depth-first (cioè in profondità, visita prima una directory e poi tutto il suo contenuto) e su ogni file e directory incontrati invoca l'oggetto visitor.

Visitor design pattern    Il metodo walkFileTree usa un altro esempio di design pattern (come template che abbiamo già incontrato) chiamato non a caso visitor. In generale il visitatore, visitor, è un oggetto che visita gli elementi di una struttura eseguendo delle operazioni su di essi ma senza bisogno di conoscere l'implementazione della struttura e senza che l'implementazione della struttura debba essere modificata per accogliere il visitatore. È spesso usato per permettere la visita di alberi in particolare alberi di parsing.

L'oggetto visitor passato al metodo walkFileTree permette di fare qualcosa su ogni file e directory incontrati durante la visita. Nel nostro caso vogliamo usarlo per riportare nella stringa di output la loro rappresentazione. L'oggetto visitor deve implementare l'interfaccia:

FileVisitor<T>
Interfaccia per visitatori di file e directory con i seguenti metodi (astratti).
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
Invocato all'inizio della visita della directory dir prima che il suo contenuto sia visitato.
FileVisitResult postVisitDirectory(T dir, IOException exc)
Invocato alla fine della visita della directory dir, dopo che il suo contenuto è stato visitato.
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
Invocato quando è visitato il file file.
FileVisitResult visitFileFailed(T file, IOException exc)
Invocato se la visita del file file fallisce.
Alcune informazioni circa file e directory sono fornite dal parametro attrs di tipo BasicFileAttributes. Ad esempio attrs.creationTime() è la data di creazione e attrs.size() è la lunghezza in bytes del file. Il valore ritornato dai metodi dell'interfaccia FileVisitor è una costante dell'enum FileVisitResult che comunica al metodo che fa la visita se si vuole continuare la visita, FileVisitResult.CONTINUE, se si vuole saltare la visita di una directory e del suo contenuto, FileVisitResult.SKIP_SUBTREE, o se si vuole terminare, FileVisitResult.TERMINATE. Il package java.nio.file offre anche una classe SimpleFileVisitor<T> con una implementazione di default dei metodi di FileVisitor<T> per facilitare l'implementazione dell'interfaccia nel caso in cui non si voglia implementare tutti e quattro i metodi. Noi la useremo perché, per semplicità, non vogliamo implementare il metodo visitFileFailed. Adesso possiamo definire il nostro metodo che crea una stringa che rappresenta l'albero delle directory e file (lo definiamo nella classe mp.util.Utils perché potrebbe essere utile in futuro):

/** Ritorna una stringa che rappresenta l'albero di directory e file a partire
 * dal percorso specificato.
 * @param root  percorso della directory radice
 * @return una stringa che rappresenta l'albero di directory e file
 * @throws IOException se si verifi qualche errore nell'accesso ai file/dir */
public static String fileTreeToString(Path root) throws IOException {
    // Classe locale che realizza il visitatore di un albero di dir e file
    class Visitor extends SimpleFileVisitor<Path> {
        @Override
        public FileVisitResult preVisitDirectory(Path dir,
                               BasicFileAttributes attrs) throws IOException {
            if (Files.isHidden(dir)) return FileVisitResult.SKIP_SUBTREE;
            s += pre + (pre.isEmpty() ? "" : "---")+dir.getFileName()+"\n";
            pre += "    |";
            return FileVisitResult.CONTINUE;
        }
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                               throws IOException {
            s += pre + "---"+file.getFileName()+" "+attrs.size()+"\n";
            return FileVisitResult.CONTINUE;
        }
        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                               throws IOException {
            pre = pre.substring(0, pre.length() - 5);
            s += pre + "\n";
            return FileVisitResult.CONTINUE;
        }

        String s = "", pre = "";
    }
    Visitor vis = new Visitor();       // Crea il visitatore e lo passa al
    Files.walkFileTree(root, vis);     // metodo che effettua la visita
    return vis.s;                      // Al termine della visita, in s c'è
}                                      // la rappresentazione dell'albero

La classe locale Visitor estende la classe SimpleFileVisitor<Path> e ridefinisce tre dei quattro metodi. La stringa che alla fine conterrà la rappresentazione dell'albero è mantenuta nel campo s dell'oggetto. Inoltre dobbiamo anche usare e continuamente aggiornare una stringa pre per l'indentazione dei nomi dei file/dir. Si osservi che pre è aumentato di " |" ogni volta che inizia la visita di una directory in preVisitDirectory ed è riportato indietro quando la visita della directory finisce in postVisitDirectory. In preVisitDirectory saltiamo la visita di una directory e del suo contenuto se è nascosta, cioè il metodo Files.isHidden ritorna true. Definiamo un semplice metodo di prova:

private static void test_fileTreeToString() {
    Scanner input = new Scanner(System.in);
    out.println("Test fileTreeToString: Digita un pathname: ");
    String pathname = input.nextLine();
    Path root = Paths.get(pathname).toAbsolutePath();
    try {
        out.println(fileTreeToString(root));
    } catch(IOException e) { out.println(e); }
}

Se usato rispetto al project MetProg2 con il percorso relativo alla working directory otteniamo:

MetProg2
    |---files
    |    |---alice_it_latin1.txt 169414
    |    |---alice_it_latin1_sample.txt 1343
    |    |---alice_it_utf8.txt 171262
    |    |---alice_it_utf8_sample.txt 1362
    |
    |---out
    |    |---mp
    |    |    |---app
    |    |    |    |---Checker.class 409
    |    |    |
    |    |    |---AziendaApp$1.class 184
    |    |    |---AziendaApp$CheckIndirizzo.class 784
    |    |    |---AziendaApp$CheckNomeCognome.class 931
    |    |    |---AziendaApp$CheckTelefono.class 778
    |    |    |---AziendaApp.class 4301
    |    |    |---Dipendente$1.class 184
    |    |    |---Dipendente$Contatti.class 1019
    |    |    |---Dipendente.class 3217
    |    |    |---Dirigente.class 961
    |    |    |---DPair.class 1150
    |    |    |---Generic.class 5071
    |    |    |---LPoint.class 1167
    |    |    |---MathApp.class 1479
    |    |    |---Pair.class 1101
    |    |    |---Pair2.class 497
    |    |    |---tapp
    |    |    |    |---MenuApp.class 1498
    |    |    |
    |    |    |---TestFiles.class 4767
    |    |    |---Tests.class 6921
    |    |    |---util
    |    |    |    |---HTree.class 3085
    |    |    |    |---Utils$1.class 735
    |    |    |    |---Utils$Align.class 1078
    |    |    |    |---Utils.class 8776
    |    |    |
    |    |
    |
    |---README.md 85
    |---src
    |    |---mp
    |    |    |---app
    |    |    |    |---Checker.java 925
    |    |    |
    |    |    |---AziendaApp.java 4869
    |    |    |---Dipendente.java 4177
    |    |    |---Dirigente.java 1223
    |    |    |---Generic.java 6893
    |    |    |---MathApp.java 1101
    |    |    |---tapp
    |    |    |    |---MenuApp.java 1925
    |    |    |
    |    |    |---TestFiles.java 3816
    |    |    |---Tests.java 7509
    |    |    |---util
    |    |    |    |---HTree.java 2740
    |    |    |    |---Utils.java 10430
    |    |    |
    |    |
    |

Finora abbiamo visto esempi di classi locali per creare oggetti il cui scopo è mantenere dati durante un calcolo o implementare un'interfaccia che serve per usare un altro metodo. Ma gli oggetti creati possono avere una vita più lunga potendo essere ritornati dal metodo affinché siano usati all'esterno di esso.

Classi locali anonime

Negli esempi precedenti il nome della classe locale non è importante perché è usato solamente per creare un unico oggetto (e d'altronde il nome non è visibile al di fuori del metodo in cui la classe locale è definita). Questa situazione accade piuttosto spesso e così Java dà la possibilità di usare una classe interna anonima (anonymous inner class) o, più brevemente, classe anonima. Una classe anonima è definita e istanziata simultaneamente in un'unica espressione. La sintassi dell'espressione definitoria-creazionale (che definisce la classe e la istanzia) è leggermente differente a seconda che la classe anonima estenda una classe (astratta o concreta) o implementi un'interfaccia:

new NomeC(eventuali parametri) {  // NomeC, nome della classe che viene estesa
    corpo della classe anonima    // I parametri sono quelli di un costruttore
}                                 // della classe NomeC

new NomeI() {                       // NomeI, nome dell'interfaccia implementata
    corpo della classe anonima      // Implementazione dei metodi di NomeI
}

Dalla sintassi è chiaro perché sono chiamate classi anonime: non c'è il nome della nuova classe. Inoltre, le classi anonime non possono avere costruttori. Nel caso la classe anonima estenda una classe, il costruttore usato è uno di quelli della classe che viene estesa. Nel caso dell'interfaccia, il costruttore è quello di default senza parametri. Per il resto sono uguali alle classi locali. L'espressione definitoria-creazionale può essere usata come una qualsiasi altra espressione creazionale. Ad esempio, se volessimo definire una classe anonima che estende la classe mp.LPoint potremmo scrivere in un qualche metodo:

LPoint p = new LPoint(0, 0, "A") {
    . . .
};

Questo è essenzialmente equivalente (eccetto che per il nome MyLP della classe locale) a:

class MyLP extends LPoint {
    public MyLP() { super(0, 0, "A"); }
    . . .
}
LPoint p = new MyLP();

Un altro esempio, il metodo binomM2, siccome usa una sola istanza della classe locale, può essere riscritto con una classe anonima:

/** Implementazione alternativa di {@link mp.LocalClasses#binomM2(int,int)} */
public static BigInteger binomM3(int n, int k) {
    return new Object() {
        BigInteger compute(int n, int k) {
            if (c[n][k] == null) {
                if (k == 0 || k == n) c[n][k] = BigInteger.valueOf(1);
                else c[n][k] = compute(n - 1, k).add(compute(n - 1, k - 1));
            }
            return c[n][k];
        }

        BigInteger[][] c = new BigInteger[n+1][k+1];
    }.compute(n, k);
}

La classe anonima semplicemente estende Object. Anche il metodo fileTreeToString potrebbe essere riscritto tramite classe anonima dato che usa una sola istanza della classe locale.

Classi anonime per factory methods

Le classi anonime sono spesso usate per implementare un'interfaccia. Questo accade frequentemente quando si vuole definire un cosiddetto factory method, cioè un metodo che fabbrica oggetti, tipicamente oggetti che implementano una certa interfaccia.

Factory design pattern    Questo è un altro esempio di design pattern, chiamato factory, cioè fabbrica di oggetti. È usato quando si devono creare oggetti che implementano un'interfaccia ma la classe degli oggetti creati non è importante. In questo modo la classe degli oggetti non è inultimente accoppiata con l'interfaccia che deve essere implementata, dando così maggiore libertà di scelta nell'implementazione, cioè libertà di scegliere la classe degli oggetti. È usato in tantissimi contesti e in particolare è molto usato nella libreria di Java.

La nostra interfaccia mp.app.Checker può essere migliorata aggiungendo metodi statici che fabbricano Checker di uso comune. Aggiungiamo allora un factory method che ritorna un Checker per controllare che i caratteri della stringa appartengano a un insieme specificato

public interface Checker {
    . . .

    /** Ritorna un Checker che controlla che la stringa non sia {@code null}
     * e che i suoi caratteri siano in {@code chars} o, se {@code letters} è
     * {@code true}, che siano lettere.
     * @param chars  caratteri permessi
     * @param letters  se {@code true}, sono permesse anche le lettere
     * @return un Checker che controlla che i caratteri siano quelli dati */
    static Checker getCheckChars(String chars, boolean letters) {
        return new Checker() {
            @Override
            public String valid(String s) {
                if (s == null) return "Non può essere null";
                for (char c : s.toCharArray())
                    if (chars.indexOf(c) < 0 && 
                       (!letters || !Character.isLetter(c)))
                        return "Il carattere '"+c+"' non è valido";
                return null;
            }
        };
    }
}

Si osservi che l'oggetto Checker ritornato usa i valori dei parametri chars e letters. Tali valori sono costanti, cioè non possono cambiare, altrimenti la classe anonima non avrebbe potuto usarli. Quando una classe locale (anonima o non) usa i valori di variabili locali o parametri del metodo in cui è definita si dice che cattura i loro valori. Per essere più precisi è l'oggetto istanza delle classe locale che cattura tali valori.

Definiamo anche un altro factory method in Checker per controllare la lunghezza:

/** Ritorna un Checker che controlla che la lunghezza della stringa sia
 * compresa tra i limiti specificati.
 * @param min,max  minima e massima lunghezza
 * @return un Checker che controlla che la lunghezza sia nei limiti dati */
static Checker getCheckLen(int min, int max) {
    return new Checker() {
        @Override
        public String valid(String s) {
            if (s == null) return "Non può essere null";
            int len = s.length();
            if (len < min || len > max)
                return "La lunghezza deve essere tra "+min+" e "+max;
            return null;
        }
    };
}

Ora vogliamo definire un metodo che ci permette di combinare più Checker, cioè che ritorna un Checker che esegue i controlli relativi a due o più Checker specificati.

/** Ritorna un Checker che controlla che la stringa passi tutti i controlli
 * dei Checker specificati. Il metodo valid del Checker ritornato, se la
 * stringa non passa tutti i controlli, ritorna l'errore del primo Checker
 * insoddisfatto.
 * @param cc  i Checker che eseguono i controlli
 * @return un Checker che controlla che tutti i Checker siano soddisfatti */
static Checker getCheckAll(Checker...cc) {
    return new Checker() {
        @Override
        public String valid(String s) {
            for (Checker c : cc) {
                String r = c.valid(s);
                if (r != null) return r;
            }
            return null;
        }
    };
}

Così se vogliamo un Checker che controlla sia che i caratteri sono lettere sia che la lunghezza non supera 8, possiamo crearlo molto semplicemente

import mp.app.Checker;
import static mp.app.Checker.*;
. . .
    Checker checkCL = getCheckAll(getCheckChars("", true), getCheckLen(0, 8));

Nella libreria di Java ci sono relativamente poche interfacce che hanno factory methods perché solamente a partire dalla recente versione 8 di Java un'interfaccia può avere metodi statici. Questa è, ad esempio, la ragione per cui è stata introdotta la classe Collections invece di aggiungere i metodi di Collections (che sono tutti statici) all'interfaccia Collection.

In una prossima lezione vedremo che quando si deve implementare un'interfaccia che ha un solo metodo (astratto), come Checker, Java mette a disposizione una sintassi più snella e conveniente di quella delle classi anonime.

Esercizi

[Stirling]    I numeri di Stirling del secondo tipo sono denominati S(n, k) e sono il numero di modi di partizionare un insieme di n elementi in k sottoinsiemi non vuoti. Tali numeri soddisfano la seguente formula ricorsiva:

S(n, k) = kS(n − 1, k) + S(n − 1, k − 1)

con i casi base S(0, 0) = 1 e S(n, 0) = 0 per n > 0. Definire un metodo BigInteger stirling(int n, int k) che calcola S(n, k). Usare la formula ricorsiva e la memoizzazione per rendere efficiente il calcolo. Per maggiori informazioni sui numeri di Stirling si veda Stirling Numbers.

[FileTreeToString1]    Definire una versione più versatile del metodo fileTreeToString che permette di personalizzare le stringhe "---" e " |".

[FileTreeToString2]    Definire una versione del metodo fileTreeToString che prende in input anche un oggetto di tipo BiPredicate<Path,BasicFileAttributes> il quale deve essere usato per decidere se la directory o il file incontrato durante la visita deve essere rappresentato o meno nella stringa di output. Siccome il nuovo metodo è così versatile, deve di default anche includere le directory nascoste. Mettere alla prova il metodo, ad esempio, producendo l'albero che comprende solamente le directory e file con una data di creazione posteriore a una certa data.

[CopyFileTree]    Definire il metodo void copyFileTree(Path src, Path dst) throws IOException che copia tutti i file e directory contenuti a qualsiasi livello in src nella nuova destinazione dst. Il metodo non deve seguire i link simbolici, li deve solamente copiare.
Suggerimento: usare il metodo Files.walkFileTree implementando opportunamente l'interfaccia FileVisitor<Path> usando il metodo Path copy(Path source, Path target, CopyOption... options) per copiare file e dir.

[CopyFileTree+]    Migliorare il metodo dell'esercizio precedente in modo che prenda in input anche un oggetto di tipo BiPredicate<Path,BasicFileAttributes> per determinare quali directory e file devono essere copiati.

[FileTreeToHTree]    Definire un metodo HTree<Path> fileTreeToHTree(Path root) che ritorna un HTree<Path> che rappresenta l'albero di file e directory contenuto in root.
Suggerimento: usare il metodo Files.walkFileTree implementando opportunamente l'interfaccia FileVisitor<Path>. Potrebbe essere necessario usare uno stack per mantenere il cammino di directory dalla radice fino al file/dir corrente.

[CheckNum]    Aggiungere all'interfaccia Checker il metodo statico Checker getCheckNum(int...radices) che ritorna un Checker che controlla che la stringa sia interpretabile come un intero scritto in una delle basi (radices) specificate secondo il metodo statico parseUnsignedLong di Long.

[CheckAny]    Aggiungere all'interfaccia Checker il metodo statico Checker getCheckAny(Checker...cc) che ritorna un Checker che controlla che almeno uno dei Checker in cc è soddisfatto. Se nessuno è soddisfatto il metodo valid ritorna la concatenazione delle stringhe di errore di tutti i Checker in cc.

[HTree]    Implementare l'interfaccia Iterable<T> nella classe HTree<T> in modo tale che iteri i nodi dell'albero in depth-first, cioè in profondità, per ogni nodo prima il nodo e poi i suoi discendenti.

31 Mar 2016


  1. Una variabile locale o parametro è effettivamente final se è stata inizializzata (un parametro è sempre inizializzato dal valore che gli è stato assegnato al momento dell'invocazione del metodo) e il suo valore non viene più modificato.

  2. Il coefficiente binomiale dipende da due interi nk ≥ 0, ed è anche detto n su k, ha valore uguale al numero di sottoinsiemi di cardinalità k di un insieme di cardinalità n. Per maggiori informazioni si veda Binomial Coefficient.

  3. Il termine memoizzazione deriva dall'inglese memoization che a sua volta è stato coniato dal latino memorandum. Per maggiori informazioni si veda Memoization.