Comparar cadenas con == que se declaran finales en Java

220

Tengo una pregunta simple sobre cadenas en Java. El siguiente segmento de código simple solo concatena dos cadenas y luego las compara ==.

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

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

La expresión de comparación concat=="string"regresa falsecomo obvia (entiendo la diferencia entre equals()y ==).


Cuando estas dos cadenas se declaran finalasí,

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

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

La expresión de comparación concat=="string", en este caso, regresa true. ¿Por qué hace la finaldiferencia? ¿Tiene que ver algo con el grupo de internos o simplemente estoy siendo engañado?

Minúsculo
fuente
22
Siempre me pareció tonto que igual era la forma predeterminada de verificar el contenido igual, en lugar de tener == hacer eso y solo usar referenceEquals o algo similar para verificar si los punteros son los mismos.
Davio
25
Este no es un duplicado de "¿Cómo comparo cadenas en Java?" de cualquier manera. El OP entiende la diferencia entre equals()y ==en el contexto de las cadenas, y está haciendo una pregunta más significativa.
arshajii
@Davio Pero, ¿cómo funcionaría eso si la clase no lo es String? Creo que es muy lógico, no tonto, realizar la comparación de contenido equals, un método que podemos anular para determinar cuándo consideramos que dos objetos son iguales y hacer que la comparación de identidad se realice por ==. Si la comparación de contenido se hiciera por ==no podríamos anular eso para definir lo que queremos decir con "contenido igual", y tener el significado equalsy ==revertir solo para Strings sería una tontería. Además, independientemente de esto, no veo ninguna ventaja de todos modos en ==hacer la comparación de contenido en lugar de hacerlo equals.
SantiBailors
@SantiBailors tienes razón en que así es como funciona en Java, también he usado C # donde == está sobrecargado para la igualdad de contenido. Una ventaja adicional de usar == es que es nulo-seguro: (null == "algo") devuelve falso. Si usa iguales para 2 objetos, debe saber si puede ser nulo o si corre el riesgo de que se produzca una NullPointerException.
Davio

Respuestas:

232

Cuando declara una variable String(que es inmutable ) como final, y la inicializa con una expresión constante en tiempo de compilación, también se convierte en una expresión constante en tiempo de compilación, y el compilador en el que se utiliza inserta su valor. Entonces, en su segundo ejemplo de código, después de incluir los valores, el compilador traduce la concatenación de cadenas a:

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

que en comparación con "string"te dará true, porque los literales de cadena están internados .

De JLS §4.12.4 - finalVariables :

Una variable de tipo o tipo primitivo String, que se finalinicializa con una expresión constante en tiempo de compilación (§15.28), se denomina variable constante .

También de JLS §15.28 - Expresión constante:

Las expresiones de tipo constantes en tiempo de compilación Stringsiempre están "internados" para compartir instancias únicas, utilizando el método String#intern().


Este no es el caso en su primer ejemplo de código, donde las Stringvariables no lo son final. Por lo tanto, no son expresiones constantes en tiempo de compilación. La operación de concatenación allí se retrasará hasta el tiempo de ejecución, lo que conducirá a la creación de un nuevo Stringobjeto. Puede verificar esto comparando el código de bytes de ambos códigos.

El primer ejemplo de código (no finalversión) se compila con el siguiente 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 está almacenando stry ingen dos variables separadas, y está utilizando StringBuilderpara realizar la operación de concatenación.

Mientras que su segundo ejemplo de código ( finalversión) se ve así:

  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 lo tanto, alinea directamente la variable final para crear String stringen tiempo de compilación, que se carga por ldcoperación en el paso 0. Luego, el segundo literal de cadena se carga por ldcoperación en el paso 7. No implica la creación de ningún Stringobjeto nuevo en tiempo de ejecución. La cadena ya se conoce en tiempo de compilación, y están internadas.

