Nihai kötü tanımlanmış mı?


186

İlk olarak, bir bulmaca: Aşağıdaki kod ne yazdırıyor?

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;
    }
}

Cevap:

0

Aşağıdaki spoiler.


Eğer yazdırmak için Xölçek (uzun) ve yeniden tanımlamak içinde X = scale(10) + 3, baskılar olacak X = 0o zaman X = 3. Bu X, geçici olarak 0ve daha sonra olarak ayarlandığı anlamına gelir 3. Bu bir ihlaldir final!

Statik değiştirici, son değiştirici ile birlikte sabitleri tanımlamak için de kullanılır. Son değiştirici, bu alanın değerinin değiştirilemeyeceğini gösterir .

Kaynak: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [vurgu eklendi]


Sorum: Bu bir hata mı? Is finalkötü tanımlanmış?


İşte ben ilgilendiğim kod. Xİki farklı değer atanır: 0ve 3. Bunun bir ihlali olduğuna inanıyorum 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;
    }
}

Bu soru, Java statik son alan başlatma sırasının olası bir kopyası olarak işaretlendi . Bu soru olduğuna inanıyoruz değil benim sorum ile birlikte halkalı başlatma adresleri ise diğer soru adresleri başlatma sırası beri yinelenen finaletiketi. Sadece diğer sorudan, sorumun neden bir hata yapmadığını neden anlayamıyorum.

Bu özellikle ernesto'nun elde ettiği çıktıya bakarak açıktır: aetiketlendiğinde finalaşağıdaki çıktıyı alır:

a=5
a=5

sorumun ana kısmını içermiyor: Bir finaldeğişken kendi değişkenini nasıl değiştirir?


17
Bu Xüyeye gönderme yapmanın yolu , süper sınıf kurucusu bitmeden önce bir alt sınıf üyesine başvurmak gibidir, bu sizin probleminizdir ve tanımı değil final.
daniu

4
JLS'den: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.
Ivan

1
@Ivan, Bu sabit hakkında değil, örnek değişkeni hakkında. Ama bölümü ekleyebilir misin?
AxelH

9
Not olarak: Bunların hiçbirini üretim kodunda asla yapmayın. Birisi JLS'deki boşluklardan yararlanmaya başlarsa herkes için süper kafa karıştırıcıdır.
Zabuzard

13
Bilginize aynı durumu C # 'da da oluşturabilirsiniz. C #, sabit bildirimlerde döngülerin derleme zamanında yakalanacağına söz verir, ancak salt okunur bildirimler hakkında böyle bir söz vermez ve pratikte alanın ilk sıfır değerinin başka bir alan başlatıcısı tarafından gözlemlendiği durumlara girebilirsiniz. Bunu yaptığınız zaman acıyorsa, yapma . Derleyici sizi kurtaramaz.
Eric Lippert

Yanıtlar:


217

Çok ilginç bir keşif. Bunu anlamak için Java Dil Spesifikasyonunu ( JLS ) incelememiz gerekiyor.

Nedeni finalsadece bir ödeve izin vermesidir . Ancak varsayılan değer atama değildir . Aslında, bu tür her değişken (sınıf değişkeni, örnek değişkeni, dizi bileşeni) atamadan önce başlangıçtaki varsayılan değerine işaret eder . İlk atama referansı değiştirir.


Sınıf değişkenleri ve varsayılan değer

Aşağıdaki örneğe bir göz atın:

private static Object x;

public static void main(String[] args) {
    System.out.println(x); // Prints 'null'
}

İşaret ettiği xhalde nullvarsayılan değerine açıkça bir değer atamadık . Bunu §4.12.5 ile karşılaştırın :

Değişkenlerin İlk Değerleri

Her sınıf değişkeni , örnek değişkeni veya dizi bileşeni , oluşturulduğunda varsayılan bir değerle başlatılır ( §15.9 , §15.10.2 )

