Passer une chaîne par référence en Java?

161

J'ai l'habitude de faire ce qui suit dans C:

void main() {
    String zText = "";
    fillString(zText);
    printf(zText);
}

void fillString(String zText) {
    zText += "foo";
}

Et le résultat est:

foo

Cependant, en Java, cela ne semble pas fonctionner. Je suppose parce que l' Stringobjet est copié au lieu de passer par référencé . Je pensais que les chaînes étaient des objets, qui sont toujours passés par référence.

Qu'est-ce qui se passe ici?

Soren Johnson
la source
2
Même s'ils ont été passés par référence (en Java, ce qui est passé est une copie de la valeur de référence, mais c'est un autre thread) Les objets de chaîne SONT immuables, donc cela ne fonctionnerait pas de toute façon
OscarRyz
8
Ce n'est pas du code C.
Alston
@Phil: Cela ne fonctionnera pas non plus en C #.
Mangesh

Réponses:

196

Vous avez trois options:

  1. Utilisez un StringBuilder:

    StringBuilder zText = new StringBuilder ();
    void fillString(StringBuilder zText) { zText.append ("foo"); }
  2. Créez une classe de conteneur et transmettez une instance du conteneur à votre méthode:

    public class Container { public String data; }
    void fillString(Container c) { c.data += "foo"; }
  3. Créez un tableau:

    new String[] zText = new String[1];
    zText[0] = "";
    
    void fillString(String[] zText) { zText[0] += "foo"; }

Du point de vue des performances, le StringBuilder est généralement la meilleure option.

Aaron Digulla
la source
11
Gardez simplement à l'esprit que StringBuilder n'est pas thread-safe. Dans un environnement multithread, utilisez la classe StringBuffer ou prenez en charge manuellement la synchronisation.
Boris Pavlović
12
@Boris Pavlovic - oui, mais à moins que le même StringBuilder ne soit utilisé par différents threads, ce que l'OMI est peu probable dans un scénario donné, cela ne devrait pas être un problème. Peu importe si la méthode est appelée par différents threads, recevant différents StringBuilder.
Ravi Wallau
J'aime le plus la troisième option (tableau) car c'est la seule générale. Il permet également de passer des booléens, des int, etc. Pouvez-vous expliquer un peu plus en détail les performances de cette solution - vous prétendez que la première est meilleure du point de vue des performances.
meolic
@meolic StringBuilderest plus rapide s += "..."car il n'alloue pas de nouveaux objets String à chaque fois. En fait, lorsque vous écrivez du code Java comme s = "Hello " + name, le compilateur génère du code d'octet qui crée un StringBuilderet appelle append()deux fois.
Aaron Digulla
86

En Java, rien n'est passé par référence . Tout est passé par valeur . Les références d'objet sont passées par valeur. De plus, les chaînes sont immuables . Ainsi, lorsque vous ajoutez à la chaîne transmise, vous obtenez simplement une nouvelle chaîne. Vous pouvez utiliser une valeur de retour ou passer un StringBuffer à la place.

zedoo
la source
2
Ce n'est pas une idée fausse, c'est un concept
Patrick Cornelissen
41
Ummm..non, c'est une idée fausse que vous passez un objet par référence. Vous passez une référence par valeur.
Ed S.
30
Oui, c'est une idée fausse. C'est une idée fausse énorme et répandue. Cela conduit à une question d'entretien que je déteste: ("comment Java transmet-il des arguments"). Je déteste ça car environ la moitié des intervieweurs semblent en fait vouloir la mauvaise réponse ("primitifs par valeur, objets par référence"). La bonne réponse prend plus de temps à donner et semble dérouter certaines d'entre elles. Et ils ne seront pas convaincus: je jure que j'ai raté un écran technologique parce que le screener de type CSMajor avait entendu l'idée fausse à l'université et l'a cru comme un évangile. Feh.
CPerkins
4
@CPerkins Vous n'avez aucune idée à quel point cela me met en colère. Ça fait bouillir mon sang.
6
Tout se passe par valeur dans toutes les langues. Certaines de ces valeurs sont des adresses (références).
gerardw
52

Ce qui se passe, c'est que la référence est passée par valeur, c'est-à-dire qu'une copie de la référence est passée. Rien en java n'est passé par référence, et comme une chaîne est immuable, cette affectation crée un nouvel objet chaîne vers lequel pointe maintenant la copie de la référence . La référence d'origine pointe toujours vers la chaîne vide.

Ce serait la même chose pour n'importe quel objet, c'est-à-dire en lui attribuant une nouvelle valeur dans une méthode. L'exemple ci-dessous rend simplement ce qui se passe plus évident, mais concaténer une chaîne est vraiment la même chose.

