Niezdefiniowane zachowanie w Javie

14

Czytałem to pytanie w SO, które omawia niektóre typowe niezdefiniowane zachowania w C ++ i zastanawiałem się: czy Java ma również niezdefiniowane zachowanie?

Jeśli tak, to jakie są najczęstsze przyczyny nieokreślonego zachowania w Javie?

Jeśli nie, to jakie funkcje Javy uwalniają go od takich zachowań i dlaczego najnowsze wersje C i C ++ nie zostały zaimplementowane z tymi właściwościami?

Osiem
źródło
4
Java jest bardzo sztywno zdefiniowana. Sprawdź specyfikację języka Java.
4
@ user1249, „niezdefiniowane zachowanie” jest właściwie dość sztywno zdefiniowane.
Pacerier
Możliwe to samo na SO: stackoverflow.com/questions/376338/…
Ciro Santilli 事件 改造 中心 法轮功 六四 事件
O czym mówi Java, gdy naruszasz „Umowę”? Tak się dzieje, gdy przeciążasz .equals, aby być niezgodnym z .hashCode? docs.oracle.com/javase/7/docs/api/java/lang/… Czy to kolokwialnie niezdefiniowane, ale technicznie nie w taki sam sposób jak C ++?
Mooing Duck

Odpowiedzi:

18

W Javie zachowanie niepoprawnie zsynchronizowanego programu można uznać za niezdefiniowane.

JLS Java 7 używa słowa „niezdefiniowany” jeden raz, w 17.4.8. Wymogi wykonania i związku przyczynowego :

Używamy f|ddo oznaczenia funkcji podanej przez ograniczenie domeny fdo d. Dla wszystkich xw d, f|d(x) = f(x)i dla wszystkich xnie w d, f|d(x)jest niezdefiniowany ...

Dokumentacja interfejsu API Java określa niektóre przypadki, w których wyniki są niezdefiniowane - na przykład w (przestarzałym) Konstruktorze Data (int rok, int miesiąc, int dzień) :

Wynik jest niezdefiniowany, jeśli dany argument jest poza zakresem ...

Stan Javadocs dla ExecutorService.invokeAll (kolekcja) :

Wyniki tej metody są niezdefiniowane, jeśli dana kolekcja zostanie zmodyfikowana podczas wykonywania tej operacji ...

Mniej formalny rodzaj „niezdefiniowanego” zachowania można znaleźć na przykład w ConcurrentModificationException , gdzie dokumenty API używają terminu „najlepszy wysiłek”:

Należy pamiętać, że nie można zagwarantować szybkiego działania w przypadku awarii, ponieważ generalnie mówiąc, niemożliwe jest ustanowienie jakichkolwiek twardych gwarancji w przypadku niezsynchronizowanej jednoczesnej modyfikacji. Bezawaryjne operacje ConcurrentModificationExceptionzapewniają najlepsze starania . Dlatego błędem byłoby napisanie programu, który był uzależniony od tego wyjątku pod względem poprawności ...


dodatek

Jeden z komentarzy do pytania odnosi się do artykułu Erica Lipperta, który zawiera pomocne wprowadzenie do zagadnień tematycznych: zachowanie zdefiniowane w ramach implementacji .

Polecam ten artykuł do rozumowania niezależnego od języka, chociaż warto pamiętać, że autor celuje w C #, a nie w Javę.

Tradycyjnie mówimy, że idiom języka programowania ma niezdefiniowane zachowanie, jeśli użycie tego idiomu może mieć jakikolwiek wpływ; może działać tak, jak tego oczekujesz, może wymazać dysk twardy lub spowodować awarię komputera. Ponadto autor kompilatora nie jest zobowiązany do ostrzegania o niezdefiniowanym zachowaniu. (W rzeczywistości istnieją języki, w których programy używające idiomów „niezdefiniowane zachowanie” są dozwolone przez specyfikację języka, aby spowodować awarię kompilatora!) ...

Natomiast idiomem, który ma zachowanie zdefiniowane w implementacji, jest zachowanie, w którym autor kompilatora ma kilka możliwości zaimplementowania funkcji i musi wybrać jedną. Jak sama nazwa wskazuje, zachowanie zdefiniowane w implementacji jest co najmniej zdefiniowane. Na przykład C # pozwala implementacji na zgłoszenie wyjątku lub wygenerowanie wartości, gdy przepełni się dzielenie liczb całkowitych, ale implementacja musi wybrać jeden. Nie może usunąć twojego dysku twardego ...

Jakie są niektóre czynniki, które skłoniły komisję ds. Projektowania języków do pozostawienia określonych idiomów językowych jako zachowań niezdefiniowanych lub zdefiniowanych w implementacji?

