สตริง Java ไม่เปลี่ยนรูปจริงๆหรือ


399

เราทุกคนรู้ว่าStringไม่เปลี่ยนรูปแบบใน Java แต่ตรวจสอบรหัสต่อไปนี้:

String s1 = "Hello World";  
String s2 = "Hello World";  
String s3 = s1.substring(6);  
System.out.println(s1); // Hello World  
System.out.println(s2); // Hello World  
System.out.println(s3); // World  

Field field = String.class.getDeclaredField("value");  
field.setAccessible(true);  
char[] value = (char[])field.get(s1);  
value[6] = 'J';  
value[7] = 'a';  
value[8] = 'v';  
value[9] = 'a';  
value[10] = '!';  

System.out.println(s1); // Hello Java!  
System.out.println(s2); // Hello Java!  
System.out.println(s3); // World  

เหตุใดโปรแกรมนี้จึงทำงานเช่นนี้ และทำไมมูลค่าs1และการs2เปลี่ยนแปลง แต่ไม่ใช่s3?


394
คุณสามารถทำทุกวิธีการโง่ด้วยการสะท้อน แต่โดยทั่วไปคุณจะทำลายสติ๊กเกอร์ "รับประกันเป็นโมฆะถ้าถอดออก" ในชั้นเรียนทันทีที่คุณทำ
cHao

16
@DarshanPatel ใช้ SecurityManager เพื่อปิดใช้งานการสะท้อน
Sean Patrick Floyd

39
หากคุณต้องการยุ่งกับสิ่งต่าง ๆ คุณสามารถทำได้(Integer)1+(Integer)2=42โดยไปยุ่งกับ autoboxing ที่แคชไว้ (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )
Richard Tingle

15
คุณอาจถูกขบขันด้วยคำตอบนี้ฉันเขียนเกือบ 5 ปีที่แล้วstackoverflow.com/a/1232332/27423 - มันเกี่ยวกับรายการที่ไม่เปลี่ยนรูปแบบใน C # แต่โดยพื้นฐานแล้วมันเป็นสิ่งเดียวกัน: ฉันจะหยุดผู้ใช้ไม่ให้แก้ไขข้อมูลของฉันได้อย่างไร? และคำตอบคือคุณทำไม่ได้ การสะท้อนทำให้มันง่ายมาก ภาษากระแสหลักหนึ่งที่ไม่มีปัญหานี้คือ JavaScript เนื่องจากไม่มีระบบการสะท้อนที่สามารถเข้าถึงตัวแปรท้องถิ่นได้ในช่วงปิดดังนั้นความเป็นส่วนตัวหมายถึงความเป็นส่วนตัวอย่างแท้จริง(แม้ว่าจะไม่มีคำหลักสำหรับมัน!)
Daniel Earwicker

49
มีใครอ่านคำถามต่อท้ายหรือไม่? คำถามคือให้ฉันได้โปรดทำซ้ำ: "ทำไมโปรแกรมนี้ทำงานเช่นนี้ทำไมค่าของ s1 และ s2 เปลี่ยนไปและไม่เปลี่ยนเป็น s3" คำถามไม่ใช่ว่าทำไม s1 และ s2 เปลี่ยนไป! คำถามคือ: ทำไม s3 ไม่เปลี่ยน?
Roland Pihlakas

คำตอบ:


403

String ไม่สามารถเปลี่ยนรูปได้ * แต่นี่หมายความว่าคุณไม่สามารถเปลี่ยนแปลงได้โดยใช้ API สาธารณะ

สิ่งที่คุณกำลังทำอยู่ที่นี่คือการหลีกเลี่ยง API ปกติโดยใช้การสะท้อนกลับ เช่นเดียวกับคุณสามารถเปลี่ยนค่าของ enums เปลี่ยนตารางการค้นหาที่ใช้ใน Integer autoboxing เป็นต้น

ตอนนี้เหตุผลs1และs2ค่าการเปลี่ยนแปลงก็คือพวกเขาทั้งสองอ้างถึงสตริง interned เดียวกัน คอมไพเลอร์ทำสิ่งนี้ (ดังที่ได้กล่าวไว้โดยคำตอบอื่น ๆ )

