Przekazywanie ciągu przez odniesienie w Javie?

161

Jestem przyzwyczajony do wykonywania następujących czynności w C:

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

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

A wynik to:

foo

Jednak w Javie to nie działa. Zakładam, ponieważ Stringobiekt jest kopiowany, a nie przekazywany przez przywołanie . Myślałem, że ciągi znaków to obiekty, które zawsze są przekazywane przez odniesienie.

Co tu się dzieje?

Soren Johnson
źródło
2
Nawet jeśli zostały przekazane przez odniesienie (w Javie przekazywana jest kopia wartości referencyjnej, ale to kolejny wątek) Obiekty typu String SĄ niezmienne, więc i tak nie zadziała
OscarRyz
8
To nie jest kod C.
Alston
@Phil: To nie zadziała również w C #.
Mangesh,

Odpowiedzi:

196

Masz trzy możliwości:

  1. Użyj StringBuilder:

    StringBuilder zText = new StringBuilder ();
    void fillString(StringBuilder zText) { zText.append ("foo"); }
  2. Utwórz klasę kontenera i przekaż instancję kontenera do swojej metody:

    public class Container { public String data; }
    void fillString(Container c) { c.data += "foo"; }
  3. Utwórz tablicę:

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

Z punktu widzenia wydajności, StringBuilder jest zwykle najlepszą opcją.

Aaron Digulla
źródło
11
Pamiętaj tylko, że StringBuilder nie jest bezpieczny wątkowo. W środowisku wielowątkowym użyj klasy StringBuffer lub ręcznie zadbaj o synchronizację.
Boris Pavlović
12
@Boris Pavlovic - tak, ale jeśli ten sam StringBuilder nie jest używany przez różne wątki, co w przypadku IMO jest mało prawdopodobne w danym scenariuszu, nie powinno to stanowić problemu. Nie ma znaczenia, czy metoda jest wywoływana przez różne wątki, odbierając różne StringBuilder.
Ravi Wallau
Najbardziej podoba mi się trzecia opcja (tablica), ponieważ jest jedyną ogólną. Pozwala również na przekazywanie wartości boolean, int, itp. Czy możesz trochę dokładniej wyjaśnić działanie tego rozwiązania - twierdzisz, że to pierwsze jest lepsze z punktu widzenia wydajności.
meolic
@meolic StringBuilderjest szybszy, s += "..."ponieważ nie przydziela nowych obiektów String za każdym razem. W rzeczywistości, kiedy piszesz kod w Javie s = "Hello " + name, kompilator wygeneruje kod bajtowy, który tworzy a StringBuilderi wywołuje append()dwa razy.
Aaron Digulla
86

W Javie nic nie jest przekazywane przez odniesienie . Wszystko jest przekazywane przez wartość . Odwołania do obiektów są przekazywane według wartości. Dodatkowo ciągi znakówniezmienne . Więc kiedy dołączasz do przekazanego String, po prostu otrzymasz nowy String. Możesz użyć wartości zwracanej lub zamiast tego przekazać StringBuffer.

zedoo
źródło
2
To nie jest błędne przekonanie, to koncepcja
Patrick Cornelissen
41
Ummm… nie, to błędne przekonanie, że przekazujesz obiekt przez odniesienie. Przekazujesz odwołanie według wartości.
Ed S.
30
Tak, to błędne przekonanie. To ogromne, szeroko rozpowszechnione nieporozumienie. Prowadzi to do pytania w rozmowie kwalifikacyjnej, którego nienawidzę: („W jaki sposób Java przekazuje argumenty”). Nienawidzę tego, ponieważ wydaje się, że mniej więcej połowa ankieterów chce złej odpowiedzi („prymitywy według wartości, obiekty według odniesienia”). Udzielenie właściwej odpowiedzi zajmuje więcej czasu i wydaje się mylić niektóre z nich. I nie będą przekonani: przysięgam, że oblałem ekran techniczny, ponieważ przesiewacz typu CSMajor słyszał to błędne przekonanie na studiach i uważał je za ewangelię. Feh.
CPerkins
4
@CPerkins Nie masz pojęcia, jak bardzo mnie to złości. To sprawia, że ​​moja krew się gotuje.
6
We wszystkich językach wszystko jest przekazywane według wartości. Niektóre z tych wartości są adresami (referencjami).
gerardw
52

