Apa aturan untuk urutan evaluasi di Jawa?

86

Saya membaca beberapa teks Java dan mendapatkan kode berikut:

int[] a = {4,4};
int b = 1;
a[b] = b = 0;

Dalam teks tersebut penulis tidak memberikan penjelasan yang jelas dan efek dari baris terakhir tersebut adalah: a[1] = 0;

Saya tidak begitu yakin bahwa saya mengerti: bagaimana evaluasi itu terjadi?

ipkiss
sumber
19
Kebingungan di bawah tentang mengapa ini terjadi berarti Anda tidak boleh melakukan ini, karena banyak orang harus memikirkan apa yang sebenarnya dilakukannya, alih-alih menjadi jelas.
Martijn
Jawaban yang tepat untuk pertanyaan seperti ini adalah "jangan lakukan itu!" Penugasan harus diperlakukan sebagai pernyataan; penggunaan assignment sebagai ekspresi yang mengembalikan nilai seharusnya menimbulkan kesalahan compiler, atau setidaknya peringatan. Jangan lakukan itu.
Mason Wheeler

Jawaban:

173

Izinkan saya mengatakan ini dengan sangat jelas, karena orang salah paham sepanjang waktu:

Urutan evaluasi subekspresi tidak bergantung pada asosiativitas dan prioritas . Asosiatif dan prioritas menentukan dalam urutan apa operator dieksekusi tetapi tidak menentukan dalam urutan apa subekspresi dievaluasi. Pertanyaan Anda adalah tentang urutan di mana sub - ekspresi dievaluasi.

Pertimbangkan A() + B() + C() * D(). Perkalian lebih diutamakan daripada penjumlahan, dan penjumlahan adalah asosiatif kiri, jadi ini setara dengan (A() + B()) + (C() * D()) Tetapi mengetahui bahwa hanya memberi tahu Anda bahwa penjumlahan pertama akan terjadi sebelum penjumlahan kedua, dan perkalian akan terjadi sebelum penjumlahan kedua. Itu tidak memberi tahu Anda dalam urutan apa A (), B (), C () dan D () akan dipanggil! (Ini juga tidak memberi tahu Anda apakah perkalian terjadi sebelum atau setelah penjumlahan pertama.) Sangat mungkin untuk mematuhi aturan presedensi dan asosiativitas dengan menyusun ini sebagai:

d = D()          // these four computations can happen in any order
b = B()
c = C()
a = A()
sum = a + b      // these two computations can happen in any order
product = c * d
result = sum + product // this has to happen last

Semua aturan prioritas dan asosiatif diikuti di sana - penjumlahan pertama terjadi sebelum penjumlahan kedua, dan perkalian terjadi sebelum penjumlahan kedua. Jelas kita dapat melakukan panggilan ke A (), B (), C () dan D () dalam urutan apa pun dan tetap mematuhi aturan prioritas dan asosiatif!