Bunun yalnızca örneğimizdeki gibi değişkenler için geçerli olduğunu unutmayın. Yerel değişkenler için geçerli değildir, aşağıdaki örneğe bakın:

public static void main(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

Aynı JLS paragrafından:

Bir yerel değişken ( §14.4 , §14.14 ) gereken açık bir değer verilir kullanılmadan önce iki başlatma ile (, §14.4 ) ya da atama ( §15.26 kesin atama için kuralları (kullanılarak doğrulanabilir şekilde,) § 16 (Kesin Atama) ).


Nihai değişkenler

Şimdi bakmak finaldan, §4.12.4 :

son Değişkenler

Bir değişken nihai olarak bildirilebilir . Bir nihai değişken sadece edilebilir bir kez atanan . Son değişkenin atamadan hemen önce atanmamış olması halinde atanması derleme zamanı hatasıdır ( §16 (Kesin Atama) ).


açıklama

Şimdi örneğinize geri dönelim, biraz değiştirildi:

public static void main(String[] args) {
    System.out.println("After: " + X);
}

private static final long X = assign();

private static long assign() {
    // Access the value before first assignment
    System.out.println("Before: " + X);

    return X + 1;
}

Çıktı

Before: 0
After: 1

Ne öğrendiğimizi hatırlayın. Yöntemin İçinde assigndeğişken Xedildi atanmamış henüz bir değer. Bu nedenle, bir sınıf değişkeni olduğu için varsayılan değerine işaret eder ve JLS'ye göre bu değişkenler her zaman hemen varsayılan değerlerine işaret eder (yerel değişkenlerin aksine). Sonra assignyöntemle değişken Xdeğeri atanır 1ve yüzünden finalbiz artık bunu değiştiremez. Bu nedenle aşağıdakiler işe yaramaz final:

private static long assign() {
    // Assign X
    X = 1;

    // Second assign after method will crash
    return X + 1;
}

JLS Örneği

@Andrew sayesinde tam olarak bu senaryoyu kapsayan bir JLS paragrafı buldum, bunu da gösteriyor.

Ama önce bir göz atalım

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

Buna neden izin verilmez, oysa yöntemden erişime izin verilir? Alan henüz başlatılmadıysa, alanlara erişimin ne zaman kısıtlandığını anlatan §8.3.3'e bakın .

Sınıf değişkenleriyle ilgili bazı kuralları listeler:

fSınıfta veya arabirimde bildirilen bir sınıf değişkenine basit adla başvuru Ciçin, aşağıdaki durumlarda derleme zamanı hatasıdır :

  • Referans ya bir sınıf değişkeni başlatıcısında Cya da C( §8.7 ) ' nin statik başlatıcısında görünür ; ve

  • Referans ya fkendi bildiricisinin başlatıcısında ya da bildiricinin solunda bir noktada görünür f; ve

  • Referans, bir atama ifadesinin sol tarafında değil ( §15.26 ); ve

  • Referansı içeren en içteki sınıf veya arabirimdir C.

Basit, X = X + 1bu kurallara yakalanmış, yöntem erişim değil. Hatta bu senaryoyu listeliyor ve bir örnek veriyorlar:

Yöntemlerle erişim bu şekilde kontrol edilmez, bu nedenle:

class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}

çıktı üretir:

0

çünkü değişken başlatıcısı i, değişkenin jönceki değerine erişmek için sınıf yöntemi peek'i kullandığından, değişken jbaşlatıcısı tarafından başlatıldığı ve bu noktada hala varsayılan değerine sahip olduğu ( §4.12.5 ).


1
@Andrew Evet, sınıf değişkeni, teşekkürler. Evet, bu erişimi kısıtlayan bazı ekstra kurallar olmasaydı işe yarardı : §8.3.3 . Sınıf değişkenleri (ilk giriş) için belirtilen dört noktaya bakın . OP örneğinde yöntem yaklaşımı bu kurallara uymaz, bu nedenle Xyöntemden erişebiliriz . O kadar önemsemem. Sadece JLS'nin işleri ayrıntılı olarak nasıl tanımladığına bağlıdır. Asla böyle kod kullanmazdım, sadece JLS'deki bazı kuralları kullanıyor.
Zabuzard

