¿Por qué las variables locales no se inicializan en Java?

103

¿Hubo alguna razón por la que los diseñadores de Java sintieron que a las variables locales no se les debería dar un valor predeterminado? En serio, si a las variables de instancia se les puede dar un valor predeterminado, ¿por qué no podemos hacer lo mismo con las variables locales?

Y también conduce a problemas como se explica en este comentario a una publicación de blog :

Bueno, esta regla es más frustrante cuando se intenta cerrar un recurso en un bloque final. Si creo una instancia del recurso dentro de un intento, pero trato de cerrarlo en el final, aparece este error. Si muevo la instanciación fuera del intento, aparece otro error que indica que debe estar dentro de un intento.

Muy frustrante.

Shivasubramanian A
fuente
1
Lo siento ... esta pregunta no apareció cuando estaba escribiendo la pregunta ... sin embargo, creo que hay una diferencia entre las dos preguntas ... Quiero saber por qué los diseñadores de Java lo diseñaron de esta manera, mientras que la pregunta que señaló no hace eso ...
Shivasubramanian A
Consulte también esta pregunta relacionada de C #: stackoverflow.com/questions/1542824/…
Raedwald
Simplemente, porque es fácil para el compilador rastrear las variables locales no inicializadas. Si pudiera hacer lo mismo con otras variables, lo haría. El compilador solo intenta ayudarlo.
rustyx

Respuestas:

62

Las variables locales se declaran principalmente para hacer algunos cálculos. Entonces, es decisión del programador establecer el valor de la variable y no debe tomar un valor predeterminado. Si el programador, por error, no inicializó una variable local y toma un valor predeterminado, entonces la salida podría ser un valor inesperado. Entonces, en el caso de las variables locales, el compilador le pedirá al programador que inicialice con algún valor antes de acceder a la variable para evitar el uso de valores indefinidos.

Guerrero
fuente
23

El "problema" al que enlaza parece describir esta situación:

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

La queja del comentarista es que el compilador se resiste a la línea de la finallysección, alegando que sopodría no estar inicializada. El comentario luego menciona otra forma de escribir el código, probablemente algo como esto:

// Do some work here ...
SomeObject so = new SomeObject();
try {
  so.DoUsefulThings();
} finally {
  so.CleanUp();
}

El comentarista no está satisfecho con esa solución porque el compilador dice que el código "debe estar en un intento". Supongo que eso significa que parte del código puede generar una excepción que ya no se maneja. No estoy seguro. Ninguna versión de mi código maneja excepciones, por lo que cualquier cosa relacionada con la excepción en la primera versión debería funcionar igual en la segunda.

De todos modos, esta segunda versión de código es la forma correcta de escribirlo. En la primera versión, el mensaje de error del compilador era correcto. Es soposible que la variable no esté inicializada. En particular, si el SomeObjectconstructor falla, sono se inicializará, por lo que será un error intentar llamar so.CleanUp. Entra siempre en la trysección después de haber adquirido el recurso que finallyfinaliza la sección.

El bloque try- finallydespués de la soinicialización está ahí solo para proteger la SomeObjectinstancia, para asegurarse de que se limpie sin importar lo que suceda. Si hay otras cosas que deben ejecutarse, pero no están relacionados a si la SomeObjectinstancia se asignó la propiedad, entonces deben ir en otra try - finallybloque, probablemente uno que envuelve la que yo he mostrado.

Exigir que las variables se asignen manualmente antes de su uso no genera problemas reales. Solo conduce a problemas menores, pero su código será mejor para eso. Vas a tener variables con alcance más limitado, y try- finallylos bloques que no tratan de proteger demasiado.

Si las variables locales tuvieran valores predeterminados, entonces soen el primer ejemplo habría sido null. Eso realmente no habría resuelto nada. En lugar de obtener un error en tiempo de compilación en el finallybloque, tendrías un NullPointerExceptionacecho allí que podría ocultar cualquier otra excepción que pudiera ocurrir en la sección "Trabaja un poco aquí" del código. (¿O las excepciones en las finallysecciones se encadenan automáticamente a la excepción anterior? No recuerdo. Aun así, tendrías una excepción adicional en el camino de la real).

