Skutecznie finał vs finał-różne zachowania
Do tej pory myślałem, żeefektywnie final ifinal są mniej lub bardziej równoważne i że JLS traktowałby je podobnie, jeśli nie identycznie w rzeczywistym zachowaniu. Potem znalazłem ten wymyślony scenariusz:
final int a = 97;
System.out.println(true ? a : 'c'); // outputs a
// versus
int a = 97;
System.out.println(true ? a : 'c'); // outputs 97
Najwyraźniej JLS robi ważną różnicę między tymi dwoma tutaj i nie jestem pewien, dlaczego.
Czytam inne wątki jak
- różnica między finałem a finałem
- skutecznie zmienna końcowa vs zmienna końcowa
- co oznacza, że zmienna jest "efektywnie ostateczna"?
Co jest przyczyną takiego zachowania, czy ktoś może podać jakieś definicje JLS, które to wyjaśniają?
Edit: znalazłem inny podobny scenariusz:
final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true
// versus
String a = "a";
System.out.println(a + "b" == "ab"); // outputs false
Więc internowanie łańcuchów również zachowuje się tutaj inaczej (nie chcę używać tego fragmentu w prawdziwym kodzie, po prostu jestem ciekaw innego zachowania).
2 answers
Przede wszystkim mówimy tylko o zmiennych lokalnych . W rzeczywistości final nie ma zastosowania do pól. Jest to ważne, ponieważ semantyka dla pól final
jest bardzo zróżnicowana i podlega ciężkim optymalizacjom kompilatora i obietnicom modelu pamięci, zobacz $17.5.1 na temat semantyki pól końcowych.
Na poziomie powierzchniowym final
i effectively final
dla zmiennych lokalnych są rzeczywiście identyczne. JLS dokonuje jednak wyraźnego rozróżnienia między tymi dwoma, które faktycznie ma szeroki zakres efektów w szczególnych sytuacjach takich jak ta.
Założenie
Z JLS§4.12.4 o final
zmiennych:
A stała zmienna jest
final
zmienną typu prymitywnego lub typu String inicjalizowaną wyrażeniem stałej (§15.29). To, czy zmienna jest zmienną stałą, czy nie może mieć implikacje w odniesieniu do inicjalizacji klasy (§12.4.1), kompatybilność binarna(§13.1), osiągalność (§14.22), i definitywne przypisanie (§16.1.1).
Ponieważ int
jest prymitywna, zmienna a
jest taką stałą zmienną .
Dalej, z tego samego rozdziału o effectively final
:
Niektóre zmienne, które nie są zadeklarowane jako ostateczne, są zamiast tego uważane za ostateczne:...
Więc z tego, jak to jest sformułowane, jest jasne, że w inny przykład, a
jest Nie uważany za zmienną stałą, ponieważ jest Nie ostateczną , ale tylko W rzeczywistości ostateczną.
Zachowanie
Teraz, gdy mamy rozróżnienie, sprawdźmy, co się dzieje i dlaczego wyjście jest inne.
Używasz tutaj operatora warunkowego ? :
, więc musimy sprawdzić jego definicję. Z JLS§15.25:
Istnieją trzy rodzaje wyrażeń warunkowych, klasyfikowane według do drugiego i trzeciego wyrażenia operandowego: boolowskie wyrażenia warunkowe, numeryczne wyrażenia warunkowe i odwołują się do wyrażeń warunkowych .
W tym przypadku mówimy o numerycznych wyrażeniach warunkowych , Z JLS§15.25.2:
Typ liczbowego wyrażenia warunkowego określa się następująco:
I to jest część, w której te dwie sprawy zostają sklasyfikowane inaczej.
Finalnie
Wersja effectively final
jest zgodna z tą regułą:
Inaczej, ogólne promocja numeryczna (§5.6) stosuje się do drugiego i trzeciego operandu, a typ wyrażenia warunkowego jest promowanym typem drugiego i trzeciego operandu.
Co jest takim samym zachowaniem, jak w przypadku 5 + 'd'
, tj. int + char
, co skutkuje int
. Zobacz też JLS§5.6
Promocja liczbowa określa typ promowany wszystkich wyrażeń w kontekście liczbowym. Typ promowany jest wybrany w taki sposób, że każde wyrażenie może być przekonwertowane na typ promowany, a w przypadku operacji arytmetycznej operacja jest zdefiniowana dla wartości typu promowanego. Kolejność wyrażeń w kontekście liczbowym nie ma znaczenia dla promocji liczbowej. Zasady są następujące:
[...]
Następna, prymitywne nawrócenie (§5.1.2) i zawężanie prymitywnej konwersji (§5.1.3) są stosowane do niektórych wyrażeń, zgodnie z następującymi zasadami:
W kontekście wyboru liczbowego obowiązują następujące zasady:
Jeśli dowolne wyrażenie jest typu
int
i jest nie jest wyrażeniem stałym (§15.29), następnie promowanym typem jestint
, a inne wyrażenia, które nie należą do typuint
przechodzą rozszerzając pierwotną konwersję doint
.
Więc wszystko jest promowane do int
jako a
jest już int
. To wyjaśnia wyjście 97
.
Finał
Wersja ze zmienną final
jest zgodna z tą regułą:
Jeśli jeden z operandów jest typu
T
gdzieT
jestbyte
,short
, lubchar
, a drugim operandem jest wyrażenie stałe (§15.29) typuint
, którego wartość jest reprezentowalna w typieT
, wtedy Typ warunkowego wyrażenieT
.
Końcowa zmienna a
jest typu int
i stała wyrażenia (ponieważ jest final
). Jest reprezentowalny jako char
, stąd wynik jest typu char
. Na tym kończy się wyjście a
.
Przykład łańcucha
Przykład z równością ciągu znaków opiera się na tej samej podstawowej różnicy, final
zmienne są traktowane jako stałe wyrażenie/zmienna, a effectively final
nie jest.
W Javie, string interning opiera się na wyrażenia stałe, stąd
"a" + "b" + "c" == "abc"
Jest true
również (nie używaj tego konstruktu w prawdziwym kodzie).
Zobacz JLS§3.10.5:
Co więcej, literał Łańcuchowy zawsze odnosi się do tej samej instancji klasy String. Dzieje się tak dlatego, że literały łańcuchowe - lub, bardziej ogólnie , ciągi, które są wartościami stałych wyrażeń (§15.29) - są "internowani" tak, aby dzielić unikalne instancje, używając metody
String.intern
(§12.5).
Łatwo przeoczyć, ponieważ mówi się głównie o literałach, ale w rzeczywistości odnosi się również do stałych wyrażeń.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-04 10:35:19
Innym aspektem jest to, że jeśli zmienna jest zadeklarowana jako ostateczna w ciele metody, to ma inne zachowanie niż końcowa zmienna przekazywana jako parametr.
public void testFinalParameters(final String a, final String b) {
System.out.println(a + b == "ab");
}
...
testFinalParameters("a", "b"); // Prints false
While
public void testFinalVariable() {
final String a = "a";
final String b = "b";
System.out.println(a + b == "ab"); // Prints true
}
...
testFinalVariable();
Dzieje się tak, ponieważ kompilator wie, że używając final String a = "a"
zmienna a
zawsze będzie miała wartość "a"
, dzięki czemu a
i "a"
mogą być wymieniane bez problemów.
Inaczej, jeśli a
nie jest zdefiniowana final
lub jest zdefiniowana final
, ale jego wartość jest przypisana w trybie runtime (jak w przykładzie powyżej gdzie finalny jest parametr a
) kompilator nie wie nic przed jego użyciem. Tak więc konkatenacja odbywa się w czasie wykonywania i generowany jest nowy ciąg znaków, nie używając puli intern.
Zasadniczo zachowanie jest takie: jeśli kompilator wie, że zmienna jest stałą, może używać jej tak samo, jak przy użyciu stałej.
Jeśli zmienna nie jest definiowana jako ostateczna (lub jest ostateczna, ale jej wartość jest definiowana w trybie runtime), nie ma powodu, aby kompilator obsługiwał ją jako stałą również wtedy, gdy jego wartość jest równa stałej i jej wartość nigdy się nie zmienia.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-04 11:34:07