void foo( object o )
{
    o = new Object( );  // original reference still points to old value on the heap
}
Ed S.
la source
6
Excellent exemple!
Nickolas
18

java.lang.String est immuable.

Je déteste coller des URL, mais https://docs.oracle.com/javase/10/docs/api/java/lang/String.html est essentiel pour que vous puissiez lire et comprendre si vous êtes en java-land.

Ryan Fernandes
la source
Je ne pense pas que Soren Johnson ne sache pas ce qu'est un String, ce n'était pas la question et pas ce que je cherchais ici après 15 ans de Java.
AHH
8

les objets sont passés par référence, les primitives sont passées par valeur.

La chaîne n'est pas une primitive, c'est un objet et c'est un cas particulier d'objet.

C'est à des fins d'économie de mémoire. Dans JVM, il existe un pool de chaînes. Pour chaque chaîne créée, JVM essaiera de voir si la même chaîne existe dans le pool de chaînes et la pointera s'il y en avait déjà une.

public class TestString
{
    private static String a = "hello world";
    private static String b = "hello world";
    private static String c = "hello " + "world";
    private static String d = new String("hello world");

    private static Object o1 = new Object();
    private static Object o2 = new Object();

    public static void main(String[] args)
    {
        System.out.println("a==b:"+(a == b));
        System.out.println("a==c:"+(a == c));
        System.out.println("a==d:"+(a == d));
        System.out.println("a.equals(d):"+(a.equals(d)));
        System.out.println("o1==o2:"+(o1 == o2));

        passString(a);
        passString(d);
    }

    public static void passString(String s)
    {
        System.out.println("passString:"+(a == s));
    }
}

/* PRODUCTION */

a==b:true
a==c:true
a==d:false
a.equals(d):true
o1==o2:false
passString:true
passString:false

le == vérifie l'adresse mémoire (référence), et le .equals vérifie le contenu (valeur)

Janetsmith
la source
3
Pas tout à fait vrai. Java est toujours passé par VALUE; l'astuce est que les objets sont passés par référence qui sont passés par valeur. (difficile, je sais). Vérifiez celui-ci: javadude.com/articles/passbyvalue.htm
adhg
1
@adhg vous avez écrit "Les objets sont passés par référence qui sont passés par valeur." <--- c'est du charabia. Vous voulez dire que les objets ont des références qui sont passées par valeur
barlop
2
-1 Vous avez écrit "les objets sont passés par référence", <- FALSE. Si cela était vrai, lors de l'appel d'une méthode et de la transmission d'une variable, la méthode pourrait faire pointer / faire référence à un objet différent, mais elle ne le peut pas. Tout est passé par valeur en java. Et vous voyez toujours l'effet de la modification des attributs d'un objet, en dehors de la méthode.
barlop
voir la réponse ici stackoverflow.com/questions/40480/…
barlop
7

Tous les arguments en Java sont passés par valeur. Lorsque vous transmettez un Stringà une fonction, la valeur transmise est une référence à un objet String, mais vous ne pouvez pas modifier cette référence et l'objet String sous-jacent est immuable.

La tâche

zText += foo;

est équivalent à:

zText = new String(zText + "foo");

Autrement dit, il réaffecte (localement) le paramètre en zTexttant que nouvelle référence, qui pointe vers un nouvel emplacement mémoire, dans lequel se trouve un nouveau Stringqui contient le contenu d'origine de zTextavec "foo"appended.

L'objet d'origine n'est pas modifié et la main()variable locale de la méthode zTextpointe toujours vers la chaîne d'origine (vide).

class StringFiller {
  static void fillString(String zText) {
    zText += "foo";
    System.out.println("Local value: " + zText);
  }

  public static void main(String[] args) {
    String zText = "";
    System.out.println("Original value: " + zText);
    fillString(zText);
    System.out.println("Final value: " + zText);
  }
}

imprime:

Original value:
Local value: foo
Final value:

Si vous souhaitez modifier la chaîne, vous pouvez, comme indiqué, utiliser StringBuilderou bien un conteneur (un tableau ou une AtomicReferenceclasse de conteneur ou une classe de conteneur personnalisée) qui vous donne un niveau supplémentaire d'indirection de pointeur. Sinon, renvoyez simplement la nouvelle valeur et attribuez-la:

class StringFiller2 {
  static String fillString(String zText) {
    zText += "foo";
    System.out.println("Local value: " + zText);
    return zText;
  }

