La scelta della firma di un metodo – Lezione 38 di Java Avanzato

Dato un metodo, che firma bisogna utilizzare? Potremmo utilizzare la firma più generale possibile per quel metodi, ad esempio con tutti i parametri ad Object. In questo modo non potremo utilizzare i metodi e i campi sviluppati nelle sottoclassi, saremmo troppo limitati. In un corretto stile di programmazione, la firma di un metodo deve essere il più generale possibile a patto che consenta di svolgere il compito prefissato. Inoltre deve essere il più fedele possibile al contratto stabilito per il metodo dato e se possibile, eliminare i tipi parametrici. In questo modo il metodo diventa più semplice e non verrà introdotto inutile overhead con operazioni come la type inference e la cancellazione.
Ricordiamo che per firma di un metodo si intende solo il nome e la lista dei parametri.

Dato il contratto di un metodo vogliamo che la lista degli argomenti faccia tramite la scelta dei parametri formali consenta di:

  1. Accettare tutti i valori che soddisfano la precondizione (completezza);
  2. Rifiutare tutti gli altri valori (correttezza)

In questo modo non avremo controlli a runtime poiché la firma scelta cattura eventuali errori a tempo di compilazione. Non tutti gli errori però vengono “catturati” a compilazione, ad esempio se volessimo calcolare “la radice quadrata di un valore” non potremmo catturare tutti gli errori. In Java non esiste un tipo di dato per rappresentare i valori negativi, per cui non possiamo catturare tutti gli errori a tempo di compilazione a causa della poca espressività del sistema dei tipi in Java (ad esempio in C/C++ esiste unsigned int).
In questi casi, quando non si possono catturare tutti gli errori a tempo di compilazione, si sceglie di rilassare la correttezza accettando anche altri valori. La firma sarà completa ma non corretta e i restanti controlli verranno effettuati a runtime.
Se si decidesse di rilassare la completezza rifiutando alcuni valori validi, a runtime non potremmo più recuperarli ed avremo ristretto il contratto del metodo!

I criteri per la scelta della firma migliore di un metodo sono i seguenti:

  1. Funzionalità
  2. Completezza
  3. Correttezza
  4. Semplicità e Ulteriori garanzie

La funzionalità è obbligatoria, indica se riusciamo a scrivere quel metodo. Gli altri criteri se presenti consentono di migliorare la firma. La priorità dei criteri è in base al loro ordine, tranne per il criterio di Semplicità e di Ulteriori garanzie che hanno la stessa priorità.

L’unico criterio per la scelta del miglior tipo di ritorno di un metodo è il seguente:

  1. Specificità

Il tipo di ritorno deve essere il più specifico possibile e al tempo stesso deve fornire più informazioni possibili.

Esempio tratto dall’esame del 19 febbraio 2009 del professore M. Faella: Implementare un metodo statico interleave che prende come argomento tre LinkedList: A, B e C. Senza modificare A e B, il metodo aggiunge tutti gli elementi di A e di B a C, in modo alternato (prima un elemento di A, poi uno di B, poi un altro elemento di A, e così via). Porre particolare attenzione alla scelta della firma di interleave, in modo che sia la più generale possibile, ma senza utilizzare parametri di tipo inutili.

Di conseguenza il contratto del metodo richiede che gli elementi delle due lista devono essere sottotipi della terza lista. E’ possibile esprimere in Java la precondizione richiesta dal metodo?

  • Prima soluzione:

      void interleave(LinkedList<?> A, LinkedList<?> B, LinkedList<?> C)
    1. Funzionalità, il metodo non è funzionale perché a C posso aggiungere solo null;

    Dato che il metodo non può essere scritto è inutile verificare gli altri criteri.

  • Seconda soluzione:
    <S, T extends S, U extends S> void interleave(LinkedList<U> A,                                               LinkedList<T> B,                                               LinkedList<S> C)
    1. Funzionalità, il metodo è funzionale perché possiamo iterare su A e B senza problemi;

    2. Completezza, è completo perché accetta tutte e sole le lista su cui si vuole lavorare (A e B possono essere diverse);
    3. Correttezza, va bene perché rifiuta i tipi non correlati tra loro;
    4. Semplicità, non risulta molto semplice perché utilizza 3 parametri di tipo;
      Ulteriori garanzie, non esprime A e B non modificabili.
  • Terza soluzione:
      <S> void interleave(LinkedList<? extends S> A,
                          LinkedList<? extends S> B,                       LinkedList<S> C)
    1. Funzionalità, è funzionale, quando itero su A e B non possono conoscere il loro tipo effettivo e con Object non ho problemi. Per l’add su C utilizzerò il parametro di tipo S che con la cancellazione verrà convertito automaticamente nel tipo effettivo;
    2. Completezza, accetta tutti i valori richiesti;
    3. Correttezza, rifiuta i valori non richiesti;
    4. Semplicità, abbastanza semplice perché utilizza solo 1 parametro;
    5. Ulteriori garanzie, non posso chiamare add su A e B (ma remove si)

La firma migliore è quella proposta nella terza soluzione, scriviamo il corpo del metodo:

  public static <R extends T, S extends T, T> <S> void interleave(
                                                  LinkedList<? extends S> A,
                                                  LinkedList<? extends S> B,
                                                  LinkedList<S> C)
  {

    Iterator<? extends S> ia= a.iterator(),
    ib= b.iterator();

    while(ia.hasNext() || ib.hasNext()) {

      S x;
      if(ia.hasNext()) {
        x = ia.next();
        c.add(x);
      }
      if(ib.hasNext()) {
        x = ib.next();
        c.add(x);
      }

    }
  }

Grazie a parametri di tipo il tipo di ritorno (Object) restituito dal metodo next verrà convertito automaticamente in un sottotipo di S, quindi assegnabile alla variabile x.

Indice Lezione PrecedenteLezione Successiva

Pubblicato in Guide, Java, Programmazione Taggato con: , ,

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

*