Rob Kennedy
fuente
2
¿Por qué no tener un if (so! = null) ... en el bloque finalmente?
izb
eso todavía causará la advertencia / error del compilador; no creo que el compilador entienda eso si verifica (pero solo estoy haciendo esto fuera de la memoria, no probado).
Chii
6
Pondría solo SomeObject so = null antes de intentar y pondría una verificación nula en la cláusula finalmente. De esta forma no habrá advertencias del compilador.
Juha Syrjälä
¿Por qué complicar las cosas? Escriba el bloque probar-finalmente de esta manera, y SABERÁ que la variable tiene un valor válido. No se requiere verificación de nulos.
Rob Kennedy
1
Rob, su ejemplo "new SomeObject ()" es simple y no se deben generar excepciones allí, pero si la llamada puede generar excepciones, entonces sería mejor que ocurra dentro del bloque try para que pueda ser manejado.
Sarel Botha
12

Además, en el siguiente ejemplo, es posible que se haya lanzado una excepción dentro de la construcción SomeObject, en cuyo caso la variable 'so' sería nula y la llamada a CleanUp arrojará una NullPointerException

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

Lo que tiendo a hacer es esto:

SomeObject so = null;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  if (so != null) {
     so.CleanUp(); // safe
  }
}
Monje eléctrico
fuente
12
¿Qué preferirías hacer?
Electric Monk
2
Sí, feo. Sí, también es lo que hago.
SMBiggs
@ElectricMonk ¿Qué forma crees que es mejor, la que mostraste o la que se muestra aquí en el método getContents (..): javapractices.com/topic/TopicAction.do?Id=126
Atom
11

Tenga en cuenta que las variables de instancia / miembro finales no se inicializan de forma predeterminada. Porque esos son definitivos y no se pueden cambiar en el programa posteriormente. Esa es la razón por la que Java no les da ningún valor predeterminado y obliga al programador a inicializarlo.

Por otro lado, las variables de miembros no finales se pueden cambiar más tarde. Por lo tanto, el compilador no permite que permanezcan sin inicializar, precisamente, porque se pueden cambiar más tarde. En cuanto a las variables locales, el alcance de las variables locales es mucho más estrecho. El compilador sabe cuándo se está utilizando. Por lo tanto, tiene sentido forzar al programador a inicializar la variable.

Adeel Ansari
fuente
9

La respuesta real a su pregunta es porque las variables de método se instancian simplemente agregando un número al puntero de la pila. Ponerlos a cero sería un paso más. Para las variables de clase, se colocan en la memoria inicializada en el montón.

¿Por qué no dar un paso más? Dé un paso atrás - Nadie mencionó que la "advertencia" en este caso es algo muy bueno.

Nunca debe inicializar su variable a cero o nula en la primera pasada (cuando la está codificando por primera vez). Asignelo al valor real o no lo asigne en absoluto porque si no lo hace, Java puede decirle cuándo realmente lo arruinó. Tome la respuesta de Electric Monk como un gran ejemplo. En el primer caso, en realidad es increíblemente útil que te esté diciendo que si el try () falla porque el constructor de SomeObject arrojó una excepción, entonces terminarías con un NPE en el final. Si el constructor no puede lanzar una excepción, no debería estar en el intento.

Esta advertencia es un increíble verificador de programadores defectuosos de múltiples rutas que me ha salvado de hacer cosas estúpidas, ya que verifica cada ruta y se asegura de que si usó la variable en alguna ruta, entonces tuvo que inicializarla en cada ruta que conduzca a ella. . Ahora nunca inicializo explícitamente las variables hasta que determine que es lo correcto.

Además de eso, ¿no es mejor decir explícitamente "int size = 0" en lugar de "int size" y hacer que el próximo programador se dé cuenta de que su intención es que sea cero?

