Dlaczego ten kod Java jest kompilowany?

96

W zakresie metody lub klasy kompiluje się poniższy wiersz (z ostrzeżeniem):

int x = x = 1;

W zakresie klasy, gdzie zmienne pobierają wartości domyślne , następujący komunikat powoduje błąd „niezdefiniowane odwołanie”:

int x = x + 1;

Czy to nie pierwsza x = x = 1powinna zakończyć się tym samym błędem „niezdefiniowane odniesienie”? A może druga linia int x = x + 1powinna się skompilować? Czy jest coś, czego mi brakuje?

Marcin
źródło
1
Jeśli dodasz słowo kluczowe staticdo zmiennej zakresu klasy, jak w static int x = x + 1;, czy wystąpi ten sam błąd? Ponieważ w C # ma znaczenie, czy jest statyczny, czy niestatyczny.
Jeppe Stig Nielsen
static int x = x + 1nie działa w Javie.
Marcin
1
w języku C # zarówno w zakresie, jak int a = this.a + 1;iw int b = 1; int a = b + 1;zakresie klasy (oba są w porządku w Javie) zawodzą, prawdopodobnie z powodu §17.4.5.2 - „Inicjator zmiennej dla pola instancji nie może odwoływać się do tworzonej instancji”. Nie wiem, czy gdzieś jest to wyraźnie dozwolone, ale statyczność nie ma takiego ograniczenia. W Javie zasady są inne i static int x = x + 1int x = x + 1
zawodzą
Taka odpowiedź z kodem bajtowym rozwiewa wszelkie wątpliwości.
rgripper

Odpowiedzi:

101

tl; dr

Na polach , int b = b + 1jest nielegalne, ponieważ bjest to niezgodne z prawem do przodu odniesienia b. Możesz to naprawić, pisząc int b = this.b + 1, który kompiluje się bez skarg.

Dla zmiennych lokalnych , int d = d + 1jest nielegalne, ponieważ dnie jest zainicjowany przed użyciem. Nie dotyczy to pól, które są zawsze inicjowane domyślnie.

Możesz zobaczyć różnicę, próbując skompilować

int x = (x = 1) + x;

jako deklaracja pola i jako deklaracja zmiennej lokalnej. Pierwsza zawiedzie, ale druga odniesie sukces z powodu różnicy semantyki.

Wprowadzenie

Po pierwsze, reguły dla inicjatorów zmiennych lokalnych i pól są bardzo różne. Tak więc ta odpowiedź będzie dotyczyła zasad w dwóch częściach.

Będziemy używać tego programu testowego przez cały czas:

public class test {
    int a = a = 1;
    int b = b + 1;
    public static void Main(String[] args) {
        int c = c = 1;
        int d = d + 1;
    }
}

Deklaracja bjest nieprawidłowa i kończy się illegal forward referencebłędem.
Deklaracja djest nieprawidłowa i kończy się variable d might not have been initializedbłędem.

Fakt, że te błędy są różne, powinien wskazywać, że przyczyny błędów są również różne.

Pola

Inicjatory pól w Javie są regulowane przez JLS §8.3.2 , Inicjalizacja pól.

Zakres pola jest zdefiniowana w JLS §6.3 , Zakres deklaracji.

Odpowiednie zasady to:

  • Zakres deklaracji elementu członkowskiego mzadeklarowanego lub dziedziczonego przez typ klasy C (§ 8.1.6) to cała treść C, w tym wszelkie zagnieżdżone deklaracje typu.
  • Wyrażenia inicjujące dla zmiennych instancji mogą używać prostej nazwy dowolnej zmiennej statycznej zadeklarowanej lub dziedziczonej przez klasę, nawet takiej, której deklaracja pojawia się później tekstowo.
  • Użycie zmiennych instancji, których deklaracje pojawiają się tekstowo po użyciu, jest czasami ograniczone, nawet jeśli te zmienne instancji znajdują się w zakresie. Szczegółowe zasady rządzące odniesieniem do zmiennych instancji znajdują się w §8.3.2.3.

W §8.3.2.3 czytamy:

Deklaracja elementu członkowskiego musi pojawić się tekstowo, zanim zostanie użyta, tylko jeśli element członkowski jest instancją (odpowiednio statyczną) polem klasy lub interfejsu C i spełnione są wszystkie poniższe warunki:

  • Użycie występuje w inicjalizatorze zmiennej instancji (odpowiednio statycznej) w języku C lub w inicjatorze wystąpienia (odpowiednio statycznym) w języku C.
  • Użycie nie znajduje się po lewej stronie zadania.
  • Użycie odbywa się za pomocą prostej nazwy.
  • C to najbardziej wewnętrzna klasa lub interfejs obejmujący użycie.