Dzieje się tak, że odwołanie jest przekazywane przez wartość, tj. Przekazywana jest kopia odniesienia. Nic w java nie jest przekazywane przez odwołanie, a ponieważ ciąg jest niezmienny, to przypisanie tworzy nowy obiekt ciągu, na który wskazuje teraz kopia odwołania . Oryginalne odniesienie nadal wskazuje na pusty ciąg.

Byłoby to takie samo dla każdego obiektu, tj. Nadanie mu nowej wartości w metodzie. Poniższy przykład po prostu sprawia, że ​​to, co się dzieje, jest bardziej oczywiste, ale konkatenacja łańcucha to tak naprawdę to samo.

void foo( object o )
{
    o = new Object( );  // original reference still points to old value on the heap
}
Ed S.
źródło
6
Doskonały przykład!
Nickolas,
18

java.lang.String jest niezmienna.

Nienawidzę wklejania adresów URL, ale https://docs.oracle.com/javase/10/docs/api/java/lang/String.html jest niezbędny do przeczytania i zrozumienia, jeśli jesteś w java-land.

Ryan Fernandes
źródło
Nie sądzę, że Soren Johnson nie wie, co to jest String, to nie było pytanie i nie to, czego szukałem po 15 latach Java.
AHH,
8

obiekty są przekazywane przez odwołanie, prymitywy są przekazywane przez wartość.

Ciąg nie jest prymitywem, jest obiektem i jest specjalnym przypadkiem obiektu.

Ma to na celu oszczędzanie pamięci. W JVM istnieje pula ciągów. Dla każdego utworzonego łańcucha JVM spróbuje sprawdzić, czy ten sam łańcuch istnieje w puli ciągów i wskaże go, jeśli już istniał.

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

/* WYNIK */

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

== sprawdza adres pamięci (odniesienie), a .equals sprawdza zawartość (wartość)

janetsmith
źródło
3
Nie do końca prawda. Java jest zawsze przekazywana przez VALUE; sztuczka polega na tym, że obiekty są przekazywane przez referencje, które są przekazywane przez wartość. (trudne, wiem). Sprawdź to: javadude.com/articles/passbyvalue.htm
adhg
1
@adhg, który napisałeś „Obiekty są przekazywane przez referencje, które są przekazywane przez wartość”. <--- to bełkot. Masz na myśli, że obiekty mają odniesienia, które są przekazywane przez wartość
barlop
2
-1 Napisałeś „obiekty są przekazywane przez odniesienie”, <- FALSE. Gdyby tak było, to podczas wywoływania metody i przekazywania zmiennej metoda mogłaby sprawić, że zmienna wskazywała / odwoływała się do innego obiektu, ale nie może. W java wszystko jest przekazywane przez wartość. I nadal widzisz efekt zmiany atrybutów obiektu poza metodą.
barlop
zobacz odpowiedź tutaj stackoverflow.com/questions/40480/…
barlop
7

Wszystkie argumenty w Javie są przekazywane według wartości. Gdy przekazujesz a Stringdo funkcji, przekazana wartość jest odwołaniem do obiektu String, ale nie możesz zmodyfikować tego odwołania, a bazowy obiekt String jest niezmienny.

Przydzial

zText += foo;

jest równa:

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

Oznacza to, że (lokalnie) ponownie przypisuje parametr zTextjako nowe odniesienie, które wskazuje na nowe miejsce w pamięci, w którym jest nowe, Stringktóre zawiera oryginalną zawartość zTextz "foo"dołączoną.

