Apakah final tidak jelas?

186

Pertama, sebuah teka-teki: Apa yang dicetak oleh kode berikut?

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

Menjawab:

0

Spoiler di bawah ini.


Jika Anda mencetak Xdalam skala (panjang) dan mendefinisikan kembali X = scale(10) + 3, cetakan akan X = 0kemudian X = 3. Ini berarti bahwa Xsementara diatur ke 0dan kemudian diatur ke 3. Ini merupakan pelanggaran final!

Pengubah statis, dalam kombinasi dengan pengubah akhir, juga digunakan untuk mendefinisikan konstanta. Pengubah akhir menunjukkan bahwa nilai bidang ini tidak dapat berubah .

Sumber: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [penekanan ditambahkan]


Pertanyaan saya: Apakah ini bug? Apakah finaltidak jelas?


Berikut adalah kode yang saya minati. XDitetapkan dua nilai berbeda: 0dan 3. Saya percaya ini merupakan pelanggaran final.

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

Pertanyaan ini telah ditandai sebagai kemungkinan duplikat dari urutan inisialisasi bidang statis Java akhir . Saya percaya bahwa pertanyaan ini bukan duplikat karena pertanyaan lain membahas urutan inisialisasi sementara pertanyaan saya membahas inisialisasi siklik yang dikombinasikan dengan finaltag. Dari pertanyaan lain saja, saya tidak akan bisa mengerti mengapa kode dalam pertanyaan saya tidak membuat kesalahan.

Ini sangat jelas dengan melihat output yang didapat ernesto: saat aditandai final, ia mendapatkan output berikut:

a=5
a=5

yang tidak melibatkan bagian utama dari pertanyaan saya: Bagaimana suatu finalvariabel mengubah variabelnya?

Pembantu Kecil
sumber
17
Cara mereferensikan Xanggota ini seperti merujuk ke anggota subkelas sebelum konstruktor kelas super selesai, itu masalah Anda dan bukan definisi final.
daniu
4
Dari 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
@Van, Ini bukan tentang konstanta tetapi tentang variabel instan. Tetapi bisakah Anda menambahkan bab ini?
AxelH
9
Sama seperti catatan: Jangan pernah melakukan ini dalam kode produksi. Sangat membingungkan bagi semua orang jika seseorang mulai mengeksploitasi celah di JLS.
Zabuzard
13
FYI Anda dapat membuat situasi yang persis sama ini di C # juga. C # berjanji bahwa loop dalam deklarasi konstan akan ditangkap pada waktu kompilasi, tetapi tidak membuat janji seperti itu tentang deklarasi readonly , dan dalam praktiknya Anda bisa masuk ke situasi di mana nilai nol awal bidang diamati oleh penginisialisasi bidang lain. Jika sakit ketika Anda melakukan itu, jangan lakukan itu . Kompiler tidak akan menyelamatkan Anda.
Eric Lippert

Jawaban:

217

Temuan yang sangat menarik. Untuk memahaminya kita perlu menggali spesifikasi bahasa Jawa ( JLS ).

Alasannya adalah finalhanya memungkinkan satu penugasan . Nilai default, bagaimanapun, adalah bukan tugas . Bahkan, setiap variabel ( variabel kelas, variabel instance, komponen array) menunjuk ke nilai defaultnya dari awal, sebelum penugasan . Tugas pertama kemudian mengubah referensi.


Variabel kelas dan nilai default

Lihatlah contoh berikut:

private static Object x;

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

Kami tidak secara eksplisit menetapkan nilai x, meskipun menunjuk ke null, itu nilai default. Bandingkan dengan §4.12.5 :

Nilai Awal Variabel

Setiap variabel kelas , variabel instan, atau komponen array diinisialisasi dengan nilai default ketika dibuat ( §15.9 , §15.10.2 )

Perhatikan bahwa ini hanya berlaku untuk variabel semacam itu, seperti dalam contoh kami. Itu tidak berlaku untuk variabel lokal, lihat contoh berikut:

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

