2011-01-15 7 views
5

Quando creo gerarchie di tipi complessi (diversi livelli, diversi tipi per livello), mi piace utilizzare la parola chiave final sui metodi che implementano una dichiarazione di interfaccia. Un esempio:Parola chiave del metodo Java "final" e il suo utilizzo

interface Garble { 
    int zork(); 
} 

interface Gnarf extends Garble { 
    /** 
    * This is the same as calling {@link #zblah(0)} 
    */ 
    int zblah(); 
    int zblah(int defaultZblah); 
} 

E poi

abstract class AbstractGarble implements Garble { 
    @Override 
    public final int zork() { ... } 
} 

abstract class AbstractGnarf extends AbstractGarble implements Gnarf { 
    // Here I absolutely want to fix the default behaviour of zblah 
    // No Gnarf shouldn't be allowed to set 1 as the default, for instance 
    @Override 
    public final int zblah() { 
    return zblah(0); 
    } 

    // This method is not implemented here, but in a subclass 
    @Override 
    public abstract int zblah(int defaultZblah); 
} 

faccio questo per diversi motivi:

  1. E mi aiuta a sviluppare la gerarchia tipo. Quando aggiungo una classe alla gerarchia, è molto chiaro, quali metodi devo implementare e quali metodi non posso sovrascrivere (nel caso in cui ho dimenticato i dettagli della gerarchia)
  2. Penso che l'override di concreto sia il cattivo secondo i principi e i modelli di progettazione, come il modello template method. Non voglio che altri sviluppatori o i miei utenti lo facciano.

Quindi la parola chiave final funziona perfettamente per me. La mia domanda è:

Perché viene usato così raramente in natura? Puoi mostrarmi alcuni esempi/motivi in ​​cui final (in un caso simile al mio) sarebbe molto cattivo?

risposta

2

Perché viene usato così raramente in natura?

Ciò non corrisponde alla mia esperienza. Lo vedo usato molto frequentemente in tutti i tipi di librerie.Solo un (casuale) Esempio: Guarda le classi astratte in:

http://code.google.com/p/guava-libraries/

, per esempio com.google.common.collect.AbstractIterator. peek(), hasNext(), next() e endOfData() sono finali, lasciando solo computeNext() all'implementatore. Questo è un esempio molto comune IMO.

Il motivo principale contro l'utilizzo di final è consentire agli implementatori di modificare un algoritmo: hai citato il modello "metodo modello": può ancora avere senso modificare un metodo modello o migliorarlo con alcune azioni pre/post (senza inviare spam all'intera classe con dozzine di pre/post-hook).

Il motivo principale per cui l'utilizzo di final è evitare errori di implementazione accidentali o quando il metodo si basa su interni della classe che non sono specificati (e quindi potrebbero cambiare in futuro).

+0

+1 per il suggerimento. Questo è esattamente il caso d'uso che avevo in mente. Non l'ho quasi mai visto ... –

+0

** Risposta accettata ** perché entrambi hanno risposto alla mia domanda ** E ** mi ha dimostrato che avevo torto sul fatto che 'finale' non è spesso usato sui metodi ** E ** provato ho ragione sul mio utilizzo :) Grazie, Chris –

2

penso che non è comunemente utilizzato per due motivi:

  1. La gente non sa che esiste
  2. Le persone non hanno l'abitudine di pensare a questo proposito quando costruiscono un metodo.

In genere cado nella seconda ragione. Ho scavalcato metodi concreti su una base piuttosto comune. In alcuni casi questo è un problema, ma molte volte non è in conflitto con i principi di progettazione e in effetti potrebbe essere la soluzione migliore. Pertanto, quando sto implementando un'interfaccia, in genere non penso abbastanza profondamente a ciascun metodo per decidere se una parola chiave finale sarebbe utile. Soprattutto perché lavoro su molte applicazioni aziendali che cambiano frequentemente.

+0

+1 Buona risposta. Anche se, penso che l'utilizzo della parola chiave final non * finalizzi * le cose in una misura in cui il cambiamento non sarebbe più possibile. Ma questo dipende dal progetto. Nel mio caso, però, sto progettando entrambe l'implementazione ** E ** le interfacce, simili all'API Java Collections ... –

+0

Sì, le modifiche sono ancora possibili. Immagino che in molti degli oggetti che ho creato, sovrascrivere i metodi concreti sia comunemente accettabile, quindi i casi in cui è necessario aggiungere un metodo finale sono rari, quindi mi infastidisce di più. – jzd

+0

+1 Inoltre, i modelli di codice negli IDE in genere creano tutto ciò che non è pubblico. – WReach

4

Perché viene usato così raramente in natura?

Perché si dovrebbe scrivere una parola di più per rendere/metodo variabile finali

si può mostrare me alcuni esempi/motivi dove finale (in un caso simile al mio) sarebbe molto male?

Di solito vedo questi esempi nelle librerie di parti 3d. In alcuni casi, desidero estendere alcune lezioni e modificare alcuni comportamenti. Soprattutto è pericoloso nelle librerie non open source senza separazione di interfaccia/implementazione.

+0

+1 Per la separazione di interfacce/implementazione. A volte va bene, ma rende le gerarchie di tipo molto severe. In questo caso, 'finale' può essere davvero un dolore –

+0

+1 Ho notato a volte ho un forte desiderio di uccidere l'autore della libreria che l'ha usato. i metodi "privati" sono molto simili. Penso che non sia bello limitare l'utente della gerarchia di classi, anche se l'autore pensa che potrebbe violare qualcosa (ci sono eccezioni ovviamente, ma ogni regola ha eccezioni :)). L'utente deve decidere da solo, ma dovrebbe essere avvisato, attraverso. – Stas

+0

