Mengapa kode Java ini dikompilasi?

96

Dalam metode atau ruang lingkup kelas, baris di bawah ini dikompilasi (dengan peringatan):

int x = x = 1;

Dalam ruang lingkup kelas, di mana variabel mendapatkan nilai defaultnya , berikut ini memberikan kesalahan 'referensi tidak terdefinisi':

int x = x + 1;

Bukankah yang pertama x = x = 1harus berakhir dengan kesalahan 'referensi tidak terdefinisi' yang sama? Atau mungkin baris kedua int x = x + 1harus dikompilasi? Atau ada sesuatu yang saya lewatkan?

Marcin
sumber
1
Jika Anda menambahkan kata kunci staticdalam variabel cakupan kelas, seperti dalam static int x = x + 1;, apakah Anda akan mendapatkan kesalahan yang sama? Karena di C # itu membuat perbedaan apakah itu statis atau non-statis.
Jeppe Stig Nielsen
static int x = x + 1gagal di Java.
Marcin
1
di c # keduanya int a = this.a + 1;dan int b = 1; int a = b + 1;dalam cakupan kelas (keduanya baik-baik saja di Java) gagal, mungkin karena §17.4.5.2 - "Penginisialisasi variabel untuk bidang instance tidak dapat mereferensikan instance yang sedang dibuat." Saya tidak tahu apakah itu secara eksplisit diizinkan di suatu tempat tetapi statis tidak memiliki batasan seperti itu. Di Java aturannya berbeda dan static int x = x + 1gagal karena alasan yang int x = x + 1sama seperti
msam
Jawaban itu dengan bytecode menghilangkan semua keraguan.
rgripper

Jawaban:

101

tl; dr

Untuk bidang , int b = b + 1ilegal karena bmerupakan rujukan penerusan ilegal b. Anda sebenarnya dapat memperbaikinya dengan menulis int b = this.b + 1, yang mengkompilasi tanpa keluhan.

Untuk variabel lokal , int d = d + 1ilegal karena dtidak diinisialisasi sebelum digunakan. Ini tidak terjadi untuk bidang, yang selalu diinisialisasi default.

Anda dapat melihat perbedaannya dengan mencoba mengompilasi

int x = (x = 1) + x;

sebagai deklarasi lapangan dan sebagai deklarasi variabel lokal. Yang pertama akan gagal, tetapi yang terakhir akan berhasil, karena perbedaan semantik.

pengantar

Pertama, aturan untuk bidang dan penginisialisasi variabel lokal sangat berbeda. Jadi jawaban ini akan membahas aturan dalam dua bagian.

Kami akan menggunakan program tes ini selama:

public class test {
    int a = a = 1;
    int b = b + 1;
    public static void Main(String[] args) {
        int c = c = 1;
        int d = d + 1;
    }
}

Deklarasi btidak valid dan gagal dengan illegal forward referencekesalahan.
Deklarasi dtidak valid dan gagal dengan variable d might not have been initializedkesalahan.

Fakta bahwa kesalahan ini berbeda seharusnya mengisyaratkan bahwa alasan kesalahan juga berbeda.

Fields

Penginisialisasi bidang di Jawa diatur oleh JLS §8.3.2 , Inisialisasi Bidang.

The lingkup bidang didefinisikan di JLS §6.3 , Lingkup Deklarasi.

Aturan yang relevan adalah:

  • Cakupan deklarasi anggota yang mdideklarasikan atau diwarisi oleh kelas tipe C (§8.1.6) adalah seluruh isi C, termasuk deklarasi tipe bertingkat.
  • Ekspresi inisialisasi untuk variabel instan dapat menggunakan nama sederhana dari variabel statis yang dideklarasikan atau diwarisi oleh kelas, bahkan yang deklarasinya muncul secara tekstual kemudian.
  • Penggunaan variabel instan yang deklarasinya muncul secara tekstual setelah penggunaan terkadang dibatasi, meskipun variabel instan ini masih dalam cakupan. Lihat §8.3.2.3 untuk aturan yang tepat yang mengatur referensi maju ke variabel instan.

§8.3.2.3 mengatakan:

Deklarasi anggota harus muncul secara tekstual sebelum digunakan hanya jika anggota tersebut adalah bidang instance (masing-masing statis) dari kelas atau antarmuka C dan semua kondisi berikut berlaku:

  • Penggunaan terjadi dalam inisialisasi variabel instance (masing-masing statis) dari C atau dalam inisialisasi instance (masing-masing statis) dari C.
  • Penggunaan tidak di sisi kiri tugas.
  • Penggunaannya melalui nama yang sederhana.
  • C adalah kelas atau antarmuka terdalam yang melingkupi penggunaan.