เหตุผลs3ไม่ได้แปลกใจสำหรับฉันจริง ๆ เพราะฉันคิดว่ามันจะใช้ร่วมกันvalueอาร์เรย์ ( มันทำในรุ่นก่อนหน้าของ Javaก่อน Java 7u6) อย่างไรก็ตามเมื่อดูซอร์สโค้ดของStringเราจะเห็นว่าvalueอาร์เรย์อักขระสำหรับสตริงย่อยนั้นถูกคัดลอก (โดยใช้Arrays.copyOfRange(..)) นี่คือสาเหตุที่มันไม่เปลี่ยนแปลง

คุณสามารถติดตั้ง a SecurityManagerเพื่อหลีกเลี่ยงรหัสที่เป็นอันตรายในการทำสิ่งต่าง ๆ แต่โปรดจำไว้ว่าห้องสมุดบางแห่งขึ้นอยู่กับการใช้เทคนิคการไตร่ตรองเหล่านี้ (โดยทั่วไปคือเครื่องมือ ORM, ไลบรารี AOP เป็นต้น)

*) ตอนแรกฉันเขียนว่าStrings ไม่เปลี่ยนแปลงไม่ได้จริงๆเพียงแค่ "ไม่เปลี่ยนรูปประสิทธิภาพ" นี้อาจจะทำให้เข้าใจผิดในการดำเนินงานในปัจจุบันของStringที่อาร์เรย์มีการทำเครื่องหมายแน่นอนvalue private finalมันยังคงเป็นที่น่าสังเกตว่าไม่มีทางที่จะประกาศอาเรย์ใน Java ว่าไม่เปลี่ยนรูปดังนั้นต้องระมัดระวังไม่ให้เปิดเผยนอกคลาสแม้ว่าจะมีตัวดัดแปลงการเข้าถึงที่เหมาะสม


เนื่องจากหัวข้อนี้ได้รับความนิยมอย่างล้นหลามต่อไปนี้เป็นคำแนะนำในการอ่านเพิ่มเติม: Heinz Kabutz's Reflection Madness พูดคุยจาก JavaZone 2009 ซึ่งครอบคลุมประเด็นต่าง ๆ ใน OP พร้อมกับการสะท้อนอื่น ๆ ... บ้าคลั่ง ...

ครอบคลุมถึงสาเหตุที่บางครั้งมีประโยชน์ และทำไมส่วนใหญ่คุณควรหลีกเลี่ยง :-)


7
ที่จริงแล้วการStringฝึกงานเป็นส่วนหนึ่งของ JLS ( "สตริงตัวอักษรหมายถึงอินสแตนซ์เดียวกันของคลาสสตริง"เสมอ) แต่ฉันเห็นด้วยไม่ใช่วิธีที่ดีในการพิจารณารายละเอียดการปฏิบัติของStringชั้นเรียน
haraldK

3
อาจจะเป็นเหตุผลว่าทำไมการsubstringคัดลอกมากกว่าการใช้ "ส่วน" ของอาเรย์ที่มีอยู่เป็นอย่างอื่นถ้าฉันมีสตริงขนาดใหญ่sและดึงซับสตริงขนาดเล็กtออกมาจากนั้นฉันก็ละทิ้งsแต่เก็บไว้tจากนั้นอาเรย์ขนาดใหญ่จะยังมีชีวิตอยู่ (ไม่เก็บขยะ) ดังนั้นอาจเป็นเรื่องปกติที่แต่ละค่าสตริงจะมีอาเรย์ของตัวเองหรือไม่
Jeppe Stig Nielsen

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

2
@ โฮลเกอร์ - ใช่ความเข้าใจของฉันคือฟิลด์ออฟเซ็ตลดลงใน JVM ล่าสุด และแม้ว่าจะเป็นปัจจุบันก็ไม่ได้ใช้บ่อย
Hot Licks