Dari paragraf JLS yang sama:

Sebuah variabel lokal ( §14.4 , §14.14 ) harus secara eksplisit diberikan nilai sebelum digunakan, baik oleh inisialisasi ( §14.4 ) atau tugas ( §15.26 ), dengan cara yang dapat diverifikasi menggunakan aturan untuk tugas tertentu ( § 16 (Penugasan Pasti) ).


Variabel terakhir

Sekarang kita lihat final, dari §4.12.4 :

Variabel akhir

Suatu variabel dapat dinyatakan final . Sebuah akhir variabel hanya dapat ditugaskan untuk sekali . Ini adalah kesalahan waktu kompilasi jika variabel akhir ditugaskan kecuali jika itu pasti dihapuskan segera sebelum penugasan ( §16 (Definite Assignment) ).


Penjelasan

Sekarang kembali ke contoh Anda, sedikit dimodifikasi:

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

Ini menghasilkan

Before: 0
After: 1

Ingat apa yang telah kita pelajari. Dalam metode assignvariabel Xitu tidak ditugaskan nilai untuk belum. Oleh karena itu, ini menunjuk ke nilai default karena itu adalah variabel kelas dan menurut JLS variabel-variabel tersebut selalu langsung menunjuk ke nilai default mereka (berbeda dengan variabel lokal). Setelah assignmetode variabel Xdiberi nilai 1dan karena finalkita tidak dapat mengubahnya lagi. Jadi yang berikut ini tidak akan berfungsi karena final:

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

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

Contoh di JLS

Berkat @Andrew saya menemukan paragraf JLS yang mencakup persis skenario ini, itu juga menunjukkan itu.

Tapi pertama-tama mari kita lihat

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

Mengapa ini tidak diizinkan, sedangkan akses dari metode ini? Lihatlah §8.3.3 yang berbicara tentang kapan akses ke bidang dibatasi jika bidang tersebut belum diinisialisasi.

Ini mencantumkan beberapa aturan yang relevan untuk variabel kelas:

Untuk referensi dengan nama sederhana ke variabel kelas yang fdideklarasikan di kelas atau antarmuka C, itu adalah kesalahan waktu kompilasi jika :

  • Referensi muncul baik dalam penginisialisasi variabel kelas Catau dalam penginisialisasi statis C( §8.7 ); dan

  • Referensi muncul baik di penginisialisasi fdeklarator sendiri atau pada titik di sebelah kiri fdeklarator; dan

  • Referensi tidak di sisi kiri ekspresi penugasan ( §15.26 ); dan

  • Kelas atau antarmuka terdalam yang melampirkan referensi adalah C.

Sederhana, X = X + 1tertangkap oleh aturan itu, metode akses tidak. Mereka bahkan membuat daftar skenario ini dan memberikan contoh:

Akses dengan metode tidak dicentang dengan cara ini, jadi:

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

menghasilkan output:

0

karena penginisialisasi variabel untuk imenggunakan metode kelas mengintip untuk mengakses nilai variabel jsebelum jtelah diinisialisasi oleh penginisialisasi variabelnya, di mana titik itu masih memiliki nilai default ( §4.12.5 ).

