การกำหนดครั้งสุดท้ายไม่ดีหรือไม่?


186

ก่อนอื่นตัวต่อ: รหัสต่อไปนี้จะพิมพ์อะไร

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10);

    private static long scale(long value) {
        return X * value;
    }
}

ตอบ:

0

สปอยเลอร์ด้านล่าง


หากคุณพิมพ์Xในขนาด (ยาว) และ Redefine X = scale(10) + 3พิมพ์จะได้รับแล้วX = 0 X = 3ซึ่งหมายความว่าXมีการตั้งค่าชั่วคราวและการตั้งค่าในภายหลัง0 3นี่เป็นการละเมิดfinal!

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

ที่มา: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [เน้นเพิ่ม]


คำถามของฉัน: นี่เป็นข้อผิดพลาดหรือไม่? เป็นfinalป่วยกำหนด?


นี่คือรหัสที่ฉันสนใจใน. Xได้รับมอบหมายสองค่าที่แตกต่างกันและ0 ผมเชื่อว่านี่จะเป็นละเมิด3final

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10) + 3;

    private static long scale(long value) {
        System.out.println("X = " + X);
        return X * value;
    }
}

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

สิ่งนี้ชัดเจนโดยเฉพาะอย่างยิ่งเมื่อดูผลลัพธ์ที่ ernesto ได้รับ: เมื่อaถูกแท็กด้วยfinalเขาจะได้ผลลัพธ์ต่อไปนี้:

a=5
a=5

ซึ่งไม่เกี่ยวข้องกับส่วนหลักของคำถามของฉัน: finalตัวแปรเปลี่ยนตัวแปรอย่างไร


17
วิธีการอ้างอิงนี้สมาชิกเป็นเหมือนหมายถึงสมาชิกประเภทรองก่อนที่จะสร้างชั้นซุปเปอร์ได้เสร็จสิ้นว่าเป็นปัญหาและไม่ได้ความหมายของคุณX final
daniu

4
จาก JLS:A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Ivan

1
@Ivan นี่ไม่เกี่ยวกับค่าคงที่ แต่เกี่ยวกับตัวแปรอินสแตนซ์ แต่คุณสามารถเพิ่มบทได้หรือไม่
AxelH

9
เช่นเดียวกับหมายเหตุ: อย่าทำสิ่งนี้ในรหัสการผลิต มันเป็นเรื่องน่าสับสนอย่างยิ่งสำหรับทุกคนถ้ามีคนเริ่มใช้ช่องโหว่ใน JLS
Zabuzard

13
FYI คุณสามารถสร้างสถานการณ์เดียวกันนี้ใน C # ได้เช่นกัน C # สัญญาที่วนลูปในการประกาศอย่างต่อเนื่องจะถูกจับในเวลารวบรวม แต่ทำให้ไม่มีสัญญาดังกล่าวเกี่ยวกับการประกาศแบบอ่านอย่างเดียวและในทางปฏิบัติคุณสามารถเข้าไปในสถานการณ์ที่สังเกตเห็นค่าเริ่มต้นของฟิลด์เป็นศูนย์ ถ้ามันเจ็บเมื่อคุณทำอย่างนั้นไม่ได้ทำมัน คอมไพเลอร์จะไม่บันทึกคุณ
Eric Lippert

คำตอบ:


217

การค้นหาที่น่าสนใจมาก เพื่อให้เข้าใจได้เราจำเป็นต้องเจาะเข้าไปใน Java Language Specification ( JLS )

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


ตัวแปรคลาสและค่าเริ่มต้น

ลองดูตัวอย่างต่อไปนี้:

private static Object x;

public static void main(String[] args) {
    System.out.println(x); // Prints 'null'
}

เราไม่ได้กำหนดค่าอย่างชัดเจนถึงxแม้ว่ามันจะชี้ไปที่nullมันเป็นค่าเริ่มต้น เปรียบเทียบกับ§4.12.5 :