2
@supercat: มันไม่สำคัญว่าคุณจะมีรหัสเนทีฟหรือไม่มีการใช้งานที่แตกต่างกันสำหรับสตริงและซับสตริงภายใน JVM เดียวกันหรือมีbyte[]สตริงสำหรับสตริง ASCII และchar[]สำหรับคนอื่น ๆ ก็หมายความว่าการดำเนินการทุกครั้งจะต้องตรวจสอบว่า การดำเนินงาน สิ่งนี้ขัดขวางการฝังโค้ดลงในเมธอดโดยใช้สตริงซึ่งเป็นขั้นตอนแรกของการปรับให้เหมาะสมเพิ่มเติมโดยใช้ข้อมูลบริบทของผู้โทร นี่คือผลกระทบใหญ่
Holger

93

ใน Java หากตัวแปรดั้งเดิมของสตริงสองตัวถูกเตรียมใช้งานตามตัวอักษรเดียวกันมันจะกำหนดการอ้างอิงเดียวกันให้กับตัวแปรทั้งสอง:

String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true

การเริ่มต้น

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

สตริงย่อย

เมื่อคุณเข้าถึงสตริงโดยใช้การสะท้อนคุณจะได้รับตัวชี้จริง:

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

ดังนั้นการเปลี่ยนแปลงนี้จะเปลี่ยนสตริงที่ถือตัวชี้ไป แต่ตามที่s3สร้างขึ้นด้วยสตริงใหม่เนื่องจากsubstring()มันจะไม่เปลี่ยนแปลง

เปลี่ยนแปลง


สิ่งนี้ใช้ได้กับตัวอักษรเท่านั้นและเป็นการเพิ่มประสิทธิภาพเวลาแบบคอมไพล์
SpacePrez

2
@ Zaphod42 ไม่จริง นอกจากนี้คุณยังสามารถโทรinternด้วยตนเองบนสตริงที่ไม่ใช่ตัวอักษรและเก็บเกี่ยวผลประโยชน์
Chris Hayes

หมายเหตุ: คุณต้องการใช้internอย่างรอบคอบ การทำทุกอย่างไม่ได้ทำให้คุณได้อะไรมากนักและอาจเป็นช่วงเวลาที่ทำให้คุณปวดหัวได้เมื่อคุณเพิ่มการสะท้อนกลับเข้าไปในมิกซ์นั้น
cHao

Test1และTest1ไม่สอดคล้องกับtest1==test2และไม่ปฏิบัติตามอนุสัญญาการตั้งชื่อภาษาจาวา
c0der

50

คุณใช้การไตร่ตรองเพื่อหลีกเลี่ยงความไม่เปลี่ยนแปลงของ String ซึ่งเป็นรูปแบบของ "การโจมตี"

มีตัวอย่างมากมายที่คุณสามารถสร้างเช่นนี้ (เช่นคุณสามารถสร้างอินสแตนซ์ของVoidวัตถุได้ด้วย) แต่ก็ไม่ได้หมายความว่า String นั้นจะไม่ "ไม่เปลี่ยนรูป"

มีกรณีการใช้งานที่ประเภทของรหัสนี้อาจถูกใช้เพื่อประโยชน์ของคุณและจะเป็น "ดีเข้ารหัส" เช่นมีการล้างรหัสผ่านจากความทรงจำในช่วงเวลาที่เร็วที่สุด (ก่อน GC)

คุณอาจไม่สามารถเรียกใช้รหัสของคุณได้ทั้งนี้ขึ้นอยู่กับตัวจัดการความปลอดภัย


30

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


24

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

ผลที่สองที่คุณเห็นคือว่าทุกStringการเปลี่ยนแปลง s s1ในขณะที่มันดูเหมือนว่าคุณเปลี่ยนแปลงเพียงอย่างเดียว มันเป็นคุณสมบัติบางอย่างของตัวอักษร Java String ที่พวกเขาจะฝึกงานโดยอัตโนมัติเช่นแคช สองตัวอักษรสตริงที่มีค่าเดียวกันจริง ๆ แล้วจะเป็นวัตถุเดียวกัน เมื่อคุณสร้างสตริงด้วยnewมันจะไม่ถูกฝึกงานโดยอัตโนมัติและคุณจะไม่เห็นผลกระทบนี้