Anda sebenarnya dapat merujuk ke bidang sebelum dideklarasikan, kecuali dalam kasus tertentu. Pembatasan ini dimaksudkan untuk mencegah kode like

int j = i;
int i = j;

dari kompilasi. Spesifikasi Java mengatakan "pembatasan di atas dirancang untuk menangkap, pada waktu kompilasi, inisialisasi melingkar atau dalam format yang salah".

Apa tujuan sebenarnya dari aturan-aturan ini?

Singkatnya, aturan pada dasarnya mengatakan bahwa Anda harus mendeklarasikan bidang sebelum referensi ke bidang itu jika (a) referensi ada dalam penginisialisasi, (b) referensi tidak ditugaskan, (c) referensi adalah a nama sederhana (tidak ada kualifikasi seperti this.) dan (d) itu tidak diakses dari dalam kelas dalam. Jadi, referensi maju yang memenuhi keempat kondisi adalah ilegal, tetapi referensi ke depan yang gagal pada setidaknya satu kondisi tidak masalah.

int a = a = 1;mengkompilasi karena melanggar (b): referensi a yang sedang ditugaskan untuk, sehingga hukum untuk merujuk asebelum a's deklarasi lengkap.

int b = this.b + 1juga mengkompilasi karena melanggar (c): referensi this.bbukanlah nama yang sederhana (itu memenuhi syarat dengan this.). Konstruksi ganjil ini masih terdefinisi dengan baik, karena this.bmemiliki nilai nol.

Jadi, pada dasarnya, pembatasan referensi bidang dalam penginisialisasi mencegah int a = a + 1agar tidak berhasil dikompilasi.

Perhatikan bahwa deklarasi lapangan int b = (b = 1) + bakan gagal untuk dikompilasi, karena final bmasih merupakan referensi penerusan yang ilegal.

Variabel lokal

Deklarasi variabel lokal diatur oleh JLS §14.4 , Pernyataan Deklarasi Variabel Lokal.

The lingkup dari variabel lokal didefinisikan dalam JLS §6.3 , Lingkup Deklarasi:

  • Cakupan deklarasi variabel lokal dalam sebuah blok (§14.4) adalah blok lainnya di mana deklarasi tersebut muncul, dimulai dengan penginisialisasinya sendiri dan termasuk setiap deklarator di sebelah kanan dalam pernyataan deklarasi variabel lokal.

Perhatikan bahwa penginisialisasi berada dalam cakupan variabel yang dideklarasikan. Jadi mengapa tidak int d = d + 1;dikompilasi?

Alasannya karena aturan Jawa tentang penetapan pasti ( JLS §16 ). Penugasan pasti pada dasarnya mengatakan bahwa setiap akses ke variabel lokal harus memiliki penugasan sebelumnya ke variabel itu, dan kompiler Java memeriksa loop dan cabang untuk memastikan bahwa penugasan selalu terjadi sebelum penggunaan apa pun (inilah mengapa penugasan tertentu memiliki seluruh bagian spesifikasi yang didedikasikan untuk itu). Aturan dasarnya adalah:

  • Untuk setiap akses variabel lokal atau bidang akhir kosong x, xharus ditetapkan dengan pasti sebelum akses, atau kesalahan waktu kompilasi terjadi.

Dalam int d = d + 1;, akses ke ddiselesaikan ke denda variabel lokal, tetapi karena dbelum ditetapkan sebelum ddiakses, kompilator mengeluarkan kesalahan. Dalam int c = c = 1, c = 1terjadi pertama, yang menetapkan c, dan kemudian cdiinisialisasi ke hasil tugas itu (yaitu 1).

Perhatikan bahwa karena aturan penugasan yang pasti, deklarasi variabel lokal int d = (d = 1) + d; akan berhasil dikompilasi ( tidak seperti deklarasi lapangan int b = (b = 1) + b), karena dpasti ditetapkan pada saat final dtercapai.

