Perché dovrei usare la parola chiave "final" su un parametro del metodo in Java?


381

Non riesco a capire dove la finalparola chiave sia davvero utile quando viene utilizzata sui parametri del metodo.

Se escludiamo l'uso di classi anonime, leggibilità e dichiarazione di intenti, mi sembra quasi inutile.

Applicare che alcuni dati rimangano costanti non è così forte come sembra.

  • Se il parametro è una primitiva, non avrà alcun effetto poiché il parametro viene passato al metodo come valore e modificandolo non avrà alcun effetto al di fuori dell'ambito.

  • Se passiamo un parametro per riferimento, allora il riferimento stesso è una variabile locale e se il riferimento viene modificato all'interno del metodo, ciò non avrebbe alcun effetto al di fuori dell'ambito del metodo.

Considera l'esempio di test semplice di seguito. Questo test ha superato sebbene il metodo abbia modificato il valore del riferimento assegnato, non ha alcun effetto.

public void testNullify() {
    Collection<Integer> c  = new ArrayList<Integer>();      
    nullify(c);
    assertNotNull(c);       
    final Collection<Integer> c1 = c;
    assertTrue(c1.equals(c));
    change(c);
    assertTrue(c1.equals(c));
}

private void change(Collection<Integer> c) {
    c = new ArrayList<Integer>();
}

public void nullify(Collection<?> t) {
    t = null;
}

101
Un breve punto sulla terminologia: Java non ha affatto riferimenti pass-by. Ha un riferimento di passaggio per valore che non è la stessa cosa. Con il vero passaggio per semantica di riferimento i risultati del tuo codice sarebbero diversi.
Jon Skeet,

6
Qual è la differenza tra passa per riferimento e passa riferimento per valore?
NobleUplift,

È più facile descrivere quella differenza in un contesto C (almeno per me). Se passo un puntatore a un metodo come: <code> int foo (int bar) </code>, quel puntatore viene passato per valore. Significa che viene copiato, quindi se faccio qualcosa all'interno di quel metodo come <code> free (bar); bar = malloc (...); </code> allora ho appena fatto una brutta cosa. La chiamata gratuita libererà effettivamente il pezzo di memoria puntato (quindi qualsiasi puntatore che ho passato ora è penzolante). Tuttavia, <code> int foo (int & bar) </bar> significa che il codice è valido e il valore del puntatore passato verrà modificato.
jerslan,

1
Il primo dovrebbe essere int foo(int* bar)e l'ultimo int foo(int* &bar). Il secondo passa un puntatore per riferimento, il primo passa un riferimento per valore.
jerslan,

2
@Martin, secondo me, è una buona domanda ; vedere il titolo della domanda e il contenuto del post come spiegazione del perché la domanda viene posta. Forse sto fraintendendo le regole qui, ma questa è esattamente la domanda che volevo quando cercavo "usi dei parametri finali nei metodi" .
Victor Zamanian,

Risposte:


239

Ferma la riassegnazione di una variabile

Mentre queste risposte sono intellettualmente interessanti, non ho letto la breve risposta semplice:

Utilizzare la parola chiave final quando si desidera che il compilatore impedisca di riassegnare una variabile a un oggetto diverso.

Che la variabile sia una variabile statica, una variabile membro, una variabile locale o una variabile argomento / parametro, l'effetto è completamente lo stesso.

Esempio

Vediamo l'effetto in azione.

Considera questo semplice metodo, in cui alle due variabili ( arg e x ) è possibile assegnare nuovamente oggetti diversi.

// Example use of this method: 
//   this.doSomething( "tiger" );
void doSomething( String arg ) {
  String x = arg;   // Both variables now point to the same String object.
  x = "elephant";   // This variable now points to a different String object.
  arg = "giraffe";  // Ditto. Now neither variable points to the original passed String.
}

Contrassegna la variabile locale come finale . Ciò provoca un errore del compilatore.

void doSomething( String arg ) {
  final String x = arg;  // Mark variable as 'final'.
  x = "elephant";  // Compiler error: The final local variable x cannot be assigned. 
  arg = "giraffe";  
}

Contrassegniamo invece la variabile del parametro come finale . Anche questo provoca un errore del compilatore.

