Skutecznie finał vs finał - inne zachowanie

104

Do tej pory uważałem, że ostatecznie ostateczne i ostateczne są mniej więcej równoważne i że JLS potraktuje je podobnie, jeśli nie identyczne 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, takie jak

ale nie wdają się w takie szczegóły. W końcu na szerszym poziomie wydają się być w dużym stopniu równoważne. Ale kopiąc głębiej, najwyraźniej się różnią.

Co powoduje to zachowanie, czy ktoś może podać definicje JLS, które to wyjaśniają?


Edycja: znalazłem inny powiązany 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ńcucha również zachowuje się tutaj inaczej (nie chcę używać tego fragmentu kodu w prawdziwym kodzie, jestem tylko ciekawy innego zachowania).

Zabuzard
źródło
2
Bardzo ciekawe pytanie! Spodziewałbym się, że Java będzie zachowywać się tak samo w obu przypadkach, ale jestem teraz oświecony. Zadaję sobie pytanie, czy zawsze takie było zachowanie, czy też różni się w poprzednich wersjach
Lino
8
@Lino treść ostatni cytat w wielkim poniżej odpowiedź jest taka sama przez całą drogę z powrotem do Java 6 : „Jeśli jeden z operandów jest typu T , gdzie T jest byte, shortlub char, a drugi argument jest stałą ekspresję typ, intktórego wartość można przedstawić w typie T , typ wyrażenia warunkowego to T. " --- Nawet znalazłem dokumentację Java 1.0 w Berkeley. Ten sam tekst . --- Tak, zawsze tak było.
Andreas
1
Interesujący jest sposób, w jaki "znajdujesz" rzeczy: P Nie ma za co :)
phant0m

Odpowiedzi:

66

Przede wszystkim mówimy tylko o zmiennych lokalnych . Skutecznie ostateczna nie dotyczy pól. Jest to ważne, ponieważ semantyka finalpól jest bardzo różna i podlega ciężkim optymalizacjom kompilatora i obietnicom modelu pamięci, patrz 17.5.1 na temat semantyki pól końcowych.

Na poziomie powierzchniowym finali effectively finaldla zmiennych lokalnych są rzeczywiście identyczne. Jednak JLS dokonuje wyraźnego rozróżnienia między nimi, co w rzeczywistości ma szeroki zakres efektów w specjalnych sytuacjach, takich jak ta.


Przesłanka

Od JLS§4.12.4 o finalzmiennych:

Zmiennej stałej jest finalzmienne od pierwotnego typu lub typu ciąg , który jest inicjowany przy stałej ekspresji ( §15.29 ). To, czy zmienna jest zmienną stałą, czy nie, może mieć wpływ na inicjalizację klasy ( §12.4.1 ), zgodność binarną ( §13.1 ), osiągalność ( §14.22 ) i określone przypisanie ( §16.1.1 ).

Ponieważ intjest prymitywna, zmienna ajest taką stałą zmienną .

Dalej, z tego samego rozdziału o effectively final:

Niektóre zmienne, które nie zostały uznane za ostateczne, są zamiast tego uważane za faktycznie ostateczne:

Ze sposobu sformułowania tego wynika jasno, że w drugim przykładzie a nie jest uważana za zmienną stałą, ponieważ nie jest ostateczna , a jedynie faktycznie ostateczna.


Zachowanie

Teraz, gdy mamy rozróżnienie, spójrzmy, co się dzieje i dlaczego wynik jest inny.

Używasz operatora warunkowego ? : tutaj , więc musimy sprawdzić jego definicję. Od JLS§15.25 :

Istnieją trzy rodzaje wyrażeń warunkowych, sklasyfikowanych zgodnie z drugim i trzecim wyrażeniem operandowym : logiczne wyrażenia warunkowe , numeryczne wyrażenia warunkowe i wyrażenia warunkowe dotyczące odwołań .

W tym przypadku mówimy o pliku liczbowych wyrażeniach warunkowych z JLS§15.25.2 :

Typ liczbowego wyrażenia warunkowego jest określany w następujący sposób:

I to jest ta część, w której te dwa przypadki są inaczej klasyfikowane.

skutecznie ostateczne

Wersja, do której effectively finalpasuje ta reguła:

W przeciwnym razie ogólna promocja liczbowa ( §5.6 ) jest stosowana do drugiego i trzeciego operandu, a typ wyrażenia warunkowego to promowany typ drugiego i trzeciego operandu.

Jest to takie samo zachowanie, jak gdybyś to zrobił 5 + 'd', tj. W int + charwyniku czego int. Zobacz JLS§5.6

Promocja numeryczna określa promowany typ wszystkich wyrażeń w kontekście liczbowym. Promowany typ jest wybierany w taki sposób, że każde wyrażenie można przekonwertować na promowany typ, aw 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ępnie do niektórych wyrażeń stosuje się rozszerzającą konwersję pierwotną ( pkt 5.1.2 ) i zwężającą konwersję pierwotną (pkt 5.1.3 ), zgodnie z następującymi regułami:

W kontekście wyboru liczbowego obowiązują następujące reguły:

Jeśli jakiekolwiek wyrażenie jest typu inti nie jest wyrażeniem stałym ( §15.29 ), to promowany typ jest int, a inne wyrażenia, które nie są typu, intsą poddawane rozszerzającej konwersji pierwotnej do int.

Więc wszystko jest promowane inttak, jak ajest intjuż. To wyjaśnia wydajność 97.

finał

Wersja ze finalzmienną jest dopasowana do tej reguły:

Jeśli jeden z operandów jest typu, Tgdzie Tjest byte, shortlub char, a drugi operand jest wyrażeniem stałym ( §15.29 ) typu, intktórego wartość można przedstawić w typie T, to typ wyrażenia warunkowego to T.

Ostatnia zmienna ajest typu inti wyrażeniem stałym (ponieważ jest final). Można to przedstawić jako char, stąd wynik jest typowy char. Na tym kończy się wynik a.


Przykład ciągu

Przykład z równością ciągów opiera się na tej samej podstawowej różnicy, finalzmienne są traktowane jako stałe wyrażenie / zmienna, a effectively finaltak nie jest.

Dlatego w Javie internowanie ciągów opiera się na stałych wyrażeniach

"a" + "b" + "c" == "abc"

jest truerównież (nie używaj tej konstrukcji w prawdziwym kodzie).

Zobacz JLS §3.10.5 :

Ponadto literał łańcuchowy zawsze odnosi się do tego samego wystąpienia klasy String. Dzieje się tak, ponieważ literały łańcuchowe - lub, bardziej ogólnie , łańcuchy, które są wartościami wyrażeń stałych ( §15.29 ) - są „internowane” w celu współużytkowania unikalnych instancji przy użyciu metody String.intern( §12.5 ).

Łatwo przeoczyć, ponieważ mówi głównie o literałach, ale w rzeczywistości dotyczy to również stałych wyrażeń.

Zabuzard
źródło
8
Problem polega na tym, że można by się spodziewać ... ? a : 'c'takiego samego zachowania, niezależnie od tego, czy ajest to zmienna, czy stała . Nie ma nic złego w tym wyrażeniu. --- W przeciwieństwie do tego a + "b" == "ab"jest złym wyrażeniem , ponieważ ciągi muszą być porównywane za pomocą equals()( Jak porównać ciągi w Javie? ). Fakt, że działa on „przypadkowo”, gdy ajest stałą , jest tylko dziwactwem internalizacji literałów łańcuchowych.
Andreas
5
@Andreas Tak, ale pamiętaj, że internowanie ciągów znaków jest jasno zdefiniowaną funkcją języka Java. To nie przypadek, który może się zmienić jutro lub w innym JVM. "a" + "b" + "c" == "abc"musi znajdować się truew dowolnej prawidłowej implementacji języka Java.
Zabuzard
10
To prawda, jest to dobrze zdefiniowane dziwactwo, ale a + "b" == "ab"nadal jest to błędne wyrażenie . Nawet jeśli ty wiesz, że ajest to stała , jest zbyt podatna na błędy, aby nie wywoływać equals(). A może lepszym słowem jest kruchy , tj. Zbyt prawdopodobne, że rozpadnie się, gdy kod będzie utrzymywany w przyszłości.
Andreas
2
Zwróć uwagę, że nawet w podstawowej domenie efektywnie końcowych zmiennych, tj. Ich użycie w wyrażeniach lambda, różnica może zmienić zachowanie w czasie wykonywania, tj. Może spowodować różnicę między przechwytującym i nieprzechwytywanym wyrażeniem lambda, przy czym to ostatnie oblicza się na singleton , ale ta pierwsza produkuje nowy obiekt. Innymi słowy, (final) String str = "a"; Stream.of(null, null). <Runnable>map( x -> () -> System.out.println(str)) .reduce((a,b) -> () -> System.out.println(a == b)) .ifPresent(Runnable::run);zmienia swój wynik, gdy strjest (nie) final.
Holger
7

Innym aspektem jest to, że jeśli zmienna jest zadeklarowana jako ostateczna w treści metody, zachowuje się inaczej niż końcowa zmienna przekazana jako parametr.

public void testFinalParameters(final String a, final String b) {
  System.out.println(a + b == "ab");
}

...
testFinalParameters("a", "b"); // Prints false

podczas

public void testFinalVariable() {
   final String a = "a";
   final String b = "b";
   System.out.println(a + b == "ab");  // Prints true
}

...
testFinalVariable();

zdarza się, ponieważ kompilator wie, że korzystanie final String a = "a"z azmienna będzie zawsze mają "a"wartość tak, że ai "a"mogą być wymieniane bez problemów. Z drugiej strony, jeśli anie jest zdefiniowany finallub jest zdefiniowany, finalale jego wartość jest przypisywana w czasie wykonywania (jak w powyższym przykładzie, gdzie aparametr jest final ), kompilator nic nie wie przed jego użyciem. Zatem konkatenacja odbywa się w czasie wykonywania i generowany jest nowy ciąg, bez użycia puli stażystów.


Zasadniczo zachowanie jest takie: jeśli kompilator wie, że zmienna jest stałą, może użyć jej tak samo, jak użycie stałej.

Jeśli zmienna nie jest zdefiniowana jako ostateczna (lub jest ostateczna, ale jej wartość jest definiowana w czasie wykonywania), nie ma powodu, aby kompilator traktował ją jako stałą, również jeśli jej wartość jest równa stałej, a jej wartość nigdy nie jest zmieniana.

Davide Lorenzo MARINO
źródło
4
Nie ma w tym nic dziwnego :)
2
2
To kolejny aspekt pytania.
Davide Lorenzo MARINO
5
finelsłowo kluczowe zastosowane do parametru, ma inną semantykę niż finalzastosowane do zmiennej lokalnej itp ...
dbl
6
Używanie parametru jest tutaj niepotrzebne zaciemnianiem. Możesz po prostu zrobić final String a; a = "a";i uzyskać to samo zachowanie
yawkat