Por que as variáveis ​​locais não são inicializadas em Java?


103

Houve alguma razão pela qual os designers de Java sentiram que variáveis ​​locais não deveriam receber um valor padrão? Sério, se variáveis ​​de instância podem receber um valor padrão, então por que não podemos fazer o mesmo para variáveis ​​locais?

E também leva a problemas, conforme explicado neste comentário em uma postagem do blog :

Bem, esta regra é mais frustrante ao tentar fechar um recurso em um bloco final. Se eu instanciar o recurso em uma tentativa, mas tento fechá-lo no finalmente, recebo este erro. Se eu mover a instanciação fora do try, recebo outro erro informando que deve estar dentro de um try.

Muito frustrante.



1
Desculpe por isso ... essa pergunta não apareceu quando eu estava digitando a pergunta .. no entanto, acho que há uma diferença entre as duas perguntas ... Eu quero saber por que os designers de Java o projetaram dessa forma, enquanto a questão para a qual você apontou não pergunta isso ...
Shivasubramanian A

Veja também esta questão C # relacionada: stackoverflow.com/questions/1542824/…
Raedwald

Simplesmente - porque é fácil para o compilador rastrear variáveis ​​locais não inicializadas. Se pudesse fazer o mesmo com outras variáveis, faria. O compilador está apenas tentando ajudá-lo.
enferrujado

Respostas:


62

Variáveis ​​locais são declaradas principalmente para fazer alguns cálculos. Portanto, é decisão do programador definir o valor da variável e não deve assumir um valor padrão. Se o programador, por engano, não inicializou uma variável local e assumiu o valor padrão, então a saída pode ser algum valor inesperado. Portanto, no caso de variáveis ​​locais, o compilador pedirá ao programador para inicializar com algum valor antes de acessar a variável para evitar o uso de valores indefinidos.


23

O "problema" que você vincula parece descrever esta situação:

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

A reclamação do comentarista é que o compilador recua na linha da finallyseção, alegando que sopode não ter sido inicializado. O comentário então menciona outra maneira de escrever o código, provavelmente algo assim:

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

O comentarista não está satisfeito com a solução porque o compilador diz que o código "deve estar dentro de uma tentativa". Acho que isso significa que parte do código pode gerar uma exceção que não é mais tratada. Não tenho certeza. Nenhuma das versões do meu código lida com exceções, portanto, qualquer coisa relacionada a exceções na primeira versão deve funcionar da mesma forma na segunda.

De qualquer forma, essa segunda versão de código é a maneira correta de escrevê-lo. Na primeira versão, a mensagem de erro do compilador estava correta. A sovariável pode não ter sido inicializada. Em particular, se o SomeObjectconstrutor falhar, sonão será inicializado e, portanto, será um erro tentar chamar so.CleanUp. Sempre entre na tryseção depois de ter adquirido o recurso que a finallyseção finaliza.

O bloco try- finallyapós a soinicialização existe apenas para proteger a SomeObjectinstância, para garantir que ela seja limpa, não importa o que mais aconteça. Se houver outras coisas que precisam ser executadas, mas não estão relacionadas ao fato de a SomeObjectinstância ter sido alocada por propriedade, elas devem ir para outro bloco try- finally, provavelmente um que envolva o que eu mostrei.

Exigir que as variáveis ​​sejam atribuídas manualmente antes do uso não leva a problemas reais. Isso só leva a pequenos aborrecimentos, mas seu código será melhor para isso. Você terá variáveis ​​com escopo mais limitado e try- finallyblocos que não tentam proteger muito.

Se as variáveis ​​locais tivessem valores padrão, sono primeiro exemplo teriam sido null. Isso realmente não teria resolvido nada. Em vez de obter um erro de tempo de compilação no finallybloco, você teria um erro à NullPointerExceptionespreita que poderia ocultar qualquer outra exceção que pudesse ocorrer na seção "Faça algum trabalho aqui" do código. (Ou as exceções nas finallyseções acorrentam automaticamente à exceção anterior? Não me lembro. Mesmo assim, você teria uma exceção extra no caminho da real.)


2
por que não ter apenas um if (so! = null) ... no bloco finally?
izb

isso ainda causará o aviso / erro do compilador - eu não acho que o compilador entende isso se verificar (mas estou apenas fazendo isso fora da memória, não testado).
Chii

6
Eu colocaria apenas SomeObject so = null antes de tentar colocar a verificação de null na cláusula finally. Dessa forma, não haverá avisos do compilador.
Juha Syrjälä

