Secara efektif final vs final - Perilaku berbeda

104

Sejauh ini saya berpikir bahwa secara efektif final dan final kurang lebih setara dan bahwa JLS akan memperlakukan mereka serupa jika tidak identik dalam perilaku sebenarnya. Kemudian saya menemukan skenario yang dibuat-buat ini:

final int a = 97;
System.out.println(true ? a : 'c'); // outputs a

// versus

int a = 97;
System.out.println(true ? a : 'c'); // outputs 97

Rupanya, JLS membuat perbedaan penting antara keduanya di sini dan saya tidak yakin mengapa.

Saya membaca utas lain seperti

tetapi mereka tidak menjelaskan secara detail. Lagi pula, pada tingkat yang lebih luas, mereka tampaknya hampir setara. Namun jika digali lebih dalam, ternyata mereka berbeda.

Apa yang menyebabkan perilaku ini, adakah yang dapat memberikan definisi JLS yang menjelaskan hal ini?


Sunting: Saya menemukan skenario terkait lainnya:

final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true

// versus

String a = "a";
System.out.println(a + "b" == "ab"); // outputs false

Jadi interning string juga berperilaku berbeda di sini (saya tidak ingin menggunakan potongan ini dalam kode sebenarnya, hanya ingin tahu tentang perilaku yang berbeda).

Zabuzard
sumber
2
Pertanyaan yang sangat menarik! Saya berharap Java berperilaku sama untuk kedua kasus tersebut, tetapi sekarang saya tercerahkan. Saya bertanya pada diri sendiri apakah ini selalu perilakunya atau apakah ini berbeda di versi sebelumnya
Lino
8
@Lino Kata-kata untuk kutipan terakhir dalam jawaban yang bagus di bawah adalah sama sepanjang perjalanan kembali ke Jawa 6 : "Jika salah satu dari operan adalah tipe T di mana T adalah byte, short, atau char, dan operan lainnya adalah ekspresi konstan tipe intyang nilainya dapat direpresentasikan dalam tipe T , maka tipe ekspresi kondisionalnya adalah T. " --- Bahkan menemukan dokumen Java 1.0 di Berkeley. Teks yang sama . --- Ya, selalu seperti itu.
Andreas
1
Cara Anda "menemukan" hal-hal menarik: P
Sama

Jawaban:

66

Pertama-tama, kita hanya berbicara tentang variabel lokal . Final yang efektif tidak berlaku untuk bidang. Ini penting, karena semantik untuk finalbidang sangat berbeda dan tunduk pada pengoptimalan kompiler berat dan janji model memori, lihat $ 17.5.1 tentang semantik bidang akhir.

Di tingkat permukaan finaldan effectively finaluntuk variabel lokal memang identik. Namun, JLS membuat perbedaan yang jelas antara keduanya yang sebenarnya memiliki berbagai macam efek dalam situasi khusus seperti ini.


Premis

Dari JLS§4.12.4 tentang finalvariabel:

Sebuah variabel konstan adalah finalvariabel tipe primitif atau tipe String yang diinisialisasi dengan ekspresi konstan ( §15.29 ). Apakah suatu variabel adalah variabel konstan atau tidak, mungkin memiliki implikasi sehubungan dengan inisialisasi kelas ( §12.4.1 ), kompatibilitas biner ( §13.1 ), keterjangkauan ( §14.22 ), dan penugasan yang pasti ( §16.1.1 ).

Karena intprimitif, variabelnya aadalah variabel konstan .

Selanjutnya dari bab yang sama tentang effectively final:

Variabel tertentu yang tidak dinyatakan final malah dianggap final secara efektif: ...

Jadi dari cara ini bernada, jelas bahwa dalam contoh lain, ayang tidak dianggap sebagai variabel konstan, karena belum final , tetapi hanya efektif akhir.


Tingkah laku

Sekarang kita memiliki perbedaan, mari kita cari apa yang terjadi dan mengapa keluarannya berbeda.

