ผ่าน String โดยการอ้างอิงใน Java?


161

ฉันคุ้นเคยกับการทำสิ่งต่อไปนี้ในC:

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

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

และผลลัพธ์คือ:

foo

อย่างไรก็ตามใน Java สิ่งนี้ดูเหมือนจะไม่ทำงาน ผมถือว่าเพราะStringวัตถุถูกคัดลอกแทนการส่งผ่านโดยอ้างอิง ฉันคิดว่า Strings เป็นวัตถุซึ่งมักถูกส่งผ่านโดยการอ้างอิง

เกิดขึ้นที่นี่คืออะไร?


2
แม้ว่าพวกเขาจะถูกส่งผ่านโดยการอ้างอิง (Java ในสิ่งที่ได้รับผ่านการเป็นสำเนาของค่าอ้างอิง แต่ที่หัวข้ออื่น) วัตถุ String จะไม่เปลี่ยนรูปเพื่อที่จะไม่ทำงานอยู่ดี
OscarRyz

8
ไม่ใช่รหัส C
Alston

@Phil: มันจะไม่ทำงานใน C # ด้วย
Mangesh

คำตอบ:


196

คุณมีสามตัวเลือก:

  1. ใช้ StringBuilder:

    StringBuilder zText = new StringBuilder ();
    void fillString(StringBuilder zText) { zText.append ("foo"); }
    
  2. สร้างคลาสคอนเทนเนอร์และส่งอินสแตนซ์ของคอนเทนเนอร์ไปยังเมธอดของคุณ:

    public class Container { public String data; }
    void fillString(Container c) { c.data += "foo"; }
    
  3. สร้างอาร์เรย์:

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

จากมุมมองประสิทธิภาพ StringBuilder มักเป็นตัวเลือกที่ดีที่สุด


11
เพียงจำไว้ว่า StringBuilder ไม่ปลอดภัยเธรด ในสภาพแวดล้อมแบบมัลติเธรดใช้คลาส StringBuffer หรือดูแลการซิงโครไนซ์ด้วยตนเอง
Boris Pavlović

12
@ Boris Pavlovic - ใช่ แต่หากไม่มีการใช้ StringBuilder เดียวกันโดยเธรดที่แตกต่างกันซึ่ง IMO ไม่น่าจะเกิดขึ้นในสถานการณ์ที่กำหนดมันไม่น่าจะมีปัญหา มันไม่สำคัญว่าเมธอดจะถูกเรียกโดยเธรดที่แตกต่างกันหรือไม่และรับ StringBuilder อื่น
Ravi Wallau

ฉันชอบตัวเลือกที่สาม (อาร์เรย์) มากที่สุดเพราะเป็นตัวเลือกทั่วไปเท่านั้น มันช่วยให้ผ่านบูลีน, int, ฯลฯ คุณช่วยอธิบายรายละเอียดเกี่ยวกับประสิทธิภาพของโซลูชันนี้หน่อยได้ไหม - คุณอ้างว่าอันแรกดีกว่าจากมุมมองประสิทธิภาพ
meolic

@meolic StringBuilderเร็วกว่าs += "..."เนื่องจากจะไม่จัดสรรวัตถุ String ใหม่ทุกครั้ง ในความเป็นจริงเมื่อคุณเขียนโค้ด Java เช่นs = "Hello " + nameนั้นคอมไพเลอร์จะสร้างรหัสไบต์ซึ่งสร้างStringBuilderและเรียกappend()สองครั้ง
Aaron Digulla

86

ใน Java ไม่มีอะไรจะผ่านอ้างอิง ทุกอย่างจะผ่านค่า การอ้างอิงวัตถุถูกส่งผ่านตามค่า นอกจากนี้สายมีไม่เปลี่ยนรูป ดังนั้นเมื่อคุณต่อท้ายสตริงที่ผ่านคุณจะได้รับสตริงใหม่ คุณสามารถใช้ค่าส่งคืนหรือส่งผ่าน StringBuffer แทน


2
นี้ไม่ได้เป็นความเข้าใจผิดว่ามันเป็นแนวคิดที่
แพทริค Cornelissen

41
อืมมมม .. ไม่มีความเข้าใจผิดว่าคุณกำลังส่งวัตถุโดยการอ้างอิง คุณกำลังส่งการอ้างอิงตามค่า
Ed S.