W rzeczywistości możesz odwoływać się do pól, zanim zostały zadeklarowane, z wyjątkiem niektórych przypadków. Te ograniczenia mają na celu zapobieganie kodowaniu, takim jak

int j = i;
int i = j;

z kompilacji. Specyfikacja języka Java mówi, że „powyższe ograniczenia mają na celu wychwycenie w czasie kompilacji cyklicznych lub w inny sposób nieprawidłowych inicjalizacji”.

Do czego właściwie sprowadzają się te zasady?

Krótko mówiąc, reguły mówią po prostu, że musisz zadeklarować pole przed odwołaniem do tego pola, jeśli (a) odwołanie znajduje się w inicjatorze, (b) odwołanie nie jest przypisane, (c) odwołanie jest prosta nazwa (bez kwalifikatorów takich jak this.) i (d) nie ma do niej dostępu z poziomu klasy wewnętrznej. Zatem odniesienie w przód, które spełnia wszystkie cztery warunki, jest niedozwolone, ale odniesienie w przód, które nie powiedzie się w co najmniej jednym warunku, jest prawidłowe.

int a = a = 1;kompiluje się, ponieważ narusza (b): odniesienie a jest przypisywane, więc legalne jest odwoływanie się do niego aprzed apełną deklaracją.

int b = this.b + 1również kompiluje się, ponieważ narusza (c): odwołanie this.bnie jest prostą nazwą (jest kwalifikowane this.). Ta dziwna konstrukcja jest nadal doskonale zdefiniowana, ponieważ this.bma wartość zero.

Zasadniczo więc ograniczenia dotyczące odwołań do pól w inicjatorach uniemożliwiają int a = a + 1pomyślną kompilację.

Zwróć uwagę, że deklaracja pola nieint b = (b = 1) + b zostanie skompilowana, ponieważ wersja ostateczna nadal jest niedozwolonym odwołaniem do przodu.b

Zmienne lokalne

Lokalne deklaracje zmiennych są regulowane przez JLS §14.4 , Deklaracje deklaracji zmiennych lokalnych.

Zakres zmiennej lokalnej jest zdefiniowana w JLS §6.3 , Zakres deklaracji:

  • Zakres deklaracji zmiennej lokalnej w bloku (punkt 14.4) to reszta bloku, w którym deklaracja się pojawia, zaczynając od własnego inicjatora i włączając wszelkie dalsze deklaratory po prawej stronie w instrukcji deklaracji lokalnej zmiennej.

Zwróć uwagę, że inicjatory znajdują się w zakresie deklarowanej zmiennej. Więc dlaczego się nie int d = d + 1;kompiluje?

Powodem jest reguła Javy dotycząca określonego przypisania ( JLS §16 ). Określone przypisanie zasadniczo mówi, że każdy dostęp do zmiennej lokalnej musi mieć poprzedzające przypisanie do tej zmiennej, a kompilator Java sprawdza pętle i gałęzie, aby upewnić się, że przypisanie zawsze występuje przed jakimkolwiek użyciem (dlatego przypisanie określone ma całą sekcję specyfikacji poświęconą do niego). Podstawowa zasada to:

  • Dla każdego dostępu do zmiennej lokalnej lub pustego pola końcowego x, xmusi być ostatecznie przypisany przed dostępem lub wystąpi błąd w czasie kompilacji.

W int d = d + 1;programie dostęp do djest rozstrzygany na zmienną lokalną dobrze, ale ponieważ dnie został przypisany przed uzyskaniem ddostępu, kompilator zgłasza błąd. W int c = c = 1, c = 1dzieje się najpierw, co przypisuje c, a następnie cjest inicjowane w wyniku tego przypisania (czyli 1).

Zwróć uwagę, że ze względu na określone reguły przypisywania deklaracja zmiennej lokalnej int d = (d = 1) + d; zostanie pomyślnie skompilowana (w przeciwieństwie do deklaracji pola int b = (b = 1) + b), ponieważ djest ostatecznie przypisana do czasu osiągnięcia finału d.

