Comparaison de chaînes avec == déclarées finales en Java

220

J'ai une question simple sur les chaînes en Java. Le segment de code simple suivant concatène simplement deux chaînes, puis les compare avec ==.

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

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

L'expression de comparaison concat=="string"revient falsecomme évidente (je comprends la différence entre equals()et ==).


Lorsque ces deux chaînes sont déclarées finalcomme telles,

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

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

L'expression de comparaison concat=="string", dans ce cas, revient true. Pourquoi cela finalfait-il une différence? Faut-il faire quelque chose avec la réserve de stagiaires ou je suis juste induit en erreur?

Minuscule
la source
22
J'ai toujours trouvé stupide qu'égaux soit le moyen par défaut de vérifier un contenu égal, au lieu d'avoir == faire cela et utiliser simplement referenceEquals ou quelque chose de similaire pour vérifier si les pointeurs sont les mêmes.
Davio
25
Ce n'est pas un doublon de "Comment comparer des chaînes en Java?" de quelque manière que. L'OP comprend la différence entre equals()et ==dans le contexte des chaînes et pose une question plus significative.
arshajii
@Davio Mais comment cela fonctionnerait-il lorsque la classe ne l'est pas String? Je pense qu'il est très logique, pas idiot, d'avoir la comparaison de contenu effectuée par equals, une méthode que nous pouvons remplacer pour dire quand nous considérons deux objets égaux, et de faire la comparaison d'identité par ==. Si la comparaison du contenu était faite par ==nous ne pouvions pas passer outre pour définir ce que nous entendons par "contenu égal", et avoir le sens de equalset ==inversé uniquement pour Strings serait idiot. De plus, malgré cela, je ne vois aucun avantage à ==faire la comparaison de contenu à la place de equals.
SantiBailors
@SantiBailors vous avez raison, c'est comme ça que ça marche en Java, j'ai aussi utilisé C # où == est surchargé pour l'égalité du contenu. Un avantage supplémentaire de l'utilisation de == est qu'il est sans danger: (null == "quelque chose") renvoie false. Si vous utilisez égal à 2 objets, vous devez savoir si l'un ou l'autre peut être nul ou si vous risquez qu'une exception NullPointerException soit levée.
Davio

Réponses:

232

Lorsque vous déclarez une variable String(qui est immuable ) en tant que finalet l'initialisez avec une expression constante au moment de la compilation, elle devient également une expression constante au moment de la compilation, et sa valeur est indiquée par le compilateur où elle est utilisée. Ainsi, dans votre deuxième exemple de code, après avoir inséré les valeurs, la concaténation de chaîne est traduite par le compilateur en:

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

qui, comparé à "string", vous donnera true, car les littéraux de chaîne sont internés .

De JLS §4.12.4 - finalVariables :

Une variable de type ou type primitif String, c'est-à-dire finalinitialisée avec une expression constante au moment de la compilation (§15.28), est appelée variable constante .

Aussi de JLS §15.28 - Expression constante:

Les expressions constantes de type à la compilation Stringsont toujours "internées" afin de partager des instances uniques, à l'aide de la méthode String#intern().


Ce n'est pas le cas dans votre premier exemple de code, où les Stringvariables ne le sont pas final. Ce ne sont donc pas des expressions constantes à la compilation. L'opération de concaténation y sera retardée jusqu'à l'exécution, conduisant ainsi à la création d'un nouvel Stringobjet. Vous pouvez le vérifier en comparant le code octet des deux codes.

Le premier exemple de code (non finalversion) est compilé dans le code d'octet suivant:

  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

De toute évidence, il stocke stret ingdans deux variables distinctes et utilise StringBuilderpour effectuer l'opération de concaténation.

Alors que votre deuxième exemple de code ( finalversion) ressemble à ceci:

  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

Ainsi, il insère directement la variable finale pour créer une chaîne stringau moment de la compilation, qui est chargée par l' ldcopération à l'étape 0. Ensuite, le deuxième littéral de chaîne est chargé par ldcopération à l'étape 7. Cela n'implique pas la création d'un nouvel Stringobjet lors de l'exécution. La chaîne est déjà connue au moment de la compilation et elle est internée.