30
ใช่มันเป็นความเข้าใจผิด มันเป็นความเข้าใจผิดอย่างใหญ่หลวง มันนำไปสู่คำถามสัมภาษณ์ที่ฉันเกลียด: ("Java pass ขัดแย้ง") ฉันเกลียดมันเพราะผู้สัมภาษณ์ประมาณครึ่งหนึ่งดูเหมือนจะต้องการคำตอบที่ผิด ("สิ่งดั้งเดิมด้วยคุณค่า คำตอบที่ถูกต้องใช้เวลานานกว่านั้นและดูเหมือนจะทำให้บางคนสับสน และพวกเขาจะไม่เชื่อมั่น: ฉันสาบานว่าฉันล้มหน้าจอเทคโนโลยีเพราะผู้คัดกรองประเภท CSMajor เคยได้ยินความเข้าใจผิดในวิทยาลัยและเชื่อว่าเป็นข่าวประเสริฐ Feh
CPerkins

4
@ CPerkins คุณไม่รู้หรอกว่ามันโกรธฉันแค่ไหน มันทำให้เลือดของฉันเดือด

6
ทุกอย่างถูกส่งผ่านตามมูลค่าในทุกภาษา บางค่าเหล่านั้นเกิดขึ้นเป็นที่อยู่ (อ้างอิง)
gerardw

52

สิ่งที่เกิดขึ้นคือการอ้างอิงถูกส่งผ่านโดยค่าคือสำเนาของการอ้างอิงจะถูกส่งผ่าน ไม่มีสิ่งใดใน java ที่ถูกส่งผ่านโดยการอ้างอิงและเนื่องจากสตริงไม่เปลี่ยนรูปการกำหนดนั้นจึงสร้างวัตถุสตริงใหม่ที่สำเนาของการอ้างอิงชี้ไปที่ในขณะนี้ การอ้างอิงดั้งเดิมยังคงชี้ไปที่สตริงว่าง

สิ่งนี้จะเหมือนกันสำหรับวัตถุใด ๆ เช่นการตั้งค่าเป็นค่าใหม่ในวิธีการ ตัวอย่างด้านล่างจะทำให้สิ่งที่เกิดขึ้นชัดเจนยิ่งขึ้น แต่การต่อสตริงเป็นสิ่งเดียวกัน

void foo( object o )
{
    o = new Object( );  // original reference still points to old value on the heap
}

6
ตัวอย่างที่ยอดเยี่ยม!
ลัส

18

java.lang.String ไม่เปลี่ยนรูป

ฉันเกลียดการวาง URL แต่https://docs.oracle.com/javase/10/docs/api/java/lang/String.htmlเป็นสิ่งจำเป็นสำหรับคุณในการอ่านและทำความเข้าใจถ้าคุณอยู่ใน java-land


ฉันไม่คิดว่าโซเรนจอห์นสันไม่รู้ว่าสตริงคืออะไรนั่นไม่ใช่คำถามและไม่ใช่สิ่งที่ฉันมาที่นี่หลังจาก 15 ปีของ Java
AHH

8

วัตถุถูกส่งผ่านโดยการอ้างอิงดั้งเดิมจะถูกส่งผ่านตามค่า

สตริงไม่ใช่ดั้งเดิมมันเป็นวัตถุและเป็นกรณีพิเศษของวัตถุ

นี่คือเพื่อการประหยัดหน่วยความจำ ใน JVM มีพูลสตริง สำหรับทุกสตริงที่สร้างขึ้น JVM จะพยายามดูว่ามีสตริงเดียวกันในพูลสตริงหรือไม่และชี้ไปที่สตริงนั้นถ้ามีอยู่แล้ว

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

/ * เอาท์พุท * /

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

== กำลังตรวจหาที่อยู่หน่วยความจำ (ข้อมูลอ้างอิง) และ. Equals กำลังตรวจสอบเนื้อหา (ค่า)


3
ไม่จริงทั้งหมด Java ผ่าน VALUE เสมอ เคล็ดลับคือวัตถุจะถูกส่งผ่านโดยการอ้างอิงซึ่งถูกส่งผ่านตามค่า (หากินฉันรู้) ตรวจสอบสิ่งนี้: javadude.com/articles/passbyvalue.htm
adhg

1
@adhg คุณเขียนว่า "ออบเจ็กต์ถูกส่งผ่านโดยการอ้างอิงซึ่งผ่านค่า" <--- นั่นคือพูดพล่อยๆ คุณหมายถึงวัตถุที่มีการอ้างอิงซึ่งถูกส่งผ่านโดยค่า
barlop

2
-1 คุณเขียนว่า "วัตถุถูกส่งผ่านโดยการอ้างอิง" <- FALSE หากเป็นจริงเมื่อเรียกใช้เมธอดและส่งผ่านตัวแปรเมธอดสามารถทำให้ตัวแปรชี้ / อ้างอิงไปยังวัตถุอื่น แต่ไม่สามารถทำได้ ทุกอย่างถูกส่งผ่านโดยค่าใน java และคุณยังคงเห็นผลของการเปลี่ยนคุณสมบัติของวัตถุนอกวิธีการ
barlop

ดูคำตอบได้ที่นี่stackoverflow.com/questions/40480/…
barlop

7

อาร์กิวเมนต์ทั้งหมดใน Java ถูกส่งผ่านโดยค่า เมื่อคุณผ่านStringไปยังฟังก์ชันค่าที่ผ่านไปคือการอ้างอิงไปยังวัตถุ String แต่คุณไม่สามารถแก้ไขการอ้างอิงนั้นและพื้นฐานวัตถุ String จะไม่เปลี่ยนรูป

งานที่ได้รับมอบหมาย

zText += foo;

เทียบเท่ากับ:

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

นั่นคือมัน (ภายในเครื่อง) กำหนดพารามิเตอร์zTextใหม่เป็นการอ้างอิงใหม่ซึ่งชี้ไปยังตำแหน่งหน่วยความจำใหม่ซึ่งเป็นเนื้อหาใหม่Stringที่มีเนื้อหาดั้งเดิมของzTextพร้อมกับ"foo"ผนวก

วัตถุต้นฉบับไม่ได้ถูกแก้ไขและmain()ตัวแปรท้องถิ่นของวิธีการzTextยังคงชี้ไปที่สตริงเดิม (ว่าง)

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

พิมพ์:

Original value:
Local value: foo
Final value:

หากคุณต้องการแก้ไขสตริงคุณสามารถใช้งานได้ตามที่ระบุไว้StringBuilderหรืออย่างอื่นบางคอนเทนเนอร์ (อาเรย์หรือAtomicReferenceคลาสคอนเทนเนอร์แบบกำหนดเอง) ที่ให้คุณเพิ่มระดับการชี้ทางอ้อม หรือเพียงแค่คืนค่าใหม่และกำหนด:

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

พิมพ์:

Original value:
Local value: foo
Final value: foo

นี่อาจเป็นคำตอบที่เหมือนกับ Java ที่สุดในกรณีทั่วไป - ดูรายการJava ที่มีประสิทธิภาพ "Favor immutability"

ตามที่ระบุไว้ แต่StringBuilderมักจะให้ประสิทธิภาพที่ดีขึ้น - StringBuilderถ้าคุณมีจำนวนมากของการผนวกที่จะทำโดยเฉพาะอย่างยิ่งภายในห่วงการใช้งาน

แต่พยายามที่จะผ่านสิ่งที่ไม่เปลี่ยนรูปStringsแทนการเปลี่ยนแปลงStringBuildersหากคุณทำได้ - รหัสของคุณจะอ่านง่ายขึ้นและบำรุงรักษาได้มากขึ้น พิจารณาสร้างพารามิเตอร์ของคุณfinalและกำหนดค่า IDE ของคุณเพื่อเตือนคุณเมื่อคุณกำหนดพารามิเตอร์เมธอดให้เป็นค่าใหม่


6
คุณพูดว่า "การพูดอย่างเคร่งครัด" ราวกับว่ามันไม่เกี่ยวข้องเกือบ - ในขณะที่มันอยู่ที่หัวใจของปัญหา หาก Java มีการอ้างอิงจริง ๆแล้วก็จะไม่มีปัญหา โปรดพยายามหลีกเลี่ยงการเผยแพร่ตำนานของ "Java ส่งผ่านวัตถุโดยการอ้างอิง" - อธิบายการอ้างอิง (ที่ถูกต้อง) "การส่งผ่านค่า" เป็นประโยชน์มากขึ้น IMO
Jon Skeet

เฮ้ความคิดเห็นก่อนหน้าของฉันหายไปไหน :-) อย่างที่ฉันพูดไปก่อนหน้านี้และเมื่อจอนได้เพิ่มปัญหาที่แท้จริงที่นี่คือการอ้างอิงถูกส่งผ่านตามค่า นั่นเป็นสิ่งสำคัญมากและหลายคนไม่เข้าใจความหมายที่แตกต่างกัน
Ed S.