nneonneo
źródło
+1 dla odniesień, jednak myślę, że źle zrozumiałeś to sformułowanie: „int a = a = 1; kompiluje, ponieważ narusza (b)”, jeśli naruszyłby którykolwiek z 4 wymagań, których nie skompilowałby. Jednak tak się nie dzieje, ponieważ JEST po lewej stronie zadania (podwójny minus w brzmieniu JLS nie pomaga tutaj zbytnio). W int b = b + 1b znajduje się po prawej stronie (nie po lewej) zadania, więc naruszyłoby to ...
msam
... Nie jestem pewien, co następuje: te 4 warunki muszą być spełnione, jeśli deklaracja nie pojawia się tekstowo przed przypisaniem, w tym przypadku wydaje mi się, że deklaracja pojawia się „tekstowo” przed przypisaniem int x = x = 1, w którym w przypadku gdy nic z tego nie miałoby zastosowania.
msam,
@msam: To trochę zagmatwane, ale zasadniczo musisz naruszyć jeden z czterech warunków, aby utworzyć odniesienie do przodu. Jeśli twoje odniesienie do przodu spełnia wszystkie cztery warunki, jest nielegalne.
nneonneo
@msam: Ponadto pełna deklaracja obowiązuje dopiero po inicjalizacji.
nneonneo
@mrfishie: Wielka odpowiedź, ale specyfikacja Javy jest zaskakująco głęboka. Pytanie nie jest takie proste, jak się wydaje z pozoru. (Kiedyś napisałem kompilator podzbioru języka Java, więc znam wiele tajników JLS).
nneonneo
86
int x = x = 1;

jest równa

int x = 1;
x = x; //warning here

będąc w

int x = x + 1; 

najpierw musimy obliczyć, x+1ale wartość x nie jest znana, więc pojawia się błąd (kompilator wie, że wartość x nie jest znana)

msam
źródło
4
To plus wskazówka dotycząca prawidłowej asocjatywności z OpenSauce, którą uważam za bardzo przydatną.
TobiMcNamobi
1
Myślałem, że wartością zwracaną przypisania była wartość przypisywana, a nie wartość zmiennej.
zzzzBov
2
@zzzzBov jest poprawne. int x = x = 1;odpowiada int x = (x = 1), nie x = 1; x = x; . Nie powinieneś otrzymywać ostrzeżenia kompilatora, aby to zrobić.
nneonneo
int x = x = 1;ekwiwalent int x = (x = 1)powodu prawe asocjatywności =operatora
Grijesh Chauhan
1
@nneonneo i int x = (x = 1)jest równoważne int x; x = 1; x = x;(deklaracja zmiennej, ocena inicjalizatora pola, przypisanie zmiennej do wyniku tej oceny), stąd ostrzeżenie
msam
41

Jest to mniej więcej równoważne z:

int x;
x = 1;
x = 1;

Po pierwsze, int <var> = <expression>;jest zawsze równoważne

int <var>;
<var> = <expression>;

W tym przypadku twoje wyrażenie jest x = 1, które jest również stwierdzeniem. x = 1jest prawidłową instrukcją, ponieważ zmienna xzostała już zadeklarowana. Jest to również wyrażenie o wartości 1, do którego jest następnie przypisywana xponownie.

OpenSauce
źródło
Ok, ale jeśli poszło tak, jak powiedziałeś, dlaczego w zakresie klasowym druga instrukcja podaje błąd? Mam na myśli, że otrzymujesz 0wartość domyślną dla ints, więc spodziewałbym się, że wynikiem będzie 1, a nie undefined reference.
Marcin
Spójrz na odpowiedź @izogfif. Wygląda na to, że działa, ponieważ kompilator C ++ przypisuje zmiennym wartości domyślne. Tak samo jak java dla zmiennych na poziomie klasy.
Marcin
@Marcin: w Javie wartości int nie są inicjowane na 0, gdy są zmiennymi lokalnymi. Są inicjalizowane na 0 tylko wtedy, gdy są zmiennymi składowymi. Więc w drugiej linii x + 1nie ma zdefiniowanej wartości, ponieważ nie xjest zainicjowany.
OpenSauce
1
@OpenSauce Ale x jest zdefiniowana jako zmienna składowa („w zakresie klasy”).
Jacob Raihle
@JacobRaihle: Ach ok, nie zauważyłem tej części. Nie jestem pewien, czy kod bajtowy do zainicjowania zmiennej na 0 zostanie wygenerowany przez kompilator, jeśli zobaczy, że istnieje wyraźna instrukcja inicjalizacji. Jest tutaj artykuł, który zawiera szczegółowe informacje na temat inicjalizacji klas i obiektów, chociaż nie sądzę, aby dotyczył dokładnie tego problemu: javaworld.com/jw-11-2001/jw-1102-java101.html
OpenSauce
12

