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

Ale nie wnikają w takie szczegóły. Wszakże na szerszym poziomie wydają się być prawie równoważne. Ale kopanie głębiej, najwyraźniej różnią się.

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).

Author: Holger, 2020-09-04

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 jest int, a inne wyrażenia, które nie należą do typu int przechodzą rozszerzając pierwotną konwersję do int.

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 gdzie T jest byte, short, lub char, a drugim operandem jest wyrażenie stałe (§15.29) typu int, którego wartość jest reprezentowalna w typie T, wtedy Typ warunkowego wyrażenie T.

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ń.

 65
Author: Zabuzard,
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.

 7
Author: Davide Lorenzo MARINO,
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