#substringจนกระทั่งเมื่อเร็ว ๆ นี้ (Java 7u6) ทำงานในลักษณะที่คล้ายกันซึ่งจะอธิบายพฤติกรรมในคำถามต้นฉบับของคุณ มันไม่ได้สร้างอาร์เรย์ถ่านสำรองใหม่ แต่นำกลับมาใช้ใหม่จากสตริงเดิม มันเพิ่งสร้างวัตถุ String ใหม่ที่ใช้อ็อฟเซ็ตและความยาวเพื่อแสดงเฉพาะส่วนของอาเรย์นั้น สิ่งนี้ทำงานโดยทั่วไปในฐานะ Strings ไม่เปลี่ยนรูปเว้นแต่คุณจะหลีกเลี่ยง คุณสมบัติของ#substringยังหมายความว่าสตริงต้นฉบับทั้งหมดไม่สามารถรวบรวมขยะเมื่อสตริงย่อยสั้นที่สร้างจากมันยังคงมีอยู่

#substringปัจจุบันชวาและรุ่นปัจจุบันของคำถามที่มีพฤติกรรมไม่แปลก


2
ที่จริงแล้วการปรับเปลี่ยนการมองเห็นมี (หรืออย่างน้อยได้) ตั้งใจจะให้เป็น againts ป้องกันโค้ดที่เป็นอันตราย - แต่คุณต้องตั้ง SecurityManager (System.setSecurityManager ()) เพื่อเปิดใช้งานการป้องกัน วิธีการรักษาความปลอดภัยนี้เป็นจริงเป็นคำถามอื่น ...
sleske

2
ควรได้รับการอัปโหลดเพราะคุณเน้นว่าตัวดัดแปลงการเข้าถึงไม่ได้มีวัตถุประสงค์เพื่อรหัส 'ป้องกัน' สิ่งนี้ดูเหมือนจะเข้าใจผิดทั้งใน Java และ. NET แม้ว่าความคิดเห็นก่อนหน้านี้จะขัดแย้งกับที่; ฉันไม่รู้เกี่ยวกับ Java มากนัก แต่ใน. NET นี่เป็นเรื่องจริง ผู้ใช้ไม่ควรสมมติว่าสิ่งนี้ทำให้การแฮ็กรหัสของพวกเขา
Tom W

เป็นไปไม่ได้ที่จะละเมิดสัญญาfinalแม้ผ่านการไตร่ตรอง นอกจากนี้ดังที่กล่าวไว้ในคำตอบอื่นเนื่องจาก Java 7u6 #substringไม่ได้แชร์อาร์เรย์
ntoskrnl

ที่จริงแล้วพฤติกรรมของfinalมีการเปลี่ยนแปลงตลอดเวลา ... : -O ตามการพูดคุย "สะท้อนความบ้า" โดย Heinz ฉันโพสต์ในหัวข้ออื่นfinalหมายถึงขั้นสุดท้ายใน JDK 1.1, 1.3 และ 1.4 แต่สามารถแก้ไขได้โดยใช้การสะท้อน 1.2 เสมอ และใน 1.5 และ 6 ในกรณีส่วนใหญ่ ...
haraldK

1
finalฟิลด์สามารถเปลี่ยนแปลงได้โดยใช้nativeรหัสตามที่ทำโดยเฟรมเวิร์กการทำให้เป็นอนุกรมเมื่ออ่านฟิลด์ของอินสแตนซ์ที่ทำให้เป็นอนุกรมรวมถึงSystem.setOut(…)ที่ปรับเปลี่ยนSystem.outตัวแปรสุดท้าย หลังเป็นคุณลักษณะที่น่าสนใจที่สุดเนื่องจากการสะท้อนที่มีการแทนที่การเข้าถึงไม่สามารถเปลี่ยนstatic finalฟิลด์ได้
Holger

11

ความไม่เปลี่ยนแปลงของสตริงนั้นมาจากมุมมองของอินเตอร์เฟส คุณกำลังใช้การไตร่ตรองเพื่อข้ามอินเทอร์เฟซและแก้ไข internals ของอินสแตนซ์ของสตริงโดยตรง

