Declarando variáveis ​​dentro ou fora de um loop


236

Por que o seguinte funciona bem?

String str;
while (condition) {
    str = calculateStr();
    .....
}

Mas este é considerado perigoso / incorreto:

while (condition) {
    String str = calculateStr();
    .....
}

É necessário declarar variáveis ​​fora do loop?

Respostas:


289

O escopo das variáveis ​​locais deve sempre ser o menor possível.

No seu exemplo, presumo que nãostr seja usado fora do loop, caso contrário você não faria a pergunta, porque declará-lo dentro do loop não seria uma opção, pois não seria compilado.whilewhile

Portanto, como nãostr é usado fora do loop, o menor escopo possível é dentro do loop while.str

Portanto, a resposta é enfaticamente que strabsolutamente deve ser declarada dentro do loop while. Sem ifs, nem ands, sem mas.

O único caso em que essa regra pode ser violada é se, por algum motivo, é de vital importância que todo ciclo de clock seja retirado do código; nesse caso, convém instanciar algo em um escopo externo e reutilizá-lo em vez de re-instanciando-o em todas as iterações de um escopo interno. No entanto, isso não se aplica ao seu exemplo, devido à imutabilidade de strings em java: uma nova instância de str sempre será criada no início do seu loop e terá que ser descartada no final dele. não há possibilidade de otimizar lá.

EDIT: (injetando meu comentário abaixo na resposta)

De qualquer forma, a maneira certa de fazer as coisas é escrever todo o seu código corretamente, estabelecer um requisito de desempenho para o seu produto, medir o seu produto final em relação a esse requisito e, se ele não o atender, otimizar as coisas. E o que geralmente acaba acontecendo é que você encontra maneiras de fornecer algumas otimizações algorítmicas agradáveis ​​e formais em apenas alguns lugares que fazem com que nosso programa atenda aos requisitos de desempenho em vez de precisar percorrer toda a sua base de códigos e ajustar e hackear as coisas. para apertar os ciclos do relógio aqui e ali.


2
Consulta no último parágrafo: Se era outra String, que não é imutável, ela afeta?
Harry Joy

1
@HarryJoy Sim, claro, considere, por exemplo, StringBuilder, que é mutável. Se você usar um StringBuilder para criar uma nova string em cada iteração do loop, poderá otimizar as coisas alocando o StringBuilder fora do loop. Mas, ainda assim, essa não é uma prática aconselhável. Se você fizer isso sem uma boa razão, é uma otimização prematura.
11553 Mike Nakis

7
@HarryJoy A maneira certa de fazer as coisas é escrever todo o seu código corretamente , estabelecer um requisito de desempenho para o seu produto, medir o seu produto final em relação a esse requisito e, se não satisfazê-lo, otimize as coisas. E sabe de uma coisa? Geralmente, você poderá fornecer algumas otimizações algorítmicas agradáveis ​​e formais em apenas alguns lugares, o que fará o truque, em vez de ter que percorrer toda a sua base de código e ajustar e hackear as coisas para reduzir os ciclos de clock aqui e ali.
11138 Mike Nakis

2
@ MikeNakis eu coisa que você está pensando em escopo muito estreito.
Siten

5
Veja, as modernas CPUs multi-gigahertz, multi-core, com pipeline e cache de memória de vários níveis nos permitem focar nas seguintes práticas recomendadas sem ter que se preocupar com os ciclos de clock. Além disso, a otimização só é aconselhável se e somente se for determinado que é necessário e, quando necessário, alguns ajustes altamente localizados geralmente atingirão o desempenho desejado, portanto, não há necessidade de distribuir todo o nosso código. com pequenos hacks em nome do desempenho.
precisa saber é o seguinte

293

Comparei o código de bytes desses dois exemplos (semelhantes):

Vejamos o 1. exemplo :

package inside;