  public static void main(String[] args) {
    String zText = "";
    System.out.println("Original value: " + zText);
    zText = fillString(zText);
    System.out.println("Final value: " + zText);
  }
}

imprime:

Original value:
Local value: foo
Final value: foo

C'est probablement la solution la plus semblable à Java dans le cas général - voir l' élément Java efficace «Favoriser l'immuabilité».

Comme indiqué, cependant, StringBuildervous donnera souvent de meilleures performances - si vous avez beaucoup d'ajout à faire, en particulier dans une boucle, utilisez StringBuilder.

Mais essayez de transmettre immuable Stringsplutôt que mutable StringBuilderssi vous le pouvez - votre code sera plus facile à lire et plus maintenable. Pensez à créer vos paramètres finalet à configurer votre IDE pour vous avertir lorsque vous réaffectez un paramètre de méthode à une nouvelle valeur.

David Moles
la source
6
Vous dites «à proprement parler» comme si ce n'était presque pas pertinent - alors que c'est au cœur du problème. Si Java avait vraiment le passage par référence, ce ne serait pas un problème. S'il vous plaît essayer d'éviter de propager le mythe de « Java passe des objets par référence » - Expliquer les (correct) modèle « références sont passés par valeur » est beaucoup plus utile, l' OMI.
Jon Skeet
Hé, où est allé mon commentaire précédent? :-) Comme je l'ai déjà dit, et comme Jon l'a maintenant ajouté, le vrai problème ici est que la référence est passée par valeur. C'est très important et beaucoup de gens ne comprennent pas la différence sémantique.
Ed S.
1
C'est suffisant. En tant que programmeur Java, je n'ai rien vu de réellement passé par référence depuis plus d'une décennie, donc mon langage est devenu bâclé. Mais vous avez raison, j'aurais dû faire plus attention, en particulier pour un public de programmation C.
David Moles
5

String est un objet immuable en Java. Vous pouvez utiliser la classe StringBuilder pour effectuer le travail que vous essayez d'accomplir, comme suit:

public static void main(String[] args)
{
    StringBuilder sb = new StringBuilder("hello, world!");
    System.out.println(sb);
    foo(sb);
    System.out.println(sb);

}

public static void foo(StringBuilder str)
{
    str.delete(0, str.length());
    str.append("String has been modified");
}

Une autre option consiste à créer une classe avec une chaîne comme variable de portée (fortement déconseillée) comme suit:

class MyString
{
    public String value;
}

public static void main(String[] args)
{
    MyString ms = new MyString();
    ms.value = "Hello, World!";

}

public static void foo(MyString str)
{
    str.value = "String has been modified";
}
Fadi Hanna AL-Kass
la source
1
La chaîne n'est pas un type primitif en Java.
VWeber
C'est vrai, mais sur quoi essayez-vous d'attirer mon attention?
Fadi Hanna AL-Kass
Depuis quand Java String est une primitive?! ALORS! Chaîne passée par référence.
thedp
2
«objet immuable», c'est ce que j'avais l'intention de dire. Pour une raison quelconque, je n'ai pas relu ma réponse après que @VWeber ait fait un commentaire, mais maintenant je vois ce qu'il essayait de dire. ma faute. réponse modifiée
Fadi Hanna AL-Kass
2

La réponse est simple. En java, les chaînes sont immuables. Par conséquent, c'est comme utiliser le modificateur 'final' (ou 'const' en C / C ++). Donc, une fois assigné, vous ne pouvez pas le changer comme vous l'avez fait.

Vous pouvez changer qui la valeur à laquelle un des points de chaîne, mais vous ne pouvez pas changer la valeur réelle à laquelle cette chaîne pointe actuellement.

C'est à dire. String s1 = "hey". Vous pouvez faire s1 = "woah", et c'est tout à fait correct, mais vous ne pouvez pas réellement changer la valeur sous-jacente de la chaîne (dans ce cas: "hey") pour qu'elle soit quelque chose d'autre une fois qu'elle est assignée en utilisant plusEquals, etc. (ie. s1 += " whatup != "hey whatup").

Pour ce faire, utilisez les classes StringBuilder ou StringBuffer ou d'autres conteneurs mutables, puis appelez simplement .toString () pour reconvertir l'objet en chaîne.

Remarque: les chaînes sont souvent utilisées comme clés de hachage, ce qui explique en partie pourquoi elles sont immuables.

ak_2050
la source
Qu'il soit mutable ou immuable n'est pas pertinent. =sur une référence n'affecte JAMAIS l'objet sur lequel elle pointait, quelle que soit la classe.
newacct
2