s1และs2ทั้งคู่เปลี่ยนไปเพราะทั้งคู่ถูกกำหนดให้กับอินสแตนซ์สตริง "ฝึกงาน" คุณสามารถหาข้อมูลเพิ่มเติมเกี่ยวกับส่วนนั้นจากบทความนี้เกี่ยวกับความเสมอภาคของสตริงและการฝึกงาน คุณอาจประหลาดใจที่พบว่าในโค้ดตัวอย่างของคุณs1 == s2ผลตอบแทนtrue!


10

คุณใช้ Java รุ่นใด จาก Java 1.7.0_06, Oracle ได้เปลี่ยนการแสดงภายในของ String โดยเฉพาะซับสตริง

การอ้างอิงจากการแทนค่าภายในของ Oracle Tunes Java :

ในกระบวนทัศน์ใหม่เขตข้อมูล String offset และ count ถูกลบออกดังนั้นสตริงย่อยจะไม่แบ่งปันค่า char [] อีกต่อไป

ด้วยการเปลี่ยนแปลงนี้อาจเกิดขึ้นโดยไม่มีการสะท้อน (???)


2
หาก OP กำลังใช้ Sun / Oracle JRE รุ่นเก่าคำสั่งสุดท้ายจะพิมพ์ "Java!" (ตามที่เขาโพสต์โดยไม่ได้ตั้งใจ) สิ่งนี้มีผลต่อการแชร์อาร์เรย์ของค่าระหว่างสตริงและสตริงย่อยเท่านั้น คุณยังคงไม่สามารถเปลี่ยนค่าโดยไม่มีลูกเล่นเช่นภาพสะท้อน
haraldK

7

มีคำถามสองข้อจริงๆที่นี่:

  1. สตริงเปลี่ยนรูปไม่ได้จริงๆเหรอ?
  2. ทำไม s3 ถึงไม่เปลี่ยน?

ถึงจุดที่ 1: ยกเว้น ROM ไม่มีหน่วยความจำที่ไม่เปลี่ยนรูปแบบในคอมพิวเตอร์ของคุณ ทุกวันนี้แม้แต่ ROM บางครั้งก็สามารถเขียนได้ มีบางรหัสอยู่เสมอ (ไม่ว่าจะเป็นเคอร์เนลหรือรหัสเนทีฟที่เลี่ยงการจัดการสภาพแวดล้อมของคุณ) ที่สามารถเขียนไปยังที่อยู่หน่วยความจำของคุณ ดังนั้นใน "ความจริง" ไม่มีพวกเขาไม่เปลี่ยนแปลงไม่ได้อย่างแน่นอน

จนถึงจุดที่ 2: นี่เป็นเพราะซับสตริงอาจจะจัดสรรอินสแตนซ์สตริงใหม่ซึ่งน่าจะเป็นการคัดลอกอาเรย์ เป็นไปได้ที่จะใช้ซับสตริงในลักษณะที่จะไม่ทำสำเนา แต่ไม่ได้หมายความว่าจะทำ มีการแลกเปลี่ยนที่เกี่ยวข้อง

ตัวอย่างเช่นควรมีการอ้างอิงเพื่อreallyLargeString.substring(reallyLargeString.length - 2)ให้หน่วยความจำจำนวนมากยังมีชีวิตอยู่หรือเพียงไม่กี่ไบต์?

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

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


3
Real ROM นั้นไม่เปลี่ยนรูปเหมือนการพิมพ์ภาพถ่ายที่บรรจุในพลาสติก รูปแบบถูกตั้งค่าอย่างถาวรเมื่อเวเฟอร์ (หรือพิมพ์) ได้รับการพัฒนาทางเคมี หน่วยความจำที่เปลี่ยนแปลงได้ทางไฟฟ้ารวมถึงชิป RAMสามารถทำหน้าที่เป็น "จริง" ROM หากสัญญาณควบคุมที่จำเป็นในการเขียนไม่สามารถรวมพลังได้โดยไม่ต้องเพิ่มการเชื่อมต่อไฟฟ้าเพิ่มเติมไปยังวงจรที่ติดตั้งไว้ จริง ๆ แล้วไม่ใช่เรื่องแปลกที่อุปกรณ์ฝังตัวจะรวม RAM ที่ตั้งค่ามาจากโรงงานและดูแลรักษาโดยแบตเตอรี่สำรองและมีเนื้อหาที่ต้องได้รับการโหลดใหม่จากโรงงานหาก battey ล้มเหลว
supercat

