Comparando cadeias com == que são declaradas finais em Java

220

Eu tenho uma pergunta simples sobre cadeias de caracteres em Java. O seguinte segmento de código simples apenas concatena duas seqüências e as compara ==.

String str1="str";
String str2="ing";
String concat=str1+str2;

System.out.println(concat=="string");

A expressão de comparação concat=="string"retorna falsecomo óbvia (eu entendo a diferença entre equals()e ==).


Quando essas duas cadeias são declaradas finalassim,

final String str1="str";
final String str2="ing";
String concat=str1+str2;

System.out.println(concat=="string");

A expressão de comparação concat=="string", neste caso, retorna true. Por que finalfaz a diferença? Isso tem a ver com o pool de estagiários ou estou sendo enganado?

Muito pequeno
fonte
22
Eu sempre achei bobo que igual era a maneira padrão de verificar se o conteúdo era igual, em vez de ter == fazer isso e apenas usar referenceEquals ou algo semelhante para verificar se os ponteiros são os mesmos.
Davio
25
Esta não é uma duplicata de "Como faço para comparar seqüências de caracteres em Java?" de qualquer maneira. O OP entende a diferença entre equals()e ==no contexto de strings e está fazendo uma pergunta mais significativa.
Arshajii 17/10/2013
@ David Mas como isso funcionaria quando a classe não é String? Eu acho que é muito lógico, não bobo, fazer a comparação de conteúdo equals, um método que podemos substituir para dizer quando consideramos dois objetos iguais e ter a comparação de identidade feita por ==. Se a comparação de conteúdo fosse feita por ==nós, não poderíamos substituir isso para definir o que queremos dizer com "conteúdo igual", e ter o significado de equalse ==revertido apenas para Strings seria bobagem. Além disso, independentemente disso, não vejo nenhuma vantagem em ==fazer a comparação de conteúdo em vez de equals.
SantiBailors
@ SantiBailors, você está certo de que é assim que funciona em Java; também usei C # onde == está sobrecarregado para garantir a igualdade de conteúdo. Um bônus adicional de usar == é que ele é seguro para nulos: (null == "something") retorna false. Se você usar iguais para 2 objetos, precisará estar ciente de que pode ser nulo ou corre o risco de uma NullPointerException ser lançada.
Davio

Respostas:

232

Quando você declara uma variável String(que é imutável ) como finale a inicializa com uma expressão constante em tempo de compilação, ela também se torna uma expressão constante em tempo de compilação e seu valor é incorporado pelo compilador em que é usado. Portanto, no seu segundo exemplo de código, após incluir os valores, a concatenação de strings é convertida pelo compilador em:

String concat = "str" + "ing";  // which then becomes `String concat = "string";`

que, quando comparado a "string", fornece a você true, porque os literais de string são internados .

Do JLS §4.12.4 - finalVariáveis :

Uma variável do tipo ou tipo primitivo String, que é finalinicializada com uma expressão constante em tempo de compilação (§15.28), é chamada de variável constante .

Também de JLS §15.28 - Expressão constante:

Constantes expressões de compilação em tempo de tipo Stringsão sempre "internado" , de modo a compartilhar instâncias exclusivas, utilizando o método String#intern().


Este não é o caso no seu primeiro exemplo de código, onde as Stringvariáveis ​​não são final. Portanto, eles não são expressões constantes em tempo de compilação. A operação de concatenação será adiada até o tempo de execução, levando à criação de um novo Stringobjeto. Você pode verificar isso comparando o código de bytes dos dois códigos.

O primeiro exemplo de código (não finalversão) é compilado no seguinte código de bytes:

  Code:
   0:   ldc     #2; //String str
   2:   astore_1
   3:   ldc     #3; //String ing
   5:   astore_2
   6:   new     #4; //class java/lang/StringBuilder
   9:   dup
   10:  invokespecial   #5; //Method java/lang/StringBuilder."<init>":()V
   13:  aload_1
   14:  invokevirtual   #6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   17:  aload_2
   18:  invokevirtual   #6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   21:  invokevirtual   #7; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   24:  astore_3
   25:  getstatic       #8; //Field java/lang/System.out:Ljava/io/PrintStream;
   28:  aload_3
   29:  ldc     #9; //String string
   31:  if_acmpne       38
   34:  iconst_1
   35:  goto    39
   38:  iconst_0
   39:  invokevirtual   #10; //Method java/io/PrintStream.println:(Z)V
   42:  return

Claramente, ele está armazenando stre ingem duas variáveis ​​separadas e usando StringBuilderpara executar a operação de concatenação.

Visto que o seu segundo exemplo de código ( finalversão) se parece com o seguinte:

  Code:
   0:   ldc     #2; //String string
   2:   astore_3
   3:   getstatic       #3; //Field java/lang/System.out:Ljava/io/PrintStream;
   6:   aload_3
   7:   ldc     #2; //String string
   9:   if_acmpne       16
   12:  iconst_1
   13:  goto    17
   16:  iconst_0
   17:  invokevirtual   #4; //Method java/io/PrintStream.println:(Z)V
   20:  return

Por isso, alinha diretamente a variável final para criar String stringno tempo de compilação, que é carregado pela ldcoperação na etapa 0. Em seguida, a segunda string literal é carregada pela ldcoperação na etapa 7. Não envolve a criação de nenhum novo Stringobjeto em tempo de execução. A String já é conhecida no momento da compilação e elas são internadas.