Pierwszym ważnym czynnikiem jest: czy istnieją dwie implementacje języka na rynku, które nie zgadzają się z zachowaniem konkretnego programu? ...

Kolejnym ważnym czynnikiem jest: czy ta funkcja w naturalny sposób oferuje wiele różnych możliwości wdrożenia, z których niektóre są wyraźnie lepsze od innych? ...

Trzecim czynnikiem jest: czy cecha jest tak złożona, że ​​jej szczegółowe rozbicie byłoby trudne lub kosztowne? ...

Czwarty czynnik to: czy funkcja nakłada duże obciążenie na analizator kompilatora? ...

Piąty czynnik to: czy ta funkcja powoduje duże obciążenie środowiska wykonawczego? ...

Szósty czynnik to: czy zdefiniowanie określonego zachowania wyklucza jakąś poważną optymalizację? ...

To tylko kilka czynników, które przychodzą na myśl; jest oczywiście wiele, wiele innych czynników, które komitety projektowania języków dyskutują przed wprowadzeniem funkcji „zdefiniowano wdrożenie” lub „nie zdefiniowano”.

Powyżej jest tylko bardzo krótki opis; pełny artykuł zawiera wyjaśnienia i przykłady punktów wymienionych w tym fragmencie; jest wiele warte czytania. Na przykład szczegóły podane dla „szóstego czynnika” mogą dać wgląd w motywację wielu stwierdzeń w Java Memory Model ( JSR 133 ), pomagając zrozumieć, dlaczego niektóre optymalizacje są dozwolone, prowadząc do nieokreślonego zachowania, podczas gdy inne są zabronione, co prowadzi do ograniczenia, takie jak wcześniejsze zdarzenia i wymagania dotyczące przyczynowości .

Żaden z artykułów nie jest dla mnie szczególnie nowy, ale niech mnie szlag, jeśli kiedykolwiek zobaczyłem, że jest prezentowany w tak elegancki, spójny i zrozumiały sposób. Niesamowity.

komar
źródło
Dodam, że JMM! = Sprzęt bazowy i wynik końcowy programu wykonawczego w odniesieniu do współbieżności może różnić się od powiedzmy WinIntel vs Solaris
Martijn Verburg
2
@MartijnVerburg to całkiem dobry punkt. Jedynym powodem, dla którego waham się oznaczyć go jako „niezdefiniowany”, jest to, że model pamięci stwarza ograniczenia, takie jak
gnat,
To prawda, że ​​specyfikacja określa, jak powinna zachowywać się pod JMM, jednak Intel i wsp. Nie zawsze się z tym zgadzają ;-)
Martijn Verburg
@MartijnVerburg Myślę, że głównym celem JMM jest zapobieganie nadmiernej optymalizacji wycieków z „nie zgadzających się” producentów procesorów. O ile rozumiem Java przed 5.0 miała taki ból głowy z DEC Alpha, kiedy spekulacyjne zapisy wykonane pod maską mogły wyciec do programu jak „z powietrza” - stąd wymóg przyczynowości wszedł w JSR 133 (JMM)
gnat
9
@MartinVerburg - zadaniem implementatora JVM jest upewnienie się, że JVM zachowuje się zgodnie ze specyfikacją JLS / JMM na dowolnej obsługiwanej platformie sprzętowej. Jeśli inny sprzęt zachowuje się inaczej, zadaniem implementatora JVM jest zajęcie się nim ... i sprawienie, aby działał.
Stephen C
10

Z wierzchu głowy nie sądzę, że w Javie istnieje jakieś nieokreślone zachowanie, a przynajmniej nie w tym samym sensie, co w C ++.

Powodem tego jest to, że za Javą kryje się inna filozofia niż za C ++. Podstawowym celem projektowania Javy było umożliwienie uruchamiania programów bez zmian na różnych platformach, więc specyfikacja określa wszystko bardzo wyraźnie.

Natomiast głównym celem projektowania C i C ++ jest wydajność: nie powinno być żadnych funkcji (w tym niezależności od platformy), które kosztowałyby wydajność, nawet jeśli nie byłyby potrzebne. W tym celu specyfikacja celowo nie definiuje niektórych zachowań, ponieważ ich zdefiniowanie spowodowałoby dodatkową pracę na niektórych platformach, a tym samym zmniejszyłoby wydajność nawet dla osób, które piszą programy specjalnie dla jednej platformy i są świadome wszystkich jej osobliwości.