3
@supercat: คอมพิวเตอร์ของคุณไม่ได้เป็นหนึ่งในระบบฝังตัวเหล่านั้น :) ROM แบบใช้สายฮาร์ดจริงไม่ได้มีอยู่ทั่วไปในพีซีมาสิบปีหรือสองปี EEPROM ทุกอย่างและแฟลชวันนี้ โดยทั่วไปทุกที่อยู่ที่ผู้ใช้มองเห็นได้ซึ่งอ้างถึงหน่วยความจำหมายถึงหน่วยความจำที่เขียนได้
cHao

@cHao: ชิปแฟลชจำนวนมากอนุญาตให้บางส่วนมีการป้องกันการเขียนในแบบที่ถ้ามันสามารถยกเลิกได้เลยจะต้องใช้แรงดันไฟฟ้าที่แตกต่างกว่าที่จะต้องใช้สำหรับการทำงานปกติ (ซึ่งเมนบอร์ดจะไม่พร้อมที่จะทำ) ฉันคาดว่าเมนบอร์ดจะใช้คุณสมบัตินั้น นอกจากนี้ฉันไม่แน่ใจเกี่ยวกับคอมพิวเตอร์ในปัจจุบัน แต่ในอดีตคอมพิวเตอร์บางเครื่องมีขอบเขตของ RAM ซึ่งได้รับการป้องกันการเขียนในระหว่างขั้นตอนการบู๊ตและสามารถรีเซ็ตได้โดยไม่มีการป้องกันด้วยการรีเซ็ต (ซึ่งจะบังคับให้เริ่มต้นจาก ROM)
supercat

2
@supercat ฉันคิดว่าคุณหายไปจากจุดของหัวข้อซึ่งก็คือสตริงที่เก็บไว้ใน RAM จะไม่เปลี่ยนแปลงไม่ได้อย่างแท้จริง
Scott Wisniewski

5

หากต้องการเพิ่มคำตอบของ @ haraldK นี่คือแฮ็คความปลอดภัยซึ่งอาจนำไปสู่ผลกระทบร้ายแรงในแอป

สิ่งแรกคือการปรับเปลี่ยนเป็นสตริงคงที่เก็บไว้ใน String Pool เมื่อมีการประกาศสตริงเป็น a String s = "Hello World";มันจะถูกวางลงในกลุ่มวัตถุพิเศษเพื่อนำไปใช้ซ้ำ ปัญหาคือคอมไพเลอร์จะทำการอ้างอิงไปยังรุ่นที่แก้ไขในเวลารวบรวมและเมื่อผู้ใช้แก้ไขสตริงที่เก็บไว้ในสระว่ายน้ำตอนนี้การอ้างอิงทั้งหมดในรหัสจะชี้ไปที่รุ่นที่แก้ไข ซึ่งจะส่งผลให้เกิดข้อผิดพลาดดังต่อไปนี้:

System.out.println("Hello World"); 

จะพิมพ์:

Hello Java!

มีอีกเรื่องหนึ่งที่ฉันประสบเมื่อใช้การคำนวณอย่างหนักเกี่ยวกับสตริงที่มีความเสี่ยงดังกล่าว มีข้อผิดพลาดที่เกิดขึ้นในรูปแบบ 1 จาก 1000000 ครั้งในระหว่างการคำนวณซึ่งทำให้ผลลัพธ์ไม่ได้กำหนดไว้ ฉันสามารถค้นหาปัญหาด้วยการปิด JIT - ฉันได้รับผลลัพธ์เดียวกันเสมอเมื่อปิด JIT ฉันเดาว่าเหตุผลก็คือแฮ็คความปลอดภัยของสายอักขระนี้ซึ่งทำลายสัญญาการเพิ่มประสิทธิภาพ JIT บางส่วน