Kami memerlukan aturan yang tidak terkait dengan aturan prioritas dan asosiatif untuk menjelaskan urutan subekspresi dievaluasi. Aturan yang relevan di Java (dan C #) adalah "subekspresi dievaluasi dari kiri ke kanan". Karena A () muncul di kiri C (), A () dievaluasi terlebih dahulu, terlepas dari fakta bahwa C () terlibat dalam perkalian dan A () hanya terlibat dalam penjumlahan.

Jadi sekarang Anda memiliki cukup informasi untuk menjawab pertanyaan Anda. Dalam a[b] = b = 0aturan asosiatif mengatakan bahwa ini a[b] = (b = 0);tetapi itu tidak berarti bahwa yang b=0berjalan lebih dulu! Aturan prioritas mengatakan bahwa pengindeksan lebih diutamakan daripada tugas, tetapi itu tidak berarti bahwa pengindeks berjalan sebelum tugas paling kanan .

(PEMBARUAN: Versi sebelumnya dari jawaban ini memiliki beberapa kelalaian kecil dan praktis tidak penting di bagian berikutnya yang telah saya koreksi. Saya juga menulis artikel blog yang menjelaskan mengapa aturan ini masuk akal di Java dan C # di sini: https: // ericlippert.com/2019/01/18/indexer-error-cases/ )

Presedensi dan asosiatif hanya memberi tahu kita bahwa penetapan nol ke bharus terjadi sebelum penetapan ke a[b], karena penetapan nol menghitung nilai yang ditetapkan dalam operasi pengindeksan. Precedence dan associativity saja katakanlah apa-apa tentang apakah a[b]dievaluasi sebelum atau setelah itu b=0.

Sekali lagi, ini sama seperti: A()[B()] = C()- Yang kita tahu adalah bahwa pengindeksan harus dilakukan sebelum penugasan. Kami tidak tahu apakah A (), B (), atau C () berjalan lebih dulu berdasarkan prioritas dan asosiatif . Kami membutuhkan aturan lain untuk memberi tahu kami hal itu.

Aturannya adalah, sekali lagi, "ketika Anda memiliki pilihan tentang apa yang harus dilakukan pertama kali, selalu belok kiri ke kanan". Namun, ada kerutan yang menarik dalam skenario khusus ini. Apakah efek samping dari pengecualian yang dilempar disebabkan oleh koleksi null atau indeks di luar rentang dianggap sebagai bagian dari penghitungan sisi kiri tugas, atau bagian dari penghitungan tugas itu sendiri? Java memilih yang terakhir. (Tentu saja, ini adalah perbedaan yang hanya penting jika kodenya sudah salah , karena kode yang benar tidak mengurangi nol atau meneruskan indeks yang buruk sejak awal.)

Lalu apa yang terjadi?

  • Itu a[b]adalah di sebelah kiri b=0, jadi a[b]run pertama , menghasilkan a[1]. Namun, pemeriksaan validitas operasi pengindeksan ini tertunda.
  • Kemudian b=0terjadilah.
  • Kemudian verifikasi yang avalid dan a[1]dalam jangkauan terjadi
  • Penetapan nilai a[1]terjadi terakhir.

Jadi, meskipun dalam kasus khusus ini ada beberapa kehalusan yang perlu dipertimbangkan untuk kasus kesalahan langka yang semestinya tidak terjadi dalam kode yang benar sejak awal, secara umum Anda dapat bernalar: hal-hal di kiri terjadi sebelum hal-hal di kanan . Itulah aturan yang Anda cari. Pembicaraan tentang prioritas dan asosiatif sama-sama membingungkan dan tidak relevan.

Orang-orang melakukan kesalahan ini sepanjang waktu , bahkan orang yang seharusnya lebih tahu. Saya telah menyunting terlalu banyak buku pemrograman yang menyatakan aturannya secara tidak benar, sehingga tidak mengherankan jika banyak orang memiliki keyakinan yang salah sepenuhnya tentang hubungan antara presedensi / asosiativitas, dan urutan evaluasi - yaitu, bahwa pada kenyataannya tidak ada hubungan seperti itu. ; mereka mandiri.

Jika topik ini menarik minat Anda, lihat artikel saya tentang subjek untuk bacaan lebih lanjut:

http://blogs.msdn.com/b/ericlippert/archive/tags/precedence/

Ini tentang C #, tetapi sebagian besar dari hal ini berlaku sama baiknya untuk Java.

Eric Lippert
sumber
6
Secara pribadi saya lebih suka model mental di mana pada langkah pertama Anda membangun pohon ekspresi menggunakan presedensi dan asosiativitas. Dan pada langkah kedua mengevaluasi pohon itu secara rekursif yang dimulai dengan root. Dengan evaluasi node: Evaluasi node turunan langsung dari kiri ke kanan dan kemudian catatan itu sendiri. | Salah satu keuntungan dari model ini adalah ia dapat dengan mudah menangani kasus di mana operator biner memiliki efek samping. Tetapi keuntungan utamanya adalah bahwa itu lebih cocok untuk otak saya.
CodesInChaos
Apakah saya benar bahwa C ++ tidak menjamin ini? Bagaimana dengan Python?
Neil G
2
@Neil: C ++ tidak menjamin apa pun tentang urutan evaluasi, dan tidak pernah melakukannya. (Juga tidak C.) Python menjaminnya secara ketat dengan urutan prioritas; tidak seperti yang lainnya, tugasnya adalah R2L.
Donal Fellows
2
@aroth bagi saya Anda terdengar hanya bingung. Dan aturan prioritas hanya menyiratkan bahwa anak-anak perlu dievaluasi sebelum orang tua. Tetapi mereka tidak mengatakan apa-apa tentang urutan evaluasi anak. Java dan C # memilih kiri ke kanan, C dan C ++ memilih perilaku tidak terdefinisi.
CodesInChaos
6
@noober: Oke, pertimbangkan: M (A () + B (), C () * D (), E () + F ()). Keinginan Anda adalah agar subekspresi dievaluasi dalam urutan apa? Haruskah C () dan D () dievaluasi sebelum A (), B (), E () dan F () karena perkalian lebih diutamakan daripada penjumlahan? Mudah untuk mengatakan bahwa "jelas" urutannya harus berbeda. Menghasilkan aturan aktual yang mencakup semua kasus agak lebih sulit. Desainer C # dan Java memilih aturan yang sederhana dan mudah dijelaskan: "pergi dari kiri ke kanan". Apa pengganti yang Anda usulkan untuk itu, dan mengapa Anda yakin aturan Anda lebih baik?
Eric Lippert
32

Meskipun demikian, jawaban ahli Eric Lippert tidak terlalu membantu karena berbicara tentang bahasa yang berbeda. Ini adalah Java, di mana Spesifikasi Bahasa Java adalah deskripsi definitif dari semantik. Secara khusus, §15.26.1 relevan karena menjelaskan urutan evaluasi untuk =operator (kita semua tahu bahwa itu asosiatif-kanan, ya?). Memotongnya sedikit ke bagian yang kami pedulikan dalam pertanyaan ini:

Jika ekspresi operan kiri adalah ekspresi akses array ( §15.13 ), maka diperlukan banyak langkah:

  • Pertama, subekspresi referensi larik dari ekspresi akses larik operan kiri dievaluasi. Jika evaluasi ini selesai secara tiba-tiba, maka ekspresi tugas selesai secara tiba-tiba karena alasan yang sama; indeks subekspresi (ekspresi akses larik operan kiri) dan operan kanan tidak dievaluasi dan tidak ada penetapan yang terjadi.
  • Jika tidak, indeks subekspresi dari ekspresi akses larik operan kiri dievaluasi. Jika evaluasi ini selesai secara tiba-tiba, maka ekspresi tugas selesai secara tiba-tiba karena alasan yang sama dan operan kanan tidak dievaluasi dan tidak ada tugas yang terjadi.
  • Jika tidak, operan kanan dievaluasi. Jika evaluasi ini selesai secara tiba-tiba, maka ekspresi tugas selesai secara tiba-tiba karena alasan yang sama dan tidak ada tugas yang terjadi.

[… Kemudian menjelaskan arti sebenarnya dari tugas itu sendiri, yang dapat kita abaikan di sini untuk singkatnya…]

Singkatnya, Java memiliki urutan evaluasi yang didefinisikan sangat dekat yang cukup banyak persis dari kiri ke kanan dalam argumen untuk setiap operator atau pemanggilan metode. Penugasan array adalah salah satu kasus yang lebih kompleks, tetapi bahkan di sana masih L2R. (JLS menganjurkan agar Anda tidak menulis kode yang memerlukan batasan semantik kompleks semacam ini , dan begitu juga saya: Anda bisa mendapatkan lebih dari cukup masalah hanya dengan satu tugas per pernyataan!)

C dan C ++ jelas berbeda dengan Java di area ini: definisi bahasa mereka membiarkan urutan evaluasi tidak ditentukan secara sengaja untuk memungkinkan lebih banyak pengoptimalan. C # tampaknya seperti Java, tetapi saya tidak terlalu memahami literaturnya untuk dapat menunjukkan definisi formal. (Ini benar-benar bervariasi oleh bahasa meskipun, Ruby secara ketat L2R, seperti Tcl - meskipun tidak memiliki sebuah operator penugasan per se untuk alasan tidak relevan di sini - dan Python adalah L2R tapi R2L dalam hal tugas , yang saya temukan aneh tapi ada Anda pergi .)

Donal Fellows
sumber
11
Jadi apa yang Anda katakan adalah jawaban Eric salah karena Java mendefinisikannya secara spesifik persis seperti yang dia katakan?
konfigurator
8
Aturan yang relevan di Java (dan C #) adalah "subekspresi dievaluasi dari kiri ke kanan" - Kedengarannya seperti dia sedang membicarakan keduanya.
konfigurator
2
Sedikit bingung di sini - apakah ini membuat jawaban Eric Lippert di atas menjadi kurang benar, atau hanya mengutip referensi khusus mengapa itu benar?
GreenieMeanie
6
@Greenie: Jawaban Eric benar, tetapi seperti yang saya nyatakan, Anda tidak dapat mengambil wawasan dari satu bahasa di bidang ini dan menerapkannya ke bahasa lain tanpa berhati-hati. Jadi saya mengutip sumber definitif.
Donal Fellows
1
yang aneh adalah, tangan kanan dievaluasi sebelum variabel kiri diselesaikan; di a[-1]=c, cdievaluasi, sebelum -1dianggap tidak valid.
ZhongYu
5
a[b] = b = 0;

1) operator pengindeksan array memiliki prioritas lebih tinggi daripada operator penugasan (lihat jawaban ini ):

(a[b]) = b = 0;

2) Menurut 15.26. Operator Penugasan JLS

Ada 12 operator penugasan; semuanya secara sintaksis kanan-asosiatif (mereka mengelompokkan kanan-ke-kiri). Jadi, a = b = c berarti a = (b = c), yang memberikan nilai c ke b dan kemudian memberikan nilai b ke a.

(a[b]) = (b=0);

3) Menurut 15.7. Urutan Evaluasi JLS

