Po pierwsze, układanka: co drukuje następujący kod?
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;
}
}
Odpowiedź:
0
Spojlery poniżej.
Jeśli drukujesz X
w skali (długiej) i redefiniujesz X = scale(10) + 3
, wydruki będą X = 0
wtedy X = 3
. Oznacza to, że X
jest tymczasowo ustawiony na, 0
a później ustawiony na 3
. To jest naruszenie final
!
Modyfikator statyczny w połączeniu z modyfikatorem końcowym służy również do definiowania stałych. Ostatni modyfikator wskazuje, że wartość tego pola nie może się zmienić .
Źródło: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [podkreślenie dodane]
Moje pytanie: czy to błąd? Jest final
źle zdefiniowany?
Oto kod, który mnie interesuje.
X
Przypisano mu dwie różne wartości: 0
i 3
. Uważam, że jest to naruszenie 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;
}
}
To pytanie zostało oznaczone jako możliwy duplikat statycznej kolejności inicjalizacji pola końcowego Java . Uważam, że to pytanie nie jest duplikatem, ponieważ drugie pytanie dotyczy kolejności inicjalizacji, podczas gdy moje pytanie dotyczy cyklicznej inicjalizacji połączonej ze final
znacznikiem. Na podstawie samego drugiego pytania nie byłbym w stanie zrozumieć, dlaczego kod w moim pytaniu nie powoduje błędu.
Jest to szczególnie wyraźne, patrząc na wyniki, które otrzymuje Ernest: gdy a
jest oznaczony final
, otrzymuje następujące dane wyjściowe:
a=5
a=5
co nie obejmuje głównej części mojego pytania: w jaki sposób final
zmienna zmienia swoją zmienną?
źródło
X
członka jest jak odwoływanie się do członka podklasy przed zakończeniem konstruktora superklasy, to twój problem, a nie definicjafinal
.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.
Odpowiedzi:
Bardzo interesujące znalezisko. Aby to zrozumieć, musimy zagłębić się w specyfikację języka Java ( JLS ).
Powodem jest to, że
final
zezwala tylko na jedno zadanie . Jednak wartością domyślną nie jest przypisanie . W rzeczywistości każda taka zmienna ( zmienna klasy, zmienna instancji, komponent tablicowy) wskazuje na wartość domyślną od początku, przed przypisaniem . Pierwsze przypisanie następnie zmienia odniesienie.Zmienne klasy i wartość domyślna
Spójrz na następujący przykład:
Nie przypisaliśmy jawnie wartości
x
, choć wskazuje nanull
to, że jest to wartość domyślna. Porównaj to z §4.12.5 :Zauważ, że dotyczy to tylko takich zmiennych, jak w naszym przykładzie. Nie dotyczy zmiennych lokalnych, zobacz następujący przykład:
Z tego samego akapitu JLS:
Ostateczne zmienne
Teraz spójrzmy na
final
, z §4.12.4 :Wyjaśnienie
Wracając do twojego przykładu, nieco zmodyfikowany:
Wyprowadza
Przypomnij sobie, czego się nauczyliśmy. Wewnątrz metody
assign
zmiennaX
była nie przypisuje wartości do wyświetlenia. Dlatego wskazuje na wartość domyślną, ponieważ jest to zmienna klasowa i zgodnie z JLS zmienne te zawsze natychmiast wskazują na ich wartości domyślne (w przeciwieństwie do zmiennych lokalnych). Poassign
metodzie zmiennejX
przypisuje się wartość1
i z tego powodufinal
nie możemy jej już zmieniać. Dlatego następujące działania nie będą działać z powodufinal
:Przykład w JLS
Dzięki @Andrew znalazłem akapit JLS, który obejmuje dokładnie ten scenariusz, a także pokazuje to.
Ale najpierw spójrzmy
Dlaczego nie jest to dozwolone, podczas gdy dostęp z tej metody jest? Spójrz na § 8.3.3, który mówi o ograniczeniu dostępu do pól, jeśli pole nie zostało jeszcze zainicjowane.
Wymienia niektóre reguły istotne dla zmiennych klas:
To proste,
X = X + 1
reguły podlegają regułom, dostęp do metody nie. Podają nawet ten scenariusz i podają przykład:źródło
X
z tej metody. Nie miałbym nic przeciwko temu. To zależy tylko od tego, jak dokładnie JLS definiuje rzeczy do działania w szczegółach. Nigdy nie używałbym takiego kodu, po prostu wykorzystuje pewne reguły w JLS.forwards references
(która również jest częścią JLS). to jest takie proste bez tej długiej odpowiedzi stackoverflow.com/a/49371279/1059372Nie ma tu nic wspólnego z finałem.
Ponieważ jest na poziomie instancji lub klasy, zachowuje wartość domyślną, jeśli nic jeszcze nie zostanie przypisane. To jest powód, dla którego widzisz
0
dostęp do niego bez przypisywania.Jeśli uzyskujesz dostęp
X
bez całkowitego przypisania, zachowuje domyślne wartości long, które są0
, stąd wyniki.źródło
To nie błąd.
Kiedy pierwsze połączenie z
scale
jest wywoływane zPróbuje to ocenić
return X * value
.X
nie została jeszcze przypisana wartość, dlategolong
używana jest domyślna wartość a (która jest0
).Tak więc ten wiersz kodu ocenia,
X * 10
tzn.0 * 10
Który jest0
.źródło
X = scale(10) + 3
. PonieważX
przywoływany z metody jest0
. Ale potem tak jest3
. Więc OP uważa, żeX
przypisano dwie różne wartości, z którymi byłoby w konflikciefinal
.return X * value
.X
Nie przypisano jeszcze wartości i dlatego przyjmuje wartość domyślną dla wartości,long
która jest0
. ”? Nie jest powiedziane, żeX
ma przypisaną wartość domyślną, ale żeX
jest ona „zastąpiona” (proszę nie cytować tego terminu;)) wartością domyślną.To wcale nie jest błąd, po prostu nie jest to nielegalna forma przekazywania referencji, nic więcej.
Jest to po prostu dozwolone przez specyfikację.
Weźmy twój przykład, to jest dokładnie to, co pasuje:
Robisz odniesienie do przodu,
scale
które nie jest nielegalne w żaden sposób, jak powiedziano wcześniej, ale pozwala uzyskać domyślną wartośćX
. znowu jest to dozwolone przez Spec (a ściślej mówiąc, nie jest zabronione), więc działa dobrzeźródło
Elementy na poziomie klasy mogą być inicjowane w kodzie w ramach definicji klasy. Skompilowany kod bajtowy nie może zainicjować elementów klasy bezpośrednio. (Członkowie instancji są traktowani podobnie, ale nie dotyczy to zadanego pytania).
Kiedy ktoś pisze coś takiego:
Wygenerowany kod bajtowy byłby podobny do następującego:
Kod inicjujący jest umieszczony w inicjatorze statycznym, który jest uruchamiany, gdy moduł ładujący klasy ładuje klasę po raz pierwszy. Przy tej wiedzy Twoja oryginalna próbka byłaby podobna do następującej:
scale(10)
do przypisaniastatic final
polaX
.scale(long)
biegnie funkcji, gdy klasa częściowo inicjowane odczytu wartości niezainicjowanego zX
którym jest domyślnym długi lub 0.0 * 10
jest przypisana doX
i moduł ładujący klasy się kończy.scale(5)
która zwielokrotnia 5 przez teraz zainicjowanąX
wartość 0, zwracając 0.Statyczne pole końcowe
X
jest przypisywane tylko raz, zachowując gwarancjęfinal
słowa kluczowego. W przypadku kolejnego zapytania dodania 3 w przypisaniu krok 5 powyżej staje się oceną,0 * 10 + 3
której wartością jest wartość,3
a główną metodą wydrukuje wynik,3 * 5
którego wartością jest wartość15
.źródło
Odczyt niezainicjowanego pola obiektu powinien spowodować błąd kompilacji. Niestety w przypadku Javy tak nie jest.
Myślę, że podstawowy powód, dla którego tak się dzieje, jest „ukryty” głęboko w definicji tworzenia instancji i konstruowania obiektów, chociaż nie znam szczegółów standardu.
W pewnym sensie finał jest źle zdefiniowany, ponieważ nawet nie osiąga zamierzonego celu z powodu tego problemu. Jeśli jednak wszystkie twoje zajęcia są poprawnie napisane, nie masz tego problemu. Oznacza to, że wszystkie pola są zawsze ustawione we wszystkich konstruktorach i żaden obiekt nigdy nie jest tworzony bez wywołania jednego z jego konstruktorów. Wydaje się to naturalne, dopóki nie będziesz musiał użyć biblioteki serializacji.
źródło