Rohit Jain
fuente
2
No hay nada que impida que otra implementación del compilador de Java no realice una Cadena final, ¿verdad?
Alvin
13
@Alvin the JLS requiere que se internen expresiones de cadena constantes en tiempo de compilación. Cualquier implementación conforme debe hacer lo mismo aquí.
Tavian Barnes
Por el contrario, ¿el JLS exige que un compilador no debe optimizar la concatenación en la primera versión no final ? ¿Está prohibido que el compilador produzca código para evaluar la comparación true?
phant0m
1
@ phant0m tomando la redacción actual de la especificación , “ El Stringobjeto se acaba de crear (§12.5) a menos que la expresión sea una expresión constante (§15.28). "Literalmente, no está permitido aplicar una optimización en la versión no final, ya que una cadena" recién creada "debe tener una identidad de objeto diferente. No sé si esto es intencional. Después de todo, la estrategia de compilación actual es delegar a una instalación de tiempo de ejecución que no documenta tales restricciones.
Holger
31

Según mi investigación, todos final Stringestán internados en Java. De una de las publicaciones del blog:

Entonces, si realmente necesita comparar dos cadenas usando == o! = Asegúrese de llamar al método String.intern () antes de hacer la comparación. De lo contrario, siempre prefiera String.equals (String) para la comparación de String.

Por lo tanto, si llama String.intern()puede comparar dos cadenas usando el ==operador. Pero aquí String.intern()no es necesario porque en Java final Stringse interna internamente.

Puede encontrar más información Comparación de cadenas usando el operador == y Javadoc para el método String.intern () .

Consulte también esta publicación de Stackoverflow para obtener más información.

Pradeep Simha
fuente
3
Las cadenas intern () no son basura recolectada y se almacenan en un espacio permgen que es bajo, por lo que se meterá en problemas como un error de falta de memoria si no se usa correctamente.
Ajeesh
@Ajeesh: las cadenas internadas pueden ser basura recolectada. Incluso las cadenas internas resultantes de expresiones constantes pueden ser basura recolectada en algunas circunstancias.
Stephen C
21

Si echas un vistazo a estos 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");
}

y está descompilado con javap -c ClassWithTheseMethods versiones que verás

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

y

  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      
       ...

Entonces, si las cadenas no son el compilador final, tendrá que usar StringBuilderpara concatenar str1y str2así

String concat=str1+str2;

será compilado a

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

lo que significa que concatse creará en tiempo de ejecución, por lo que no vendrá del conjunto de cadenas.


Además, si las cadenas son finales, el compilador puede suponer que nunca cambiarán, por lo que, en lugar de usarlo StringBuilder, puede concatenar sus valores de manera segura

String concat = str1 + str2;

se puede cambiar a

String concat = "str" + "ing";

y concatenado en

String concat = "string";

lo que significa que concatese convertirá en un sting literal que se internará en el grupo de cadenas y luego se comparará con el mismo literal de cadena de ese grupo en la ifdeclaración.

Pshemo
fuente
15

Concepto de grupo de conts de pila y cuerda ingrese la descripción de la imagen aquí

pnathan
fuente
66
¿Qué? No entiendo cómo esto tiene votos a favor. ¿Puedes aclarar tu respuesta?
Cᴏʀʏ
Creo que la respuesta prevista es que debido a que str1 + str2 no está optimizado para una cadena interna, la comparación con una cadena del conjunto de cadenas conducirá a una condición falsa.
viki.omega9
3

Veamos un código de bytes para el finalejemplo.

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
}

En 0:y 2:, String "string"se empuja a la pila (desde el grupo constante) y se almacena concatdirectamente en la variable local . Puede deducir que el compilador está creando (concatenando) el String "string"mismo en el momento de la compilación.

El finalcódigo no 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
}

Aquí tiene dos Stringconstantes, "str"y "ing"que deben concatenarse en tiempo de ejecución con a StringBuilder.

Sotirios Delimanolis
fuente
0

Sin embargo, cuando crea utilizando la notación literal de String de Java, llama automáticamente al método intern () para poner ese objeto en el conjunto de String, siempre que no esté presente en el conjunto.

¿Por qué final hace la diferencia?

El compilador sabe que la variable final nunca cambiará, cuando agregamos estas variables finales, la salida va al conjunto de cadenas debido a str1 + str2que la salida de expresión tampoco cambiará, por lo que finalmente el compilador llama al método inter después de la salida de las dos variables finales anteriores. En el caso del compilador de variables no final, no llame al método interno.

Premraj
fuente