ค่าเริ่มต้นของตัวแปร

ตัวแปรคลาสแต่ละตัวตัวแปรอินสแตนซ์หรือองค์ประกอบอาร์เรย์จะเริ่มต้นได้ด้วยค่าเริ่มต้นเมื่อสร้าง ( §15.9 , §15.10.2 )

โปรดทราบว่านี่มีไว้สำหรับตัวแปรประเภทนั้นเช่นเดียวกับในตัวอย่างของเรา มันไม่ได้เก็บไว้สำหรับตัวแปรท้องถิ่นดูตัวอย่างต่อไปนี้:

public static void main(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

จากวรรค JLS เดียวกัน:

ตัวแปรท้องถิ่น ( §14.4 , §14.14 ) จะต้องได้รับอย่างชัดเจนค่าก่อนที่มันจะถูกนำมาใช้โดยการเริ่มต้น ( §14.4 ) หรือการกำหนด ( §15.26 ) ในทางที่สามารถตรวจสอบได้โดยใช้กฎระเบียบสำหรับการกำหนดที่ชัดเจน (ก§ 16 (การกำหนดที่แน่นอน) )


ตัวแปรสุดท้าย

ตอนนี้เรามาดูfinalจาก§4.12.4 :

ตัวแปรสุดท้าย

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


คำอธิบาย

ตอนนี้กลับมาที่ตัวอย่างของคุณแก้ไขเล็กน้อย:

public static void main(String[] args) {
    System.out.println("After: " + X);
}

private static final long X = assign();

private static long assign() {
    // Access the value before first assignment
    System.out.println("Before: " + X);

    return X + 1;
}

มันออกมา

Before: 0
After: 1

จำสิ่งที่เราได้เรียนรู้ ภายในวิธีassignการตัวแปรXยังไม่ได้กำหนดค่าให้กับ ดังนั้นจึงชี้ไปที่ค่าเริ่มต้นเนื่องจากเป็นตัวแปรคลาสและตาม JLS ตัวแปรเหล่านั้นจะชี้ไปที่ค่าเริ่มต้นทันที (ตรงกันข้ามกับตัวแปรท้องถิ่น) หลังจากassignวิธีการตัวแปรXถูกกำหนดค่า1และเพราะfinalเราไม่สามารถเปลี่ยนแปลงได้อีกต่อไป ดังนั้นสิ่งต่อไปนี้จะไม่ทำงานเนื่องจากfinal:

private static long assign() {
    // Assign X
    X = 1;

    // Second assign after method will crash
    return X + 1;
}

ตัวอย่างใน JLS

ขอบคุณ @Andrew ฉันพบย่อหน้า JLS ที่ครอบคลุมถึงสถานการณ์นี้อย่างชัดเจน

แต่ก่อนอื่นมาดูกันก่อน

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

ทำไมจึงไม่อนุญาตในขณะที่การเข้าถึงจากวิธีการคืออะไร? ดูที่§8.3.3ซึ่งพูดถึงเมื่อการเข้าถึงเขตข้อมูลถูก จำกัด ถ้าเขตข้อมูลนั้นยังไม่ได้เริ่มต้น

มันแสดงกฎบางอย่างที่เกี่ยวข้องกับตัวแปรคลาส:

สำหรับการอ้างอิงโดยใช้ชื่อแบบง่ายไปยังตัวแปรคลาสที่fประกาศในคลาสหรืออินเตอร์เฟสCมันเป็นข้อผิดพลาดในการคอมไพล์ในเวลาถ้า :

  • การอ้างอิงจะปรากฏขึ้นในตัวแปรคลาสของ initializer ของCหรือใน initializer แบบสแตติกของC( §8.7 ); และ

  • การอ้างอิงจะปรากฏในตัวกำหนดค่าเริ่มต้นของfตัวประกาศของตัวเองหรือที่จุดทางด้านซ้ายของตัวfประกาศ และ

  • การอ้างอิงไม่ได้อยู่ทางซ้ายมือของนิพจน์การมอบหมาย ( §15.26 ); และ

  • Cชั้นในสุดหรืออินเตอร์เฟซการปิดล้อมอ้างอิง

ง่ายX = X + 1มันถูกจับโดยกฎเหล่านั้นวิธีการเข้าถึงไม่ได้ พวกเขายังระบุสถานการณ์นี้และให้ตัวอย่าง:

การเข้าถึงโดยวิธีการไม่ได้รับการตรวจสอบด้วยวิธีนี้ดังนั้น:

class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}