Por otro lado, no puedo encontrar una sola razón válida para que el compilador inicialice todas las variables no inicializadas a 0.

Bill K
fuente
1
Sí, y hay otros en los que más o menos tiene que inicializarse en nulo debido a la forma en que fluye el código; no debería haber dicho "nunca" actualizó la respuesta para reflejar esto.
Bill K
4

Creo que el propósito principal era mantener la similitud con C / C ++. Sin embargo, el compilador detecta y le advierte sobre el uso de variables no inicializadas que reducirán el problema a un punto mínimo. Desde la perspectiva del rendimiento, es un poco más rápido permitirle declarar variables no inicializadas, ya que el compilador no tendrá que escribir una declaración de asignación, incluso si sobrescribe el valor de la variable en la siguiente declaración.

Mehrdad Afshari
fuente
1
Podría decirse que el compilador podría determinar si siempre asigna a la variable antes de hacer algo con ella, y suprimir una asignación automática de valor predeterminado en tal caso. Si el compilador no puede determinar si una asignación ocurre antes del acceso, generaría la asignación predeterminada.
Greg Hewgill
2
Sí, pero se podría argumentar que le permite al programador saber si ha dejado la variable sin inicializar por error.
Mehrdad Afshari
1
El compilador también podría hacer eso en cualquier caso. :) Personalmente, preferiría que el compilador trate una variable no inicializada como un error. Significa que podría haber cometido un error en alguna parte.
Greg Hewgill
No soy un tipo de Java, pero me gusta la forma en que C # lo maneja. La diferencia es que en ese caso, el compilador tuvo que emitir una advertencia, lo que podría hacer que obtenga un par de cientos de advertencias para su programa correcto;)
Mehrdad Afshari
¿Advierte también sobre las variables miembro?
Adeel Ansari
4

(Puede parecer extraño publicar una nueva respuesta tanto tiempo después de la pregunta, pero surgió un duplicado ).

Para mí, la razón se reduce a esto: el propósito de las variables locales es diferente al propósito de las variables de instancia. Las variables locales se utilizan como parte de un cálculo; las variables de instancia están ahí para contener el estado. Si usa una variable local sin asignarle un valor, es casi seguro que se trata de un error lógico.

Dicho esto, podría atrasarme totalmente en exigir que las variables de instancia siempre se inicialicen explícitamente; el error ocurriría en cualquier constructor donde el resultado permita una variable de instancia no inicializada (por ejemplo, no inicializada en la declaración y no en el constructor). Pero esa no es la decisión de Gosling, et. al., tomó a principios de los 90, así que aquí estamos. (Y no estoy diciendo que hayan tomado la decisión incorrecta).

Sin embargo, no pude respaldar las variables locales predeterminadas. Sí, no deberíamos confiar en los compiladores para verificar nuestra lógica, y uno no lo hace, pero sigue siendo útil cuando el compilador detecta uno. :-)

TJ Crowder
fuente
"Dicho esto, podría estar totalmente atrasado en el requisito de que las variables de instancia siempre se inicialicen explícitamente ..." que, FWIW, es la dirección que han tomado en TypeScript.
TJ Crowder
3

Es más eficiente no inicializar variables y, en el caso de variables locales, es seguro hacerlo, porque el compilador puede rastrear la inicialización.

En los casos en los que necesite inicializar una variable, siempre puede hacerlo usted mismo, por lo que no es un problema.

estrella azul
fuente
3

La idea detrás de las variables locales es que solo existen dentro del ámbito limitado para el que son necesarias. Como tal, debería haber pocas razones para la incertidumbre en cuanto al valor, o al menos, de dónde proviene ese valor. Podría imaginar muchos errores derivados de tener un valor predeterminado para las variables locales.

Por ejemplo, considere el siguiente código simple ... ( NB, supongamos con fines de demostración que a las variables locales se les asigna un valor predeterminado, como se especifica, si no se inicializan explícitamente )

