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?


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.


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.


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.


15

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


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.


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.

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.