Anda menggunakan operator bersyarat di ? :sini, jadi kita harus memeriksa definisinya. Dari JLS§15.25 :

Ada tiga jenis ekspresi kondisional, diklasifikasikan menurut ekspresi operan kedua dan ketiga: ekspresi bersyarat boolean , ekspresi bersyarat numerik , dan ekspresi bersyarat referensi .

Dalam hal ini, kita berbicara tentang ekspresi kondisional numerik , dari JLS§15.25.2 :

Jenis ekspresi bersyarat numerik ditentukan sebagai berikut:

Dan itu adalah bagian di mana kedua kasus diklasifikasikan secara berbeda.

efektif akhir

Versi yang effectively finalcocok dengan aturan ini:

Jika tidak, promosi numerik umum ( §5.6 ) diterapkan ke operan kedua dan ketiga, dan jenis ekspresi kondisional adalah jenis yang dipromosikan dari operan kedua dan ketiga.

Yang merupakan perilaku yang sama seperti yang akan Anda lakukan 5 + 'd', yaitu int + char, yang menghasilkan int. Lihat JLS§5.6

Promosi numerik menentukan jenis yang dipromosikan dari semua ekspresi dalam konteks numerik. Jenis yang dipromosikan dipilih sedemikian rupa sehingga setiap ekspresi dapat dikonversi ke jenis yang dipromosikan, dan, dalam kasus operasi aritmatika, operasi tersebut ditentukan untuk nilai dari jenis yang dipromosikan. Urutan ekspresi dalam konteks numerik tidak signifikan untuk promosi numerik. Aturannya adalah sebagai berikut:

[...]

Selanjutnya, konversi primitif pelebaran ( §5.1.2 ) dan konversi primitif mempersempit ( §5.1.3 ) diterapkan ke beberapa ekspresi, menurut aturan berikut:

Dalam konteks pilihan numerik, aturan berikut berlaku:

Jika ada ekspresi berjenis intdan bukan merupakan ekspresi konstan ( §15.29 ), maka jenis yang dipromosikan adalah int, dan ekspresi lain yang bukan berjenis intmenjalani konversi primitif yang melebar menjadi int.

Jadi semuanya dipromosikan menjadi intapa aadanya int. Itu menjelaskan keluaran dari 97.

terakhir

Versi dengan finalvariabel cocok dengan aturan ini:

Jika salah satu operan bertipe Twhere Tis byte,, shortor char, dan operand lainnya adalah ekspresi konstan ( §15.29 ) dari tipe intyang nilainya dapat direpresentasikan dalam tipeT , maka tipe ekspresi kondisionalnya adalah T.

Variabel terakhir aadalah tipe intdan ekspresi konstan (karena memang demikian final). Ini dapat direpresentasikan sebagai char, maka hasilnya adalah tipe char. Itu menyimpulkan hasilnyaa .


Contoh string

Contoh persamaan string didasarkan pada perbedaan inti yang sama, finalvariabel diperlakukan sebagai ekspresi / variabel konstan, daneffectively final tidak.

Di Java, string interning didasarkan pada ekspresi konstan

"a" + "b" + "c" == "abc"

adalah true juga (tidak menggunakan konstruksi ini dalam kode nyata).

Lihat JLS§3.10.5 :

Selain itu, literal string selalu mengacu pada instance kelas String yang sama. Ini karena string literal - atau, lebih umum , string yang merupakan nilai ekspresi konstan ( §15.29 ) - " disimpan " sehingga dapat berbagi instance unik, menggunakan metode String.intern( §12.5 ).

Mudah untuk dilupakan karena ini terutama berbicara tentang literal, tetapi sebenarnya juga berlaku untuk ekspresi konstan.

