¿Por qué debería usar la palabra clave "final" en un parámetro de método en Java?

381

No puedo entender dónde la finalpalabra clave es realmente útil cuando se usa en los parámetros del método.

Si excluimos el uso de clases anónimas, legibilidad y declaración de intenciones, me parece casi inútil.

Hacer cumplir que algunos datos permanezcan constantes no es tan fuerte como parece.

  • Si el parámetro es primitivo, no tendrá efecto ya que el parámetro se pasa al método como un valor y cambiarlo no tendrá ningún efecto fuera del alcance.

  • Si estamos pasando un parámetro por referencia, entonces la referencia en sí es una variable local y si la referencia se cambia desde dentro del método, eso no tendría ningún efecto desde fuera del alcance del método.

Considere el ejemplo de prueba simple a continuación. Esta prueba pasa aunque el método cambió el valor de la referencia que se le dio, no tiene ningún efecto.

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;
}
Albahaca Bourque
fuente
101
Un punto rápido en la terminología: Java no tiene paso por referencia en absoluto. Tiene referencia de paso por valor que no es lo mismo. Con una semántica de paso real por referencia, los resultados de su código serían diferentes.
Jon Skeet
66
¿Cuál es la diferencia entre pasar por referencia y pasar referencia por valor?
NobleUplift
Es más fácil describir esa diferencia en un contexto C (al menos para mí). Si paso un puntero a un método como: <code> int foo (int bar) </code>, entonces ese puntero se pasa por valor. Lo que significa que se copia, así que si hago algo dentro de ese método como <code> free (bar); bar = malloc (...); </code> entonces acabo de hacer algo realmente malo. La llamada gratuita en realidad liberará la porción de memoria que está apuntando (por lo que cualquier puntero que pase ahora está colgando). Sin embargo, <code> int foo (int & bar) </bar> significa que el código es válido y el valor del puntero pasado se cambiará.
jerslan
1
Se supone que el primero es int foo(int* bar)y el último int foo(int* &bar). El último pasa un puntero por referencia, el primero pasa una referencia por valor.
jerslan
2
@ Martin, en mi opinión, es una buena pregunta ; vea el título de la pregunta y el contenido de la publicación como explicación de por qué se hace la pregunta . Tal vez no estoy entendiendo las reglas aquí, pero esta es exactamente la pregunta que quería al buscar "usos de los parámetros finales en los métodos" .
Victor Zamanian

Respuestas:

239

Detener la reasignación de una variable

Si bien estas respuestas son intelectualmente interesantes, no he leído la respuesta simple y breve:

Use la palabra clave final cuando desee que el compilador evite que una variable se reasigne a un objeto diferente.

Si la variable es una variable estática, una variable miembro, una variable local o una variable de argumento / parámetro, el efecto es completamente el mismo.

Ejemplo

Veamos el efecto en acción.

Considere este método simple, donde las dos variables ( arg y x ) pueden ser reasignadas a diferentes objetos.

// 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.
}

Marque la variable local como final . Esto da como resultado un error del compilador.

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

En su lugar, marquemos la variable del parámetro como final . Esto también resulta en un error del compilador.

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

Moraleja de la historia:

Si desea asegurarse de que una variable siempre apunte al mismo objeto, marque la variable como final .

Nunca reasigne argumentos

Como buena práctica de programación (en cualquier lenguaje), nunca debe reasignar una variable de parámetro / argumento a un objeto que no sea el objeto pasado por el método de llamada. En los ejemplos anteriores, nunca se debe escribir la línea arg =. Como los humanos cometen errores y los programadores son humanos, solicitemos ayuda al compilador. Marque cada variable de parámetro / argumento como 'final' para que el compilador pueda encontrar y marcar tales reasignaciones.

En retrospectiva

Como se señaló en otras respuestas ... Dado el objetivo de diseño original de Java de ayudar a los programadores a evitar errores tontos, como leer más allá del final de una matriz, Java debería haber sido diseñado para aplicar automáticamente todas las variables de parámetro / argumento como 'final'. En otras palabras, los argumentos no deben ser variables . Pero la retrospectiva es una visión 20/20, y los diseñadores de Java tenían las manos ocupadas en ese momento.

Entonces, ¿siempre agregar finala todos los argumentos?