nneonneo
sumber
+1 untuk referensi, namun saya pikir Anda mendapatkan kata-kata ini salah: "int a = a = 1; dikompilasi karena melanggar (b)", jika melanggar salah satu dari 4 persyaratan itu tidak akan dikompilasi. Namun tidak karena IS di sisi kiri tugas (kata-kata negatif ganda dalam JLS tidak banyak membantu di sini). Dalam int b = b + 1b ada di sebelah kanan (bukan di kiri) tugas sehingga akan melanggar ini ...
msam
... Yang saya kurang yakin adalah sebagai berikut: 4 syarat tersebut harus dipenuhi jika deklarasi tidak muncul secara tekstual sebelum tugas, dalam hal ini saya pikir deklarasi tersebut memang muncul "secara tekstual" sebelum tugas int x = x = 1, di mana jika semua ini tidak akan berlaku.
msam
@ msam: Agak membingungkan, tetapi pada dasarnya Anda harus melanggar salah satu dari empat ketentuan untuk membuat referensi ke depan. Jika referensi maju Anda memenuhi keempat kondisi, itu ilegal.
nneonneo
@msam: Juga, deklarasi lengkap hanya berlaku setelah penginisialisasi.
nneonneo
@mrfishie: Jawaban besar, tetapi ada kedalaman yang mengejutkan dalam spesifikasi Java. Pertanyaannya tidak sesederhana yang terlihat di permukaan. (Saya pernah menulis subset-of-Java compiler, jadi saya cukup familiar dengan banyak seluk beluk JLS).
nneonneo
86
int x = x = 1;

setara dengan

int x = 1;
x = x; //warning here

saat di

int x = x + 1; 

pertama kita perlu menghitung x+1tetapi nilai x tidak diketahui sehingga Anda mendapatkan kesalahan (penyusun tahu bahwa nilai x tidak diketahui)

msam
sumber
4
Ini ditambah petunjuk tentang asosiasi-kanan dari OpenSauce menurut saya sangat berguna.
TobiMcNamobi
1
Saya pikir nilai kembali dari sebuah tugas adalah nilai yang diberikan, bukan nilai variabel.
zzzzBov
2
@zzzzBov benar. int x = x = 1;setara dengan int x = (x = 1), tidak x = 1; x = x; . Anda seharusnya tidak mendapatkan peringatan kompiler untuk melakukan ini.
nneonneo
int x = x = 1;s setara dengan int x = (x = 1)karena asosiasi kanan =operator
Grijesh Chauhan
1
@nneonneo dan int x = (x = 1)setara dengan int x; x = 1; x = x;(deklarasi variabel, evaluasi penginisialisasi lapangan, penugasan variabel ke hasil evaluasi tersebut), maka peringatan
msam
41

Ini kira-kira setara dengan:

int x;
x = 1;
x = 1;

Pertama, int <var> = <expression>;selalu setara dengan

int <var>;
<var> = <expression>;

Dalam hal ini, ekspresi Anda adalah x = 1, yang juga merupakan pernyataan. x = 1adalah pernyataan yang valid, karena var xtelah dideklarasikan. Ini juga merupakan ekspresi dengan nilai 1, yang kemudian ditetapkan xlagi.

OpenSauce
sumber
Oke, tetapi jika seperti yang Anda katakan, mengapa dalam ruang lingkup kelas pernyataan kedua memberikan kesalahan? Maksud saya, Anda mendapatkan 0nilai default untuk int, jadi saya mengharapkan hasilnya menjadi 1, bukan undefined reference.
Marcin
Lihatlah jawaban @izogfif. Sepertinya berfungsi, karena compiler C ++ memberikan nilai default ke variabel. Cara yang sama dilakukan java untuk variabel tingkat kelas.
Marcin
@Marcin: di Java, int tidak diinisialisasi ke 0 ketika mereka adalah variabel lokal. Mereka hanya diinisialisasi ke 0 jika mereka adalah variabel anggota. Jadi di baris kedua Anda, x + 1tidak memiliki nilai yang ditentukan, karena xtidak diinisialisasi.
OpenSauce
1
@OpenSauce Tapi x ini didefinisikan sebagai variabel anggota ( "dalam lingkup kelas").
Jacob Raihle
@JacobRaihle: Ah ok, tidak melihat bagian itu. Saya tidak yakin bytecode untuk menginisialisasi var ke 0 akan dihasilkan oleh kompilator jika melihat ada instruksi inisialisasi eksplisit. Ada artikel di sini yang membahas beberapa detail tentang inisialisasi kelas dan objek, meskipun menurut saya artikel ini tidak membahas masalah ini: javaworld.com/jw-11-2001/jw-1102-java101.html
OpenSauce
12