Bahasa pemrograman Java menjamin bahwa operan operator tampak dievaluasi dalam urutan evaluasi tertentu, yaitu dari kiri ke kanan.

dan

Operan kiri dari operator biner tampaknya sepenuhnya dievaluasi sebelum bagian mana pun dari operan kanan dievaluasi.

Begitu:

a) (a[b])dievaluasi terlebih dahulua[1]

b) kemudian (b=0)dievaluasi ke0

c) (a[1] = 0)dievaluasi terakhir

bup
sumber
1

Kode Anda sama dengan:

int[] a = {4,4};
int b = 1;
c = b;
b = 0;
a[c] = b;

yang menjelaskan hasilnya.

Jérôme Verstrynge
sumber
7
Pertanyaannya adalah mengapa demikian.
Mat
@Mat Jawabannya adalah karena inilah yang terjadi di balik terpal mengingat kode yang diberikan dalam pertanyaan. Begitulah evaluasi terjadi.
Jérôme Verstrynge
1
Ya saya tahu. Masih belum menjawab pertanyaan melalui IMO, itulah sebabnya mengapa evaluasi terjadi.
Mat
1
@ Mat 'Mengapa seperti ini bagaimana evaluasi terjadi?' bukan pertanyaan yang diajukan. 'bagaimana evaluasi itu terjadi?' adalah pertanyaan yang diajukan.
Jérôme Verstrynge
1
@ JVerstry: Bagaimana mereka tidak setara? Subexpression referensi array operan kiri adalah operan paling kiri. Jadi mengatakan "lakukan yang paling kiri dulu" sama persis dengan mengatakan "lakukan referensi array dulu". Jika pembuat spesifikasi Java memilih untuk tidak perlu bertele-tele dan berlebihan dalam menjelaskan aturan khusus ini, bagus untuk mereka; hal semacam ini membingungkan dan seharusnya lebih bertele-tele. Tapi saya tidak melihat bagaimana karakterisasi singkat saya berbeda secara semantik dari karakterisasi prolix mereka.
Eric Lippert
0