¿Debemos agregar finala todos y cada uno de los parámetros del método que se declara?

  • En teoría sí.
  • En la práctica, no.
    ➥ Agregue finalsolo cuando el código del método es largo o complicado, donde el argumento puede confundirse con una variable local o miembro y posiblemente reasignarse.

Si acepta la práctica de no reasignar nunca un argumento, estará inclinado a agregar un finala cada uno. Pero esto es tedioso y hace que la declaración sea un poco más difícil de leer.

Para el código simple corto donde el argumento es obviamente un argumento, y no una variable local ni una variable miembro, no me molesto en agregar el final. Si el código es bastante obvio, sin posibilidad de que yo ni ningún otro programador realice tareas de mantenimiento o refactorice accidentalmente confundiendo la variable de argumento como algo más que un argumento, entonces no se moleste. En mi propio trabajo, agrego finalsolo código más largo o más involucrado donde un argumento puede confundirse con una variable local o miembro.

Otro caso agregado para la integridad

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
 }
Albahaca Bourque
fuente
23
"Como buena práctica de programación (en cualquier lenguaje), nunca debes reasignar una variable de parámetro / argumento [..]" Lo siento, realmente tengo que llamarte en este caso. La reasignación de argumentos es una práctica estándar en lenguajes como Javascript, donde la cantidad de argumentos pasados ​​(o incluso si se pasan) no está dictada por la firma del método. Por ejemplo, con una firma como: "function say (msg)" la gente se asegurará de que se asigne el argumento 'msg', así: "msg = msg || 'Hello World!';". Los mejores programadores de Javascript del mundo están rompiendo su buena práctica. Solo lea la fuente jQuery.
Stijn de Witt
37
@StijndeWitt Su ejemplo muestra el problema mismo de reasignar la variable de argumento. Pierde información sin obtener nada a cambio: (a) Ha perdido el valor original pasado, (b) Ha perdido la intención del método de llamada (¡La persona que llama pasó 'Hola Mundo!' O hemos omitido). Tanto a & b son útiles para las pruebas, el código largo y cuando el valor se modifica más adelante. Respaldo mi afirmación: los argumentos arg nunca deben reasignarse. El código debe ser: message = ( msg || 'Hello World"' ). Simplemente no hay razón para no usar una var. El único costo es unos pocos bytes de memoria.
Basil Bourque
99
@Basil: es más código (en bytes) y en Javascript que cuenta. Fuertemente. Como con muchas cosas, está basado en opiniones. Es completamente posible ignorar por completo esta práctica de programación y aún así escribir un código excelente. La práctica de programación de una persona no la convierte en práctica de todos. Quédese todo lo que quiera, elijo escribirlo diferente de todos modos. ¿Eso me convierte en un mal programador o mi código es un mal código?
Stijn de Witt
14
Usar message = ( msg || 'Hello World"' )me arriesga más tarde accidentalmente msg. Cuando el contrato que pretendo es "el comportamiento sin argumento / nulo / indefinido es indistinguible de aprobar "Hello World"", es una buena práctica de programación comprometerse con él al principio de la función. [Esto se puede lograr sin reasignación comenzando con, if (!msg) return myfunc("Hello World");pero eso se vuelve difícil de manejar con múltiples argumentos.] En los raros casos en que la lógica en la función debería importar si se usó el valor predeterminado, prefiero designar un valor centinela especial (preferiblemente público) .
Beni Cherniavsky-Paskin
66
@ BeniCherniavsky-Paskin, el riesgo que describe es solo por la similitud entre messagey msg. Pero si lo llamara algo así processedMsgo algo más que proporciona un contexto adicional, la posibilidad de un error es mucho menor. Concéntrese en lo que dice, no en "cómo" lo dice. ;)
alfasin
230

A veces es bueno ser explícito (para facilitar la lectura) que la variable no cambia. Aquí hay un ejemplo simple donde usar finalpuede ahorrar algunos posibles dolores de cabeza:

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

Si olvida la palabra clave 'this' en un setter, entonces la variable que desea establecer no se establece. Sin embargo, si usó la finalpalabra clave en el parámetro, el error se detectaría en el momento de la compilación.