Di java atau dalam bahasa modern apa pun, penugasan berasal dari kanan.

Misalkan jika Anda memiliki dua variabel x dan y,

int z = x = y = 5;

Pernyataan ini valid dan begitulah cara compiler membaginya.

y = 5;
x = y;
z = x; // which will be 5

Tapi dalam kasusmu

int x = x + 1;

Kompilator memberikan pengecualian karena terbagi seperti ini.

x = 1; // oops, it isn't declared because assignment comes from the right.
Sri Harsha Chilakapati
sumber
Peringatan ada pada x = x tidak x = 1
Asim Ghaffar
8

int x = x = 1; tidak sama dengan:

int x;
x = 1;
x = x;

javap membantu kami lagi, ini adalah instruksi JVM yang dihasilkan untuk kode ini:

0: iconst_1    //load constant to stack
1: dup         //duplicate it
2: istore_1    //set x to constant
3: istore_1    //set x to constant

lebih seperti:

int x = 1;
x = 1;

Tidak ada alasan untuk menampilkan kesalahan referensi yang tidak ditentukan. Sekarang ada penggunaan variabel sebelum inisialisasi, jadi kode ini sepenuhnya sesuai dengan spesifikasi. Faktanya tidak ada penggunaan variabel sama sekali , hanya tugas. Dan compiler JIT akan melangkah lebih jauh, ini akan menghilangkan konstruksi seperti itu. Sejujurnya, saya tidak mengerti bagaimana kode ini terhubung ke spesifikasi inisialisasi dan penggunaan variabel JLS. Tidak ada penggunaan, tidak ada masalah. ;)

Harap perbaiki jika saya salah. Saya tidak tahu mengapa jawaban lain, yang mengacu pada banyak paragraf JLS mengumpulkan begitu banyak nilai tambah. Paragraf-paragraf ini tidak memiliki kesamaan dengan kasus ini. Hanya dua tugas serial dan tidak lebih.

Jika kita menulis:

int b, c, d, e, f;
int a = b = c = d = e = f = 5;

adalah sama dengan:

f = 5
e = 5
d = 5
c = 5
b = 5
a = 5

Ekspresi paling kanan hanya diberikan ke variabel satu per satu, tanpa rekursi apa pun. Kita bisa mengacaukan variabel sesuka kita:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;
Mikhail
sumber
7

Jika int x = x + 1;Anda menambahkan 1 ke x, jadi berapa nilainya x, itu belum dibuat.

Tetapi di int x=x=1;akan mengkompilasi tanpa kesalahan karena Anda menetapkan 1 untuk x.

Alya'a Gamal
sumber
5

Potongan kode pertama Anda berisi yang kedua, =bukan plus. Ini akan dikompilasi di mana saja sementara potongan kode kedua tidak akan dikompilasi di tempat mana pun.

Joe Elleson
sumber
5

Di bagian kode kedua, x digunakan sebelum deklarasinya, sedangkan di bagian pertama kode itu hanya ditetapkan dua kali yang tidak masuk akal tetapi valid.

WilQu
sumber
5

Mari kita uraikan langkah demi langkah, asosiatif yang benar

int x = x = 1

x = 1, tetapkan 1 ke variabel x

int x = x, tetapkan apa x untuk dirinya sendiri, sebagai int. Karena x sebelumnya ditetapkan sebagai 1, ia mempertahankan 1, meskipun dengan cara yang berlebihan.

Itu mengkompilasi dengan baik.

int x = x + 1

x + 1, tambahkan satu ke variabel x. Namun, x yang tidak ditentukan ini akan menyebabkan kesalahan kompilasi.

int x = x + 1, sehingga kesalahan kompilasi baris ini karena bagian kanan dari sama tidak akan dikompilasi menambahkan satu ke variabel yang tidak ditetapkan

steventnorris
sumber
Tidak, ini asosiatif kanan jika ada dua =operator, jadi sama dengan int x = (x = 1);.
Jeppe Stig Nielsen
Ah, pesanan saya batal. Maaf soal itu. Seharusnya melakukannya secara terbalik. Saya sudah mengubahnya sekarang.
steventnorris
3

Yang kedua int x=x=1adalah kompilasi karena Anda menetapkan nilai ke x tetapi dalam kasus lain di int x=x+1sini variabel x tidak diinisialisasi, Ingat dalam java variabel lokal tidak diinisialisasi ke nilai default. Catatan Jika it's ( int x=x+1) dalam ruang lingkup kelas juga maka itu juga akan memberikan kesalahan kompilasi karena variabel tidak dibuat.