1
ยุติธรรมพอสมควร ในฐานะโปรแกรมเมอร์ Java ฉันไม่ได้เห็นอะไรเลยที่ผ่านการอ้างอิงจริงในทศวรรษที่ดีกว่าดังนั้นภาษาของฉันจึงเลอะเทอะ แต่คุณพูดถูกฉันควรจะระมัดระวังมากขึ้น
เดวิดไฝ

5

String เป็นวัตถุที่ไม่เปลี่ยนรูปแบบใน Java คุณสามารถใช้คลาส StringBuilder เพื่อทำงานที่คุณพยายามทำให้สำเร็จดังนี้:

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

อีกทางเลือกหนึ่งคือการสร้างคลาสที่มีสตริงเป็นตัวแปรขอบเขต (หมดกำลังใจอย่างมาก) ดังนี้:

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

1
สตริงไม่ใช่ประเภทดั้งเดิมใน Java
VWeber

จริง แต่สิ่งที่คุณพยายามดึงดูดความสนใจของฉันคืออะไร?
Fadi Hanna AL-Kass

ตั้งแต่เมื่อไหร่ที่ Java String เป็นดั่งเดิม?! ดังนั้น! สตริงที่ส่งผ่านโดยการอ้างอิง
thedp

2
'วัตถุที่ไม่เปลี่ยนรูป' คือสิ่งที่ฉันตั้งใจจะพูด ฉันด้วยเหตุผลบางอย่างไม่ได้อ่านคำตอบของฉันอีกครั้งหลังจากที่ @VWeber แสดงความคิดเห็น แต่ตอนนี้ฉันเห็นสิ่งที่เขาพยายามจะพูด ความผิดฉันเอง. แก้ไขคำตอบแล้ว
Fadi Hanna AL-Kass