อาจเป็นปัญหาความปลอดภัยของเธรดที่ถูกหลอกลวงโดยเวลาในการดำเนินการที่ช้าลงและการเกิดพร้อมกันน้อยลงโดยไม่มี JIT
Ted Pennings

@TedPennings จากคำอธิบายของฉันฉันสามารถทำได้ฉันไม่ต้องการไปลงรายละเอียดมากเกินไป จริง ๆ แล้วฉันใช้เวลาสองสามวันพยายาม จำกัด มัน มันเป็นอัลกอริทึมแบบเธรดเดียวซึ่งคำนวณระยะห่างระหว่างสองข้อความที่เขียนในสองภาษาที่แตกต่างกัน ฉันพบการแก้ไขที่เป็นไปได้สองอย่างสำหรับปัญหา - อันหนึ่งคือการปิด JIT และอันที่สองคือการเพิ่มตัวอักษรแบบไม่มี op String.format("")ภายในหนึ่งในลูปด้านใน มีโอกาสที่จะเป็นปัญหาอื่น ๆ แล้ว JIT- ล้มเหลว แต่ฉันเชื่อว่าเป็น JIT เพราะปัญหานี้ไม่เคยทำซ้ำอีกครั้งหลังจากเพิ่ม no-op นี้
Andrey Chaschev

ฉันทำสิ่งนี้กับ JDK รุ่นแรก ~ 7u9 ดังนั้นจึงเป็นไปได้
Andrey Chaschev

1
@Andrey Chaschev:“ ฉันพบสองวิธีแก้ไขที่เป็นไปได้สำหรับปัญหา” …การแก้ไขที่เป็นไปได้ครั้งที่สามไม่ใช่การแฮ็กเข้าไปในStringinternals ไม่ได้เข้ามาในใจของคุณ?
Holger

1
@Ted Pennings: ปัญหาความปลอดภัยของเธรดและปัญหา JIT มักจะเหมือนกันมาก JIT ได้รับอนุญาตให้สร้างรหัสซึ่งอาศัยการfinalรับประกันความปลอดภัยของเธรดฟิลด์ซึ่งจะแตกเมื่อทำการแก้ไขข้อมูลหลังจากการสร้างวัตถุ ดังนั้นคุณสามารถดูได้ว่าเป็นปัญหา JIT หรือปัญหา MT ตามที่คุณต้องการ ปัญหาที่แท้จริงคือการแฮ็กเข้าStringและแก้ไขข้อมูลที่คาดว่าจะไม่เปลี่ยนรูป
Holger

5

ตามแนวคิดของการรวมกำไรตัวแปร String ทั้งหมดที่มีค่าเดียวกันจะชี้ไปยังที่อยู่หน่วยความจำเดียวกัน ดังนั้น s1 และ s2 ซึ่งทั้งสองมีค่าเหมือนกันคือ“ Hello World” จะชี้ไปที่ตำแหน่งหน่วยความจำเดียวกัน (พูด M1)

ในทางตรงกันข้าม s3 มี "โลก" ดังนั้นมันจะชี้ไปที่การจัดสรรหน่วยความจำที่แตกต่างกัน (พูด M2)

ดังนั้นสิ่งที่เกิดขึ้นก็คือค่าของ S1 กำลังเปลี่ยนแปลง (โดยใช้ค่าถ่าน []) ดังนั้นค่าที่ตำแหน่งหน่วยความจำ M1 ที่ชี้โดย s1 และ s2 จึงถูกเปลี่ยน

ดังนั้นผลลัพธ์ตำแหน่งหน่วยความจำ M1 จึงถูกแก้ไขซึ่งทำให้เกิดการเปลี่ยนแปลงค่าของ s1 และ s2

แต่ค่าของตำแหน่ง M2 ยังคงไม่เปลี่ยนแปลงดังนั้น s3 จึงมีค่าเดิมเหมือนกัน


5

เหตุผลที่ s3 ไม่ได้เปลี่ยนจริงเพราะใน Java เมื่อคุณทำสตริงย่อยอาร์เรย์อักขระค่าสำหรับสตริงย่อยจะถูกคัดลอกภายใน (โดยใช้ Arrays.copyOfRange ())