Rohit Jain
la source
2
Rien n'empêche une autre implémentation du compilateur Java de ne pas interner une chaîne finale, n'est-ce pas?
Alvin
13
@Alvin le JLS nécessite que les expressions de chaîne constante au moment de la compilation soient internées. Toute implémentation conforme doit faire la même chose ici.
Tavian Barnes
Inversement, le JLS impose-t-il qu'un compilateur ne doit pas optimiser la concaténation dans la première version non finale ? Est-il interdit au compilateur de produire du code, sur lequel la comparaison serait évaluée true?
phant0m
1
@ phant0m en prenant le libellé actuel de la spécification , « L' Stringobjet est nouvellement créé (§12.5) à moins que l'expression ne soit une expression constante (§15.28). »Littéralement, l'application d'une optimisation dans la version non finale n'est pas autorisée, car une chaîne« nouvellement créée »doit avoir une identité d'objet différente. Je ne sais pas si c'est intentionnel. Après tout, la stratégie de compilation actuelle consiste à déléguer à une fonction d'exécution qui ne documente pas ces restrictions.
Holger
31

Selon mes recherches, tous final Stringsont internés en Java. Depuis l'un des articles du blog:

Donc, si vous avez vraiment besoin de comparer deux chaînes en utilisant == ou! = Assurez-vous d'appeler la méthode String.intern () avant de faire la comparaison. Sinon, préférez toujours String.equals (String) pour la comparaison de chaînes.

Cela signifie donc que si vous appelez, String.intern()vous pouvez comparer deux chaînes à l'aide de l' ==opérateur. Mais ici, ce String.intern()n'est pas nécessaire car en Java final Stringsont internés en interne.

Vous pouvez trouver plus d'informations Comparaison de chaînes en utilisant l'opérateur == et Javadoc pour la méthode String.intern () .

Consultez également ce post Stackoverflow pour plus d'informations.

Pradeep Simha
la source
3
les chaînes intern () ne sont pas récupérées et elles sont stockées dans l'espace permgen qui est faible, vous aurez donc des ennuis comme une erreur de mémoire insuffisante si vous ne les utilisez pas correctement.
Ajeesh
@Ajeesh - Les chaînes internées peuvent être récupérées. Même les chaînes internes résultant d'expressions constantes peuvent être récupérées dans certaines circonstances.
Stephen C
21

Si vous regardez ces méthodes

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

et son décompilé avec des javap -c ClassWithTheseMethods versions que vous verrez

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

et

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

Donc, si les chaînes ne sont pas le compilateur final devra utiliser StringBuilderpour concaténer str1et str2ainsi

String concat=str1+str2;

sera compilé pour

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

ce qui signifie qu'il concatsera créé au moment de l'exécution et ne proviendra donc pas du pool de chaînes.


De plus, si les chaînes sont finales, le compilateur peut supposer qu'elles ne changeront jamais, au lieu de l'utiliser, StringBuilderil peut concaténer ses valeurs en toute sécurité afin

String concat = str1 + str2;

peut être changé en

String concat = "str" + "ing";

et concaténé en

String concat = "string";

ce qui signifie que concatecela deviendra le littéral sting qui sera interné dans le pool de chaînes, puis comparé au même littéral de chaîne de ce pool dans l' ifinstruction.

Pshemo
la source
15

Concept de piscine de conts de pile et de chaîne entrez la description de l'image ici

pnathan
la source
6
Quoi? Je ne comprends pas comment cela a des votes positifs. Pouvez-vous clarifier votre réponse?
Cᴏʀʏ
Je pense que la réponse voulue est que, comme str1 + str2 n'est pas optimisé pour une chaîne interne, la comparaison avec une chaîne du pool de chaînes entraînera une fausse condition.
viki.omega9
3

Voyons un code d'octet pour l' finalexemple

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
}

À 0:et 2:, le String "string"est poussé sur la pile (à partir du pool constant) et stocké concatdirectement dans la variable locale . Vous pouvez en déduire que le compilateur crée (concatène) le String "string"lui - même au moment de la compilation.

Le finalcode non octet

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
}

Ici, vous avez deux Stringconstantes, "str"et "ing"qui doivent être concaténées au moment de l'exécution avec a StringBuilder.

Sotirios Delimanolis
la source
0

Cependant, lorsque vous créez en utilisant la notation littérale String de Java, il appelle automatiquement la méthode intern () pour placer cet objet dans le pool String, à condition qu'il ne soit pas déjà présent dans le pool.

Pourquoi le final fait-il une différence?

Le compilateur sait que la variable finale ne changera jamais, lorsque nous ajoutons ces variables finales, la sortie va au pool de chaînes car la str1 + str2sortie d'expression ne changera jamais non plus, donc le compilateur appelle finalement inter méthode après la sortie des deux variables finales ci-dessus. Dans le cas d'un compilateur de variables non final, n'appelez pas la méthode interne.

Premraj
la source