2

คำตอบนั้นง่าย ในสตริง java จะไม่เปลี่ยนรูป ดังนั้นมันจึงชอบใช้ 'สุดท้าย' ปรับปรุง (หรือ 'const' ใน C / C ++) ดังนั้นเมื่อได้รับมอบหมายแล้วคุณจะไม่สามารถเปลี่ยนแปลงได้เหมือนอย่างที่เคยทำ

คุณสามารถเปลี่ยนซึ่งค่าที่จุดสตริง แต่คุณไม่สามารถเปลี่ยนค่าจริงที่สายนี้ในปัจจุบันคือการชี้

กล่าวคือ String s1 = "hey". คุณสามารถทำได้s1 = "woah"และก็ไม่เป็นไร แต่คุณไม่สามารถเปลี่ยนค่าพื้นฐานของสตริง (ในกรณีนี้: "เฮ้") เป็นอย่างอื่นเมื่อได้รับมอบหมายโดยใช้ plusEquals ฯลฯ (เช่นs1 += " whatup != "hey whatup")

ในการทำเช่นนั้นให้ใช้คลาส StringBuilder หรือ StringBuffer หรือคอนเทนเนอร์ที่ไม่แน่นอนอื่น ๆ จากนั้นเพียงเรียก. toString () เพื่อแปลงวัตถุกลับไปเป็นสตริง

หมายเหตุ: สตริงมักจะใช้เป็นแป้นแฮชดังนั้นจึงเป็นส่วนหนึ่งของสาเหตุที่ทำให้ไม่สามารถเปลี่ยนแปลงได้


ไม่ว่ามันจะไม่แน่นอนหรือไม่เปลี่ยนรูปไม่เกี่ยวข้อง =ในการอ้างอิงไม่ส่งผลกระทบต่อวัตถุที่ใช้ในการชี้ไปที่โดยไม่คำนึงถึงระดับ
newacct

2

String เป็นคลาสพิเศษใน Java มันคือ Thread Safe ซึ่งหมายถึง "เมื่อมีสร้างอินสแตนซ์ของสตริงเนื้อหาของอินสแตนซ์ของสตริงจะไม่เปลี่ยนแปลง"

นี่คือสิ่งที่เกิดขึ้น

 zText += "foo";

อันดับแรกคอมไพเลอร์ Java จะได้รับค่าของอินสแตนซ์ zText String จากนั้นสร้างอินสแตนซ์สตริงใหม่ที่มีค่าเป็น zText ต่อท้าย "foo" ดังนั้นคุณจึงรู้ว่าทำไมอินสแตนซ์ที่ชี้ไปยัง zText ไม่เปลี่ยนแปลง มันเป็นตัวอย่างใหม่โดยสิ้นเชิง ในความเป็นจริงแม้แต่สตริง "foo" ก็เป็นอินสแตนซ์ของสตริงใหม่ ดังนั้นสำหรับคำสั่งนี้ Java จะสร้างสองอินสแตนซ์ของสตริงหนึ่งอันคือ "foo" และอีกอันคือค่าของ zText ผนวก "foo" กฎนั้นง่าย: ค่าของอินสแตนซ์ของสตริงจะไม่มีการเปลี่ยนแปลง

สำหรับเมธอด fillString คุณสามารถใช้ StringBuffer เป็นพารามิเตอร์หรือคุณสามารถเปลี่ยนแปลงได้ดังนี้

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

... แต่ถ้าคุณมีการกำหนด 'fillString' ตามรหัสนี้คุณจริงๆควรจะให้มันเป็นชื่อที่เหมาะสมมากขึ้น!
สตีเฟ่นซี

ใช่. วิธีนี้ควรตั้งชื่อเป็น "appendString" หรือมากกว่านั้น
DeepNightTwo


1

งานนี้ใช้ 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");
}
}