Por que complicar as coisas? Escreva o bloco try-finally dessa maneira e você SABE que a variável tem um valor válido. Nenhuma verificação de nulos necessária.
Rob Kennedy

1
Rob, seu exemplo "new SomeObject ()" é simples e nenhuma exceção deve ser gerada lá, mas se a chamada pode gerar exceções, então seria melhor que ela ocorresse dentro do bloco try para que pudesse ser tratada.
Sarel Botha

12

Além disso, no exemplo abaixo, uma exceção pode ter sido lançada dentro da construção SomeObject, caso em que a variável 'so' seria nula e a chamada para CleanUp lançará um NullPointerException

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

O que costumo fazer é o seguinte:

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

12
O que você prefere fazer?
Monge Elétrico

2
Sim, feio. Sim, é o que eu faço também.
SMBiggs

@ElectricMonk Qual forma você acha melhor, a que você mostrou ou a que é mostrada aqui no método getContents (..): javapractices.com/topic/TopicAction.do?Id=126
Atom

11

Observe que as variáveis ​​de instância / membro finais não são inicializadas por padrão. Porque esses são finais e não podem ser alterados no programa posteriormente. Essa é a razão pela qual o Java não fornece nenhum valor padrão para eles e força o programador a inicializá-lo.

Por outro lado, as variáveis ​​de membro não finais podem ser alteradas posteriormente. Conseqüentemente, o compilador não permite que eles permaneçam não inicializados, precisamente porque eles podem ser alterados posteriormente. Em relação às variáveis ​​locais, o escopo das variáveis ​​locais é muito mais restrito. O compilador sabe quando está sendo usado. Portanto, forçar o programador a inicializar a variável faz sentido.


9

A resposta real à sua pergunta é porque as variáveis ​​do método são instanciadas simplesmente adicionando um número ao ponteiro da pilha. Zerá-los seria uma etapa extra. Para variáveis ​​de classe, elas são colocadas na memória inicializada no heap.

Por que não dar um passo extra? Dê um passo para trás - Ninguém mencionou que o "aviso" neste caso é uma coisa muito boa.

Você nunca deve inicializar sua variável para zero ou nulo na primeira passagem (quando você está codificando-a pela primeira vez). Atribua-o ao valor real ou não atribua-o de forma alguma, porque se não o fizer, então o java pode dizer quando você realmente estragar tudo. Veja a resposta de Electric Monk como um ótimo exemplo. No primeiro caso, é realmente muito útil dizer que se o try () falhar porque o construtor de SomeObject lançou uma exceção, você acabaria com um NPE no finally. Se o construtor não pode lançar uma exceção, não deve ser na tentativa.

Este aviso é um verificador de programador ruim de vários caminhos incrível que me salvou de fazer coisas estúpidas, uma vez que verifica todos os caminhos e garante que, se você usar a variável em algum caminho, será necessário inicializá-la em todos os caminhos que levam a ele . Agora, nunca inicializo variáveis ​​explicitamente até determinar que é a coisa certa a fazer.

Além disso, não é melhor dizer explicitamente "int size = 0" em vez de "int size" e fazer o próximo programador descobrir que você pretende que seja zero?

Por outro lado, não consigo encontrar um único motivo válido para que o compilador inicialize todas as variáveis ​​não inicializadas com 0.


1
Sim, e há outros em que mais ou menos precisa ser inicializado como nulo por causa da forma como o código flui - eu não deveria ter dito "nunca" atualizei a resposta para refletir isso.
Bill K de

4

Acho que o objetivo principal era manter a semelhança com C / C ++. No entanto, o compilador detecta e avisa sobre o uso de variáveis ​​não inicializadas, o que reduzirá o problema a um ponto mínimo. Do ponto de vista do desempenho, é um pouco mais rápido permitir que você declare variáveis ​​não inicializadas, pois o compilador não terá que escrever uma instrução de atribuição, mesmo se você substituir o valor da variável na próxima instrução.


1
Indiscutivelmente, o compilador pode determinar se você sempre atribui à variável antes de fazer qualquer coisa com ela, e suprimir uma atribuição automática de valor padrão nesse caso. Se o compilador não puder determinar se uma atribuição ocorre antes do acesso, ele gerará a atribuição padrão.
Greg Hewgill

2
Sim, mas pode-se argumentar que permite ao programador saber se ele deixou a variável não inicializada por engano.
Mehrdad Afshari

1
O compilador também pode fazer isso em ambos os casos. :) Pessoalmente, prefiro que o compilador trate uma variável não inicializada como um erro. Isso significa que posso ter cometido um erro em algum lugar.
Greg Hewgill