void doSomething( final String arg ) {  // Mark argument as 'final'.
  String x = arg;   
  x = "elephant"; 
  arg = "giraffe";  // Compiler error: The passed argument variable arg cannot be re-assigned to another object.
}

Morale della storia:

Se vuoi assicurarti che una variabile punti sempre sullo stesso oggetto, segna la variabile finale .

Non riassegnare mai argomenti

Come buona pratica di programmazione (in qualsiasi lingua), non si dovrebbe mai riassegnare una variabile parametro / argomento a un oggetto diverso dall'oggetto passato dal metodo chiamante. Negli esempi sopra, non si dovrebbe mai scrivere la riga arg =. Poiché gli umani commettono errori e i programmatori sono umani, chiediamo al compilatore di aiutarci. Contrassegna ogni variabile parametro / argomento come 'finale' in modo che il compilatore possa trovare e contrassegnare tali riassegnazioni.

Ripensandoci

Come notato in altre risposte ... Dato l'obiettivo di progettazione originale di Java di aiutare i programmatori a evitare errori stupidi come leggere oltre la fine di un array, Java avrebbe dovuto essere progettato per imporre automaticamente tutte le variabili di parametro / argomento come "finale". In altre parole, gli argomenti non dovrebbero essere variabili . Ma il senno di poi è una visione 20/20, e i designer Java avevano le mani piene al momento.

Quindi, aggiungere sempre finala tutti gli argomenti?

Dovremmo aggiungere finala ogni singolo parametro del metodo dichiarato?

  • In teoria si.
  • In pratica, no.
    ➥ Aggiungi finalsolo quando il codice del metodo è lungo o complicato, dove l'argomento può essere scambiato per una variabile locale o membro ed eventualmente riassegnato.

Se acquisti nella pratica di non riassegnare mai un argomento, sarai propenso ad aggiungere un finala ciascuno. Ma questo è noioso e rende la dichiarazione un po 'più difficile da leggere.

Per un breve codice semplice in cui l'argomento è ovviamente un argomento e non una variabile locale né una variabile membro, non mi preoccupo di aggiungere il final. Se il codice è abbastanza ovvio, senza alcuna possibilità per me né per nessun altro programmatore che fa manutenzione o refactoring che confonde accidentalmente la variabile argomento come qualcosa di diverso da un argomento, allora non preoccuparti. Nel mio lavoro, aggiungo finalsolo nel codice più lungo o più coinvolto in cui un argomento potrebbe essere scambiato per una variabile locale o membro.

Un altro caso aggiunto per la completezza

public class MyClass {
    private int x;
    //getters and setters
}

void doSomething( final MyClass arg ) {  // Mark argument as 'final'.

   arg =  new MyClass();  // Compiler error: The passed argument variable arg  cannot be re-assigned to another object.

   arg.setX(20); // allowed
  // We can re-assign properties of argument which is marked as final
 }

23
"Come buona pratica di programmazione (in qualsiasi lingua), non dovresti mai riassegnare una variabile parametro / argomento [..]" Mi dispiace davvero doverti richiamare su questo. La riassegnazione di argomenti è una pratica standard in linguaggi come Javascript, in cui la quantità di argomenti passati (o anche se ve ne sono stati passati) non è dettata dalla firma del metodo. Ad esempio, con una firma del tipo: "function say (msg)" le persone si assicureranno che l'argomento "msg" sia assegnato, in questo modo: "msg = msg || 'Hello World!';". I migliori programmatori Javascript al mondo stanno infrangendo la tua buona pratica. Basta leggere la fonte jQuery.
Stijn de Witt,

37
@StijndeWitt Il tuo esempio mostra il problema stesso di riassegnare la variabile argomento. Perdi informazioni senza ottenere nulla in cambio: (a) Hai perso il valore originale trasferito, (b) Hai perso l'intenzione del metodo di chiamata (Il chiamante ha passato 'Hello World!' O abbiamo predefinito). Entrambi a & b sono utili per test, codice lungo e quando il valore viene ulteriormente modificato in un secondo momento. Attendo la mia dichiarazione: arg Vars non dovrebbe mai essere riassegnato. Il codice dovrebbe essere: message = ( msg || 'Hello World"' ). Semplicemente non c'è motivo di non usare una var separata. L'unico costo è di pochi byte di memoria.
Basil Bourque,