ยิ่งใช้ 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");
}
}

1

String ไม่เปลี่ยนรูปในภาษาจาวา คุณไม่สามารถแก้ไข / เปลี่ยนแปลงสตริงตัวอักษร / วัตถุที่มีอยู่

String s = "Hello"; s = s + "สวัสดี";

ที่นี่การอ้างอิงก่อนหน้านี้ถูกแทนที่ด้วยการอ้างอิงใหม่ชี้ไปที่ค่า "HelloHi"

อย่างไรก็ตามสำหรับการนำความไม่แน่นอนเรามี StringBuilder และ StringBuffer

StringBuilder s = new StringBuilder (); s.append ( "สวัสดี");

ผนวกค่าใหม่นี้ "สวัสดี" เพื่ออ้างอิงเดียวกัน //


0

Aaron Digulla มีคำตอบที่ดีที่สุดจนถึงตอนนี้ รูปแบบของตัวเลือกที่สองของเขาคือการใช้ wrapper หรือ container class MutableObject ของคอมมอนส์ lang library version 3+:

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

คุณบันทึกการประกาศของคลาสคอนเทนเนอร์ ข้อเสียคือการพึ่งพา lib คอมมอนส์ แต่ lib นั้นมีฟังก์ชั่นที่มีประโยชน์มากมายและเกือบทุกโปรเจคที่ใหญ่กว่าที่ผมเคยใช้มา


0

สำหรับการส่งวัตถุ (รวมถึงสตริง) โดยการอ้างอิงใน java คุณอาจผ่านมันเป็นสมาชิกของอะแดปเตอร์โดยรอบ วิธีแก้ปัญหาแบบทั่วไปอยู่ที่นี่:

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

0

สำหรับคนที่อยากรู้อยากเห็นมากขึ้น

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

ตอนนี้ด้วยการเรียกใช้รหัสข้างต้นคุณสามารถดูว่าที่อยู่hashcodesเปลี่ยนแปลงด้วยตัวแปร String วัตถุใหม่ถูกจัดสรรให้กับตัวแปร s ในฟังก์ชั่นchangetoเมื่อมีการเปลี่ยนแปลง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.