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 X
dalam skala (panjang) dan mendefinisikan kembali X = scale(10) + 3
, cetakan akan X = 0
kemudian X = 3
. Ini berarti bahwa X
sementara diatur ke 0
dan 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 final
tidak jelas?
Berikut adalah kode yang saya minati.
X
Ditetapkan dua nilai berbeda: 0
dan 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 final
tag. 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 a
ditandai final
, ia mendapatkan output berikut:
a=5
a=5
yang tidak melibatkan bagian utama dari pertanyaan saya: Bagaimana suatu final
variabel mengubah variabelnya?
sumber
X
anggota ini seperti merujuk ke anggota subkelas sebelum konstruktor kelas super selesai, itu masalah Anda dan bukan definisifinal
.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.
Jawaban:
Temuan yang sangat menarik. Untuk memahaminya kita perlu menggali spesifikasi bahasa Jawa ( JLS ).
Alasannya adalah
final
hanya 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:
Kami tidak secara eksplisit menetapkan nilai
x
, meskipun menunjuk kenull
, itu nilai default. Bandingkan dengan §4.12.5 :Perhatikan bahwa ini hanya berlaku untuk variabel semacam itu, seperti dalam contoh kami. Itu tidak berlaku untuk variabel lokal, lihat contoh berikut:
Dari paragraf JLS yang sama:
Variabel terakhir
Sekarang kita lihat
final
, dari §4.12.4 :Penjelasan
Sekarang kembali ke contoh Anda, sedikit dimodifikasi:
Ini menghasilkan
Ingat apa yang telah kita pelajari. Dalam metode
assign
variabelX
itu 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). Setelahassign
metode variabelX
diberi nilai1
dan karenafinal
kita tidak dapat mengubahnya lagi. Jadi yang berikut ini tidak akan berfungsi karenafinal
:Contoh di JLS
Berkat @Andrew saya menemukan paragraf JLS yang mencakup persis skenario ini, itu juga menunjukkan itu.
Tapi pertama-tama mari kita lihat
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:
Sederhana,
X = X + 1
tertangkap oleh aturan itu, metode akses tidak. Mereka bahkan membuat daftar skenario ini dan memberikan contoh:sumber
X
dari 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.forwards references
(yang merupakan bagian dari JLS juga). ini sangat sederhana tanpa jawaban yang panjang ini stackoverflow.com/a/49371279/1059372Tidak 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
0
ketika mengaksesnya tanpa menetapkan.Jika Anda mengakses
X
tanpa menetapkan sepenuhnya, itu memegang nilai default yang lama0
, karenanya hasilnya.sumber
Bukan bug.
Saat panggilan pertama ke
scale
dipanggil dariMencoba untuk mengevaluasi
return X * value
.X
belum diberi nilai dan oleh karena itu nilai default untuk along
digunakan (yaitu0
).Sehingga garis mengevaluasi kode untuk
X * 10
yaitu0 * 10
yang0
.sumber
X = scale(10) + 3
. KarenaX
, ketika dirujuk dari metode, adalah0
. Tapi setelah itu3
. Jadi OP berpikirX
diberikan dua nilai yang berbeda, yang akan bertentanganfinal
.return X * value
.X
Belum diberi nilai dan karena itu mengambil nilai default untuk yanglong
mana0
. "? Tidak dikatakanX
ditugaskan dengan nilai default tetapiX
"diganti" (tolong jangan mengutip istilah itu;)) dengan nilai default.Ini sama sekali bukan bug, sederhananya itu bukan bentuk rujukan maju ilegal , tidak lebih.
Ini hanya diizinkan oleh Spesifikasi.
Untuk mengambil contoh Anda, ini persis tempat ini cocok:
Anda sedang melakukan referensi ke depan
scale
yang tidak ilegal dengan cara apa pun seperti yang dikatakan sebelumnya, tetapi memungkinkan Anda untuk mendapatkan nilai defaultX
. sekali lagi, ini diizinkan oleh Spec (lebih tepatnya itu tidak dilarang), jadi berfungsi dengan baiksumber
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:
Bytecode yang dihasilkan akan serupa dengan yang berikut:
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:
scale(10)
untuk menetapkanstatic final
bidangX
.scale(long)
Fungsi berjalan sementara kelas sebagian diinisialisasi membaca nilai diinisiasi dariX
yang default dari panjang atau 0.0 * 10
ditugaskanX
dan loader kelas selesai.scale(5)
yang mengalikan 5 dengan nilai sekarang yang diinisialisasiX
dari 0 menghasilkan 0.Bidang final statis
X
hanya ditetapkan satu kali, menjaga jaminan yang dimiliki olehfinal
kata kunci. Untuk kueri berikutnya untuk menambahkan 3 dalam penugasan, langkah 5 di atas menjadi evaluasi0 * 10 + 3
yang nilainya3
dan metode utama akan mencetak hasil3 * 5
yang nilainya15
.sumber
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.
sumber