Final vs Final อย่างมีประสิทธิภาพ - พฤติกรรมที่แตกต่างกัน


104

จนถึงตอนนี้ฉันคิดว่าขั้นสุดท้ายและขั้นสุดท้ายที่มีประสิทธิผลนั้นเทียบเท่ากันไม่มากก็น้อยและ JLS จะปฏิบัติต่อสิ่งเหล่านี้เหมือนกันหากไม่เหมือนกันในพฤติกรรมจริง จากนั้นฉันพบสถานการณ์ที่สร้างขึ้นนี้:

final int a = 97;
System.out.println(true ? a : 'c'); // outputs a

// versus

int a = 97;
System.out.println(true ? a : 'c'); // outputs 97

เห็นได้ชัดว่า JLS สร้างความแตกต่างที่สำคัญระหว่างทั้งสองที่นี่และฉันไม่แน่ใจว่าทำไม

ฉันอ่านกระทู้อื่น ๆ เช่น

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

อะไรเป็นสาเหตุของพฤติกรรมนี้ใครสามารถให้คำจำกัดความ JLS ที่อธิบายสิ่งนี้ได้บ้าง


แก้ไข: ฉันพบสถานการณ์อื่นที่เกี่ยวข้อง:

final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true

// versus

String a = "a";
System.out.println(a + "b" == "ab"); // outputs false

ดังนั้นการฝึกงานของสตริงจึงทำงานแตกต่างกันที่นี่ (ฉันไม่ต้องการใช้ข้อมูลโค้ดนี้ในโค้ดจริงเพียงแค่อยากรู้เกี่ยวกับพฤติกรรมที่แตกต่างกัน)


2
คำถามที่น่าสนใจมาก! ฉันคาดหวังว่า Java จะทำงานเหมือนกันสำหรับทั้งสองกรณี แต่ตอนนี้ฉันรู้แจ้งแล้ว ฉันถามตัวเองว่านี่เป็นพฤติกรรมเสมอหรือว่ามันแตกต่างจากรุ่นก่อน ๆ
Lino

8
@Lino ถ้อยคำสำหรับคำพูดสุดท้ายในคำตอบที่ดีด้านล่างเป็นเหมือนกันทุกทางกลับไปJava 6 : "ถ้าตัวถูกดำเนินการอย่างใดอย่างหนึ่งคือประเภทTที่Tคือbyte, shortหรือcharและถูกดำเนินการอื่น ๆ คือการแสดงออกอย่างต่อเนื่องของ ประเภทintที่มีค่าแทนได้ในประเภทTจากนั้นประเภทของนิพจน์เงื่อนไขคือT " --- พบเอกสาร Java 1.0 ที่ Berkeley ข้อความเดียวกัน --- ใช่มันเป็นแบบนั้นมาตลอด
Andreas

1
วิธีที่คุณ "ค้นหา" สิ่งที่น่าสนใจ: P ยินดีต้อนรับ :)
phant0m

คำตอบ:


66

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

ในระดับพื้นผิวfinalและeffectively finalสำหรับตัวแปรท้องถิ่นนั้นเหมือนกัน อย่างไรก็ตาม JLS ทำให้เกิดความแตกต่างอย่างชัดเจนระหว่างทั้งสองซึ่งจริงๆแล้วมีเอฟเฟกต์ที่หลากหลายในสถานการณ์พิเศษเช่นนี้


สถานที่

จากJLS§4.12.4เกี่ยวกับfinalตัวแปร:

ตัวแปรคงเป็นfinalตัวแปรชนิดดั้งเดิมหรือพิมพ์สตริงที่ถูกเตรียมมีการแสดงออกอย่างต่อเนื่อง ( §15.29 ) ไม่ว่าตัวแปรจะเป็นตัวแปรคงที่หรือไม่อาจมีผลกระทบต่อการเริ่มต้นคลาส ( §12.4.1 ), ความเข้ากันได้แบบไบนารี ( §13.1 ), ความสามารถในการเข้าถึง (( 14.22 ) และการกำหนดที่แน่นอน ( §16.1.1 )

ตั้งแต่intดั้งเดิมตัวแปรaเป็นเช่นตัวแปรคงที่

นอกจากนี้จากบทเดียวกันเกี่ยวกับeffectively final:

ตัวแปรบางตัวที่ไม่ได้รับการประกาศขั้นสุดท้ายจะถือว่าเป็นขั้นสุดท้ายอย่างมีประสิทธิผล: ...

ดังนั้นจากวิธีการนี้จะห่วงก็เป็นที่ชัดเจนว่าในตัวอย่างอื่น ๆ ที่aจะไม่ได้รับการพิจารณาเป็นตัวแปรคงที่ตามที่มันเป็นไม่ได้สุดท้ายแต่เพียงสุดท้ายได้อย่างมีประสิทธิภาพ


พฤติกรรม

ตอนนี้เรามีความแตกต่างแล้วให้ค้นหาสิ่งที่เกิดขึ้นและเหตุใดผลลัพธ์จึงแตกต่างกัน

คุณกำลังใช้ตัวดำเนินการเงื่อนไข? :ที่นี่ดังนั้นเราต้องตรวจสอบคำจำกัดความ จากJLS§15.25 :

: มีสามชนิดของการแสดงออกตามเงื่อนไขที่สองและสามสำนวนที่ถูกดำเนินการจะแยกนิพจน์บูลีนแบบมีเงื่อนไข , การแสดงออกที่มีเงื่อนไขที่เป็นตัวเลขและการแสดงออกอ้างอิงเงื่อนไข

ในกรณีนี้เรากำลังพูดถึงนิพจน์เงื่อนไขตัวเลขจากJLS§15.25.2 :

ประเภทของนิพจน์เงื่อนไขตัวเลขถูกกำหนดดังนี้:

และนั่นคือส่วนที่ทั้งสองกรณีได้รับการจัดประเภทแตกต่างกัน

ขั้นสุดท้ายอย่างมีประสิทธิภาพ

เวอร์ชันที่effectively finalตรงตามกฎนี้:

มิฉะนั้นการเลื่อนระดับตัวเลขทั่วไป( §5.6 ) จะถูกนำไปใช้กับตัวถูกดำเนินการที่สองและสามและประเภทของนิพจน์เงื่อนไขคือประเภทที่เลื่อนระดับของตัวถูกดำเนินการที่สองและที่สาม

ซึ่งเป็นลักษณะการทำงานเช่นเดียวกับถ้าคุณจะทำ5 + 'd'เช่นซึ่งผลในการint + char intดูJLS§5.6

การส่งเสริมตัวเลขจะกำหนดประเภทที่เลื่อนระดับของนิพจน์ทั้งหมดในบริบทตัวเลข ประเภทที่เลื่อนขั้นถูกเลือกเพื่อให้แต่ละนิพจน์สามารถแปลงเป็นชนิดที่เลื่อนขั้นได้และในกรณีของการดำเนินการทางคณิตศาสตร์การดำเนินการจะถูกกำหนดสำหรับค่าของชนิดที่เลื่อนขั้น ลำดับของนิพจน์ในบริบทตัวเลขไม่สำคัญสำหรับการเลื่อนระดับตัวเลข กฎมีดังนี้:

[... ]

จากนั้นการขยายการแปลงแบบดั้งเดิมให้กว้างขึ้น ( §5.1.2 ) และการจำกัด การแปลงแบบดั้งเดิม ( §5.1.3 ) จะถูกนำไปใช้กับบางนิพจน์ตามกฎต่อไปนี้:

ในบริบทการเลือกตัวเลขจะใช้กฎต่อไปนี้:

หากการแสดงออกใด ๆ ที่เป็นของชนิดintและเป็นที่ไม่ได้แสดงออกอย่างต่อเนื่อง ( §15.29 ) จากนั้นประเภทการเลื่อนตำแหน่งเป็นintและการแสดงออกอื่น ๆ ที่ไม่ได้เป็นประเภทintได้รับการขยับขยายแปลงดั้งเดิมintไป

ดังนั้นทุกอย่างจะเลื่อนตำแหน่งให้intเป็นaเป็นintแล้ว ที่อธิบายผลลัพธ์ของ97.

สุดท้าย

เวอร์ชันที่มีfinalตัวแปรตรงตามกฎนี้:

หากหนึ่งในตัวถูกดำเนินการคือประเภทTที่Tเป็นbyte, shortหรือcharและถูกดำเนินการอื่น ๆ ที่เป็นการแสดงออกอย่างต่อเนื่อง ( §15.29 ) ประเภทintที่มีค่าซึ่งแสดงในประเภทนั้นประเภทของการแสดงออกที่มีเงื่อนไขคือTT

ตัวแปรสุดท้ายaเป็นประเภทintและนิพจน์คงที่ (เนื่องจากเป็นfinal) มันเป็นแทนได้ดังนั้นผลที่เป็นประเภทchar สรุปว่าการส่งออกchara


ตัวอย่างสตริง

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

ใน Java สตริงภายในจะขึ้นอยู่กับนิพจน์คงที่ดังนั้น

"a" + "b" + "c" == "abc"

เป็นtrueเช่นกัน (dont ใช้นี้สร้างในรหัสจริง)

ดูJLS§3.10.5 :

ยิ่งไปกว่านั้นสตริงลิเทอรัลมักจะอ้างถึงอินสแตนซ์ของคลาสสตริงเดียวกันเสมอ เนื่องจากสตริงลิเทอรัล - หรือโดยทั่วไปแล้วสตริงที่เป็นค่าของนิพจน์คงที่ ( §15.29 ) - เป็น"ภายใน"เพื่อแชร์อินสแตนซ์ที่ไม่ซ้ำกันโดยใช้เมธอดString.intern( §12.5 )

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


8
ปัญหาคือว่าคุณจะคาดหวัง... ? a : 'c'ที่จะประพฤติเหมือนกันไม่ว่าaจะเป็นตัวแปรหรือค่าคงที่ ไม่มีอะไรผิดปกติกับการแสดงออก --- ตรงกันข้ามa + "b" == "ab"เป็นนิพจน์ที่ไม่ถูกต้องเนื่องจากต้องเปรียบเทียบสตริงโดยใช้equals()( ฉันจะเปรียบเทียบสตริงใน Java ได้อย่างไร ) ความจริงที่ว่ามัน "บังเอิญ" ทำงานเมื่อaเป็นค่าคงที่เป็นเพียงมุมกลับของตัวอักษรภายในของสตริง
Andreas

5
@Andreas ใช่ แต่โปรดทราบว่าการฝึกงานของสตริงเป็นคุณลักษณะที่กำหนดไว้อย่างชัดเจนของ Java ไม่ใช่เรื่องบังเอิญที่อาจเปลี่ยนไปในวันพรุ่งนี้หรือใน JVM อื่น "a" + "b" + "c" == "abc"ต้องอยู่trueในการใช้งาน Java ที่ถูกต้อง
Zabuzard

10
ทรูก็เป็นมุมแหลมที่ดีที่กำหนด แต่a + "b" == "ab"ยังคงมีการแสดงออกที่ไม่ถูกต้อง แม้ว่าคุณจะรู้ว่าaเป็นค่าคงที่equals()ก็เป็นข้อผิดพลาดมากเกินไปมีแนวโน้มที่จะไม่โทร หรืออาจจะเปราะบางเป็นคำที่ดีกว่าเช่นมีแนวโน้มที่จะขาดออกจากกันเมื่อรหัสได้รับการบำรุงรักษาในอนาคต
Andreas

2
โปรดสังเกตว่าแม้ในโดเมนหลักของตัวแปรสุดท้ายที่มีประสิทธิภาพเช่นการใช้ในนิพจน์แลมบ์ดาความแตกต่างอาจเปลี่ยนแปลงพฤติกรรมรันไทม์กล่าวคือสามารถสร้างความแตกต่างระหว่างนิพจน์แลมบ์ดาที่จับและไม่จับได้ แต่เดิมผลิตวัตถุใหม่ ในคำอื่น ๆ(final) String str = "a"; Stream.of(null, null). <Runnable>map( x -> () -> System.out.println(str)) .reduce((a,b) -> () -> System.out.println(a == b)) .ifPresent(Runnable::run);การเปลี่ยนแปลงผลของมันเมื่อstrเป็น final(ไม่ได้)
Holger

7

อีกแง่หนึ่งก็คือหากมีการประกาศตัวแปรสุดท้ายในเนื้อความของวิธีการนั้นจะมีพฤติกรรมที่แตกต่างจากตัวแปรสุดท้ายที่ส่งเป็นพารามิเตอร์

public void testFinalParameters(final String a, final String b) {
  System.out.println(a + b == "ab");
}

...
testFinalParameters("a", "b"); // Prints false

ในขณะที่

public void testFinalVariable() {
   final String a = "a";
   final String b = "b";
   System.out.println(a + b == "ab");  // Prints true
}

...
testFinalVariable();

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


โดยพื้นฐานแล้วลักษณะการทำงานคือ: ถ้าคอมไพเลอร์รู้ว่าตัวแปรเป็นค่าคงที่สามารถใช้มันได้เช่นเดียวกับการใช้ค่าคงที่

หากไม่ได้กำหนดตัวแปรขั้นสุดท้าย (หรือเป็นค่าสุดท้าย แต่กำหนดค่าไว้ที่รันไทม์) ก็ไม่มีเหตุผลที่คอมไพลเลอร์จะจัดการเป็นค่าคงที่เช่นกันหากค่าของมันเท่ากับค่าคงที่และค่าของมันจะไม่เปลี่ยนแปลง


4
ไม่มีอะไรแปลกในนั้น :)
dbl

2
มันเป็นอีกแง่มุมหนึ่งของคำถาม
Davide Lorenzo MARINO

5
finelคีย์เวิร์ดที่ใช้กับพารามิเตอร์มีความหมายแตกต่างจากที่finalใช้กับตัวแปรโลคัล ฯลฯ ...
dbl

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