4
Sorun, yapıcıdan örnek yöntemlerini çağırabilmenizdir, muhtemelen izin verilmemesi gerekir. Öte yandan, süper çağırmadan önce, yararlı ve güvenli olacak yerlilerin atanmasına izin verilmez. Git şekil.
Monica

1
@Andrew muhtemelen burada gerçekten bahsettiğin tek kişi sensin (JLS'nin bir forwards referencesparçası olan). Bu tuvalet yanıtı olmadan çok basit stackoverflow.com/a/49371279/1059372
Eugene

1
"İlk atama referansı değiştirir." Bu durumda, bir referans türü değil, ilkel bir türdür.
fabian

1
Bu cevap biraz uzunsa doğrudur. :-) Bence tl; dr OP JLS değil, "[bir son] alan değişemez" diyen bir öğretici gösterdi olduğunu. Oracle'ın öğreticileri oldukça iyi olsa da, tüm uç durumları kapsamıyorlar. OP'nin sorusu için, gerçek, JLS final tanımına gitmemiz gerekir - ve bu tanım, bir son alanın değerinin asla değişemeyeceği iddiasında bulunmaz (OP'nin haklı olarak meydan okuduğu).
yshavit

22

Burada final ile ilgisi yok.

Örneğin veya sınıf düzeyinde olduğundan, henüz bir şey atanmazsa varsayılan değeri tutar. 0Atanmadan eriştiğinizde görmenizin nedeni budur .

XTamamen atamadan erişirseniz , varsayılan değer olan uzun değerlerini 0, dolayısıyla sonuçları tutar.


3
Bu konuda zor olan şey, değeri atamazsanız, varsayılan değerle atanmayacaktır, ancak kendisini "son" değeri atamak için kullandıysanız, ...
AxelH

2
@AxelH Bununla ne demek istediğini anlıyorum. Ama işte böyle işlemeli aksi halde dünya çöküyor;).
Suresh Atta

20

Hata değil.

İlk çağrı scaleçağrıldığında

private static final long X = scale(10);

Değerlendirmeye çalışır return X * value. Xhenüz bir değer atanmamıştır ve bu nedenle a için varsayılan değer longkullanılır (yani 0).

Yani bu kod satırı, X * 10yani 0 * 10olanı olarak değerlendirir 0.


8
OP'nin kafa karıştırıcı olduğunu sanmıyorum. Şaşkın olan şey X = scale(10) + 3. Çünkü X, yöntemden bahsedildiğinde 0,. Ama daha sonra öyle 3. Bu yüzden OP X, çatışacak iki farklı değer atandığını düşünüyor final.
Zabuzard

4
@Zabuza bu "ile izah edilmez Bu değerlendirmeye çalışır return X * value. XBir varsayılan değer alır bu nedenle bir değer atanmıştır henüz ve longhangi 0. "? XVarsayılan değerle atandığı söylenmez , ancak varsayılan değere göre X"değiştirilir" (lütfen bu terimi alıntılamayın;)).
AxelH

14

Bu hiç bir hata değil, sadece yasadışı bir ileri referans biçimi değil , başka bir şey değil.

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

Sadece şartname tarafından izin verilir.

Örneğinizi almak için, tam olarak bunun eşleştiği yer:

private static final long X = scale(10) + 3;

Bir yapıyoruz ileriye referans için scaleönce söylediğim gibi herhangi bir şekilde yasadışı değil, ancak varsayılan değerini almasını sağlar X. Yine, buna Spec tarafından izin verilir (daha kesin olmak gerekirse, yasak değildir), bu yüzden iyi çalışır


iyi cevap! Spesifikasyonun neden ikinci vakanın derlenmesine izin verdiğini merak ediyorum . Son alanın "tutarsız" durumunu görmenin tek yolu bu mu?
Andrew Tobilko