Não sou um cara de Java, mas gosto da maneira C # de lidar com isso. A diferença é que, nesse caso, o compilador teve que emitir um aviso, o que pode fazer com que você receba algumas centenas de avisos para o programa correto;)
Mehrdad Afshari

Ele avisa para as variáveis ​​de membro também?
Adeel Ansari

4

(Pode parecer estranho postar uma nova resposta muito tempo depois da pergunta, mas apareceu uma duplicata .)

Para mim, a razão se resume a isto: o propósito das variáveis ​​locais é diferente do propósito das variáveis ​​de instância. As variáveis ​​locais devem ser usadas como parte de um cálculo; as variáveis ​​de instância existem para conter o estado. Se você usar uma variável local sem atribuir um valor a ela, isso quase certamente é um erro lógico.

Dito isso, eu poderia ficar totalmente atrás de exigir que as variáveis ​​de instância sempre fossem inicializadas explicitamente; o erro ocorreria em qualquer construtor onde o resultado permite uma variável de instância não inicializada (por exemplo, não inicializada na declaração e não no construtor). Mas essa não é a decisão Gosling, et. al., tirou no início dos anos 90, então aqui estamos. (E não estou dizendo que eles fizeram a chamada errada.)

Eu não poderia ficar atrás de variáveis ​​locais padrão, no entanto. Sim, não devemos confiar em compiladores para verificar novamente nossa lógica, e ninguém não, mas ainda é útil quando o compilador detecta um. :-)


"Dito isso, eu poderia ficar totalmente atrás de exigir que as variáveis ​​de instância sempre fossem inicializadas explicitamente ..." que, FWIW, é a direção que eles tomaram no TypeScript.
TJ Crowder

3

É mais eficiente não inicializar variáveis ​​e, no caso de variáveis ​​locais, é seguro fazê-lo, porque a inicialização pode ser rastreada pelo compilador.

Nos casos em que você precisa que uma variável seja inicializada, você sempre pode fazer isso sozinho, portanto, não é um problema.


3

A ideia por trás das variáveis ​​locais é que elas existem apenas dentro do escopo limitado para o qual são necessárias. Como tal, deve haver pouca razão para incerteza quanto ao valor, ou pelo menos, de onde esse valor está vindo. Eu poderia imaginar muitos erros decorrentes de ter um valor padrão para variáveis ​​locais.

Por exemplo, considere o seguinte código simples ... ( NB, vamos supor, para fins de demonstração, que variáveis ​​locais recebem um valor padrão, conforme especificado, se não inicializado explicitamente )

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

Quando tudo estiver dito e feito, assumindo que o compilador atribuiu um valor padrão de '\ 0' para letterGrade , este código como escrito funcionaria corretamente. No entanto, e se esquecermos a instrução else?

Uma execução de teste do nosso código pode resultar no seguinte

Enter grade
43
Your grade is

Esse resultado, embora fosse esperado, certamente não era a intenção do codificador. De fato, provavelmente na grande maioria dos casos (ou pelo menos em um número significativo), o valor padrão não seria o valor desejado , portanto, na grande maioria dos casos, o valor padrão resultaria em erro. Faz mais sentido forçar o codificador a atribuir um valor inicial a uma variável local antes de usá-la, uma vez que a dificuldade de depuração causada pelo esquecimento do = 1in for(int i = 1; i < 10; i++)supera em muito a conveniência de não ter que incluir o = 0in for(int i; i < 10; i++).

É verdade que os blocos try-catch-finally podem ficar um pouco confusos (mas não é realmente um catch-22 como a citação parece sugerir), quando, por exemplo, um objeto lança uma exceção verificada em seu construtor, ainda para um razão ou outra, algo deve ser feito a este objeto no final do bloco em finalmente. Um exemplo perfeito disso é quando se trata de recursos, que devem ser fechados.

Uma maneira de lidar com isso no passado pode ser assim ...

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

No entanto, a partir do Java 7, este bloco finalmente não é mais necessário usando try-with-resources, dessa forma.

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

Dito isso, (como o nome sugere) isso só funciona com recursos.

E embora o exemplo anterior seja um pouco nojento, talvez isso fale mais sobre a maneira como try-catch-finally ou essas classes são implementadas do que sobre variáveis ​​locais e como elas são implementadas.

É verdade que os campos são inicializados com um valor padrão, mas isso é um pouco diferente. Quando você diz, por exemplo, int[] arr = new int[10];assim que inicializa este array, o objeto existe na memória em um determinado local. Vamos supor por um momento que não haja valores padrão, mas, em vez disso, o valor inicial é qualquer série de 1s e 0s que esteja naquele local de memória no momento. Isso pode levar a um comportamento não determinístico em vários casos.