Pertimbangkan contoh lain yang lebih mendalam di bawah ini.

Sebagai Aturan Umum:

Sebaiknya sediakan tabel Urutan Aturan dan Asosiasi yang Diutamakan untuk dibaca saat menyelesaikan pertanyaan ini, misalnya http://introcs.cs.princeton.edu/java/11precedence/

Ini contoh yang bagus:

System.out.println(3+100/10*2-13);

Pertanyaan: Apa Output dari Baris di atas?

Jawaban: Terapkan Aturan Prioritas dan Asosiatif

Langkah 1: Menurut aturan prioritas: / dan * operator mengambil prioritas di atas operator + -. Oleh karena itu, titik awal untuk menjalankan persamaan ini akan dipersempit menjadi:

100/10*2

Langkah 2: Menurut aturan dan prioritas: / dan * memiliki prioritas yang sama.

Karena operator / dan * memiliki prioritas yang sama, kita perlu melihat asosiasi antara operator tersebut.

Menurut ATURAN ASOSIATIVITAS dari dua operator tertentu ini, kami mulai mengeksekusi persamaan dari LEFT TO RIGHT yaitu 100/10 dieksekusi terlebih dahulu:

100/10*2
=100/10
=10*2
=20

Langkah 3: Persamaan tersebut sekarang berada dalam status eksekusi berikut:

=3+20-13

Menurut aturan dan prioritas: + dan - memiliki prioritas yang sama.

Sekarang kita perlu melihat asosiasi antara operator + dan - operator. Menurut asosiasi dari dua operator tertentu ini, kami mulai mengeksekusi persamaan dari KIRI ke KANAN yaitu 3 + 20 dieksekusi terlebih dahulu:

=3+20
=23
=23-13
=10

10 adalah keluaran yang benar saat dikompilasi

Sekali lagi, penting untuk memiliki tabel Urutan Aturan Prioritas dan Asosiasi dengan Anda saat menyelesaikan pertanyaan ini, misalnya http://introcs.cs.princeton.edu/java/11precedence/

Mark Burleigh
sumber
1
Anda mengatakan bahwa "asosiasi antara operator + dan - operator" adalah "RIGHT TO LEFT". Coba gunakan logika itu dan evaluasi 10 - 4 - 3.
Pshemo
1
Saya menduga bahwa kesalahan ini mungkin disebabkan oleh fakta bahwa di atas introcs.cs.princeton.edu/java/11 preseden + adalah operator unary (yang memiliki asosiasi kanan ke kiri), tetapi aditif + dan - sama seperti perkalian * /% tersisa untuk asosiatif yang benar.
Pshemo
Terlihat bagus dan diubah sesuai, terima kasih Pshemo
Mark Burleigh
Jawaban ini menjelaskan presedensi dan asosiatif, tetapi, seperti dijelaskan Eric Lippert , pertanyaannya adalah tentang urutan evaluasi, yang sangat berbeda. Faktanya, ini tidak menjawab pertanyaan itu.
Fabio mengatakan Reinstate Monica