public class Test {
    public static void main(String[] args) {
        while(true){
            String str = String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}

depois javac Test.java, javap -c Testvocê receberá:

public class inside.Test extends java.lang.Object{
public inside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}

Vejamos 2. exemplo :

package outside;

public class Test {
    public static void main(String[] args) {
        String str;
        while(true){
            str =  String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}

depois javac Test.java, javap -c Testvocê receberá:

public class outside.Test extends java.lang.Object{
public outside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}

As observações mostram que não há diferença entre esses dois exemplos. É o resultado das especificações da JVM ...

Porém, em nome da melhor prática de codificação, é recomendável declarar a variável no menor escopo possível (neste exemplo, ela está dentro do loop, pois esse é o único local em que a variável é usada).


3
É o resultado da soecificação da JVM, não da 'otimização do compilador'. Os slots de pilha exigidos por um método são todos alocados na entrada do método. É assim que o bytecode é especificado.
Marquês de Lorne

2
@Arhimed há mais uma razão para colocá-lo dentro do loop (ou apenas o bloco '{}'): o compilador reutilizará a memória alocada no quadro da pilha para a variável em outro escopo se você declarar nesse outro escopo alguma variável em excesso .
Serge

1
Se o loop for feito através de uma lista de objetos de dados, isso fará alguma diferença para o volume de dados? Provavelmente 40 mil.
Mithun Khatri

7
Para qualquer um de vocês, finalamantes: declarando strcomo finalno insidecaso do pacote também faz diferença =)
skia.heliou

27

Declarar objetos no menor escopo melhora a legibilidade .

O desempenho não importa para os compiladores de hoje. (Neste cenário)
Da perspectiva da manutenção, a segunda opção é melhor.
Declare e inicialize variáveis ​​no mesmo local, no escopo mais restrito possível.

Como Donald Ervin Knuth disse:

"Devemos esquecer pequenas eficiências, digamos, 97% das vezes: a otimização prematura é a raiz de todo mal"

ie) situação em que um programador permite que considerações de desempenho afetem o design de um pedaço de código. Isso pode resultar em um design que não é tão limpo quanto poderia ter sido ou um código incorreto, porque o código é complicado pela otimização e o programador é distraído pela otimização .


1
"2ª opção tem desempenho um pouco mais rápido" => você mediu? De acordo com uma das respostas, o bytecode é o mesmo, então não vejo como o desempenho pode ser diferente.
assylias 27/09/12

Sinto muito, mas isso não é realmente o caminho certo para testar o desempenho de um programa java (e como você pode testar o desempenho de um loop infinito, afinal?)
assylias

Eu concordo com seus outros pontos - é só que acredito que não há diferença de desempenho.
assylias 27/09/12

11

se você quiser usar strlooop externo também; declare-o do lado de fora. caso contrário, a segunda versão está correta.


11

Pule para a resposta atualizada ...

Para quem se preocupa com o desempenho, retire o System.out e limite o loop a 1 byte. Usando double (teste 1/2) e usando String (3/4), os tempos decorridos em milissegundos são fornecidos abaixo no Windows 7 Professional de 64 bits e no JDK-1.7.0_21. Os bytecodes (também fornecidos abaixo para test1 e test2) não são os mesmos. Eu estava com preguiça de testar com objetos mutáveis ​​e relativamente complexos.

Duplo

Teste1 realizado: 2710 ms

Teste2 realizado: 2790 ms

String (basta substituir double por string nos testes)

Teste 3 realizado: 1200 ms

Teste4 realizado: 3000 ms

Compilando e obtendo bytecode

javac.exe LocalTest1.java

javap.exe -c LocalTest1 > LocalTest1.bc


public class LocalTest1 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        double test;
        for (double i = 0; i < 1000000000; i++) {
            test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }

}

public class LocalTest2 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (double i = 0; i < 1000000000; i++) {
            double test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }
}


Compiled from "LocalTest1.java"
public class LocalTest1 {
  public LocalTest1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore        5
       7: dload         5
       9: ldc2_w        #3                  // double 1.0E9d
      12: dcmpg
      13: ifge          28
      16: dload         5
      18: dstore_3
      19: dload         5
      21: dconst_1
      22: dadd
      23: dstore        5
      25: goto          7
      28: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      31: lstore        5
      33: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      36: new           #6                  // class java/lang/StringBuilder
      39: dup
      40: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      43: ldc           #8                  // String Test1 Took:
      45: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      48: lload         5
      50: lload_1
      51: lsub
      52: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      55: ldc           #11                 // String  msecs
      57: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      60: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      63: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      66: return
}


Compiled from "LocalTest2.java"
public class LocalTest2 {
  public LocalTest2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore_3
       6: dload_3
       7: ldc2_w        #3                  // double 1.0E9d
      10: dcmpg
      11: ifge          24
      14: dload_3
      15: dstore        5
      17: dload_3
      18: dconst_1
      19: dadd
      20: dstore_3
      21: goto          6
      24: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      27: lstore_3
      28: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      31: new           #6                  // class java/lang/StringBuilder
      34: dup
      35: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      38: ldc           #8                  // String Test1 Took:
      40: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      43: lload_3
      44: lload_1
      45: lsub
      46: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      49: ldc           #11                 // String  msecs
      51: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      54: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      57: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      60: return
}

RESPOSTA ATUALIZADA

Realmente não é fácil comparar o desempenho com todas as otimizações da JVM. No entanto, é um pouco possível. Melhores testes e resultados detalhados no Google Caliper

  1. Alguns detalhes no blog: você deve declarar uma variável dentro de um loop ou antes dele?
  2. Repositório do GitHub: https://github.com/gunduru/jvdt
  3. Resultados do teste para caso duplo e loop de 100M (e sim todos os detalhes da JVM): https://microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4