9
@Basil: conta più codice (in byte) e in Javascript. Pesantemente. Come per molte cose è basato sull'opinione. È del tutto possibile ignorare completamente questa pratica di programmazione e scrivere comunque un codice eccellente. La pratica di programmazione di una persona non rende la pratica di tutti. Aspetta tutto quello che vuoi, scelgo di scriverlo comunque in modo diverso. Questo mi rende un cattivo programmatore o il mio codice cattivo codice?
Stijn de Witt,

14
L'utilizzo message = ( msg || 'Hello World"' )mi rischia in seguito di utilizzare accidentalmente msg. Quando il contratto che intendo è "il comportamento con arg no / null / undefined è indistinguibile dal passaggio "Hello World"", è una buona pratica di programmazione impegnarsi ad esso all'inizio della funzione. [Questo può essere ottenuto senza riassegnazione iniziando con, if (!msg) return myfunc("Hello World");ma diventa ingombrante con più argomenti.] Nei rari casi in cui la logica nella funzione dovrebbe interessare se è stato utilizzato il valore predefinito, preferirei designare un valore sentinella speciale (preferibilmente pubblico) .
Beni Cherniavsky-Paskin,

6
@ BeniCherniavsky-Paskin il rischio che descrivi è solo a causa della somiglianza tra messagee msg. Ma se lo chiamasse qualcosa di simile processedMsgo qualcos'altro che fornisce ulteriore contesto - la possibilità di un errore è molto più bassa. Concentrati su ciò che dice non su "come" lo dice. ;)
alfasin,

230

A volte è bello essere espliciti (per leggibilità) che la variabile non cambia. Ecco un semplice esempio in cui l'utilizzo finalpuò salvare alcuni possibili mal di testa:

public void setTest(String test) {
    test = test;
}

Se si dimentica la parola chiave "this" su un setter, la variabile che si desidera impostare non viene impostata. Tuttavia, se si utilizzava la finalparola chiave sul parametro, il bug verrebbe rilevato al momento della compilazione.


65
tuttavia vedrai l'avvertimento "L'assegnazione al test variabile non ha alcun effetto"
AvrDragon

12
@AvrDragon Ma potremmo anche ignorare l'avvertimento. Quindi, è sempre meglio avere qualcosa che ci impedirà di andare oltre, come un errore di compilazione, che otterremo usando la parola chiave finale.
Sumit Desai,

10
@AvrDragon Dipende dall'ambiente di sviluppo. Non dovresti fare affidamento sull'IDE per catturare cose del genere per te, a meno che tu non voglia sviluppare cattive abitudini.
b1nary.atr0phy,

25
@ b1naryatr0phy in realtà è un avvertimento del compilatore, non solo IDE-tip
AvrDragon

8
@SumitDesai "Ma potremmo anche ignorare l'avvertimento. Quindi, è sempre meglio avere qualcosa che ci impedirà di andare oltre, come un errore di compilazione, che otterremo usando la parola chiave finale." Prendo il tuo punto, ma questa è un'affermazione molto forte che penso che molti sviluppatori Java non sarebbero d'accordo. Gli avvisi del compilatore sono disponibili per una ragione e uno sviluppatore competente non dovrebbe aver bisogno di un errore per "forzarli" a considerarne le implicazioni.
Stuart Rossiter,

127

Sì, escludendo le lezioni anonime, la leggibilità e la dichiarazione di intenti è quasi senza valore. Queste tre cose sono inutili però?

Personalmente tendo a non usare finalvariabili e parametri locali a meno che non stia usando la variabile in una classe interna anonima, ma posso certamente vedere il punto di coloro che vogliono chiarire che il valore del parametro stesso non cambierà (anche se l'oggetto a cui si riferisce cambia il suo contenuto). Per coloro che trovano che si aggiunge alla leggibilità, penso che sia una cosa del tutto ragionevole da fare.

Il tuo punto sarebbe più importante se qualcuno fosse in realtà sostenendo che si ha a mantenere costante i dati in un modo che non è così - ma non riesco a ricordare di aver visto alcuna di queste rivendicazioni. Stai suggerendo che esiste un corpus significativo di sviluppatori che suggerisce che finalha più effetto di quello che realmente fa?

EDIT: avrei dovuto davvero riassumere tutto questo con un riferimento a Monty Python; la domanda sembra in qualche modo simile alla domanda "Cosa hanno mai fatto i romani per noi?"


17
Ma per parafrasare Krusty con il suo danese, che cosa hanno fatto LATELY per noi ? =)
James Schek,