ผลิตผลลัพธ์:

0

เนื่องจากตัวแปร initializer สำหรับการiใช้วิธีการเรียนดูการเข้าถึงค่าของตัวแปรjก่อนที่จะjได้รับการเริ่มต้นโดย initializer ตัวแปร ณ จุดที่มันยังคงมีค่าเริ่มต้น ( §4.12.5 )


1
@ แอนดรูว์ใช่คลาสตัวแปรขอบคุณ ใช่มันจะทำงานถ้ามีจะไม่บางกฎพิเศษที่ จำกัด การเข้าถึงเช่น: §8.3.3 ดูสี่จุดที่ระบุไว้สำหรับตัวแปรคลาส (รายการแรก) เมธอดเมธอดในตัวอย่าง OPs ไม่ได้ถูกจับโดยกฎเหล่านั้นดังนั้นเราสามารถเข้าถึงได้Xจากเมธอด ฉันจะไม่คิดว่ามาก มันขึ้นอยู่กับว่า JLS กำหนดสิ่งที่จะทำงานอย่างละเอียด ฉันไม่เคยใช้โค้ดแบบนั้นมันแค่เอาเปรียบกฎบางอย่างใน JLS
Zabuzard

4
ปัญหาคือคุณสามารถเรียกวิธีการอินสแตนซ์จากตัวสร้างสิ่งที่อาจไม่ควรได้รับอนุญาต ในทางกลับกันการกำหนดคนในท้องถิ่นก่อนเรียก super ซึ่งจะเป็นประโยชน์และปลอดภัยนั้นไม่ได้รับอนุญาต ไปคิด
Reinstate Monica

1
@ แอนดรูว์คุณอาจเป็นคนเดียวที่นี่ที่กล่าวถึงจริงforwards references (นั่นเป็นส่วนหนึ่งของ JLS ด้วย) นี้ง่ายมากโดยไม่มีคำตอบของ looong stackoverflow.com/a/49371279/1059372
Eugene

1
"การมอบหมายครั้งแรกจากนั้นเปลี่ยนการอ้างอิง" ในกรณีนี้มันไม่ใช่ประเภทอ้างอิง แต่เป็นชนิดดั้งเดิม
fabian

1
คำตอบนี้ถูกต้องถ้านานหน่อย :-) ฉันคิดว่า tl; dr คือ OP อ้างถึงการสอนที่บอกว่า "ฟิลด์ [สุดท้าย] ไม่สามารถเปลี่ยนได้" ไม่ใช่ JLS ในขณะที่บทช่วยสอนของ Oracle นั้นค่อนข้างดี สำหรับคำถามของ OP เราต้องไปตามจริงคำจำกัดความของ JLS สุดท้ายและคำจำกัดความนั้นไม่ได้ทำการอ้างสิทธิ์ (ที่ท้าทาย OP โดยชอบธรรม) ว่าค่าของฟิลด์สุดท้ายไม่สามารถเปลี่ยนแปลงได้
yshavit

22

ไม่มีอะไรเกี่ยวข้องกับรอบชิงชนะเลิศที่นี่

เนื่องจากเป็นอินสแตนซ์หรือระดับคลาสจะเก็บค่าเริ่มต้นหากยังไม่ได้รับมอบหมาย นั่นคือเหตุผลที่คุณเห็น0เมื่อคุณเข้าถึงโดยไม่ต้องกำหนด