W Javie lub jakimkolwiek innym nowoczesnym języku przypisanie pochodzi z prawej strony.

Załóżmy, że masz dwie zmienne x i y,

int z = x = y = 5;

Ta instrukcja jest prawidłowa i tak kompilator je dzieli.

y = 5;
x = y;
z = x; // which will be 5

Ale w twoim przypadku

int x = x + 1;

Kompilator dał wyjątek, ponieważ dzieli się w ten sposób.

x = 1; // oops, it isn't declared because assignment comes from the right.
Sri Harsha Chilakapati
źródło
ostrzeżenie jest włączone x = x nie x = 1
Asim Ghaffar
8

int x = x = 1; nie jest równe:

int x;
x = 1;
x = x;

javap znowu nam pomaga, oto instrukcja JVM wygenerowana dla tego kodu:

0: iconst_1    //load constant to stack
1: dup         //duplicate it
2: istore_1    //set x to constant
3: istore_1    //set x to constant

bardziej jak:

int x = 1;
x = 1;

Nie ma powodu, aby zgłaszać niezdefiniowany błąd odwołania. Obecnie używa się zmiennej przed jej inicjalizacją, więc ten kod jest w pełni zgodny ze specyfikacją. W rzeczywistości nie ma w ogóle użycia zmiennej , tylko przypisania. A kompilator JIT pójdzie jeszcze dalej, wyeliminuje takie konstrukcje. Mówiąc szczerze, nie rozumiem, jak ten kod jest powiązany ze specyfikacją JLS dotyczącą inicjalizacji i użycia zmiennych. Bez użycia bez problemów. ;)

Popraw, jeśli się mylę. Nie mogę zrozumieć, dlaczego inne odpowiedzi, które odnoszą się do wielu akapitów JLS, mają tak wiele plusów. Te akapity nie mają nic wspólnego z tym przypadkiem. Tylko dwa zlecenia seryjne i nic więcej.

Jeśli napiszemy:

int b, c, d, e, f;
int a = b = c = d = e = f = 5;

jest równe:

f = 5
e = 5
d = 5
c = 5
b = 5
a = 5

Najbardziej prawe wyrażenie jest po prostu przypisywane do zmiennych jedna po drugiej, bez żadnej rekursji. Możemy manipulować zmiennymi w dowolny sposób:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;
Michaił
źródło
7

W int x = x + 1;dodaniu 1 do x, więc to, co jest wartością x, to nie jest jeszcze utworzony.

Ale program int x=x=1;skompiluje się bez błędu, ponieważ przypisujesz 1 do x.

Alya'a Gamal
źródło
5

Twój pierwszy fragment kodu zawiera drugi =zamiast plusa. Spowoduje to skompilowanie się w dowolnym miejscu, podczas gdy drugi fragment kodu nie zostanie skompilowany w żadnym miejscu.

Joe Elleson
źródło
5

W drugim fragmencie kodu x jest używane przed deklaracją, podczas gdy w pierwszym fragmencie kodu jest po prostu przypisane dwukrotnie, co nie ma sensu, ale jest poprawne.

WilQu
źródło
5

Rozbijmy to krok po kroku, prawidłowa asocjacja

int x = x = 1

x = 1, przypisz 1 do zmiennej x

int x = x, przypisz sobie, czym jest x, jako int. Ponieważ x był wcześniej przypisany jako 1, zachowuje 1, aczkolwiek w sposób zbędny.

To dobrze się komponuje.

int x = x + 1

x + 1dodaj jeden do zmiennej x. Jednak niezdefiniowanie x spowoduje błąd kompilacji.

int x = x + 1, więc ta linia błędów kompilacji, ponieważ prawa część równych nie będzie kompilować dodawania jednego do nieprzypisanej zmiennej

steventnorris
źródło
Nie, jest prawostronny, gdy są dwa =operatory, więc jest taki sam jak int x = (x = 1);.
Jeppe Stig Nielsen
Ach, moje rozkazy wyłączone. Przepraszam za to. Powinienem był to zrobić wstecz. Zamieniłem to teraz.
steventnorris
3

Drugi int x=x=1jest kompilowany, ponieważ przypisujesz wartość do x, ale w innym przypadku int x=x+1tutaj zmienna x nie jest inicjowana. Pamiętaj, że zmienne lokalne java nie są inicjowane do wartości domyślnej. Uwaga Jeśli to ( int x=x+1) jest również w zakresie klasy, to również spowoduje błąd kompilacji, ponieważ zmienna nie jest tworzona.