Yuval. È divertente! Immagino che la pace possa accadere anche se è forzata dalla spada!
gonzobrains

1
La domanda sembra più simile alla domanda "Cosa non hanno fatto i romani per noi?", Perché è più una critica a ciò che la parola chiave finale non fa.
NobleUplift,

"Stai suggerendo che esiste un corpus significativo di sviluppatori che suggerisce che final ha più effetti di quanti ne abbia realmente?" Per me questo è il problema principale: sospetto fortemente che una proporzione significativa di sviluppatori che la usano pensi che imponga l'immutabilità degli elementi passati del chiamante quando non lo fa. Naturalmente, si viene quindi attratti dal dibattito sul fatto che gli standard di codifica debbano "proteggere da" incomprensioni concettuali (di cui uno sviluppatore "competente" dovrebbe essere a conoscenza) o meno (e questo si dirige quindi verso opinioni fuori campo -tipo di domanda)!
Stuart Rossiter,

1
@SarthakMittal: il valore non verrà copiato se non lo si utilizza effettivamente, se è quello che ti stai chiedendo.
Jon Skeet,

76

Mi spiego un po 'su un caso in cui si deve usare finale, che Jon già accennato:

Se crei una classe interna anonima nel tuo metodo e utilizzi una variabile locale (come un parametro del metodo) all'interno di quella classe, il compilatore ti costringe a rendere definitivo il parametro:

public Iterator<Integer> createIntegerIterator(final int from, final int to)
{
    return new Iterator<Integer>(){
        int index = from;
        public Integer next()
        {
            return index++;
        }
        public boolean hasNext()
        {
            return index <= to;
        }
        // remove method omitted
    };
}

Qui i parametri frome todevono essere definitivi per poter essere utilizzati all'interno della classe anonima.

Il motivo di tale requisito è questo: le variabili locali vivono nello stack, quindi esistono solo durante l'esecuzione del metodo. Tuttavia, l'istanza di classe anonima viene restituita dal metodo, quindi può durare a lungo. Non è possibile conservare lo stack, poiché è necessario per le successive chiamate al metodo.

Quindi, ciò che Java fa invece è mettere copie di quelle variabili locali come variabili di istanza nascoste nella classe anonima (puoi vederle esaminando il codice byte). Ma se non fossero definitivi, ci si potrebbe aspettare che la classe anonima e il metodo vedano le modifiche apportate dall'altra alla variabile. Per mantenere l'illusione che ci sia solo una variabile anziché due copie, deve essere definitiva.


1
Mi hai perso da "Ma se non sono definitivi ...." Puoi provare a riformularlo, forse non ho avuto abbastanza caffè.
hhafez,

1
Hai una variabile locale da - la domanda è cosa succede se usi l'istanza anon class all'interno del metodo e cambia i valori di from - le persone si aspetterebbero che la modifica sia visibile nel metodo, poiché vedono solo una variabile. Per evitare questa confusione, deve essere definitiva.
Michael Borgwardt,

Non crea una copia, è semplicemente un riferimento a qualsiasi oggetto a cui è stato fatto riferimento.
Vickirk,

1
@vickirk: sicuro che crea una copia del riferimento, in caso di tipi di riferimento.
Michael Borgwardt,

A parte il fatto che non abbiamo classi anonime che fanno riferimento a quelle variabili, sei consapevole se c'è qualche differenza tra un finalparametro di funzione e un parametro di funzione non finale agli occhi di HotSpot?
Pacerier,

25

Uso final sempre i parametri.

Aggiunge così tanto? Non proprio.

Lo spegnerei? No.

Il motivo: ho trovato 3 bug in cui le persone avevano scritto codice sciatto e non sono riuscito a impostare una variabile membro negli accessori. Tutti i bug si sono rivelati difficili da trovare.

Mi piacerebbe vedere questo reso predefinito in una futura versione di Java. Il passaggio per valore / riferimento fa scattare un sacco di programmatori junior.