@Stas, non ne sono sicuro. Soprattutto, quando disegno metodi di convenienza il cui comportamento dipende da altri metodi, voglio usare 'final' estensivamente. Adatterò il mio esempio per questo ... –

1

Perché viene usato così raramente in natura?

Perché non dovrebbe essere necessario. Inoltre non chiude completamente l'implementazione, quindi in effetti potrebbe darti un falso senso di sicurezza.

Non dovrebbe essere necessario a causa dello Liskov substitution principle. Il metodo ha un contratto e in un diagramma di ereditarietà progettato correttamente che il contratto è pieno (altrimenti è un bug). Esempio:

interface Animal { 
    void bark(); 
} 

abstract class AbstractAnimal implements Animal{ 
    final void bark() { 
     playSound("whoof.wav"); // you were thinking about a dog, weren't you? 
    } 
} 

class Dog extends AbstractAnimal { 
    // ok 
} 

class Cat extends AbstractAnimal() { 
    // oops - no barking allowed! 
} 

Non permettendo a una sottoclasse di fare la cosa giusta (per esempio) si potrebbe introdurre un bug. Oppure potresti richiedere ad un altro sviluppatore di mettere un albero ereditario dell'interfaccia Garble proprio accanto al tuo perché il tuo metodo finale non gli consente di fare ciò che dovrebbe fare.

Il falso senso di sicurezza è tipico di un metodo finale non statico. Un metodo statico non dovrebbe usare lo stato dall'istanza (non può). Un metodo non statico probabilmente lo fa. Probabilmente lo fa anche il tuo metodo finale (non statico), ma non lo possiede le variabili di istanza - possono essere diverse dal previsto. Quindi aggiungi un onere allo sviluppatore della classe che eredita il modulo AbstractGarble - per garantire che i campi di istanza siano in uno stato previsto da l'implementazione in qualsiasi momento. Senza dare allo sviluppatore un modo per preparare lo stato prima di chiamare il metodo come in:

int zblah() { 
    prepareState(); 
    return super.zblah(); 
} 

A mio parere non si deve chiudere un'implementazione in modo tale a meno che non si dispone di una buona ragione. Se si documenta il contratto del metodo e si fornisce un test della junit, si dovrebbe essere in grado di fidarsi degli altri sviluppatori. Usando il test Junit possono effettivamente verificare lo Liskov substitution principle.

Come nota a margine, occasionalmente chiudo un metodo. Soprattutto se si trova sulla parte di confine di un quadro. Il mio metodo fa alcune contabilità e poi continua a un metodo astratto da attuare da qualcun altro:

final boolean login() { 
    bookkeeping(); 
    return doLogin(); 
} 
abstract boolean doLogin(); 

In questo modo nessuno si dimentica di fare la contabilità, ma in grado di fornire un accesso personalizzato.Che ti piaccia una configurazione del genere è ovviamente a tua discrezione :)

+0

Punto interessante! Non sapevo del principio di sostituzione di Liskov. Tuttavia non sono completamente d'accordo con te sulla tua argomentazione. Il metodo 'Animal.bark()' è fuori posto e il design è sbagliato. Se il design è sbagliato, ovviamente, le implementazioni non dovrebbero essere chiuse, perché gli utenti potrebbero aver bisogno di creare soluzioni alternative, come 'Cat.bark()', per superare i difetti di progettazione ... Se sono attento al mio design e fornisci * metodi template * che possono essere ancora sovrascritti, quindi penso di poter avere un sacco di coppie 'login()'/'doLogin()' come suggerito –

+0

@Lukas Eder Sono d'accordo che l'esempio Animal era cattivo. Non riuscivo a pensare ad un esempio migliore. È tuttavia un pericolo reale che i requisiti cambino al di là di ciò che pensavi possibile nel tempo. Purtroppo, mi è successo un paio di volte :( – extraneon

3

Io uso sempre final quando scrivo una classe astratta e voglio chiarire quali metodi sono corretti. Penso che questa sia la funzione più importante di questa parola chiave.

Ma quando non ti aspetti che una classe venga estesa comunque, perché la confusione? Ovviamente se stai scrivendo una libreria per qualcun altro, cerchi di salvaguardarlo il più possibile ma quando scrivi "codice utente finale", c'è un punto in cui cercare di rendere il tuo codice infallibile servirà solo a infastidire gli sviluppatori di manutenzione che cercheranno di capire come aggirare il labirinto che hai costruito.

Lo stesso vale per le lezioni finali. Sebbene alcune classi debbano per loro stessa natura essere definitive, troppo spesso uno sviluppatore miope contrassegnerà semplicemente tutte le classi foglia nell'albero di inheirance come final.

Dopo tutto, la codifica ha due scopi distinti: dare istruzioni al computer e trasmettere informazioni ad altri sviluppatori che leggono il codice. Il secondo è ignorato la maggior parte delle volte, anche se è quasi importante quanto far funzionare il tuo codice. Inserire parole chiave non necessarie final è un buon esempio di ciò: non modifica il modo in cui il codice si comporta, quindi il suo unico scopo dovrebbe essere la comunicazione. Ma cosa comunichi? Se contrassegni un metodo come final, un manutentore presupporrà che tu abbia avuto un buon readon per farlo. Se si scopre che non l'hai fatto, tutto ciò che hai ottenuto è stato confondere gli altri.

Il mio approccio è (e potrei essere completamente sbagliato qui ovviamente): non scrivere nulla a meno che non cambi il modo in cui il codice funziona o trasmette informazioni utili.

+0

Hai ragione Sto scrivendo codice di libreria, e penso di avere delle buone ragioni per comunicare la semantica di "finale" che stai suggerendo ... Io sono difficilmente si usa 'final' nella logica di ogni giorno dell'applicazione.Il rischio di uso improprio è troppo piccolo. –