Zabuzard
sumber
1
@ Andrew Ya, variabel kelas, terima kasih. Ya, itu akan berhasil jika tidak ada aturan tambahan yang membatasi akses tersebut: §8.3.3 . Lihatlah empat poin yang ditentukan untuk variabel kelas (entri pertama). Pendekatan metode dalam contoh OP tidak tertangkap oleh aturan-aturan tersebut, oleh karena itu kita dapat mengakses Xdari metode tersebut. Saya tidak akan terlalu keberatan. Itu hanya tergantung pada bagaimana sebenarnya JLS mendefinisikan sesuatu untuk bekerja secara detail. Saya tidak akan pernah menggunakan kode seperti itu, hanya mengeksploitasi beberapa aturan di JLS.
Zabuzard
4
Masalahnya adalah Anda dapat memanggil metode instan dari konstruktor, sesuatu yang mungkin seharusnya tidak diizinkan. Di sisi lain, menugaskan penduduk setempat sebelum menelepon super, yang akan berguna dan aman, tidak diizinkan. Sosok pergi
Pasang kembali Monica
1
@Andrew Anda mungkin satu-satunya di sini yang benar - benar disebutkan forwards references(yang merupakan bagian dari JLS juga). ini sangat sederhana tanpa jawaban yang panjang ini stackoverflow.com/a/49371279/1059372
Eugene
1
"Tugas pertama kemudian mengubah referensi." Dalam hal ini bukan tipe referensi, tetapi tipe primitif.
Fabian
1
Jawaban ini benar, jika agak panjang. :-) Saya pikir tl; dr adalah bahwa OP mengutip tutorial yang mengatakan bahwa "bidang [final] tidak dapat berubah," bukan JLS. Meskipun tutorial Oracle cukup bagus, mereka tidak mencakup semua kasus tepi. Untuk pertanyaan OP, kita perlu melihat definisi final JLS yang sebenarnya - dan definisi itu tidak membuat klaim (bahwa OP berhak menantang) bahwa nilai bidang final tidak pernah dapat berubah.
yshavit
23

Tidak ada hubungannya dengan final di sini.

Karena di tingkat instance atau kelas, itu memegang nilai default jika belum ada yang ditugaskan. Itulah alasan Anda melihat 0ketika mengaksesnya tanpa menetapkan.

Jika Anda mengakses Xtanpa menetapkan sepenuhnya, itu memegang nilai default yang lama 0, karenanya hasilnya.

Suresh Atta
sumber
3
Apa yang sulit tentang ini adalah bahwa jika Anda tidak menetapkan nilai, itu tidak akan ditetapkan dengan nilai default, tetapi jika Anda menggunakannya untuk menetapkan sendiri nilai "final", itu akan ...
AxelH
2
@Aktif Saya mengerti apa yang Anda maksud dengan itu. Tapi begitulah seharusnya bekerja kalau tidak dunia akan runtuh;).
Suresh Atta
20

Bukan bug.

Saat panggilan pertama ke scaledipanggil dari

private static final long X = scale(10);

Mencoba untuk mengevaluasi return X * value. Xbelum diberi nilai dan oleh karena itu nilai default untuk a longdigunakan (yaitu 0).

Sehingga garis mengevaluasi kode untuk X * 10yaitu 0 * 10yang 0.

OldCurmudgeon
sumber
8
Saya tidak berpikir itu yang membingungkan OP. Yang membingungkan adalah X = scale(10) + 3. Karena X, ketika dirujuk dari metode, adalah 0. Tapi setelah itu 3. Jadi OP berpikir Xdiberikan dua nilai yang berbeda, yang akan bertentangan final.
Zabuzard
4
@Zabuza bukankah ini menjelaskan dengan " Ini mencoba untuk mengevaluasi return X * value. XBelum diberi nilai dan karena itu mengambil nilai default untuk yang longmana 0. "? Tidak dikatakan Xditugaskan dengan nilai default tetapi X"diganti" (tolong jangan mengutip istilah itu;)) dengan nilai default.
AxelH
14

Ini sama sekali bukan bug, sederhananya itu bukan bentuk rujukan maju ilegal , tidak lebih.

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

Ini hanya diizinkan oleh Spesifikasi.

Untuk mengambil contoh Anda, ini persis tempat ini cocok:

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

Anda sedang melakukan referensi ke depanscale yang tidak ilegal dengan cara apa pun seperti yang dikatakan sebelumnya, tetapi memungkinkan Anda untuk mendapatkan nilai default X. sekali lagi, ini diizinkan oleh Spec (lebih tepatnya itu tidak dilarang), jadi berfungsi dengan baik