Jerry Sha
fuente
65
por cierto, verá la advertencia "La asignación a la prueba de variables no tiene ningún efecto" de todos modos
AvrDragon
12
@AvrDragon Pero, también podríamos ignorar la advertencia. Por lo tanto, siempre es mejor tener algo que nos impida ir más allá, como un error de compilación, que obtendremos al usar la palabra clave final.
Sumit Desai
10
@AvrDragon Eso depende del entorno de desarrollo. De todos modos, no debe confiar en el IDE para atrapar cosas como esta, a menos que desee desarrollar malos hábitos.
b1nary.atr0phy
25
@ b1naryatr0phy en realidad es una advertencia del compilador, no solo IDE-tip
AvrDragon
8
@SumitDesai "Pero también podríamos ignorar la advertencia. Por lo tanto, siempre es mejor tener algo que nos impida avanzar más, como un error de compilación, que obtendremos al usar la palabra clave final". Entiendo su punto de vista, pero esta es una declaración muy fuerte con la que creo que muchos desarrolladores de Java no estarían de acuerdo. Las advertencias del compilador están ahí por una razón y un desarrollador competente no debería necesitar un error para 'obligarlos' a considerar sus implicaciones.
Stuart Rossiter
127

Sí, excluyendo clases anónimas, legibilidad y declaración de intenciones es casi inútil. Sin embargo, ¿son inútiles esas tres cosas?

Personalmente, tiendo a no usar finalpara variables y parámetros locales a menos que esté usando la variable en una clase interna anónima, pero ciertamente puedo ver el punto de aquellos que quieren dejar en claro que el valor del parámetro en sí no cambiará (incluso si el objeto al que hace referencia cambia su contenido). Para aquellos que encuentran que eso aumenta la legibilidad, creo que es algo completamente razonable.

Su punto sería más importante si alguien eran en realidad afirmando que se mantenga constante de datos de una manera que no lo hace - pero no puedo recordar haber visto ninguna de dichas reivindicaciones. ¿Estás sugiriendo que hay un cuerpo significativo de desarrolladores que sugiere que finaltiene más efecto de lo que realmente tiene?

EDITAR: Realmente debería haber resumido todo esto con una referencia de Monty Python; la pregunta parece algo similar a preguntar "¿Qué han hecho los romanos por nosotros?"

Jon Skeet
fuente
17
Pero parafraseando a Krusty con su danés, ¿qué han hecho LATAMENTE por nosotros ? =)
James Schek
Yuval ¡Eso es gracioso! ¡Supongo que la paz puede suceder incluso si la espada la impone!
Gonzobrains
1
La pregunta parece más similar a preguntar, "¿Qué no han hecho los romanos por nosotros?", Porque es más una crítica de lo que no hace la palabra clave final .
NobleUplift
"¿Estás sugiriendo que hay un cuerpo significativo de desarrolladores sugiriendo que la final tiene más efecto de lo que realmente tiene?" Para mí, ese es el problema principal: sospecho que una proporción significativa de desarrolladores que lo usan piensan que impone la inmutabilidad de los elementos pasados de la persona que llama cuando no lo hace. Por supuesto, uno se ve envuelto en el debate sobre si los estándares de codificación deberían 'proteger contra' malentendidos conceptuales (que un desarrollador 'competente' debería tener en cuenta) o no (y esto luego se dirige hacia opiniones fuera del alcance del SO -tipo de pregunta)!
Stuart Rossiter
1
@SarthakMittal: el valor no se copiará a menos que realmente lo use, si eso es lo que se está preguntando.
Jon Skeet
76

Déjame explicarte un poco sobre el único caso en el que tienes que usar el final, que Jon ya mencionó:

Si crea una clase interna anónima en su método y usa una variable local (como un parámetro de método) dentro de esa clase, entonces el compilador lo obliga a hacer que el parámetro sea final:

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

Aquí los parámetros fromy todeben ser finales para que puedan usarse dentro de la clase anónima.

La razón de este requisito es esta: las variables locales viven en la pila, por lo tanto, existen solo mientras se ejecuta el método. Sin embargo, la instancia de clase anónima se devuelve del método, por lo que puede vivir mucho más tiempo. No puede preservar la pila, porque es necesaria para las llamadas de método posteriores.

Entonces, lo que Java hace es colocar copias de esas variables locales como variables de instancia ocultas en la clase anónima (puede verlas si examina el código de bytes). Pero si no fueran finales, uno podría esperar que la clase anónima y el método vean los cambios que el otro hace a la variable. Para mantener la ilusión de que solo hay una variable en lugar de dos copias, tiene que ser final.