DeclaradoAnterior 1.759.209 DeclaradoEm 2.222.308

  • Declarado antes de 1.759.209 ns
  • Declarado dentro de 2.242,308 ns

Código de teste parcial para declaração dupla

Isso não é idêntico ao código acima. Se você apenas codificar um loop fictício, a JVM o ignora, pelo menos você precisa atribuir e retornar algo. Isso também é recomendado na documentação do Caliper.

@Param int size; // Set automatically by framework, provided in the Main
/**
* Variable is declared inside the loop.
*
* @param reps
* @return
*/
public double timeDeclaredInside(int reps) {
    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Declaration and assignment */
        double test = i;

        /* Dummy assignment to fake JVM */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}

/**
* Variable is declared before the loop.
*
* @param reps
* @return
*/
public double timeDeclaredBefore(int reps) {

    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Actual test variable */
    double test = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Assignment */
        test = i;

        /* Not actually needed here, but we need consistent performance results */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}

Resumo: declaradoAnterior indica melhor desempenho - muito pequeno - e é contra o menor princípio de escopo. A JVM realmente deve fazer isso por você


Metodologia de teste inválida e você não fornece nenhuma explicação para seus resultados.
Marquês de Lorne

1
@EJP Isso deve ficar bem claro para quem tem interesse no assunto. A metodologia é retirada da resposta do PrimosK para fornecer informações mais úteis. Para ser sincero, não tenho idéia de como melhorar essa resposta. Talvez você possa clicar em editar e nos mostrar como fazê-lo corretamente.
Onur Günduru

2
1) O Java Bytecode é otimizado (reordenado, recolhido etc.) no tempo de execução, portanto, não se preocupe muito com o que está escrito nos arquivos .class. 2) existem 1.000.000.000 de execuções para obter uma vitória no desempenho de 2,8s, então isso é cerca de 2,8ns por execução versus estilo de programação seguro e adequado. Um vencedor claro para mim. 3) Como você não fornece informações sobre aquecimento, seus tempos são bastante inúteis.
Codificado em

@ Melhores testes codificados por hardware / micro benchmarking com paquímetro apenas para loops duplos e 100M. Resultados on-line, se você quiser outros casos, sinta-se à vontade para editar.
Onur Günduru

Obrigado, isso elimina os pontos 1) e 3). Mas mesmo que o tempo subisse para ~ 5ns por ciclo, esse ainda seria um momento a ser ignorado. Na teoria, existe um pequeno potencial de otimização; na realidade, o que você faz por ciclo é geralmente muito mais caro. Portanto, o potencial seria de alguns segundos no máximo em uma corrida de alguns minutos ou até horas. Existem outras opções com maior potencial disponível (por exemplo, Fork / Join, Streams paralelos) que eu verificaria antes de gastar tempo com esse tipo de otimizações de baixo nível.
Codificado em

7

Uma solução para esse problema pode ser fornecer um escopo variável que encapsule o loop while:

{
  // all tmp loop variables here ....
  // ....
  String str;
  while(condition){
      str = calculateStr();
      .....
  }
}

Eles seriam automaticamente referenciados quando o escopo externo terminar.


6

No interior, quanto menor o escopo, a variável é visível para melhor.


5

Se você não precisar usar o strloop after the while (relacionado ao escopo), a segunda condição, ou seja,

  while(condition){
        String str = calculateStr();
        .....
    }

é melhor, pois se você definir um objeto na pilha somente se conditionfor verdadeiro. Ou seja, use-o se você precisar


2
Observe que, mesmo na primeira variante, nenhum objeto é construído se a condição for falsa.
Philipp Wendler

@ Phillip: Sim, você está certo. Foi mal. Eu estava pensando como está agora. O que você acha?
Cratylus

1
Bem, "definir um objeto na pilha" é um termo um tanto estranho no mundo Java. Além disso, alocar uma variável na pilha geralmente é um erro no tempo de execução, então por que se preocupar? Escopo para ajudar o programador é o verdadeiro problema.
precisa saber é o seguinte


3

Como muitas pessoas apontaram,

String str;
while(condition){
    str = calculateStr();
    .....
}

NÃO é melhor que isso:

while(condition){
    String str = calculateStr();
    .....
}

Portanto, não declare variáveis ​​fora de seu escopo se você não estiver reutilizando ...


1
exceto provavelmente desta maneira: link
Dainius Kreivys

2

A declaração String str fora do loop wile permite que ela seja referenciada dentro e fora do loop while. A declaração String str dentro do loop while permite que ela seja referenciada apenas dentro do loop while.




1