Oryginalny obiekt nie jest modyfikowany, a main()zmienna lokalna metody zTextnadal wskazuje na oryginalny (pusty) ciąg.

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

wydruki:

Original value:
Local value: foo
Final value:

Jeśli chcesz zmodyfikować ciąg, możesz, zgodnie z opisem, StringBuilderlub jakiś kontener (tablicę lub AtomicReferenceniestandardową klasę kontenera), który zapewnia dodatkowy poziom niezależności wskaźnika. Alternatywnie, po prostu zwróć nową wartość i przypisz ją:

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

wydruki:

Original value:
Local value: foo
Final value: foo

Jest to prawdopodobnie najbardziej podobne do Javy rozwiązanie w ogólnym przypadku - zobacz Efektywny element Java „Korzystna niezmienność”.

Jak już wspomniano, StringBuilderczęsto zapewnia lepszą wydajność - jeśli masz dużo do zrobienia, szczególnie wewnątrz pętli, użyj StringBuilder.

Ale spróbuj przekazać niezmienne, Stringsa nie zmienne, StringBuildersjeśli możesz - Twój kod będzie łatwiejszy do odczytania i łatwiejszy w utrzymaniu. Rozważ utworzenie parametrów finali skonfigurowanie środowiska IDE, aby ostrzegało Cię, gdy ponownie przypisujesz parametr metody do nowej wartości.

David Moles
źródło
6
Mówisz „ściśle mówiąc”, jakby to było prawie nieistotne - podczas gdy to jest sednem problemu. Gdyby Java naprawdę miała przekazywanie przez odniesienie, nie byłoby problemu. Należy unikać propagowanie mitu „Java obiektów przechodzi przez odniesienie” - wyjaśniając (poprawne) „referencje są przekazywane przez wartość” model jest dużo bardziej pomocne, IMO.
Jon Skeet
Hej, gdzie poszedł mój poprzedni komentarz? :-) Jak powiedziałem wcześniej i jak dodał Jon, prawdziwym problemem jest to, że odwołanie jest przekazywane przez wartość. To bardzo ważne i wiele osób nie rozumie różnicy semantycznej.
Ed S.
1
Słusznie. Jako programista Java nie widziałem niczego faktycznie przekazywanego przez odniesienie od ponad dziesięciu lat, więc mój język stał się niechlujny. Ale masz rację, powinienem był bardziej uważać, szczególnie w przypadku publiczności programistycznej.
David Moles
5

Ciąg jest niezmiennym obiektem w Javie. Możesz użyć klasy StringBuilder, aby wykonać zadanie, które próbujesz wykonać, w następujący sposób:

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

Inną opcją jest utworzenie klasy z ciągiem jako zmienną zakresu (zdecydowanie odradzane) w następujący sposób:

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
źródło
1
Ciąg nie jest typem pierwotnym w języku Java.
VWeber,
To prawda, ale na co dokładnie próbujesz zwrócić moją uwagę?
Fadi Hanna AL-Kass
Od kiedy Java String jest prymitywem ?! WIĘC! Ciąg przekazany przez odwołanie.
thedp
2
„niezmienny przedmiot” to to, co zamierzałem powiedzieć. Z jakiegoś powodu nie przeczytałem ponownie mojej odpowiedzi po tym, jak @VWeber skomentował, ale teraz widzę, co próbował powiedzieć. mój błąd. odpowiedź zmodyfikowana
Fadi Hanna AL-Kass
2

Odpowiedź jest prosta. W java ciągi znaków są niezmienne. Stąd przypomina to użycie modyfikatora „final” (lub „const” w C / C ++). Tak więc raz przypisany nie możesz go zmienić tak, jak to zrobiłeś.

Można zmienić których wartość, do której ciąg punktów, ale nie można zmienić rzeczywistej wartości, do których ten ciąg jest obecnie wskazującego.

