Dlaczego int i = 1024 * 1024 * 1024 * 1024 kompiluje się bez błędu?

152

Limit intwynosi od -2147483648 do 2147483647.

Jeśli wprowadzę

int i = 2147483648;

wtedy Eclipse wyświetli czerwone podkreślenie pod „2147483648”.

Ale jeśli to zrobię:

int i = 1024 * 1024 * 1024 * 1024;

będzie się dobrze skompilować.

public class Test {
    public static void main(String[] args) {        

        int i = 2147483648;                   // error
        int j = 1024 * 1024 * 1024 * 1024;    // no error

    }
}

Może to podstawowe pytanie w Javie, ale nie mam pojęcia, dlaczego drugi wariant nie powoduje błędów.

WUJ
źródło
10
Nawet jeśli kompilator normalnie „zwinąłby” obliczenia do pojedynczej wartości w ramach optymalizacji, nie zrobi tego, jeśli wynikiem będzie przepełnienie, ponieważ żadna optymalizacja nie powinna zmienić zachowania programu.
Hot Licks
1
I nie może zinterpretować 2147483648: to dosłowne nie ma sensu.
Denys Séguret,
1
Java nie zgłasza przepełnień całkowitych - operacja „kończy się niepowodzeniem” po cichu.
Hot Licks,
5
@JacobKrall: C # zgłosi to jako defekt, niezależnie od tego, czy sprawdzanie jest włączone; wszystkie obliczenia, które składają się tylko z wyrażeń stałych, są automatycznie sprawdzane, chyba że znajdują się wewnątrz niezaznaczonego regionu.
Eric Lippert,
54
Odradzam Ci zadawanie pytań typu „dlaczego nie” na StackOverflow; trudno na nie odpowiedzieć. Pytanie „dlaczego nie” zakłada, że ​​świat oczywiście powinien być taki, jakim nie jest i że musi istnieć dobry powód, aby taki był. To założenie prawie nigdy nie jest aktualne. Bardziej precyzyjnym pytaniem byłoby coś w rodzaju „która sekcja specyfikacji opisuje sposób obliczania arytmetyki stałych liczb całkowitych?” lub „jak obsługiwane są przepełnienia liczb całkowitych w Javie?”
Eric Lippert

Odpowiedzi:

233

Nie ma nic złego w tym stwierdzeniu; po prostu mnożysz 4 liczby i przypisujesz je do liczby int, po prostu zdarza się, że jest przepełnienie. Różni się to od przypisywania pojedynczego literału , który byłby sprawdzany w czasie kompilacji.

To literał poza zakresem powoduje błąd, a nie przypisanie :

System.out.println(2147483648);        // error
System.out.println(2147483647 + 1);    // no error

Z drugiej strony longliterał skompilowałby się dobrze:

System.out.println(2147483648L);       // no error

Zwróć uwagę, że w rzeczywistości wynik jest nadal obliczany w czasie kompilacji, ponieważ 1024 * 1024 * 1024 * 1024jest wyrażeniem stałym :

int i = 1024 * 1024 * 1024 * 1024;

staje się:

   0: iconst_0      
   1: istore_1      

Zwróć uwagę, że wynik ( 0) jest po prostu ładowany i zapisywany i nie ma miejsca żadne mnożenie.


Od JLS §3.10.1 (dzięki @ChrisK za poruszenie tego w komentarzach):

Jest to błąd czasu kompilacji, jeśli literał dziesiętny typu intjest większy niż 2147483648(2 31 ) lub jeśli literał dziesiętny 2147483648występuje w dowolnym miejscu innym niż operand jednoargumentowego operatora minus ( §15.15.4 ).

arshajii
źródło
12
A jeśli chodzi o mnożenie, JLS mówi: Jeśli mnożenie liczb całkowitych przepełni się, to wynikiem są mniej znaczące bity iloczynu matematycznego, reprezentowane w jakimś wystarczająco dużym formacie uzupełnienia do dwóch. W rezultacie, jeśli wystąpi przepełnienie, znak wyniku może nie być taki sam, jak znak iloczynu matematycznego dwóch wartości operandów.
Chris K,
3
Doskonała odpowiedź. Niektórym wydaje się, że przepełnienie to jakiś błąd lub awaria, ale tak nie jest.
Wouter Lievens,
3
@ iowatiger08 Semantykę języka określa JLS, który jest niezależny od JVM (więc nie powinno mieć znaczenia, którego JVM używasz).
arshajii,
4
@WouterLievens, przepełnienie jest zwykle „nietypowym” stanem, jeśli nie bezpośrednim błędem. Jest to wynik matematyki o skończonej precyzji, której większość ludzi nie spodziewa się intuicyjnie, kiedy zajmują się matematyką. W niektórych przypadkach -1 + 1jest to nieszkodliwe; ale 1024^4może to zaślepić ludzi z całkowicie nieoczekiwanymi wynikami, dalekimi od tego, czego oczekiwaliby widzieć. Myślę, że powinno być przynajmniej ostrzeżenie lub uwaga dla użytkownika i nie należy go po cichu ignorować.
Phil Perry,
1
@ iowatiger08: Rozmiar int jest ustalony; to jednak nie zależy na JVM. Java to nie C.
Martin Schröder
43