Eugene
sumber
jawaban yang bagus! Saya hanya ingin tahu mengapa spec mengizinkan case kedua dikompilasi. Apakah ini satu-satunya cara untuk melihat keadaan "tidak konsisten" dari bidang terakhir?
Andrew Tobilko
@Andrew ini telah mengganggu saya untuk waktu yang cukup lama juga, saya cenderung berpikir itu adalah C ++ atau C melakukannya (tidak tahu apakah ini benar)
Eugene
@ Andrew: Karena melakukan sebaliknya akan menyelesaikan teorema ketidaklengkapan Turing.
Joshua
9
@ Joshua: Saya pikir Anda mencampur sejumlah konsep berbeda di sini: (1) masalah terputus-putus, (2) masalah keputusan, (3) teorema ketidaklengkapan Godel, dan (4) bahasa pemrograman Turing-complete. Penulis kompiler tidak berusaha memecahkan masalah "apakah variabel ini benar-benar ditetapkan sebelum digunakan?" sempurna karena masalah itu setara dengan memecahkan Masalah Pemutusan, dan kami tahu kami tidak bisa melakukannya.
Eric Lippert
4
@EricLippert: Haha oops. Turing ketidaklengkapan dan menghentikan masalah menempati tempat yang sama di pikiran saya.
Joshua
4

Anggota tingkat kelas dapat diinisialisasi dalam kode dalam definisi kelas. Bytecode yang dikompilasi tidak dapat menginisialisasi anggota kelas sebaris. (Anggota Instance ditangani dengan cara yang sama, tetapi ini tidak relevan untuk pertanyaan yang diberikan.)

Ketika seseorang menulis sesuatu seperti berikut:

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

Bytecode yang dihasilkan akan serupa dengan yang berikut:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

Kode inisialisasi ditempatkan dalam penginisialisasi statis yang dijalankan ketika loader kelas pertama memuat kelas. Dengan pengetahuan ini, sampel asli Anda akan mirip dengan yang berikut:

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 memuat RecursiveStatic sebagai titik masuknya jar.
  2. Pemuat kelas menjalankan penginisialisasi statis ketika definisi kelas dimuat.
  3. Penginisialisasi memanggil fungsi scale(10)untuk menetapkan static finalbidang X.
  4. The scale(long)Fungsi berjalan sementara kelas sebagian diinisialisasi membaca nilai diinisiasi dari Xyang default dari panjang atau 0.
  5. Nilai 0 * 10ditugaskan Xdan loader kelas selesai.
  6. JVM menjalankan public static void method utama scale(5)yang mengalikan 5 dengan nilai sekarang yang diinisialisasi Xdari 0 menghasilkan 0.

Bidang final statis Xhanya ditetapkan satu kali, menjaga jaminan yang dimiliki oleh finalkata kunci. Untuk kueri berikutnya untuk menambahkan 3 dalam penugasan, langkah 5 di atas menjadi evaluasi 0 * 10 + 3yang nilainya 3dan metode utama akan mencetak hasil 3 * 5yang nilainya 15.

psaxton
sumber
3

Membaca bidang yang tidak diinisialisasi dari suatu objek seharusnya menghasilkan kesalahan kompilasi. Sayangnya untuk Java, tidak.

Saya pikir alasan mendasar mengapa ini adalah kasus "tersembunyi" jauh di dalam definisi tentang bagaimana objek dibuat dan dibangun, meskipun saya tidak tahu detail standar.

Dalam arti tertentu, final tidak didefinisikan dengan jelas karena bahkan tidak mencapai apa yang dinyatakan tujuannya karena masalah ini. Namun, jika semua kelas Anda ditulis dengan benar, Anda tidak memiliki masalah ini. Berarti semua bidang selalu diatur dalam semua konstruktor dan tidak ada objek yang pernah dibuat tanpa memanggil salah satu konstruktornya. Itu tampak alami sampai Anda harus menggunakan perpustakaan serialisasi.

Kafein
sumber