Michael Borgwardt
fuente
1
Me perdiste de "Pero si no son finales ..." ¿Puedes intentar reformularlo? Tal vez no he tenido suficiente café.
hhafez 02 de
1
Tiene una variable local de - la pregunta es qué sucede si usa la instancia de clase anon dentro del método y cambia los valores de from - la gente esperaría que el cambio sea visible en el método, ya que solo ven una variable. Para evitar esta confusión, debe ser final.
Michael Borgwardt
No hace una copia, es simplemente una referencia a cualquier objeto al que se haya hecho referencia.
vickirk
1
@vickirk: seguro de que hace una copia de la referencia, en caso de tipos de referencia.
Michael Borgwardt
Por cierto, suponiendo que no tengamos clases anónimas que hagan referencia a esas variables, ¿sabe si hay alguna diferencia entre un finalparámetro de función y un parámetro de función no final a los ojos de HotSpot?
Pacerier
25

Yo uso final todo el tiempo en los parámetros.

¿Agrega eso tanto? Realmente no.

¿Lo apagaría? No.

La razón: encontré 3 errores en los que la gente había escrito código descuidado y no había podido establecer una variable miembro en los accesores. Todos los errores resultaron difíciles de encontrar.

Me gustaría ver que esto sea el predeterminado en una futura versión de Java. El paso por valor / referencia hace tropezar a un montón de programadores junior.

Una cosa más ... mis métodos tienden a tener un número bajo de parámetros, por lo que el texto adicional en una declaración de método no es un problema.

Fortyrunner
fuente
44
Estaba a punto de sugerir esto también, que el final será el predeterminado en futuras versiones y que debe especificar la palabra clave "mutable" o mejor que se concibe. Aquí hay un buen artículo sobre esto: lpar.ath0.com/2008/08/26/java-annoyance-final-parameters
Jeff Axelrod
Ha pasado mucho tiempo, pero ¿podría dar un ejemplo del error que atrapó?
user949300
Vea la respuesta más votada. Eso tiene un muy buen ejemplo donde la variable miembro no está establecida y en su lugar el parámetro está mutado.
Fortyrunner
18

Usar final en un parámetro de método no tiene nada que ver con lo que sucede con el argumento en el lado del llamante. Solo tiene la intención de marcarlo como que no cambia dentro de ese método. Cuando trato de adoptar un estilo de programación más funcional, veo el valor en eso.

Alemán
fuente
2
Exactamente, no es parte de la interfaz de la función, solo la implementación. Es confuso que Java permita (pero dirsregards) final en parámetros en interfaces / declaraciones de métodos abstractos.
Beni Cherniavsky-Paskin
8

Personalmente, no uso final en los parámetros del método, porque agrega demasiado desorden a las listas de parámetros. Prefiero hacer cumplir que los parámetros del método no se cambien a través de algo como Checkstyle.

Para las variables locales que uso final siempre que sea posible, incluso dejo que Eclipse lo haga automáticamente en mi configuración para proyectos personales.

Ciertamente me gustaría algo más fuerte como C / C ++ const.

starblue
fuente
No estoy seguro de que IDE y referencias de herramientas sean aplicables a la publicación de OP o al tema. A saber, "final" es una verificación en tiempo de compilación de que la referencia no se modifica / asalta. Además, para hacer cumplir realmente tales cosas, vea la respuesta sobre la no protección a los miembros menores de las referencias finales. Al construir una API, por ejemplo, usar un IDE o una herramienta no ayudará a las partes externas a usar / extender dicho código.
Darrell Teague el
4

Como Java pasa copias de los argumentos, siento que la relevancia de esto finales bastante limitada. Supongo que el hábito proviene de la era de C ++, donde podría prohibir que se cambie el contenido de referencia haciendo un const char const *. Siento que este tipo de cosas te hace creer que el desarrollador es inherentemente estúpido como un maldito y necesita estar protegido contra todos los personajes que escribe. Con toda humildad, puedo decir que escribo muy pocos errores aunque omito final(a menos que no quiera que alguien anule mis métodos y clases). Tal vez solo soy un desarrollador de la vieja escuela.

Lawrence
fuente
1

Nunca uso final en una lista de parámetros, solo agrega desorden como han dicho los encuestados anteriores. También en Eclipse puede configurar la asignación de parámetros para generar un error, por lo que usar final en una lista de parámetros me parece bastante redundante. Curiosamente, cuando habilité la configuración de Eclipse para la asignación de parámetros que generaba un error, captó este código (así es como recuerdo el flujo, no el código real):

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

    if (i > 100)
        A += C;

    return A;
}