Rohit Jain
fonte
2
Não há nada impedindo que outra implementação do compilador Java não interne uma String final, certo?
Alvin
13
@Alvin, o JLS, exige que as expressões de seqüência constante em tempo de compilação sejam internadas. Qualquer implementação em conformidade deve fazer a mesma coisa aqui.
Tavian Barnes
Por outro lado, o JLS exige que um compilador não otimize a concatenação na primeira versão não final ? O compilador está proibido de produzir código, que teria a comparação avaliada true?
Phant0m
1
@ phant0m usando a redação atual da especificação : “ O Stringobjeto foi criado recentemente (§12.5), a menos que a expressão seja uma expressão constante (§15.28). ”Literalmente, a aplicação de uma otimização na versão não final não é permitida, pois uma sequência de caracteres“ recém-criada ”deve ter uma identidade de objeto diferente. Não sei se isso é intencional. Afinal, a estratégia de compilação atual é delegar para um recurso de tempo de execução que não documenta essas restrições.
Holger
31

De acordo com minha pesquisa, todos final Stringestão internados em Java. De uma das postagens do blog:

Portanto, se você realmente precisar comparar duas String usando == ou! =, Chame o método String.intern () antes de fazer a comparação. Caso contrário, sempre prefira String.equals (String) para comparação de String.

Portanto, se você ligar, String.intern()poderá comparar duas cadeias usando o ==operador. Mas aqui String.intern()não é necessário porque em Java final Stringsão internamente internados.

Você pode encontrar mais informações Comparação de strings usando o operador == e Javadoc para o método String.intern () .

Consulte também esta postagem do Stackoverflow para obter mais informações.

Pradeep Simha
fonte
3
strings intern () não são coletadas como lixo e são armazenadas em um espaço permgen baixo, assim você terá problemas como erro de falta de memória se não for usado corretamente.
ajeesh
@Ajeesh - Strings estendidas podem ser coletadas no lixo. Mesmo cadeias internas resultantes de expressões constantes podem ser coletadas em algumas circunstâncias.
Stephen C
21

Se você der uma olhada nesses métodos

public void noFinal() {
    String str1 = "str";
    String str2 = "ing";
    String concat = str1 + str2;

    System.out.println(concat == "string");
}

public void withFinal() {
    final String str1 = "str";
    final String str2 = "ing";
    String concat = str1 + str2;

    System.out.println(concat == "string");
}

e é descompilado com javap -c ClassWithTheseMethods versões que você verá

  public void noFinal();
    Code:
       0: ldc           #15                 // String str
       2: astore_1      
       3: ldc           #17                 // String ing
       5: astore_2      
       6: new           #19                 // class java/lang/StringBuilder
       9: dup           
      10: aload_1       
      11: invokestatic  #21                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
      14: invokespecial #27                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
      17: aload_2       
      18: invokevirtual #30                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: invokevirtual #34                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      ...

e

  public void withFinal();
    Code:
       0: ldc           #15                 // String str
       2: astore_1      
       3: ldc           #17                 // String ing
       5: astore_2      
       6: ldc           #44                 // String string
       8: astore_3      
       ...

Portanto, se Strings não forem o compilador final, terá que usar StringBuilderpara concatenar str1e str2assim

String concat=str1+str2;

será compilado para

String concat = new StringBuilder(str1).append(str2).toString();

o que significa que concatserá criado em tempo de execução e, portanto, não virá do pool de String.


Além disso, se Strings são finais, o compilador pode assumir que nunca mudará, em vez de usá- StringBuilderlo pode concatenar com segurança seus valores,

String concat = str1 + str2;

pode ser alterado para

String concat = "str" + "ing";

e concatenado em

String concat = "string";

o que significa que concatese tornará literal de picada, que será internado no pool de strings e, em seguida, comparado com a mesma literal de string desse pool na ifinstrução.

Pshemo
fonte
15

Conceito de pool de cones de pilha e string insira a descrição da imagem aqui

pnathan
fonte
6
O que? Eu não entendo como isso tem votos positivos. Você pode esclarecer sua resposta?
Cᴏʀʏ
Eu acho que a resposta pretendida é que, porque str1 + str2 não é otimizado para uma string internada, a comparação com uma string do pool de strings levará a uma condição falsa.
viki.omega9
3

Vamos ver um código de bytes para o finalexemplo

Compiled from "Main.java"
public class Main {
  public Main();
    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: ldc           #2                  // String string
       2: astore_3
       3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: aload_3
       7: ldc           #2                  // String string
       9: if_acmpne     16
      12: iconst_1
      13: goto          17
      16: iconst_0
      17: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
      20: return
}

At 0:e 2:, o String "string"é empurrado para a pilha (do pool constante) e armazenado concatdiretamente na variável local . Você pode deduzir que o compilador está criando (concatenando) a String "string"si mesmo no momento da compilação.

O finalcódigo não byte

Compiled from "Main2.java"
public class Main2 {
  public Main2();
    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: ldc           #2                  // String str
       2: astore_1
       3: ldc           #3                  // String ing
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
      17: aload_2
      18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
      21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      24: astore_3
      25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
      28: aload_3
      29: ldc           #9                  // String string
      31: if_acmpne     38
      34: iconst_1
      35: goto          39
      38: iconst_0
      39: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      42: return
}

Aqui você tem duas Stringconstantes "str"e "ing"que precisam ser concatenadas em tempo de execução com a StringBuilder.

Sotirios Delimanolis
fonte
0

Entretanto, quando você cria usando a notação literal String de Java, ele chama automaticamente o método intern () para colocar esse objeto no pool de String, desde que ele não estivesse presente no pool.

Por que final faz a diferença?

O compilador sabe que a variável final nunca muda, quando adicionamos essas variáveis ​​finais, a saída vai para String Pool por causa da str1 + str2saída da expressão também nunca vai mudar. Então, finalmente, o compilador chama o método inter após a saída das duas variáveis ​​finais acima. No caso de compilador variável não final, não chame o método interno.

Premraj
fonte