Krushna
sumber
2
int x = x + 1;

berhasil dikompilasi di Visual Studio 2008 dengan peringatan

warning C4700: uninitialized local variable 'x' used`
izogfif.dll
sumber
2
Menarik. Apakah C / C ++?
Marcin
@Marcin: ya, ini C ++. @msam: maaf, saya pikir saya melihat tag cbukannya javatapi rupanya itu adalah pertanyaan lain.
izogfif
Ini dikompilasi karena dalam C ++, compiler menetapkan nilai default untuk tipe primitif. Menggunakanbool y; dan y==trueakan mengembalikan false.
Sri Harsha Chilakapati
@SriHarshaChilakapati, apakah ini semacam standar dalam compiler C ++? Karena saat saya kompilasivoid main() { int x = x + 1; printf("%d ", x); } di Visual Studio 2008, di Debug saya mendapatkan pengecualian Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.dan di Rilis saya mendapatkan nomor yang 1896199921dicetak di konsol.
izogfif
1
@SriHarshaChilakapati Berbicara tentang bahasa lain: Dalam C #, untuk staticbidang (variabel statis tingkat kelas), aturan yang sama berlaku. Misalnya bidang dideklarasikan sebagai public static int x = x + 1;kompilasi tanpa peringatan dalam Visual C #. Mungkin sama di Jawa?
Jeppe Stig Nielsen
2

x tidak diinisialisasi dalam x = x + 1 ;.

Bahasa pemrograman Java diketik secara statis, yang artinya semua variabel harus dideklarasikan terlebih dahulu sebelum dapat digunakan.

Lihat tipe data primitif

Mohan Raj B
sumber
3
Kebutuhan untuk menginisialisasi variabel sebelum menggunakan nilainya tidak ada hubungannya dengan pengetikan statis. Diketik statis: Anda perlu mendeklarasikan tipe variabel itu. Inisialisasi-sebelum-penggunaan: ini harus memiliki nilai yang dapat dibuktikan sebelum Anda dapat menggunakan nilainya.
Jon Bright
@JonBright: Kebutuhan untuk mendeklarasikan tipe variabel juga tidak ada hubungannya dengan pengetikan statis. Misalnya, ada bahasa yang diketik secara statis dengan jenis inferensi.
hammar
@hammar, menurut saya, Anda dapat membantahnya dengan dua cara: dengan tipe inferensi, Anda secara implisit mendeklarasikan tipe variabel dengan cara yang dapat disimpulkan oleh sistem. Atau, jenis inferensi adalah cara ketiga, di mana variabel tidak diketik secara dinamis pada waktu proses, tetapi berada di tingkat sumber, bergantung pada penggunaannya dan kesimpulan yang dibuat. Bagaimanapun, pernyataan itu tetap benar. Tapi Anda benar, saya tidak memikirkan sistem tipe lain.
Jon Bright
2

Baris kode tidak dapat dikompilasi dengan peringatan karena cara kerja kode sebenarnya. Saat Anda menjalankan kode int x = x = 1, Java akan membuat variabel terlebih dahulu x, seperti yang ditentukan. Kemudian menjalankan kode tugas ( x = 1). Karena xsudah ditentukan, sistem tidak memiliki kesalahan pengaturan xke 1. Ini mengembalikan nilai 1, karena itu sekarang adalah nilai x. Oleh karena itu, xsekarang akhirnya ditetapkan sebagai 1.
Java pada dasarnya mengeksekusi kode seolah-olah seperti ini:

int x;
x = (x = 1); // (x = 1) returns 1 so there is no error

Namun, dalam potongan kode kedua Anda int x = x + 1,, + 1pernyataan itu perlu xdidefinisikan, yang pada saat itu belum. Karena pernyataan penugasan selalu berarti kode di sebelah kanan =dijalankan pertama kali, kode akan gagal karena xtidak ditentukan. Java akan menjalankan kode seperti ini:

int x;
x = x + 1; // this line causes the error because `x` is undefined
cpdt
sumber
-1

Pernyataan membaca yang lebih lengkap dari kanan ke kiri dan kami merancang untuk melakukan yang sebaliknya. Karena itulah awalnya dia kesal. Jadikan ini kebiasaan membaca pernyataan (kode) dari kanan ke kiri Anda tidak akan mengalami masalah seperti itu.

Ramiz Uddin
sumber