String est une classe spéciale en Java. C'est Thread Safe, ce qui signifie "Une fois qu'une instance String est créée, le contenu de l'instance String ne sera jamais changé".

Voici ce qui se passe

 zText += "foo";

Tout d'abord, le compilateur Java obtiendra la valeur de l'instance zText String, puis créera une nouvelle instance String dont la valeur est zText en ajoutant "foo". Vous savez donc pourquoi l'instance vers laquelle zText pointe n'a pas changé. C'est une instance totalement nouvelle. En fait, même String "foo" est une nouvelle instance de String. Donc, pour cette instruction, Java créera deux instances String, l'une est "foo", une autre est la valeur de zText append "foo". La règle est simple: la valeur de l'instance String ne sera jamais modifiée.

Pour la méthode fillString, vous pouvez utiliser un StringBuffer comme paramètre, ou vous pouvez le modifier comme ceci:

String fillString(String zText) {
    return zText += "foo";
}
DeepNightDeux
la source
... si vous deviez définir 'fillString' selon ce code, vous devriez vraiment lui donner un nom plus approprié!
Stephen C
Oui. Cette méthode doit être nommée "appendString" ou plus.
DeepNightTwo
1

Les chaînes sont immuables en Java.

Grzegorz Oledzki
la source
1

Cela fonctionne avec StringBuffer

public class test {
 public static void main(String[] args) {
 StringBuffer zText = new StringBuffer("");
    fillString(zText);
    System.out.println(zText.toString());
 }
  static void fillString(StringBuffer zText) {
    zText .append("foo");
}
}

Encore mieux utiliser StringBuilder

public class test {
 public static void main(String[] args) {
 StringBuilder zText = new StringBuilder("");
    fillString(zText);
    System.out.println(zText.toString());
 }
  static void fillString(StringBuilder zText) {
    zText .append("foo");
}
}
Mohammed Rafeeq
la source
1

La chaîne est immuable en java. vous ne pouvez pas modifier / changer une chaîne littérale / objet existante.

String s = "Bonjour"; s = s + "salut";

Ici, la référence précédente s est remplacée par les nouvelles références pointant vers la valeur "HelloHi".

Cependant, pour apporter la mutabilité, nous avons StringBuilder et StringBuffer.

StringBuilder s = nouveau StringBuilder (); s.append ("Salut");

ceci ajoute la nouvelle valeur "Hi" aux mêmes références. //

gauravJ
la source
0

Aaron Digulla a la meilleure réponse à ce jour. Une variante de sa deuxième option consiste à utiliser le wrapper ou la classe de conteneur MutableObject de la bibliothèque commons lang version 3+:

void fillString(MutableObject<String> c) { c.setValue(c.getValue() + "foo"); }

vous enregistrez la déclaration de la classe conteneur. L'inconvénient est une dépendance à la lang lib commune. Mais la lib a beaucoup de fonctions utiles et presque tous les grands projets sur lesquels j'ai travaillé l'ont utilisée.

user2606740
la source
0

Pour passer un objet (y compris String) par référence dans java, vous pouvez le passer en tant que membre d'un adaptateur environnant. Une solution avec un générique est ici:

import java.io.Serializable;

public class ByRef<T extends Object> implements Serializable
{
    private static final long serialVersionUID = 6310102145974374589L;

    T v;

    public ByRef(T v)
    {
        this.v = v;
    }

    public ByRef()
    {
        v = null;
    }

    public void set(T nv)
    {
        v = nv;
    }

    public T get()
    {
        return v;
    }

// ------------------------------------------------------------------

    static void fillString(ByRef<String> zText)
    {
        zText.set(zText.get() + "foo");
    }

    public static void main(String args[])
    {
        final ByRef<String> zText = new ByRef<String>(new String(""));
        fillString(zText);
        System.out.println(zText.get());
    }
}
Sam Ginrich
la source
0

Pour quelqu'un qui est plus curieux

class Testt {
    static void Display(String s , String varname){
        System.out.println(varname + " variable data = "+ s + " :: address hashmap =  " + s.hashCode());
    }

    static void changeto(String s , String t){
        System.out.println("entered function");
        Display(s , "s");
        s = t ;
        Display(s,"s");
        System.out.println("exiting function");
    }

    public static void main(String args[]){
        String s =  "hi" ;
        Display(s,"s");
        changeto(s,"bye");
        Display(s,"s");
    }
}

Maintenant, en exécutant ce code ci-dessus, vous pouvez voir comment l'adresse hashcodeschange avec la variable String s. un nouvel objet est alloué à la variable s en fonction changetolorsque s est modifié

Rajesh kumar
la source