Istnieje nawet przykład, w którym Java była zmuszona z mocą wsteczną wprowadzić ograniczoną formę nieokreślonego zachowania z tego właśnie powodu: słowo kluczowe strictfp zostało wprowadzone w Javie 1.2, aby umożliwić obliczenia zmiennoprzecinkowe odbiegające dokładnie od standardu IEEE 754, jak wcześniej wymagała specyfikacja , ponieważ zrobienie tego wymagało dodatkowej pracy i spowolniło wszystkie obliczenia zmiennoprzecinkowe na niektórych popularnych procesorach, aw niektórych przypadkach powodując gorsze wyniki.

Michael Borgwardt
źródło
2
Myślę, że ważne jest zwrócenie uwagi na inny główny cel Javy: bezpieczeństwo i izolację. Myślę, że to także jest przyczyną braku „nieokreślonego” zachowania (jak w C ++).
K.Steff
3
@ K.Steff: Hyper-modern C / C ++ jest całkowicie nieodpowiedni do jakichkolwiek zdalnych kwestii związanych z bezpieczeństwem. Biorąc pod uwagę int x=-1; foo(); x<<=1;hipernowoczesną filozofię, sprzyjałoby przepisywanie foo, aby każda ścieżka, która nie wychodzi, musiała być nieosiągalna. To, jeśli foojest if (should_launch_missiles) { launch_missiles(); exit(1); }kompilatorem, może (i zdaniem niektórych osób powinno) uprościć to po prostu launch_missiles(); exit(1);. Tradycyjny UB polegał na losowym wykonywaniu kodu, ale kiedyś obowiązywały go prawa czasu i przyczynowości. Nowy ulepszony UB nie jest związany z żadnym.
supercat
3

Java bardzo mocno stara się wytępić niezdefiniowane zachowanie, właśnie z powodu lekcji wcześniejszych języków. Na przykład zmienne na poziomie klasy są inicjowane automatycznie; zmienne lokalne nie są automatycznie inicjowane ze względu na wydajność, ale istnieje zaawansowana analiza przepływu danych, aby uniemożliwić każdemu napisanie programu, który byłby w stanie to wykryć. Referencje nie są wskaźnikami, więc nieprawidłowe referencje nie mogą istnieć, a dereferencje nullpowodują określony wyjątek.

Oczywiście istnieją pewne zachowania, które nie są w pełni określone, i możesz pisać niewiarygodne programy, jeśli się zorientujesz. Na przykład, jeśli wykonujesz iterację normalną (nieposortowaną) Set, język gwarantuje, że zobaczysz każdy element dokładnie raz, ale nie w kolejności, w jakiej je zobaczysz. Kolejność może być taka sama przy kolejnych uruchomieniach lub może ulec zmianie; lub może pozostać taki sam, o ile nie wystąpią żadne inne alokacje lub dopóki nie zaktualizujesz JDK itp. Pozbycie się wszystkich takich efektów jest prawie niemożliwe ; na przykład musiałbyś jawnie zamówić lub randomizować wszystkie operacje Kolekcje, a to po prostu nie jest warte małej dodatkowej, niezdefiniowanej.

Kilian Foth
źródło
Odniesienia są wskaźnikami pod inną nazwą
ciekawy
@curiousguy - zakłada się, że „referencje” nie pozwalają na użycie arytmetycznej manipulacji ich wartością liczbową, co jest często dozwolone w przypadku „wskaźników”. Ten pierwszy jest zatem bezpieczniejszym konstruktem niż drugi; w połączeniu z systemem zarządzania pamięcią, który nie pozwala na ponowne użycie pamięci obiektu, gdy istnieje prawidłowe odwołanie do niego, odwołania zapobiegają błędom użycia pamięci. Wskaźniki nie mogą tego zrobić, nawet jeśli stosowane jest odpowiednie zarządzanie pamięcią.
Jules
@Jules To kwestia terminologii: możesz nazwać jedną rzecz wskaźnikiem lub referencją i zdecydować się na użycie „referencji” w „bezpiecznych” językach i „wskaźnika” w językach, które pozwalają na użycie arytmetyki wskaźników i ręcznego zarządzania pamięcią. (AFAIK „wskaźnik arytmetyki” jest wykonywany tylko w C / C ++.)
ciekawy
2

Musisz zrozumieć „Niezdefiniowane zachowanie” i jego pochodzenie.

Niezdefiniowane zachowanie oznacza zachowanie, które nie jest zdefiniowane przez standardy. C / C ++ ma zbyt wiele różnych implementacji kompilatora i dodatkowe funkcje. Te dodatkowe funkcje wiązały kod z kompilatorem. Stało się tak, ponieważ nie było scentralizowanego rozwoju języka. Tak więc niektóre zaawansowane funkcje niektórych kompilatorów stały się „niezdefiniowanymi zachowaniami”.

Podczas gdy w Javie specyfikacja języka jest kontrolowana przez Sun-Oracle i nikt inny nie próbuje tworzyć specyfikacji, a tym samym nie ma niezdefiniowanych zachowań.