Ancora una cosa ... i miei metodi tendono ad avere un basso numero di parametri, quindi il testo extra su una dichiarazione di metodo non è un problema.


4
Stavo per suggerire anche questo, che sarà il default nelle versioni future e che devi specificare una parola chiave "mutabile" o migliore concepita. Ecco un bell'articolo su questo: lpar.ath0.com/2008/08/26/java-annoyance-final-parameters
Jeff Axelrod

È passato molto tempo, ma potresti fornire un esempio del bug che hai scoperto?
user949300,

Vedi la risposta più votata. Questo ha un ottimo esempio in cui la variabile membro non è impostata e invece il parametro è mutato.
Fortyrunner,

18

L'uso di final in un parametro di metodo non ha nulla a che fare con ciò che accade all'argomento sul lato chiamante. Ha solo lo scopo di contrassegnarlo come non cambia all'interno di quel metodo. Mentre provo ad adottare uno stile di programmazione più funzionale, vedo il valore in questo.


2
Esatto, non fa parte dell'interfaccia della funzione, ma solo dell'implementazione. È confuso che java consenta (ma dirsregards) final sui parametri nelle interfacce / dichiarazioni di metodi astratti.
Beni Cherniavsky-Paskin,

8

Personalmente non uso il finale sui parametri del metodo, perché aggiunge troppa confusione agli elenchi di parametri. Preferisco imporre che i parametri del metodo non vengano modificati tramite qualcosa come Checkstyle.

Per le variabili locali che uso final quando possibile, lascio persino che Eclipse lo faccia automaticamente nella mia configurazione per progetti personali.

Vorrei certamente qualcosa di più forte come C / C ++ const.


Non sono sicuro che i riferimenti IDE e strumento siano applicabili alla pubblicazione di OP o all'argomento. Vale a dire "final" è un controllo in fase di compilazione che il riferimento non viene modificato / modificato. Inoltre, per imporre davvero tali cose, vedi la risposta sulla mancanza di protezione per i membri figli dei riferimenti finali. Quando si crea un'API, ad esempio, l'utilizzo di un IDE o di uno strumento non aiuterà le parti esterne a utilizzare / estendere tale codice.
Darrell Teague,

4

Poiché Java trasmette copie di argomenti, ritengo che la rilevanza di finalsia piuttosto limitata. Immagino che l'abitudine provenga dall'era del C ++ in cui è possibile vietare la modifica del contenuto di riferimento facendo un const char const *. Sento che questo genere di cose ti fa credere che lo sviluppatore sia intrinsecamente stupido come un cazzo e abbia bisogno di essere protetto da tutti i personaggi che scrive. In tutta umiltà posso dire che scrivo pochissimi bug anche se ometto final(a meno che non voglia che qualcuno ignori i miei metodi e le mie classi). Forse sono solo uno sviluppatore di vecchia scuola.


1

Non uso mai final in un elenco di parametri, aggiunge solo confusione, come hanno detto i precedenti intervistati. Anche in Eclipse puoi impostare l'assegnazione dei parametri per generare un errore, quindi usare final in un elenco di parametri mi sembra abbastanza ridondante. È interessante notare che quando ho abilitato l'impostazione Eclipse per l'assegnazione dei parametri generando un errore su di esso ha catturato questo codice (questo è proprio il modo in cui ricordo il flusso, non il codice effettivo.): -

private String getString(String A, int i, String B, String C)
{
    if (i > 0)
        A += B;

    if (i > 100)
        A += C;

    return A;
}

Nel ruolo dell'avvocato del diavolo, cosa c'è di sbagliato nel fare questo?


Prestare attenzione a differenziare un IDE da una JVM di runtime. Qualunque cosa faccia un IDE è irrilevante quando il codice byte compilato viene eseguito sul server a meno che IDE non abbia aggiunto codice per proteggersi dall'abbraccio di variabili membro come un difetto nel codice quando si intendeva che una variabile non dovesse essere riassegnata ma erroneamente lo era - quindi lo scopo di parola chiave finale.
Darrell Teague,

1

Risposta breve: finalaiuta un pochino ma ... usa invece la programmazione difensiva sul lato client.

