Mengapa hanya variabel akhir yang dapat diakses di kelas anonim?

355
  1. ahanya bisa final di sini. Mengapa? Bagaimana saya dapat menetapkan kembali adalam onClick()metode tanpa menyimpannya sebagai anggota pribadi?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
  2. Bagaimana saya bisa mengembalikan 5 * aketika diklik? Maksudku,

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                 int b = a*5;
                 return b; // but return type is void 
            }
        });
    }
Andrii Abramov
sumber
1
Saya tidak berpikir bahwa kelas anonim Java menyediakan jenis penutupan lambda yang Anda harapkan, tetapi seseorang tolong perbaiki saya jika saya salah ...
user541686
4
Apa yang ingin Anda capai? Handler klik dapat dieksekusi ketika "f" selesai.
Ivan Dubrov
@Lambert jika Anda ingin menggunakan dalam metode onClick itu harus final @Ivan bagaimana bisa metode f () berperilaku seperti metode onClick () mengembalikan int ketika diklik
2
Itulah yang saya maksud - itu tidak mendukung penutupan penuh karena tidak memungkinkan akses ke variabel non-final.
user541686
4
Catatan: pada Java 8, variabel Anda hanya perlu final secara efektif
Peter Lawrey

Jawaban:

489

Seperti disebutkan dalam komentar, beberapa di antaranya menjadi tidak relevan di Jawa 8, di mana finaldapat tersirat. Namun, hanya variabel akhir yang efektif yang dapat digunakan dalam kelas batin anonim atau ekspresi lambda.


Ini pada dasarnya karena cara Jawa mengelola penutupan .

Saat Anda membuat instance kelas dalam anonim, variabel apa pun yang digunakan dalam kelas itu memiliki nilainya disalin melalui konstruktor yang di-autogenerasi. Ini menghindari kompiler harus autogenerate berbagai tipe tambahan untuk menahan keadaan logis dari "variabel lokal", seperti misalnya kompiler C # tidak ... (Ketika C # menangkap variabel dalam fungsi anonim, itu benar-benar menangkap variabel - yang closure dapat memperbarui variabel dengan cara yang dilihat oleh bagian utama dari metode, dan sebaliknya.)

Karena nilai telah disalin ke instance kelas dalam anonim, akan terlihat aneh jika variabel dapat dimodifikasi oleh sisa metode - Anda bisa memiliki kode yang tampaknya berfungsi dengan variabel out-of-date ( karena itu efektif apa yang akan terjadi ... Anda akan bekerja dengan salinan yang diambil pada waktu yang berbeda). Demikian juga jika Anda bisa membuat perubahan dalam kelas dalam anonim, pengembang mungkin berharap perubahan itu akan terlihat dalam tubuh metode terlampir.

Membuat variabel final menghapus semua kemungkinan ini - karena nilainya tidak dapat diubah sama sekali, Anda tidak perlu khawatir tentang apakah perubahan tersebut akan terlihat. Satu-satunya cara untuk memungkinkan metode dan kelas dalam anonim melihat perubahan masing-masing adalah dengan menggunakan jenis deskripsi yang bisa berubah. Ini bisa berupa kelas penutup itu sendiri, sebuah array, jenis pembungkus yang bisa berubah ... hal seperti itu. Pada dasarnya ini sedikit seperti berkomunikasi antara satu metode dan yang lain: perubahan yang dilakukan pada parameter dari satu metode tidak terlihat oleh pemanggilnya, tetapi perubahan yang dilakukan pada objek yang dirujuk oleh parameter terlihat.

Jika Anda tertarik dengan perbandingan yang lebih terperinci antara penutupan Java dan C #, saya memiliki artikel yang membahasnya lebih lanjut. Saya ingin fokus pada sisi Jawa dalam jawaban ini :)

Jon Skeet
sumber
4
@Van: Seperti C #, pada dasarnya. Itu datang dengan tingkat kompleksitas yang adil, jika Anda ingin jenis fungsionalitas yang sama seperti C # di mana variabel dari cakupan yang berbeda dapat "dipakai" beberapa kali.
Jon Skeet
11
Ini semua benar untuk Java 7, perlu diingat bahwa dengan Java 8, penutupan telah diperkenalkan dan sekarang memang mungkin untuk mengakses bidang non-final dari kelas dari kelas dalamnya.
Mathias Bader
22
@MathiasBader: Benarkah? Saya pikir itu pada dasarnya masih mekanisme yang sama, kompiler sekarang cukup pintar untuk menyimpulkan final(tetapi masih perlu efektif akhir).
Thilo
3
@Mathias Bader: Anda selalu dapat mengakses bidang non-final , yang tidak harus bingung dengan variabel lokal , yang harus final dan masih harus final secara efektif, sehingga Java 8 tidak mengubah semantik.
Holger
41