To znaczy. String s1 = "hey". Możesz zrobić s1 = "woah", i to jest całkowicie w porządku, ale tak naprawdę nie możesz zmienić podstawowej wartości ciągu (w tym przypadku: "hey") na coś innego po przypisaniu go za pomocą plusEquals itp. (Tj. s1 += " whatup != "hey whatup").

Aby to zrobić, użyj klas StringBuilder lub StringBuffer lub innych zmiennych kontenerów, a następnie po prostu wywołaj .toString (), aby przekonwertować obiekt z powrotem na łańcuch.

Uwaga: Ciągi znaków są często używane jako klucze mieszające, dlatego jest to jeden z powodów, dla których są one niezmienne.

ak_2050
źródło
Nie ma znaczenia, czy jest zmienny, czy niezmienny. =na odniesienie NIGDY nie wpływa na obiekt, na który wskazywał, niezależnie od klasy.
newacct
2

String to specjalna klasa w Javie. Jest bezpieczny dla wątków, co oznacza „Po utworzeniu instancji String zawartość instancji String nigdy się nie zmieni”.

Oto, co się dzieje

 zText += "foo";

Najpierw kompilator Java pobierze wartość instancji zText String, a następnie utworzy nową instancję String, której wartością jest zText z dołączonym „foo”. Więc wiesz, dlaczego instancja, na którą wskazuje zText, nie uległa zmianie. To zupełnie nowa instancja. W rzeczywistości nawet String „foo” jest nową instancją String. Tak więc dla tej instrukcji Java utworzy dwie instancje typu String, jedna to „foo”, a druga to wartość z zText append „foo”. Zasada jest prosta: wartość instancji String nigdy nie zostanie zmieniona.

W przypadku metody fillString możesz użyć StringBuffer jako parametru lub zmienić go w ten sposób:

String fillString(String zText) {
    return zText += "foo";
}
DeepNightTwo
źródło
... chociaż gdybyś miał zdefiniować 'fillString' zgodnie z tym kodem, naprawdę powinieneś nadać mu bardziej odpowiednią nazwę!
Stephen C
Tak. Ta metoda powinna mieć nazwę „appendString” lub podobną.
DeepNight Dwa
1

To działa przy użyciu 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");
}
}

Jeszcze lepiej użyj 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
źródło
1

Ciąg jest niezmienny w java. nie można modyfikować / zmieniać istniejącego literału ciągu / obiektu.

String s = "Witaj"; s = s + "cześć";

W tym przypadku poprzednie odniesienie s zostało zastąpione nowym odniesieniem s wskazującym na wartość „HelloHi”.

Jednak w celu wprowadzenia zmienności mamy StringBuilder i StringBuffer.

StringBuilder s = new StringBuilder (); s.append ("Cześć");

spowoduje to dodanie nowej wartości „Cześć” do tego samego odniesienia. //

gauravJ
źródło
0

Aaron Digulla ma jak dotąd najlepszą odpowiedź. Odmianą jego drugiej opcji jest użycie klasy opakowania lub kontenera MutableObject biblioteki commons lang w wersji 3+:

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

zapisujesz deklarację klasy kontenera. Wadą jest zależność od wspólnej biblioteki lang. Ale biblioteka ma sporo przydatnych funkcji i prawie każdy większy projekt, nad którym pracowałem, używał jej.

user2606740
źródło
0

Aby przekazać obiekt (w tym String) przez odwołanie w java, można przekazać go jako element członkowski otaczającego adaptera. Oto rozwiązanie z generycznym:

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
źródło
0

Dla bardziej ciekawych

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

Teraz, uruchamiając powyższy kod, możesz zobaczyć, jak hashcodeszmienia się adres za pomocą zmiennej String s. nowy obiekt jest przydzielany do zmiennej s w funkcji, changetogdy zmienia się s

Rajesh kumar
źródło