Metodologie di Programmazione: Lezione 9

Riccardo Silvestri

Classi locali

A volte si devono creare oggetti la cui classe attuale non è importante mentre ha importanza l'interfaccia implementata o la classe estesa. Oppure l'oggetto creato è usato solamente all'interno di un metodo e non è esportato all'esterno. In questi casi definire una classe normale (top level) o una classe annidata per creare tali oggetti sembra essere quasi uno spreco e comunque ciò rende troppo ampia la visibilità della classe. Proprio 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 porta anche a una sintassi più snella e se ben usata più leggibile.

Classi locali

Le 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.

Come primi esempi vedremo situazioni in cui una classe locale serve a poter 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));
    }
}

Abbiamo usato i BigInteger che rappresentano interi di dimensioni arbitrariamente grandi perché i coefficienti binomiali anche con parametri n e k relativamente piccoli hanno valori molto grandi. Il problema con questa implementazione è che è piuttosto inefficiente. Lo possiamo vedere se la proviamo 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 in molti altri casi di 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 quelli di C(27, 14) e C(27, 13) sono ripetuti tre volte, e così via a crescere. Per evitare di ripetere inutilmente calcoli già fatti, si può usare la tecnica della memoizzazione3 che consiste nel salvare i 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 tale tabella ma non sarebbe una scelta soddisfacente perché il campo che è un dettaglio implementativo del metodo binom sarebbe visibile in tutta la classe. Allora possiamo 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 non basterebbero).

Grazie alle classi locali possiamo in questo caso nascondere tutti 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 un percorso di una directory e ritorna in una stringa la rappresentazione dell'albero delle directory e file contenuti a qualsiasi livello nella directory di input. Per fare ciò 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.
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 la loro rappresentazione. Tale oggetto deve implementare la seguente 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 il file o directory visitato sono fornite tramite il parametro attrs di tipo BasicFileAttributes. Ad esempio con attrs.creationTime() si ottiene la data di creazione e con attrs.size() la lunghezza in bytes del file. Il valore ritornato da tutti i metodi dell'interfaccia FileVisitor è di tipo FileVisitResult che è una enumerazione e che serve a comunicare al metodo che effettua 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 produce 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 rootpath  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 rootpath) throws IOException {
    // Classe locale che realizza un 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() - 4);
            s += pre + "\n";
            return FileVisitResult.CONTINUE;
        }

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

Quindi 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 mantenere e continuamente aggiornare una stringa pre per l'indentazione dei nomi dei file/dir. Si noti che pre è aumentato di " |" quando si inizia la visita di una directory in preVisitDirectory ed è riportato indietro quando finisce la vista della directory in postVisitDirectory. In preVisitDirectory saltiamo la visita di una directory e del suo contenuto se è nascosta, cioè il metodo Files.isHidden ritorna true. Adesso scriviamo 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 MetProg con il percorso relativo alla working directory otteniamo:

MetProg
   |---files
   |   |---alice_it_latin1.txt 173401
   |   |---alice_it_latin1_sample.txt 1367
   |   |---alice_it_utf8.txt 175249
   |   |---alice_it_utf8_sample.txt 1386
   |
   |---MetProg.iml 423
   |---out
   |   |---production
   |   |   |---MetProg
   |   |   |   |---mp
   |   |   |   |   |---app
   |   |   |   |   |   |---Checker.class 409
   |   |   |   |   |
   |   |   |   |   |---AziendaApp$1.class 184
   |   |   |   |   |---AziendaApp$CheckIndirizzo.class 784
   |   |   |   |   |---AziendaApp$CheckNomeCognome.class 931
   |   |   |   |   |---AziendaApp$CheckTelefono.class 778
   |   |   |   |   |---AziendaApp.class 4144
   |   |   |   |   |---Dipendente$1.class 184
   |   |   |   |   |---Dipendente$Contatti.class 1019
   |   |   |   |   |---Dipendente.class 3274
   |   |   |   |   |---Dirigente.class 972
   |   |   |   |   |---DPair.class 1146
   |   |   |   |   |---Generic.class 3337
   |   |   |   |   |---LPoint.class 1188
   |   |   |   |   |---MathApp.class 1569
   |   |   |   |   |---Pair.class 498
   |   |   |   |   |---tapp
   |   |   |   |   |   |---MenuApp.class 1454
   |   |   |   |   |
   |   |   |   |   |---TestFile.class 4387
   |   |   |   |   |---Tests.class 3737
   |   |   |   |   |---util
   |   |   |   |   |   |---HTree.class 3085
   |   |   |   |   |   |---Utils$1.class 811
   |   |   |   |   |   |---Utils$1Visitor.class 2761
   |   |   |   |   |   |---Utils$Align.class 1074
   |   |   |   |   |   |---Utils.class 10981
   |   |   |   |   |
   |   |   |   |
   |   |   |
   |   |
   |
   |---src
   |   |---mp
   |   |   |---app
   |   |   |   |---Checker.java 920
   |   |   |
   |   |   |---AziendaApp.java 4679
   |   |   |---Dipendente.java 4376
   |   |   |---Dirigente.java 1305
   |   |   |---Generic.java 4216
   |   |   |---MathApp.java 1072
   |   |   |---tapp
   |   |   |   |---MenuApp.java 1585
   |   |   |
   |   |   |---TestFile.java 3715
   |   |   |---Tests.java 4590
   |   |   |---util
   |   |   |   |---HTree.java 2709
   |   |   |   |---Utils.java 14299
   |   |   |
   |   |
   |

Finora abbiamo visto esempi di classi locali per creare oggetti il cui scopo è di mantenere dei dati durante un calcolo o implementare un'interfaccia che serve per poter operare con un altro metodo. Ma gli oggetti creati possono avere anche una vita più lunga e anzi possono essere ritornati dal metodo affinché possano essere 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
}

Da questa 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 a una classe locale. L'espressione definitoria-creazionale può essere usata come una qualsiasi altra espressione creazionale. Per chiarire meglio il caso di una classe anonima che estende una classe che ha almeno un costruttore esplicito. 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 pp = new MyLP();

Vediamo subito un esempio. Il metodo binomM2 siccome usa una sola istanza della classe locale definita 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);
}

In questo caso 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 definita.

Classi anonime per factory methods

Le classi anonime sono spesso usate per implementare un'interfaccia. E questo è ciò che spesso accade quando si vuole definire un cosiddetto factory method, cioè un metodo che fabbrica oggetti, tipicamente oggetti che implementano una certa interfaccia. Ad esempio, 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 uno specificato insieme.

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;
        }
    };
}

Ad esempio, se vogliamo un Checker che controlli sia che i caratteri siano lettere sia che la lunghezza non superi 8, possiamo crearlo molto semplicemente così

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

[NumeriDiStirling]    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 memoizzaione 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 un 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, se presenti 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'esercito precedente in modo che prenda in input anche un oggetto di tipo BiPredicate<Path,BasicFileAttributes> che deve essere usato 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 un 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 specificate secondo il metodo statico parseUnsignedLong di Long.

[CheckAny]    Aggiungere all'interfaccia Checker un 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.

23 Mar 2015


  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.