Ada trik yang memungkinkan kelas anonim untuk memperbarui data di lingkup luar.

private void f(Button b, final int a) {
    final int[] res = new int[1];
    b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            res[0] = a * 5;
        }
    });

    // But at this point handler is most likely not executed yet!
    // How should we now res[0] is ready?
}

Namun, trik ini tidak terlalu bagus karena masalah sinkronisasi. Jika handler dipanggil nanti, Anda perlu 1) menyinkronkan akses ke res jika handler dipanggil dari utas yang berbeda 2) perlu memiliki semacam tanda atau indikasi bahwa res telah diperbarui

Trik ini berfungsi baik, jika kelas anonim dipanggil di utas yang sama segera. Suka:

// ...

final int[] res = new int[1];
Runnable r = new Runnable() { public void run() { res[0] = 123; } };
r.run();
System.out.println(res[0]);

// ...
Ivan Dubrov
sumber
2
Terima kasih atas jawaban anda. Saya tahu semua ini dan solusi saya lebih baik dari ini. pertanyaan saya adalah "mengapa hanya final"?
6
Jawabannya adalah begitulah penerapannya :)
Ivan Dubrov
1
Terima kasih. Saya telah menggunakan trik di atas sendiri. Saya tidak yakin apakah itu ide yang bagus. Jika Java tidak mengizinkannya, mungkin ada alasan bagus. Jawaban Anda mengklarifikasi bahwa List.forEachkode saya aman.
RuntimeException
Baca stackoverflow.com/q/12830611/2073130 untuk diskusi yang baik tentang alasan di balik "mengapa hanya final".
lcn
ada beberapa solusi. Milik saya adalah: final int resf = res; Awalnya saya menggunakan pendekatan array, tetapi saya merasa memiliki sintaks yang terlalu rumit. AtomicReference mungkin sedikit lebih lambat (mengalokasikan objek).
zakmck
17

Kelas anonim adalah kelas dalam dan aturan ketat berlaku untuk kelas dalam (JLS 8.1.3) :

Setiap variabel lokal, parameter metode formal atau parameter penangan pengecualian yang digunakan tetapi tidak dideklarasikan di kelas dalam harus dinyatakan final . Variabel lokal apa pun, yang digunakan tetapi tidak dideklarasikan di kelas batin harus ditentukan sebelum tubuh kelas batin .

Saya belum menemukan alasan atau penjelasan tentang jls atau jvms, tetapi kita tahu, bahwa kompiler membuat file kelas terpisah untuk setiap kelas dalam dan itu harus memastikan, bahwa metode dideklarasikan pada file kelas ini ( pada level kode byte) setidaknya memiliki akses ke nilai-nilai variabel lokal.

( Jon memiliki jawaban lengkap - Saya menjaga yang ini tidak terhapus karena orang mungkin tertarik pada aturan JLS)

Andreas Dolk
sumber
11

Anda dapat membuat variabel level kelas untuk mendapatkan nilai yang dikembalikan. maksudku

class A {
    int k = 0;
    private void f(Button b, int a){
        b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            k = a * 5;
        }
    });
}

sekarang Anda bisa mendapatkan nilai K dan menggunakannya di mana Anda inginkan.

Jawaban mengapa Anda adalah:

Instance kelas dalam lokal terkait dengan kelas Utama dan dapat mengakses variabel lokal akhir dari metode yang berisi. Ketika instance menggunakan lokal final dari metode yang mengandung, variabel mempertahankan nilai yang dipegangnya pada saat pembuatan instance, bahkan jika variabel telah keluar dari ruang lingkup (ini adalah secara efektif Java mentah, versi penutupan terbatas).

Karena kelas dalam lokal bukan anggota kelas atau paket, itu tidak dinyatakan dengan tingkat akses. (Namun, jelaskan bahwa anggotanya sendiri memiliki tingkat akses seperti di kelas normal.)

Ashfak Balooch
sumber
Saya menyebutkan bahwa "tanpa menjadikannya sebagai anggota pribadi"
6

Nah, di Jawa, variabel bisa final bukan hanya sebagai parameter, tetapi sebagai bidang tingkat kelas, seperti