s1 และ s2 เหมือนกันเพราะใน Java ทั้งคู่อ้างถึงสตริง interned เดียวกัน มันเกิดจากการออกแบบใน Java


2
คำตอบนี้เพิ่มอะไรให้กับคำตอบก่อนหน้าคุณอย่างไร
สีเทา

โปรดทราบว่านี่เป็นพฤติกรรมที่ค่อนข้างใหม่และไม่ได้รับประกันโดยสเป็คใด ๆ
Paŭlo Ebermann

การใช้งานของการString.substring(int, int)เปลี่ยนแปลงด้วย Java 7u6 ก่อน 7u6 ที่ JVM ก็จะให้ชี้ไปที่เดิมString's char[]ร่วมกับดัชนีและระยะเวลาใน หลังจาก 7u6 มันจะคัดลอกซับสตริงไปยังใหม่Stringมีข้อดีและข้อเสีย
Eric Jablow

2

สตริงไม่เปลี่ยนรูป แต่ผ่านการสะท้อนคุณได้รับอนุญาตให้เปลี่ยนคลาส String คุณเพิ่งนิยามคลาส String ใหม่ว่าไม่แน่นอนในเวลาจริง คุณสามารถกำหนดวิธีการเป็นสาธารณะหรือส่วนตัวหรือคงที่ถ้าคุณต้องการ


2
ถ้าคุณเปลี่ยนทัศนวิสัยของฟิลด์ / วิธีการมันจะไม่มีประโยชน์เพราะในเวลารวบรวมพวกมันเป็นแบบส่วนตัว
Bohemian

1
คุณสามารถเปลี่ยนการช่วยสำหรับการเข้าถึงในวิธีการ แต่คุณไม่สามารถเปลี่ยนสถานะสาธารณะ / ส่วนตัวของพวกเขาและคุณไม่สามารถทำให้พวกเขาคงที่
สีเทา

1

[คำปฏิเสธนี่เป็นรูปแบบการให้ความเห็นอย่างจงใจเพราะฉันรู้สึกว่าคำตอบ "อย่าทำแบบนี้ที่บ้านเด็ก" มากกว่านี้]

บาปเป็นบรรทัดfield.setAccessible(true);ที่บอกว่าจะละเมิด API สาธารณะโดยการอนุญาตให้เข้าถึงสนามส่วนตัว เป็นช่องโหว่ความปลอดภัยขนาดยักษ์ที่สามารถล็อคลงได้โดยกำหนดค่าตัวจัดการความปลอดภัย

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

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

ต้องบอกว่ารหัสบรรทัดนั้นมีประโยชน์จริง ๆ เมื่อคุณมีปืนถือหัวของคุณบังคับให้คุณทำสิ่งอันตรายเช่นนั้น การใช้ประตูหลังนั้นมักเป็นกลิ่นรหัสที่คุณต้องอัปเกรดเป็นรหัสห้องสมุดที่ดีกว่าซึ่งคุณไม่ต้องทำบาป การใช้รหัสบรรทัดที่เป็นอันตรายอีกอย่างคือการเขียน "voodoo framework" (orm, container container, ... ) ผู้คนจำนวนมากได้รับศาสนาเกี่ยวกับกรอบดังกล่าว (ทั้งเพื่อและต่อต้านพวกเขา) ดังนั้นฉันจะหลีกเลี่ยงการเชิญชวนสงครามเปลวไฟโดยไม่พูดอะไรเลยนอกจากโปรแกรมเมอร์ส่วนใหญ่ไม่จำเป็นต้องไปที่นั่น


1

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

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

นี่คือรายละเอียดเพิ่มเติมที่คุณสามารถไปและคว้าข้อมูลเพิ่มเติมจาก: วิธีการทำงานในการเก็บขยะ Java


0

สตริงไม่สามารถเปลี่ยนแปลงได้ในธรรมชาติเนื่องจากไม่มีวิธีการแก้ไขวัตถุสตริง นั่นคือเหตุผลว่าพวกเขาแนะนำStringBuilderและStringBufferชั้นเรียน

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