@Andrew bu da beni çok fazla rahatsız etti, düşünmeye meyilli C ++ veya C bunu (bu doğru olup olmadığını bilmiyorum)
Eugene

@Andrew: Çünkü aksini yapmak Turing eksiklik teoremini çözmek olacaktır.
Joshua

9
@Joshua: Sanırım burada bir dizi farklı kavramı karıştırıyorsunuz: (1) durma problemi, (2) karar problemi, (3) Godel'in eksiklik teoremi ve (4) Turing-tamam programlama dilleri. Derleyici yazarları sorunu çözmeye çalışmazlar "bu değişken kullanılmadan önce kesinlikle atanır mı?" mükemmel çünkü bu problem Durdurma Problemini çözmektir, ve bilemeyiz.
Eric Lippert

4
@EricLippert: Hata! Eksikliği ve durma problemini aklımda aynı yer kaplar.
Joshua

4

Sınıf düzeyindeki üyeler, sınıf tanımındaki kodda başlatılabilir. Derlenen bayt kodu, sınıf üyelerini satır içinde başlatamaz. (Eşgörünüm üyeleri de benzer şekilde ele alınır, ancak bu sağlanan soru ile ilgili değildir.)

Kişi aşağıdaki gibi bir şey yazdığında:

public class Demo1 {
    private static final long DemoLong1 = 1000;
}

Oluşturulan bayt kodu aşağıdakine benzer:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

Başlatma kodu, sınıf yükleyicisi sınıfı ilk yüklediğinde çalıştırılan statik bir başlatıcıya yerleştirilir. Bu bilgiyle orijinal örneğiniz aşağıdakine benzer:

public class RecursiveStatic {
    private static final long X;

    private static long scale(long value) {
        return X * value;
    }

    static {
        X = scale(10);
    }

    public static void main(String[] args) {
        System.out.println(scale(5));
    }
}
  1. JVM, RecursiveStatic'i kavanozun giriş noktası olarak yükler.
  2. Sınıf tanımı yüklendiğinde sınıf yükleyici statik başlatıcıyı çalıştırır.
  3. Başlatıcı scale(10), static finalalanı atamak için işlevi çağırır X.
  4. scale(long)Fonksiyonu çalışır sınıf, kısmen başlatılmamış değeri okuma başlatıldı ise Xuzun ya da 0 varsayılan olan.
  5. Değeri 0 * 10atanmıştırX ve sınıf yükleyici tamamlandı.
  6. JVM, scale(5)5'i X0'ın 0 olarak başlatılmış olan başlangıç değeriyle çarparak genel statik void ana yöntem çağrısını çalıştırır .

Statik son alan Xyalnızca bir kez atanır ve finalanahtar kelimenin garantisini korur . Ödeve 3 eklemenin sonraki sorgusu için, yukarıdaki 5. adım 0 * 10 + 3, değeri olan değer olur 3ve ana yöntem, sonucu 3 * 5olan değeri yazdırır 15.


3

Bir nesnenin başlatılmamış bir alanının okunması derleme hatasına neden olmalıdır. Ne yazık ki Java için değil.

Bu durumun temel sebebinin, standartların ayrıntılarını bilmememe rağmen, nesnelerin nasıl başlatıldığı ve inşa edildiğinin tanımı içinde "gizli" olduğunu düşünüyorum.

Bir anlamda, final kötü tanımlanmıştır çünkü belirtilen amacının bu sorundan dolayı ne olduğunu bile gerçekleştiremez. Ancak, tüm sınıflarınız düzgün yazılmışsa, bu sorunla karşılaşmazsınız. Yani tüm alanlar her zaman tüm kurucularda ayarlanır ve kurucularından biri çağrılmadan hiçbir nesne oluşturulmaz. Bir serileştirme kitaplığı kullanmak zorunda kalana kadar bu doğal görünüyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.