Krushna
źródło
2
int x = x + 1;

kompiluje się pomyślnie w programie Visual Studio 2008 z ostrzeżeniem

warning C4700: uninitialized local variable 'x' used`
izogfif
źródło
2
Ciekawe. Czy to C / C ++?
Marcin
@Marcin: tak, to jest C ++. @msam: przepraszam, myślę, że czamiast tagu widziałem tag, javaale najwyraźniej było to drugie pytanie.
izogfif
Kompiluje się, ponieważ w C ++ kompilatory przypisują wartości domyślne dla typów pierwotnych. Użyj bool y;i y==truezwróci false.
Sri Harsha Chilakapati
@SriHarshaChilakapati, czy jest to jakiś standard w kompilatorze C ++? Ponieważ kiedy kompiluję void main() { int x = x + 1; printf("%d ", x); }w Visual Studio 2008, w Debug otrzymuję wyjątek, Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.aw Release otrzymuję numer 1896199921wydrukowany w konsoli.
izogfif
1
@SriHarshaChilakapati Mówiąc o innych językach: w C # dla staticpola (zmienna statyczna na poziomie klasy) obowiązują te same zasady. Na przykład pole zadeklarowane jako public static int x = x + 1;kompiluje się bez ostrzeżenia w programie Visual C #. Prawdopodobnie to samo w Javie?
Jeppe Stig Nielsen
2

x nie jest inicjalizowany w x = x + 1;.

Język programowania Java jest typowany statycznie, co oznacza, że ​​wszystkie zmienne muszą być najpierw zadeklarowane, zanim będą mogły być używane.

Zobacz prymitywne typy danych

Mohan Raj B.
źródło
3
Potrzeba zainicjowania zmiennych przed użyciem ich wartości nie ma nic wspólnego z typowaniem statycznym. Statycznie wpisane: musisz zadeklarować, jakiego typu jest zmienna. Zainicjuj przed użyciem: musi mieć wartość, którą można udowodnić, zanim będzie można użyć wartości.
Jon Bright
@JonBright: Potrzeba deklarowania typów zmiennych również nie ma nic wspólnego z typowaniem statycznym. Na przykład istnieją języki statyczne z wnioskiem o typie.
hammar
@hammar, tak jak ja to widzę, możesz argumentować na dwa sposoby: z wnioskiem o typie deklarujesz niejawnie typ zmiennej w sposób, który system może wywnioskować. Lub wnioskowanie o typie jest trzecim sposobem, w którym zmienne nie są dynamicznie wpisywane w czasie wykonywania, ale są na poziomie źródła, w zależności od ich użycia i wyciągniętych w ten sposób wniosków. Tak czy inaczej, stwierdzenie pozostaje prawdziwe. Ale masz rację, nie myślałem o innych systemach typu.
Jon Bright,
2

Wiersz kodu nie kompiluje się z ostrzeżeniem, ponieważ kod faktycznie działa. Po uruchomieniu kodu int x = x = 1Java najpierw tworzy zmienną xzgodnie z definicją. Następnie uruchamia kod przypisania ( x = 1). Ponieważ xjest już zdefiniowane, system nie ma błędów, ustawiając wartość xna 1. Zwraca to wartość 1, ponieważ jest to teraz wartość x. W związku z tym xjest teraz ostatecznie ustawiony na 1.
Java zasadniczo wykonuje kod tak, jakby to było to:

int x;
x = (x = 1); // (x = 1) returns 1 so there is no error

Jednak w drugim kawałkiem kodu, int x = x + 1The + 1stwierdzenie wymaga x, aby zdefiniować, który przez to nie jest. Ponieważ instrukcje przypisania zawsze oznaczają, że kod po prawej stronie =jest uruchamiany jako pierwszy, kod zakończy się niepowodzeniem, ponieważ xjest niezdefiniowany. Java uruchomiłaby kod w ten sposób:

int x;
x = x + 1; // this line causes the error because `x` is undefined
grób
źródło
-1

Zgodny czytaj oświadczenia od prawej do lewej i postanowiliśmy zrobić coś przeciwnego. Dlatego na początku to denerwowało. Niech to przyzwyczajenie do czytania instrukcji (kodu) od prawej do lewej, nie będziesz miał takiego problemu.

Ramiz Uddin
źródło