Jugando al abogado del diablo, ¿qué tiene de malo hacer esto?

Chris Milburn
fuente
Tenga cuidado de diferenciar un IDE de una JVM de tiempo de ejecución. Cualquier cosa que haga un IDE es irrelevante cuando el código de byte compilado se ejecuta en el servidor, a menos que IDE agregue código para protegerlo contra el atraco de las variables miembro, como una falla en el código cuando se pretendía que una variable no se reasignara, sino que fue erróneamente, de ahí el propósito de palabra clave final
Darrell Teague el
1

Respuesta corta: finalayuda un poco, pero ... usa la programación defensiva en el lado del cliente.

De hecho, el problema finales que solo impone que la referencia no se modifique, permitiendo alegremente que los miembros del objeto a los que se hace referencia muten, sin que el llamante lo sepa. Por lo tanto, la mejor práctica a este respecto es la programación defensiva en el lado de la persona que llama, creando instancias profundamente inmutables o copias profundas de objetos que están en peligro de ser asaltados por API sin escrúpulos.

Darrell Teague
fuente
2
"El problema con final es que solo impone que la referencia no cambie " - No es cierto, Java mismo lo impide. Una variable pasada a un método no puede tener su referencia cambiada por ese método.
Madbreaks
Investigue antes de publicar ... stackoverflow.com/questions/40480/…
Darrell Teague el
En pocas palabras, si fuera cierto que las referencias a las referencias no pueden cambiarse, no habría discusión sobre copia defensiva, inmutabilidad, no hay necesidad de una palabra clave final, etc.
Darrell Teague
O me estás entendiendo mal o te equivocas. Si paso una referencia de objeto a un método, y ese método lo reasigna, la referencia original permanece intacta para (yo) la persona que llama cuando el método completa su ejecución. Java es estrictamente de paso por valor. Y eres descaradamente presuntuoso al afirmar que no he hecho ninguna investigación.
Madbreaks
Voto a favor porque op preguntó por qué usar final, y usted dio solo una razón incorrecta.
Madbreaks
0

Una razón adicional para agregar declaraciones finales a los parámetros es que ayuda a identificar las variables que necesitan ser renombradas como parte de una refactorización de "Método de extracción". Descubrí que agregar el final a cada parámetro antes de comenzar una refactorización de métodos grandes me dice rápidamente si hay algún problema que deba abordar antes de continuar.

Sin embargo, generalmente los elimino como superfluos al final de la refactorización.

Michael Rutherfurd
fuente
-1

Seguimiento de lo que postea Michel. Yo mismo hice otro ejemplo para explicarlo. Espero que pueda ayudar.

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

Del código anterior, en el método thisIsWhy () , en realidad no asignamos el [argumento MyObj obj] a una referencia real en MyParam. En cambio, solo usamos el [argumento MyObj obj] en el método dentro de MyParam.

Pero después de que terminemos el método thisIsWhy () , ¿ debería existir el argumento (objeto) MyObj?

Parece que debería, porque podemos ver en main todavía llamamos al método showObjName () y necesita llegar a obj . MyParam todavía usa / alcanza el argumento del método, ¡incluso el método ya fue devuelto!

Cómo Java realmente logra esto es generar una copia también es una referencia oculta del argumento MyObj obj dentro del objeto MyParam (pero no es un campo formal en MyParam para que no podamos verlo)

Como llamamos "showObjName", usará esa referencia para obtener el valor correspondiente.

Pero si no pusimos el argumento final, lo que lleva a una situación, podemos reasignar una nueva memoria (objeto) al argumento MyObj obj .

¡Técnicamente no hay choque en absoluto! Si se nos permite hacer eso, a continuación será la situación:

  1. Ahora tenemos un punto oculto [MyObj obj] a una [Memoria A en el montón] ahora en vivo en el objeto MyParam.
  2. También tenemos otro [MyObj obj] que es el punto de argumento para que una [Memoria B en el montón] ahora viva en el método thisIsWhy.

Sin choque, pero "¡¡¡CONFUSENTE !!" Porque todos usan el mismo "nombre de referencia" que es "obj" .

Para evitar esto, configúrelo como "final" para evitar que el programador haga el código "propenso a errores".

KunYu Tsai
fuente