System.out.println("Enter grade");
int grade = new Scanner(System.in).nextInt(); //I won't bother with exception handling here, to cut down on lines.
char letterGrade; //let us assume the default value for a char is '\0'
if (grade >= 90)
    letterGrade = 'A';
else if (grade >= 80)
    letterGrade = 'B';
else if (grade >= 70)
    letterGrade = 'C';
else if (grade >= 60)
    letterGrade = 'D';
else
    letterGrade = 'F';
System.out.println("Your grade is " + letterGrade);

Cuando todo está dicho y hecho, asumiendo que el compilador asignó un valor predeterminado de '\ 0' a letterGrade , este código tal como está escrito funcionaría correctamente. Sin embargo, ¿qué pasa si olvidamos la declaración else?

Una ejecución de prueba de nuestro código puede resultar en lo siguiente

Enter grade
43
Your grade is

Este resultado, aunque era de esperar, seguramente no fue la intención del codificador. De hecho, probablemente en la gran mayoría de los casos (o al menos, en un número significativo de los mismos), el valor predeterminado no sería el valor deseado , por lo que en la gran mayoría de los casos el valor predeterminado resultaría en un error. Tiene más sentido forzar al codificador a asignar un valor inicial a una variable local antes de usarla, ya que el problema de depuración causado por olvidar el = 1in for(int i = 1; i < 10; i++)supera con creces la conveniencia de no tener que incluir el = 0in for(int i; i < 10; i++).

Es cierto que los bloques try-catch-finalmente pueden volverse un poco desordenados (pero en realidad no es un catch-22 como parece sugerir la cita), cuando, por ejemplo, un objeto arroja una excepción marcada en su constructor, pero para uno razón u otra, se debe hacer algo con este objeto al final del bloque en finalmente. Un ejemplo perfecto de esto es cuando se trata de recursos, que deben cerrarse.

Una forma de manejar esto en el pasado podría ser así ...

Scanner s = null; //declared and initialized to null outside the block. This gives us the needed scope, and an initial value.
try {
    s = new Scanner(new FileInputStream(new File("filename.txt")));
    int someInt = s.nextInt();
} catch (InputMismatchException e) {
    System.out.println("Some error message");
} catch (IOException e) {
    System.out.println("different error message"); 
} finally {
    if (s != null) //in case exception during initialization prevents assignment of new non-null value to s.
        s.close();
}

Sin embargo, a partir de Java 7, este bloque finalmente ya no es necesario usando try-with-resources, como ese.

try (Scanner s = new Scanner(new FileInputStream(new File("filename.txt")))) {
...
...
} catch(IOException e) {
    System.out.println("different error message");
}

Dicho esto, (como sugiere el nombre) esto solo funciona con recursos.

Y aunque el ejemplo anterior es un poco asqueroso, esto quizás se refiera más a la forma en que se implementan try-catch-finalmente o estas clases que a las variables locales y cómo se implementan.

Es cierto que los campos se inicializan con un valor predeterminado, pero esto es un poco diferente. Cuando dice, por ejemplo, int[] arr = new int[10];tan pronto como haya inicializado esta matriz, el objeto existe en la memoria en una ubicación determinada. Supongamos por un momento que no hay valores predeterminados, sino que el valor inicial es cualquier serie de 1 y 0 que esté en esa ubicación de memoria en ese momento. Esto podría llevar a un comportamiento no determinista en varios casos.

Supongamos que tenemos ...

int[] arr = new int[10];
if(arr[0] == 0)
    System.out.println("Same.");
else
    System.out.println("Not same.");

Sería perfectamente posible que Same.se mostrara en una ejecución y Not same.se mostrara en otra. El problema podría volverse aún más grave una vez que comience a hablar de variables de referencia.

String[] s = new String[5];