Suponha que temos ...

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

Seria perfeitamente possível que Same.fosse exibido em uma execução e Not same.pudesse ser exibido em outra. O problema pode se tornar ainda mais grave quando você começar a falar sobre variáveis ​​de referência.

String[] s = new String[5];

De acordo com a definição, cada elemento de s deve apontar para uma String (ou é nulo). No entanto, se o valor inicial for qualquer série de 0s e 1s que ocorra neste local da memória, não apenas não há garantia de que você obterá os mesmos resultados todas as vezes, mas também não há garantia de que o objeto s [0] aponta to (assumindo que aponta para algo significativo) até mesmo é um String (talvez seja um Coelho,: p )! Essa falta de preocupação com o tipo iria contra quase tudo que faz o Java Java. Portanto, embora ter valores padrão para variáveis ​​locais possa ser visto como opcional, na melhor das hipóteses, ter valores padrão para variáveis ​​de instância é mais próximo de uma necessidade .


1

se eu não estou errado, outro motivo poderia ser

Dar o valor padrão das variáveis ​​de membro faz parte do carregamento da classe

O carregamento da classe é uma coisa de tempo de execução em java significa que quando você cria um objeto, a classe é carregada com o carregamento da classe, apenas as variáveis ​​de membro são inicializadas com o valor padrão JVM não leva tempo para fornecer o valor padrão para suas variáveis ​​locais porque alguns métodos nunca serão chamado porque a chamada de método pode ser condicional, então por que perder tempo para dar a eles o valor padrão e reduzir o desempenho se esses padrões nunca serão usados.


0

O Eclipse ainda fornece avisos sobre variáveis ​​não inicializadas, então torna-se bastante óbvio de qualquer maneira. Pessoalmente, acho que é uma coisa boa que este seja o comportamento padrão, caso contrário, seu aplicativo pode usar valores inesperados e, em vez de o compilador lançar um erro, ele não fará nada (mas talvez dê um aviso) e então você estará arranhando sua cabeça por que certas coisas não se comportam da maneira que deveriam.


0

As variáveis ​​locais são armazenadas em uma pilha, mas as variáveis ​​de instância são armazenadas no heap, portanto, há algumas chances de que um valor anterior na pilha seja lido em vez de um valor padrão, como acontece no heap. Por isso o jvm não permite usar uma variável local sem inicializá-la.


2
Completamente errado ... todos os não-primitivos Java são armazenados no heap, independentemente de quando e como eles são construídos
gshauger

Antes do Java 7, as variáveis ​​de instância eram armazenadas no heap e as variáveis ​​locais eram encontradas na pilha. No entanto, qualquer objeto ao qual uma variável local faz referência será encontrado no heap. A partir do Java 7, o "Java Hotspot Server Compiler" pode realizar uma "análise de escape" e decidir alocar alguns objetos na pilha em vez do heap.
mamills de

0

A variável de instância terá valores padrão, mas as variáveis ​​locais não podem ter valores padrão. Como as variáveis ​​locais estão basicamente em métodos / comportamento, seu objetivo principal é fazer algumas operações ou cálculos. Portanto, não é uma boa ideia definir valores padrão para variáveis ​​locais. Caso contrário, é muito difícil e demorado verificar os motivos de respostas inesperadas.


-1

A resposta é que as variáveis ​​de instância podem ser inicializadas no construtor de classe ou qualquer método de classe, mas no caso de variáveis ​​locais, uma vez que você definiu o que quer que seja no método que permanece para sempre na classe.


-2

Eu poderia pensar em seguir 2 motivos

  1. Como a maioria das respostas disse colocando a restrição de inicializar a variável local, é garantido que a variável local receba um valor como o programador deseja e garante que os resultados esperados sejam calculados.
  2. Variáveis ​​de instância podem ser ocultadas declarando variáveis ​​locais (mesmo nome) - para garantir o comportamento esperado, variáveis ​​locais são forçadas a inicializar um valor. (Evitaria totalmente isso)

Os campos não podem ser substituídos. No máximo, eles podem estar ocultos, e não consigo ver como a ocultação interfere na verificação de inicialização.
Meriton

Oculto à direita. Se alguém decidir criar uma variável local com o mesmo nome de instância, devido a esta restrição, a variável local será inicializada com o valor propositalmente selecionado (diferente do valor da variável de instância)
Mitra
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.