หากคุณเข้าถึงXโดยไม่ได้กำหนดอย่างสมบูรณ์มันจะเก็บค่าเริ่มต้นของ long ซึ่งก็คือ0ผลลัพธ์


3
สิ่งที่ยุ่งยากเกี่ยวกับเรื่องนี้คือถ้าคุณไม่ได้กำหนดค่ามันจะไม่ถูกกำหนดด้วยค่าเริ่มต้น แต่ถ้าคุณใช้เพื่อกำหนดค่า "สุดท้าย" ตัวเองมันจะ ...
AxelH

2
@AxelH ฉันเห็นสิ่งที่คุณหมายถึงโดยที่ แต่นั่นเป็นวิธีการทำงานมิฉะนั้นโลกจะล่มสลาย;)
Suresh Atta

20

ไม่ใช่ข้อบกพร่อง

เมื่อมีสายแรกที่โทรมาscaleจาก

private static final long X = scale(10);

return X * valueมันพยายามที่จะประเมิน Xยังไม่ได้รับการกำหนดค่าดังนั้นจึงใช้ค่าเริ่มต้นสำหรับ a long(ซึ่งก็คือ0)

ดังนั้นบรรทัดที่ประเมินรหัสX * 10คือซึ่งเป็น0 * 100


8
ฉันไม่คิดว่านั่นเป็นสิ่งที่ OP สับสน ความสับสนคือX = scale(10) + 3อะไร ตั้งแต่เมื่ออ้างอิงจากวิธีการที่เป็นX แต่หลังจากนั้นจะเป็น0 3ดังนั้น OP คิดว่าจะได้รับมอบหมายสองค่าที่แตกต่างกันซึ่งจะขัดแย้งกับX final
Zabuzard

4
@Zabuza นี้ไม่ได้อธิบายกับ " มันพยายามที่จะประเมินผลการศึกษาreturn X * value. Xไม่ได้รับการกำหนดค่าเลยและดังนั้นจึงต้องใช้ค่าเริ่มต้นสำหรับการlongซึ่งเป็น0. "? ไม่ได้กล่าวว่าXถูกกำหนดด้วยค่าเริ่มต้น แต่Xเป็น "แทนที่" (โปรดอย่าพูดคำนั้น;)) โดยค่าเริ่มต้น
AxelH

14

ไม่ใช่ข้อผิดพลาดเลยเพียงแค่วางไม่ใช่รูปแบบการอ้างอิงล่วงหน้าที่ผิดกฎหมายแต่อย่างใด

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

มันได้รับอนุญาตจากสเปค

ในการรับตัวอย่างของคุณนี่คือสิ่งที่ตรงกับที่:

private static final long X = scale(10) + 3;

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


คำตอบที่ดี! ฉันแค่อยากรู้ว่าทำไมสเป็คทำให้เคสที่สองคอมไพล์ได้ เป็นวิธีเดียวที่จะเห็นสถานะ "ไม่สอดคล้อง" ของเขตข้อมูลสุดท้ายหรือไม่
Andrew Tobilko

@Andrew นี้ได้ใส่ใจฉันสำหรับเวลาว่างค่อนข้างมากเกินไปฉันกำลังมีแนวโน้มที่จะคิดว่ามัน c ++ หรือ C ไม่ได้ (ไม่ได้คิดว่านี้เป็นจริง)
ยู

@Andrew: เพราะทำอย่างอื่นจะแก้ทฤษฎีทัวริงไม่สมบูรณ์
Joshua