A strvariável estará disponível e reservará algum espaço na memória, mesmo depois de executada abaixo do código.

 String str;
    while(condition){
        str = calculateStr();
        .....
    }

A strvariável não estará disponível e também será liberada a memória que foi alocada para a strvariável no código abaixo.

while(condition){
    String str = calculateStr();
    .....
}

Se seguimos o segundo, certamente isso reduzirá a memória do sistema e aumentará o desempenho.


0

Declarar dentro do loop limita o escopo da respectiva variável. Tudo depende do requisito do projeto no escopo da variável.


0

Na verdade, a pergunta acima é uma questão de programação. Como você gostaria de programar seu código? Onde você precisa que o 'STR' seja acessado? Não há como declarar uma variável que é usada localmente como uma variável global. Noções básicas de programação, acredito.


-1

Esses dois exemplos resultam na mesma coisa. No entanto, o primeiro fornece o uso da strvariável fora do loop while; o segundo não é.


-1

Aviso para quase todo mundo nesta pergunta: Aqui está um código de exemplo onde, dentro do loop, pode ser facilmente 200 vezes mais lento no meu computador com Java 7 (e o consumo de memória também é um pouco diferente). Mas é sobre alocação e não apenas escopo.

public class Test
{
    private final static int STUFF_SIZE = 512;
    private final static long LOOP = 10000000l;

    private static class Foo
    {
        private long[] bigStuff = new long[STUFF_SIZE];

        public Foo(long value)
        {
            setValue(value);
        }

        public void setValue(long value)
        {
            // Putting value in a random place.
            bigStuff[(int) (value % STUFF_SIZE)] = value;
        }

        public long getValue()
        {
            // Retrieving whatever value.
            return bigStuff[STUFF_SIZE / 2];
        }
    }

    public static long test1()
    {
        long total = 0;

        for (long i = 0; i < LOOP; i++)
        {
            Foo foo = new Foo(i);
            total += foo.getValue();
        }

        return total;
    }

    public static long test2()
    {
        long total = 0;

        Foo foo = new Foo(0);
        for (long i = 0; i < LOOP; i++)
        {
            foo.setValue(i);
            total += foo.getValue();
        }

        return total;
    }

    public static void main(String[] args)
    {
        long start;

        start = System.currentTimeMillis();
        test1();
        System.out.println(System.currentTimeMillis() - start);

        start = System.currentTimeMillis();
        test2();
        System.out.println(System.currentTimeMillis() - start);
    }
}

Conclusão: Dependendo do tamanho da variável local, a diferença pode ser enorme, mesmo com variáveis ​​não tão grandes.

Só para dizer que, às vezes, fora ou dentro do loop importa.


1
Claro, o segundo é mais rápido, mas você está fazendo coisas diferentes: test1 está criando muitos Foo-Objects com grandes matrizes, test2 não. O test2 está reutilizando o mesmo objeto Foo repetidamente, o que pode ser perigoso em ambientes multithread.
Codificado em

Perigoso em ambiente multithread ??? Por favor, explique o porquê. Estamos falando de uma variável local. É criado em cada chamada do método.
Rt15 15/01

Se você passar o Foo-Object para uma operação que está processando os dados de forma assíncrona, a operação ainda poderá estar funcionando na instância do Foo enquanto você estiver alterando os dados. Nem precisa ser multithread para ter efeitos colaterais. Então instância-reutilização é bastante perigosa, quando você não sabe o que ainda está usando a instância
Hardcoded

Ps: seu método setValue deve ser bigStuff[(int) (value % STUFF_SIZE)] = value;(tente um valor de 2147483649L)
codificado

Falando sobre efeitos colaterais: você comparou os resultados de seus métodos?
Codificado em

-1

Eu acho que o tamanho do objeto também importa. Em um dos meus projetos, declaramos e inicializamos uma grande matriz bidimensional que estava fazendo com que o aplicativo lançasse uma exceção de falta de memória. Em vez disso, removemos a declaração do loop e limpamos a matriz no início de cada iteração.


-2

Você corre o risco de NullPointerExceptionse seu calculateStr()método retornar nulo e tentar chamar um método em str.

De maneira mais geral, evite ter variáveis ​​com um valor nulo . A propósito, é mais forte para atributos de classe.


2
Isso não tem nenhuma relação com a questão. A probabilidade de NullPointerException (em futuras chamadas de função) não dependeria de como uma variável é declarada.
Desert Ice

1
Acho que não, porque a pergunta é "Qual é a melhor maneira de fazer isso?". IMHO eu preferiria um código mais seguro.
Rémi Doolaeghe 22/11/2012

1
Há risco zero de uma NullPointerException.Se este código tentou return str;seria encontrar um erro de compilação.
Marquês de Lorne
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.