1024 * 1024 * 1024 * 1024i 2147483648nie mają tej samej wartości w Javie.

W rzeczywistości 2147483648 NIE JEST NAWET WARTOŚCIĄ (chociaż 2147483648Ljest) w Javie. Kompilator dosłownie nie wie, co to jest ani jak go używać. Więc jęczy.

1024jest prawidłową liczbą int w Javie, a poprawna intpomnożona przez inną poprawną intjest zawsze poprawna int. Nawet jeśli nie jest to ta sama wartość, której można by się intuicyjnie spodziewać, ponieważ obliczenia będą przepełnione.

Przykład

Rozważmy następujący przykład kodu:

public static void main(String[] args) {
    int a = 1024;
    int b = a * a * a * a;
}

Czy spodziewałbyś się, że spowoduje to błąd kompilacji? Teraz robi się trochę bardziej ślisko.
A co jeśli umieścimy pętlę z 3 iteracjami i pomnożymy w pętli?

Kompilator może optymalizować, ale nie może zmieniać zachowania programu w tym czasie.


Kilka informacji o tym, jak faktycznie załatwiamy tę sprawę:

W Javie i wielu innych językach liczby całkowite będą składały się ze stałej liczby bitów. Obliczenia, które nie mieszczą się w podanej liczbie bitów, zostaną przepełnione ; Obliczenie jest wykonywane w zasadzie Modulus 2 ^ 32, Jawa, po czym wartość jest przekształcany z powrotem do podpisanego całkowitej.

Inne języki lub interfejsy API używają dynamicznej liczby bitów ( BigIntegerw Javie), zgłaszają wyjątek lub ustawiają wartość na magiczną wartość, taką jak not-a-number.

Cruncher
źródło
8
Dla mnie twoje stwierdzenie „ 2147483648NIE JEST NAWET WARTOŚCIĄ (chociaż 2147483648Ljest)” naprawdę ugruntowało sprawę, którą @arshajii starał się podkreślić.
kdbanman
Ach, przepraszam, tak, to byłem ja. Brakowało mi w Twojej odpowiedzi pojęcia przepełnienia / arytmetyki modularnej. Pamiętaj, że możesz cofnąć zmiany, jeśli nie zgadzasz się z moją zmianą.
Maarten Bodewes
@owlstead Twoja zmiana jest zgodna ze stanem faktycznym. Powodem, dla którego tego nie uwzględniłem, było to, że: niezależnie od tego, jak 1024 * 1024 * 1024 * 1024jest traktowane, naprawdę chciałem podkreślić, że to nie to samo, co pisanie 2147473648. Istnieje wiele sposobów (i wymieniłeś kilka), na które język mógłby sobie z tym poradzić. Jest rozsądnie oddzielony i przydatny. Więc zostawię to. Wiele informacji staje się coraz bardziej potrzebnych, gdy masz wysoko sklasyfikowaną odpowiedź na popularne pytanie.
Cruncher
16

Nie mam pojęcia, dlaczego drugi wariant nie powoduje błędów.

Proponowane przez Ciebie zachowanie - to znaczy generowanie komunikatu diagnostycznego, gdy obliczenia generują wartość większą niż największa wartość, która może być przechowywana jako liczba całkowita - to cecha . Aby móc korzystać z dowolnej funkcji, należy ją przemyśleć, uznać za dobry pomysł, zaprojektować, określić, zaimplementować, przetestować, udokumentować i dostarczyć użytkownikom.

W przypadku Javy co najmniej jedna z rzeczy na tej liście się nie wydarzyła i dlatego nie masz tej funkcji. Nie wiem który; musiałbyś zapytać projektanta Java.

W przypadku C # wszystkie te rzeczy się wydarzyły - około czternaście lat temu - i dlatego odpowiedni program w C # wygenerował błąd od czasu C # 1.0.