public class Test
{
 public final int a = 3;

atau sebagai variabel lokal, seperti

public static void main(String[] args)
{
 final int a = 3;

Jika Anda ingin mengakses dan memodifikasi variabel dari kelas anonim, Anda mungkin ingin menjadikan variabel tersebut sebagai variabel tingkat kelas di kelas yang dilampirkan .

public class Test
{
 public int a;
 public void doSomething()
 {
  Runnable runnable =
   new Runnable()
   {
    public void run()
    {
     System.out.println(a);
     a = a+1;
    }
   };
 }
}

Anda tidak dapat memiliki variabel sebagai final dan memberikannya nilai baru. finalberarti hanya itu: nilainya tidak dapat diubah dan final.

Dan karena sudah final, Java dapat dengan aman menyalinnya ke kelas anonim lokal. Anda tidak mendapatkan referensi ke int (terutama karena Anda tidak dapat memiliki referensi ke primitif seperti int di Jawa, hanya referensi ke Objek ).

Itu hanya menyalin nilai dari ke int implisit yang disebut di kelas anonim Anda.

Zach L
sumber
3
Saya kaitkan "variabel tingkat kelas" dengan static. Mungkin lebih jelas jika Anda menggunakan "variabel instan" sebagai gantinya.
eljenso
1
baik, saya menggunakan level-level karena tekniknya akan bekerja dengan variabel instance dan statis.
Zach L
kita sudah tahu bahwa final dapat diakses tetapi kita ingin tahu mengapa? dapatkah Anda menambahkan lebih banyak penjelasan tentang sisi mengapa?
Saurabh Oza
6

Alasan mengapa akses telah dibatasi hanya untuk variabel final lokal adalah bahwa jika semua variabel lokal dapat diakses maka mereka pertama-tama harus disalin ke bagian terpisah di mana kelas batin dapat memiliki akses ke mereka dan mempertahankan beberapa salinan dari variabel lokal yang bisa berubah dapat menyebabkan data tidak konsisten. Sedangkan variabel akhir tidak berubah dan karenanya sejumlah salinan kepada mereka tidak akan berdampak pada konsistensi data.

Swapnil Gangrade
sumber
Ini bukan bagaimana ini diterapkan dalam bahasa seperti C # yang mendukung fitur ini. Faktanya, kompiler mengubah variabel dari variabel lokal ke variabel instan, atau ia membuat struktur data tambahan untuk variabel-variabel ini yang dapat melampaui lingkup kelas luar. Namun, tidak ada "beberapa salinan variabel lokal"
Mike76
Mike76 Saya belum melihat implementasi C #, tetapi Scala melakukan hal kedua yang Anda sebutkan saya pikir: Jika suatu Intsedang dipindahkan ke dalam penutupan, ubah variabel itu ke instance IntRef(pada dasarnya Integerpembungkus bisa berubah ). Setiap akses variabel kemudian ditulis ulang sesuai.
Adowrath
3

Untuk memahami alasan pembatasan ini, pertimbangkan program berikut:

public class Program {

    interface Interface {
        public void printInteger();
    }
    static Interface interfaceInstance = null;

    static void initialize(int val) {
        class Impl implements Interface {
            @Override
            public void printInteger() {
                System.out.println(val);
            }
        }
        interfaceInstance = new Impl();
    }

    public static void main(String[] args) {
        initialize(12345);
        interfaceInstance.printInteger();
    }
}

The interfaceInstance tetap dalam memori setelah inisialisasi kembali metode, tetapi parameter val tidak. JVM tidak dapat mengakses variabel lokal di luar cakupannya, jadi Java membuat panggilan selanjutnya untuk mencetakInteger berfungsi dengan menyalin nilai val ke bidang implisit dengan nama yang sama dalam interfaceInstance . The interfaceInstance dikatakan telah menangkap nilai dari parameter lokal. Jika parameter tidak final (atau efektif final) nilainya dapat berubah, menjadi tidak sinkron dengan nilai yang ditangkap, berpotensi menyebabkan perilaku tidak intuitif.

Benjamin Curtis Drake
sumber
2

Metode-metode dalam kelas batin anonomyous dapat digunakan baik setelah utas yang menelurkannya telah berakhir. Dalam contoh Anda, kelas dalam akan dipanggil pada utas pengiriman acara dan tidak pada utas yang sama dengan yang membuatnya. Oleh karena itu, ruang lingkup variabel akan berbeda. Jadi untuk melindungi masalah ruang lingkup tugas variabel Anda harus menyatakannya final.

S73417H
sumber
2

Ketika kelas dalam anonim didefinisikan dalam tubuh metode, semua variabel yang dinyatakan final dalam lingkup metode tersebut dapat diakses dari dalam kelas batin. Untuk nilai skalar, setelah ditetapkan, nilai variabel akhir tidak dapat berubah. Untuk nilai objek, referensi tidak dapat berubah. Ini memungkinkan kompiler Java untuk "menangkap" nilai variabel pada saat run-time dan menyimpan salinan sebagai bidang di kelas dalam. Setelah metode luar dihentikan dan bingkai tumpukannya telah dihapus, variabel asli hilang tetapi salinan pribadi kelas dalam tetap ada di memori kelas itu sendiri.

( http://en.wikipedia.org/wiki/Final_%28Java%29 )

ola_star
sumber
1
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

        @Override
        public void onClick(ClickEvent event) {
            a[0] = a[0] * 5;

        }
    });
}
Bruce Zu
sumber
0