Zredagowano specjalnie odpowiadając na pytanie

  1. Java jest wolna od niezdefiniowanych zachowań, ponieważ standardy zostały utworzone przed kompilatorami
  2. Współczesne kompilatory C / C ++ mają mniej lub bardziej znormalizowane implementacje, ale funkcje zaimplementowane przed standaryzacją wciąż pozostają oznaczone jako „niezdefiniowane zachowanie”, ponieważ ISO zachowało te aspekty.
Sarvex
źródło
2
Być może masz rację, że nie ma UB w Javie, ale nawet gdy jedna jednostka kontroluje wszystko, mogą istnieć powody, aby mieć UB, więc podany przez ciebie powód nie prowadzi do wniosku.
AProgrammer
2
Poza tym zarówno C, jak i C ++ są znormalizowane przez ISO. Chociaż może istnieć wiele kompilatorów, istnieje tylko jeden standard na raz.
MSalters
1
@SarvexJatasra, nie zgadzam się, że jest to jedyne źródło UB. Na przykład jeden UB dereferencjonuje zwisający wskaźnik i istnieją dobre powody, aby pozostawić UB w dowolnym języku, który nie ma GC, nawet jeśli teraz uruchomisz specyfikację. A te powody nie mają nic wspólnego z istniejącą praktyką lub istniejącymi kompilatorami.
AProgrammer
2
@ SarvexJatasra, podpisany przepełnienie to UB, ponieważ standard wyraźnie to mówi (jest to nawet przykład podany z definicją UB). Odsyłanie nieprawidłowego wskaźnika jest również UB z tego samego powodu, jak mówi standard.
AProgrammer
2
@ bames53: Żadna z wymienionych zalet nie wymagałaby poziomu kompilacji hipernowoczesnych szerokości geograficznych przy UB. Z wyjątkiem dostępu poza pamięcią i przepełnienia stosu, które mogą „naturalnie” wywoływać losowe wykonywanie kodu, nie mogę wymyślić żadnej użytecznej optymalizacji, która wymagałaby szerszej swobody niż stwierdzenie, że większość operacji UB daje nieokreślone wartości (które mogą zachowywać się tak, jakby miały „dodatkowe bity”) i mogą mieć konsekwencje poza tym tylko wtedy, gdy dokumenty implementacyjne wyraźnie zastrzegają sobie prawo do narzucenia takich wartości; doktorzy mogą nadać „Nieograniczone zachowanie” ...
supercat
1

Java zasadniczo eliminuje wszystkie niezdefiniowane zachowania występujące w C / C ++. (Na przykład: przepełnienie liczby całkowitej ze znakiem, dzielenie przez zero, niezainicjowane zmienne, zerowe wskazanie wskaźnika, przesunięcie o więcej niż szerokość bitów, podwójne zwolnienie, nawet „brak nowej linii na końcu kodu źródłowego”.) Ale Java ma kilka niejasnych, niezdefiniowanych zachowań, które są rzadko spotykane przez programistów.

  • Java Native Interface (JNI), sposób, w jaki Java może wywoływać kod C lub C ++. Istnieje wiele sposobów na zepsucie JNI, takich jak błędne podpisanie funkcji, wykonywanie nieprawidłowych wywołań do usług JVM, niszczenie pamięci, niepoprawne przydzielanie / zwalnianie rzeczy i wiele innych. Popełniłem już te błędy i ogólnie cała JVM ulega awarii, gdy dowolny wątek wykonujący kod JNI popełnia błąd.

  • Thread.stop(), co jest przestarzałe. Zacytować:

    Dlaczego jest Thread.stopprzestarzałe?

    Ponieważ jest to z natury niebezpieczne. Zatrzymanie wątku powoduje odblokowanie wszystkich zablokowanych monitorów. (Monitory są odblokowane, gdy ThreadDeathwyjątek rozprzestrzenia się na stosie.) Jeśli którykolwiek z obiektów chronionych wcześniej przez te monitory był w niespójnym stanie, inne wątki mogą teraz wyświetlać te obiekty w niespójnym stanie. Takie przedmioty są uważane za uszkodzone. Gdy wątki działają na uszkodzone obiekty, może dojść do dowolnego zachowania. To zachowanie może być subtelne i trudne do wykrycia lub może być wyraźne. W przeciwieństwie do innych niesprawdzonych wyjątków, ThreadDeathcicho zabija wątki; dlatego użytkownik nie ma ostrzeżenia, że ​​jego program może być uszkodzony. Zepsucie może przejawiać się w dowolnym momencie po faktycznym uszkodzeniu, nawet godziny lub dni w przyszłości.

    https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

Nayuki
źródło