Zabuzard
sumber
8
Masalahnya adalah Anda berharap ... ? a : 'c'untuk berperilaku sama apakah aitu variabel atau konstanta . Tampaknya tidak ada yang salah dengan ekspresi itu. --- Sebaliknya, a + "b" == "ab"adalah ekspresi yang buruk , karena string perlu dibandingkan menggunakan equals()( Bagaimana cara membandingkan string di Java? ). Fakta bahwa itu "secara tidak sengaja" bekerja ketika aadalah sebuah konstanta , hanyalah sebuah permainan kata dari internal string literal.
Andreas
5
@Andreas Ya, tetapi perhatikan bahwa interning string adalah fitur Java yang didefinisikan dengan jelas. Ini bukan kebetulan yang mungkin berubah besok atau di JVM yang berbeda. "a" + "b" + "c" == "abc"harus truedalam implementasi Java yang valid.
Zabuzard
10
Benar, ini adalah permainan kata yang terdefinisi dengan baik, tetapi a + "b" == "ab"masih merupakan ekspresi yang salah . Bahkan jika Anda tahu bahwa itu aadalah sebuah konstanta , itu terlalu rentan untuk tidak menelepon equals(). Atau mungkin rapuh adalah kata yang lebih baik, yaitu terlalu mungkin untuk berantakan ketika kode dipertahankan di masa mendatang.
Andreas
2
Perhatikan bahwa bahkan dalam domain primer dari variabel akhir yang efektif, yaitu penggunaannya dalam ekspresi lambda, perbedaannya dapat mengubah perilaku runtime, yaitu dapat membuat perbedaan antara ekspresi lambda yang menangkap dan tidak, yang terakhir mengevaluasi ke tunggal , tapi bekas menghasilkan benda baru. Dengan kata lain, (final) String str = "a"; Stream.of(null, null). <Runnable>map( x -> () -> System.out.println(str)) .reduce((a,b) -> () -> System.out.println(a == b)) .ifPresent(Runnable::run);mengubah hasilnya ketika strada (tidak) final.
Holger
7

Aspek lain adalah bahwa jika variabel dinyatakan final dalam tubuh metode, ia memiliki perilaku yang berbeda dari variabel akhir yang diteruskan sebagai parameter.

public void testFinalParameters(final String a, final String b) {
  System.out.println(a + b == "ab");
}

...
testFinalParameters("a", "b"); // Prints false

sementara

public void testFinalVariable() {
   final String a = "a";
   final String b = "b";
   System.out.println(a + b == "ab");  // Prints true
}

...
testFinalVariable();

hal itu terjadi karena compiler tahu bahwa menggunakan final String a = "a"satu avariabel akan selalu memiliki "a"nilai sehingga adan "a"dapat dipertukarkan tanpa masalah. Berbeda, jika atidak ditentukan finalatau ditentukan finaltetapi nilainya ditetapkan pada saat runtime (seperti dalam contoh di atas di mana final adalah aparameternya) kompilator tidak tahu apa-apa sebelum digunakan. Jadi penggabungan terjadi saat runtime dan string baru dibuat, tidak menggunakan kumpulan internal.


Pada dasarnya perilakunya adalah: jika kompilator mengetahui bahwa suatu variabel adalah konstanta dapat menggunakannya sama dengan menggunakan konstanta.

Jika variabel tidak ditentukan final (atau final tetapi nilainya ditentukan saat runtime) tidak ada alasan bagi compiler untuk menanganinya sebagai konstanta juga jika nilainya sama dengan konstanta dan nilainya tidak pernah berubah.

Davide Lorenzo MARINO
sumber
4
Tidak ada yang aneh dalam hal itu :)
dbl
2
Ini adalah aspek lain dari pertanyaan itu.
Davide Lorenzo MARINO
5
finelkata kunci diterapkan ke parameter, memiliki semantik berbeda dari yang finalditerapkan ke variabel lokal, dll ...
dbl
6
Menggunakan parameter adalah kebingungan yang tidak perlu di sini. Anda bisa saja melakukan final String a; a = "a";dan mendapatkan perilaku yang sama
yawkat