9
@ โจชัว: ฉันคิดว่าคุณกำลังผสมผสานแนวคิดที่แตกต่างกันจำนวนมากที่นี่: (1) ปัญหาการหยุดชะงัก (2) ปัญหาการตัดสินใจ (3) ทฤษฎีความไม่สมบูรณ์ของ Godel และ (4) ภาษาโปรแกรมทัวริงที่สมบูรณ์ ผู้เขียนคอมไพเลอร์ไม่พยายามที่จะแก้ปัญหา "ตัวแปรนี้ได้รับมอบหมายอย่างแน่นอนก่อนที่มันจะถูกใช้หรือไม่" สมบูรณ์แบบเพราะปัญหานั้นเทียบเท่ากับการแก้ปัญหาการหยุดชะงักและเรารู้ว่าเราไม่สามารถทำได้
Eric Lippert

4
@EricLippert: ฮ่า ๆ ๆ การบ่มเพาะความไม่สมบูรณ์และปัญหาการหยุดพักอยู่ในที่เดียวกับฉัน
Joshua

4

สมาชิกระดับชั้นสามารถเริ่มต้นได้ในรหัสภายในคำจำกัดความของชั้นเรียน ไบต์ที่รวบรวมไว้ไม่สามารถเริ่มต้นสมาชิกในชั้นแบบอินไลน์ (สมาชิกอินสแตนซ์ได้รับการจัดการในทำนองเดียวกัน แต่นี่ไม่เกี่ยวข้องกับคำถามที่ให้ไว้)

เมื่อหนึ่งเขียนอะไรบางอย่างเช่นต่อไปนี้:

public class Demo1 {
    private static final long DemoLong1 = 1000;
}

รหัสไบต์ที่สร้างจะคล้ายกับสิ่งต่อไปนี้:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

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

public class RecursiveStatic {
    private static final long X;

    private static long scale(long value) {
        return X * value;
    }

    static {
        X = scale(10);
    }

    public static void main(String[] args) {
        System.out.println(scale(5));
    }
}
  1. JVM โหลด RecursiveStatic เป็นจุดเริ่มต้นของ jar
  2. ตัวโหลดคลาสจะเรียกใช้งาน initializer คงที่เมื่อโหลดคลาสนิยามแล้ว
  3. initializer ที่เรียกฟังก์ชั่นscale(10)ที่จะกำหนดฟิลด์static finalX
  4. scale(long)ฟังก์ชั่นการทำงานในขณะที่ชั้นจะเริ่มต้นบางส่วนอ่านค่าเตรียมของXซึ่งเป็นค่าเริ่มต้นของยาวหรือ 0
  5. ค่าของ0 * 10ถูกกำหนดให้กับXและตัวโหลดคลาสเสร็จสมบูรณ์
  6. JVM จะเรียกใช้วิธีการโมฆะสาธารณะแบบสแตติกหลักที่เรียกใช้scale(5)ซึ่งคูณ 5 ด้วยXค่าเริ่มต้นที่ตอนนี้เป็น0 ส่งคืน 0

ฟิลด์สุดท้ายคงที่Xจะได้รับมอบหมายเพียงครั้งเดียวเพื่อรักษาการรับประกันที่ถือโดยfinalคำหลัก สำหรับคำที่ตามมาของการเพิ่มที่ 3 ในการกำหนดขั้นตอนที่ 5 ดังกล่าวข้างต้นจะกลายเป็นการประเมินผลของ0 * 10 + 3ซึ่งเป็นค่า3และวิธีการหลักจะพิมพ์ผลของซึ่งเป็นค่าที่3 * 515


3

การอ่านฟิลด์ที่ไม่มีการกำหนดค่าเริ่มต้นของวัตถุควรส่งผลให้เกิดข้อผิดพลาดในการรวบรวม น่าเสียดายสำหรับ Java มันไม่ได้

ฉันคิดว่าเหตุผลพื้นฐานว่าทำไมในกรณีนี้คือ "ซ่อนเร้น" ในคำจำกัดความของวิธีการสร้างและสร้างวัตถุแม้ว่าฉันจะไม่ทราบรายละเอียดของมาตรฐาน

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

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