In effetti, il problema finalè che impone solo che il riferimento sia invariato, permettendo allegramente di mutare i membri dell'oggetto referenziato, all'insaputa del chiamante. Quindi la migliore pratica in questo senso è la programmazione difensiva dal lato del chiamante, creando istanze profondamente immutabili o copie profonde di oggetti che rischiano di essere rapinati da API senza scrupoli.


2
"il problema con final è che impone solo che il riferimento sia invariato" - Falso, lo stesso Java lo impedisce. Una variabile passata a un metodo non può avere il suo riferimento modificato con quel metodo.
Madbreaks

Effettua una ricerca prima di pubblicare ... stackoverflow.com/questions/40480/…
Darrell Teague

In parole povere, se fosse vero che i riferimenti ai riferimenti non possono essere modificati, non ci sarebbe discussione sulla copia difensiva, l'immutabilità, la necessità della parola chiave finale, ecc.
Darrell Teague,

O mi stai fraintendendo, o ti sbagli. Se passo un riferimento a un oggetto a un metodo e tale metodo lo riassegna, il riferimento originale rimane intatto per (me) il chiamante quando il metodo completa la sua esecuzione. Java è rigorosamente pass-by-value. E sei sfacciatamente presuntuoso affermare che non ho fatto ricerche.
Madbreaks,

Downvoting perché op ha chiesto perché usare final e hai fornito solo una ragione errata.
Madbreaks,

0

Un ulteriore motivo per aggiungere la dichiarazione finale ai parametri è che aiuta a identificare le variabili che devono essere rinominate come parte di un refactoring "Metodo di estrazione". Ho scoperto che l'aggiunta di final a ciascun parametro prima di iniziare un refactoring di metodo di grandi dimensioni mi dice rapidamente se ci sono problemi che devo affrontare prima di continuare.

Tuttavia, generalmente li rimuovo come superflui alla fine del refactoring.


-1

Seguito da quello che post di Michel. Io stesso ho fatto un altro esempio per spiegarlo. Spero possa essere d'aiuto.

public static void main(String[] args){
    MyParam myParam = thisIsWhy(new MyObj());
    myParam.setArgNewName();

    System.out.println(myParam.showObjName());
}

public static MyParam thisIsWhy(final MyObj obj){
    MyParam myParam = new MyParam() {
        @Override
        public void setArgNewName() {
            obj.name = "afterSet";
        }

        @Override
        public String showObjName(){
            return obj.name;
        }
    };

    return myParam;
}

public static class MyObj{
    String name = "beforeSet";
    public MyObj() {
    }
}

public abstract static class MyParam{
    public abstract void setArgNewName();
    public abstract String showObjName();
}

Dal codice precedente, nel metodo thisIsWhy () , in realtà non ha assegnato il [argomento MyObj obj] ad un riferimento reale in MyParam. Invece, usiamo semplicemente [argomento MyObj obj] nel metodo all'interno di MyParam.

Ma dopo aver finito il metodo thisIsWhy () , dovrebbe ancora esistere l'argomento (oggetto) MyObj?

Sembra che dovrebbe, perché possiamo vedere in linea principale che chiamiamo ancora il metodo showObjName () e deve raggiungere obj . MyParam utilizzerà / raggiungerà ancora l'argomento del metodo anche se il metodo è già stato restituito!

Il modo in cui Java realizza davvero questo è generare una copia è anche un riferimento nascosto all'argomento MyObj obj all'interno dell'oggetto MyParam (ma non è un campo formale in MyParam, quindi non possiamo vederlo)

Come chiamiamo "showObjName", utilizzerà quel riferimento per ottenere il valore corrispondente.

Ma se non mettessimo l'argomento finale, il che porta a una situazione, possiamo riassegnare una nuova memoria (oggetto) all'argomento MyObj obj .

Tecnicamente non c'è alcun conflitto! Se ci è permesso farlo, di seguito sarà la situazione:

  1. Ora abbiamo un punto [MyObj obj] nascosto su un [Memory A in heap] che ora vive nell'oggetto MyParam.
  2. Abbiamo anche un altro [MyObj obj] che è l'argomento punto di un [Memory B in heap] che ora vive nel metodo thisIsWhy.

Nessuno scontro, ma "CONFUSING !!" Perché usano tutti lo stesso "nome di riferimento" che è "obj" .

Per evitarlo, impostalo come "finale" per evitare che il programmatore esegua il codice "soggetto a errori".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.