Karena Jon memiliki rincian implementasi, jawaban lain yang mungkin adalah bahwa JVM tidak ingin menangani catatan tertulis yang telah mengakhiri aktivasinya.

Pertimbangkan kasus penggunaan di mana lambda Anda alih-alih diterapkan, disimpan di beberapa tempat dan dijalankan kemudian.

Saya ingat bahwa di Smalltalk Anda akan mendapatkan toko ilegal ketika Anda melakukan modifikasi tersebut.

mathk
sumber
0

Coba kode ini,

Buat Daftar Array dan berikan nilai di dalamnya dan kembalikan:

private ArrayList f(Button b, final int a)
{
    final ArrayList al = new ArrayList();
    b.addClickHandler(new ClickHandler() {

         @Override
        public void onClick(ClickEvent event) {
             int b = a*5;
             al.add(b);
        }
    });
    return al;
}
Wasim
sumber
OP menanyakan alasan mengapa diperlukan sesuatu. Oleh karena itu Anda harus menunjukkan bagaimana kode Anda mengatasinya
NitinSingh
0

Kelas anonim Java sangat mirip dengan penutupan Javascript, tetapi Java mengimplementasikannya dengan cara yang berbeda. (periksa jawaban Andersen)

Jadi, agar tidak membingungkan Pengembang Java dengan perilaku aneh yang mungkin terjadi bagi mereka yang berasal dari latar belakang Javascript. Saya kira itu sebabnya mereka memaksa kami untuk menggunakan final, ini bukan batasan JVM.

Mari kita lihat contoh Javascript di bawah ini:

var add = (function () {
  var counter = 0;

  var func = function () {
    console.log("counter now = " + counter);
    counter += 1; 
  };

  counter = 100; // line 1, this one need to be final in Java

  return func;

})();


add(); // this will print out 100 in Javascript but 0 in Java

Dalam Javascript, counternilainya akan menjadi 100, karena hanya ada satu countervariabel dari awal hingga akhir.

Tetapi di Jawa, jika tidak ada final, itu akan dicetak 0, karena ketika objek batin sedang dibuat, 0nilainya disalin ke properti tersembunyi objek kelas dalam itu. (ada dua variabel integer di sini, satu di metode lokal, satu lagi di properti tersembunyi kelas dalam)

Jadi setiap perubahan setelah pembuatan objek dalam (seperti baris 1), itu tidak akan mempengaruhi objek dalam. Jadi itu akan membuat kebingungan antara dua hasil dan perilaku yang berbeda (antara Java dan Javascript).

Saya percaya itu sebabnya, Jawa memutuskan untuk memaksanya menjadi final, sehingga datanya 'konsisten' dari awal hingga akhir.

GMsoF
sumber
0

Variabel akhir Java di dalam kelas batin

kelas batin hanya bisa digunakan

  1. referensi dari kelas luar
  2. variabel lokal akhir dari luar lingkup yang merupakan tipe referensi (mis Object ...)
  3. tipe value (primitive) (eg int...) dapat dibungkus dengan tipe referensi final. IntelliJ IDEAdapat membantu Anda menyamarkannya ke satu elemen array

Ketika a non static nested( inner class) [Tentang] dihasilkan oleh kompiler - kelas baru - <OuterClass>$<InnerClass>.classdibuat dan parameter terikat dilewatkan ke konstruktor [Variabel lokal pada tumpukan] . Ini mirip dengan penutupan

variabel akhir adalah variabel yang tidak dapat ditetapkan kembali. variabel referensi akhir masih dapat diubah dengan memodifikasi keadaan

Mungkin saja akan aneh karena sebagai programmer Anda bisa membuatnya seperti ini

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //address 1
    int a = 5;

    Button button = new Button();

    //just as an example
    button.addClickHandler(new ClickHandler() {


        @Override
        public void onClick(ClickEvent event) {

            myClass.something(); //<- what is the address ?
            int b = a; //<- 5 or 10 ?

            //illusion that next changes are visible for Outer class
            myClass = new MyClass();
            a = 15;
        }
    });

    myClass = new MyClass(); //address 2
    int a = 10;
}
yoAlex5
sumber
-2

Mungkin trik ini memberi Anda ide

Boolean var= new anonymousClass(){
    private String myVar; //String for example
    @Overriden public Boolean method(int i){
          //use myVar and i
    }
    public String setVar(String var){myVar=var; return this;} //Returns self instane
}.setVar("Hello").method(3);
Aneh
sumber