Eric Lippert
źródło
45
To nie dodaje niczego pomocnego. Chociaż nie mam nic przeciwko zadawaniu pytań w Javie, w ogóle nie odpowiedział na pytanie PO.
Seiyria,
29
@Seiyria: Oryginalny plakat zawiera pytanie „dlaczego nie?” pytanie - "dlaczego świat nie jest taki, jaki moim zdaniem powinien być?" nie jest precyzyjnym technicznym pytaniem o rzeczywisty kod i dlatego jest to złe pytanie dla StackOverflow. Fakt, że prawidłowa odpowiedź na niejasne i nietechniczne pytanie jest niejasna i nietechniczna, nie powinien być zaskakujący. Zachęcam oryginalnego plakatu do zadawania lepszego pytania i unikania „dlaczego nie”? pytania.
Eric Lippert,
18
@Seiyria: Zaakceptowana odpowiedź, którą zauważam, również nie odpowiada na to niejasne i nietechniczne pytanie; pytanie brzmi "dlaczego to nie jest błąd?" a akceptowana odpowiedź brzmi „ponieważ jest legalna”. To jest po prostu powtórzenie pytania ; odpowiadając „dlaczego niebo nie jest zielone?” z „ponieważ jest niebieskie” nie odpowiada na pytanie. Ale ponieważ pytanie jest złe, w ogóle nie winię odpowiadającego; odpowiedź jest całkowicie rozsądną odpowiedzią na kiepskie pytanie.
Eric Lippert,
13
Panie Eric, oto pytanie, które zamieściłem: „Dlaczego int i = 1024 * 1024 * 1024 * 1024; bez raportu o błędzie w zaćmieniu?”. a odpowiedź arshajii jest dokładnie tym, co ja (może więcej). Czasami nie jestem w stanie wyrazić żadnych pytań w bardzo dokładny sposób. Myślę, że dlatego niektórzy ludzie modyfikują niektóre opublikowane pytania dokładniej w Stackoverflow. Myślę, że jeśli chcę uzyskać odpowiedź „ponieważ jest to legalne”, nie opublikuję tego pytania. Postaram się zadać kilka „zwykłych pytań”, ale proszę, zrozumcie kogoś takiego jak ja, który jest studentem i nie jest tak profesjonalny. Dzięki.
WUJ,
5
@WUJ Ta odpowiedź IMHO zapewnia dodatkowy wgląd i perspektywę. Po przeczytaniu wszystkich odpowiedzi stwierdziłem, że ta odpowiedź jest równie ważna, jak inne udzielone odpowiedzi. Podnosi również świadomość, że programiści nie są jedynymi wdrażającymi niektóre produkty oprogramowania.
SoftwareCarpenter,
12

Oprócz odpowiedzi arshajii chcę pokazać jeszcze jedną rzecz:

To nie przypisanie powoduje błąd, ale po prostu użycie literału . Kiedy próbujesz

long i = 2147483648;

zauważysz, że powoduje to również błąd kompilacji, ponieważ prawa strona nadal jest intliterą -literal i poza zakresem.

Zatem operacje z int-wartościami (i to w tym przypisania) mogą przepełniać się bez błędu kompilacji (i bez błędu czasu wykonania), ale kompilator po prostu nie może obsłużyć tych zbyt dużych literałów.

piet.t
źródło
1
Dobrze. Przypisanie int do long obejmuje niejawne rzutowanie. Ale wartość nigdy nie może istnieć jako int w pierwszej kolejności do rzucenia :)
Cruncher.
4

O: Ponieważ to nie jest błąd.

Tło: mnożenie 1024 * 1024 * 1024 * 1024doprowadzi do przepełnienia. Przepełnienie jest bardzo często błędem. Różne języki programowania powodują różne zachowania, gdy występują przepełnienia. Na przykład C i C ++ nazywają to „niezdefiniowanym zachowaniem” dla liczb całkowitych ze znakiem, a zachowanie jest zdefiniowane jako liczby całkowite bez znaku (weź wynik matematyczny, dodaj, UINT_MAX + 1o ile wynik jest ujemny, odejmij, UINT_MAX + 1jeśli wynik jest większy niż UINT_MAX).

W przypadku Javy, jeśli wynik operacji z intwartościami nie mieści się w dozwolonym zakresie, koncepcyjnie Java dodaje lub odejmuje 2 ^ 32, aż wynik znajdzie się w dozwolonym zakresie. Więc oświadczenie jest całkowicie legalne i nie jest błędne. Po prostu nie daje rezultatów, na jakie liczyłeś.

Z pewnością możesz się spierać, czy to zachowanie jest pomocne i czy kompilator powinien dać ci ostrzeżenie. Osobiście powiedziałbym, że ostrzeżenie byłoby bardzo przydatne, ale błąd byłby nieprawidłowy, ponieważ jest to legalna Java.

gnasher729
źródło