Según la definición, cada elemento de s debe apuntar a una cadena (o es nulo). Sin embargo, si el valor inicial es cualquier serie de 0 y 1 que ocurra en esta ubicación de memoria, no solo no hay garantía de que obtendrá los mismos resultados cada vez, sino que tampoco hay garantía de que el objeto s [0] apunte a (asumiendo que apunta a algo significativo) incluso es una Cadena (quizás sea un Conejo,: p )! Esta falta de preocupación por el tipo iría en contra de casi todo lo que hace Java Java. Entonces, si bien tener valores predeterminados para las variables locales podría verse como opcional en el mejor de los casos, tener valores predeterminados para las variables de ejemplo está más cerca de una necesidad .

kumquatfelafel
fuente
1

si no me equivoco, otra razón podría ser

Dar el valor predeterminado de las variables miembro es parte de la carga de clases

La carga de clases es una cosa en tiempo de ejecución en Java, lo que significa que cuando crea un objeto, la clase se carga con la carga de clases, solo las variables miembro se inicializan con el valor predeterminado JVM no toma tiempo para dar un valor predeterminado a sus variables locales porque algunos métodos nunca serán llamado porque la llamada al método puede ser condicional, entonces, ¿por qué tomarse el tiempo para darles un valor predeterminado y reducir el rendimiento si esos valores predeterminados nunca se van a utilizar?

Abhinav Chauhan
fuente
0

Eclipse incluso le da advertencias de variables no inicializadas, por lo que se vuelve bastante obvio de todos modos. Personalmente, creo que es bueno que este sea el comportamiento predeterminado; de lo contrario, su aplicación puede usar valores inesperados y, en lugar de que el compilador arroje un error, no hará nada (pero tal vez dará una advertencia) y luego estará rascando su cabeza en cuanto a por qué ciertas cosas no se comportan como deberían.

Kezzer
fuente
0

Las variables locales se almacenan en una pila, pero las variables de instancia se almacenan en el montón, por lo que hay algunas posibilidades de que se lea un valor anterior en la pila en lugar de un valor predeterminado como sucede en el montón. Por esa razón, jvm no permite usar una variable local sin inicializarla.

David Santamaría
fuente
2
Totalmente mal ... todos los no primitivos de Java se almacenan en el montón, independientemente de cuándo y cómo se construyan
gshauger
Antes de Java 7, las variables de instancia se almacenan en el montón y las variables locales se encuentran en la pila. Sin embargo, cualquier objeto al que haga referencia una variable local se encontrará en el montón. A partir de Java 7, el "compilador del servidor Java Hotspot" podría realizar un "análisis de escape" y decidir asignar algunos objetos en la pila en lugar del montón.
mamills
0

La variable de instancia tendrá valores predeterminados, pero las variables locales no pueden tener valores predeterminados. Dado que las variables locales están básicamente en métodos / comportamiento, su objetivo principal es realizar algunas operaciones o cálculos. Por lo tanto, no es una buena idea establecer valores predeterminados para las variables locales. De lo contrario, es muy difícil y requiere mucho tiempo verificar las razones de las respuestas inesperadas.

Xiaogang
fuente
-1

La respuesta es que las variables de instancia se pueden inicializar en el constructor de la clase o en cualquier método de la clase, pero en el caso de las variables locales, una vez que haya definido lo que sea en el método, permanecerá para siempre en la clase.

Akash5288
fuente
-2

Podría pensar en seguir 2 razones

  1. Como dicen la mayoría de las respuestas al poner la restricción de inicializar la variable local, se asegura que a la variable local se le asigna un valor como el programador quiere y asegura que se calculen los resultados esperados.
  2. Las variables de instancia se pueden ocultar declarando variables locales (mismo nombre): para garantizar el comportamiento esperado, las variables locales están obligadas a inicializar un valor. (Sin embargo, evitaría totalmente esto)
Mitra
fuente
Los campos no se pueden anular. A lo sumo, se pueden ocultar y no veo cómo la ocultación interferiría con una comprobación de inicialización.
meriton
Derecha oculta Si uno decide crear una variable local con el mismo nombre que la instancia, debido a esta restricción, la variable local se inicializará con un valor seleccionado con el propósito (que no sea el valor de la variable de instancia)
Mitra