Dlaczego zmienne lokalne nie są inicjowane w Javie?

104

Czy był jakiś powód, dla którego projektanci Java uznali, że zmiennym lokalnym nie należy nadawać wartości domyślnej? Poważnie, jeśli zmiennym instancji można nadać wartość domyślną, to dlaczego nie możemy zrobić tego samego dla zmiennych lokalnych?

Prowadzi to również do problemów, jak wyjaśniono w tym komentarzu do wpisu na blogu :

Cóż, ta zasada jest najbardziej frustrująca, gdy próbuje się zamknąć zasób w ostatecznym bloku. Jeśli utworzę wystąpienie zasobu podczas próby, ale spróbuję go zamknąć w końcu, pojawia się ten błąd. Jeśli przenoszę instancję poza try, pojawia się kolejny błąd informujący, że a musi znajdować się w try.

Bardzo frustrujące.

Shivasubramanian A
źródło
1
Przepraszam za to ... to pytanie nie wyskoczyło, kiedy je wpisałem .. jednak myślę, że jest różnica między tymi dwoma pytaniami ... Chcę wiedzieć, dlaczego projektanci Java zaprojektowali to w ten sposób, podczas gdy pytanie, na które wskazałeś, nie dotyczy tego ...
Shivasubramanian A
Zobacz także to powiązane pytanie w języku C #: stackoverflow.com/questions/1542824/…
Raedwald
Po prostu - ponieważ kompilator może łatwo śledzić niezainicjowane zmienne lokalne. Gdyby mógł zrobić to samo z innymi zmiennymi, zrobiłby to. Kompilator po prostu próbuje ci pomóc.
rustyx

Odpowiedzi:

62

Zmienne lokalne są deklarowane głównie w celu wykonania pewnych obliczeń. Więc to decyzja programisty o ustawieniu wartości zmiennej i nie powinna ona przyjmować wartości domyślnej. Jeśli programista przez pomyłkę nie zainicjował zmiennej lokalnej i przyjęła ona wartość domyślną, wówczas na wyjściu mogłaby pojawić się nieoczekiwana wartość. Tak więc w przypadku zmiennych lokalnych kompilator poprosi programistę o zainicjowanie z pewną wartością, zanim uzyska dostęp do zmiennej, aby uniknąć użycia niezdefiniowanych wartości.

Wojownik
źródło
23

„Problem”, do którego prowadzi link, wydaje się opisywać tę sytuację:

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

Komentator skarży się, że kompilator wzdraga się przed linią w finallysekcji, twierdząc, że somoże nie być zainicjowany. W komentarzu wspomniano wówczas o innym sposobie pisania kodu, prawdopodobnie mniej więcej tak:

// Do some work here ...
SomeObject so = new SomeObject();
try {
  so.DoUsefulThings();
} finally {
  so.CleanUp();
}

Komentator jest niezadowolony z tego rozwiązania, ponieważ kompilator mówi następnie, że kod „musi być w trakcie próby”. Myślę, że oznacza to, że część kodu może zgłosić wyjątek, który nie jest już obsługiwany. Nie jestem pewny. Żadna wersja mojego kodu nie obsługuje żadnych wyjątków, więc wszystko związane z wyjątkami w pierwszej wersji powinno działać tak samo w drugiej.

W każdym razie ta druga wersja kodu jest poprawnym sposobem jej zapisania. W pierwszej wersji komunikat o błędzie kompilatora był poprawny. soZmienna może być niezainicjowanymi. W szczególności, jeśli SomeObjectkonstruktor zawiedzie, sonie zostanie zainicjowany, więc próba wywołania będzie błędem so.CleanUp. Zawsze wchodź do trysekcji po zdobyciu zasobu, który finallysekcja finalizuje.

try- finallyblok po soinicjalizacji jest nie tylko ochrona SomeObjectprzykład, aby upewnić się, że zostanie oczyszczony bez względu na to, co jeszcze się dzieje. Jeśli są inne rzeczy, które muszą zostać uruchomione, ale nie są one związane z tym, czy SomeObjectinstancja została przydzielona jako właściwość, powinny przejść do innego try - finallybloku, prawdopodobnie takiego, który zawija ten, który pokazałem.

Wymaganie ręcznego przypisywania zmiennych przed użyciem nie prowadzi do prawdziwych problemów. Prowadzi to tylko do drobnych problemów, ale twój kod będzie do tego lepszy. Będziesz mieć zmienne o bardziej ograniczonym zakresie i try- finallybloki, które nie próbują chronić zbytnio.

Gdyby zmienne lokalne miały wartości domyślne, to sow pierwszym przykładzie byłyby null. To naprawdę niczego by nie rozwiązało. Zamiast pojawiać się w finallybloku błąd kompilacji , mógłbyś tam NullPointerExceptionczaić się, który mógłby ukryć każdy inny wyjątek, który mógłby wystąpić w sekcji kodu „Wykonaj jakąś pracę”. (A może wyjątki w finallysekcjach są automatycznie łączone z poprzednim wyjątkiem? Nie pamiętam. Mimo to miałbyś dodatkowy wyjątek na drodze prawdziwego).

Rob Kennedy
źródło
2
dlaczego nie mieć po prostu if (więc! = null) ... w ostatnim bloku?
izb
to nadal będzie powodować ostrzeżenie / błąd kompilatora - nie sądzę, aby kompilator to rozumiał, jeśli sprawdzam (ale robię to tylko z pamięci, nie testowałem).
Chii
6
Umieściłem po prostu SomeObject so = null przed próbą i ustawiłem sprawdzenie null w klauzuli ostatecznie. W ten sposób nie będzie ostrzeżeń kompilatora.
Juha Syrjälä
Po co komplikować sprawy? Napisz blok try-last w ten sposób, a WIESZ, że zmienna ma prawidłową wartość. Nie jest wymagane sprawdzanie wartości zerowej.
Rob Kennedy,
1
Rob, twój przykład "new SomeObject ()" jest prosty i nie powinno tam być żadnych wyjątków, ale jeśli wywołanie może generować wyjątki, byłoby lepiej, gdyby wystąpiło wewnątrz bloku try, aby można było go obsłużyć.
Sarel Botha
12

Ponadto w poniższym przykładzie wyjątek mógł zostać zgłoszony wewnątrz konstrukcji SomeObject, w którym to przypadku zmienna „so” miałaby wartość null, a wywołanie CleanUp wyrzuci wyjątek NullPointerException

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

Zwykle robię to:

SomeObject so = null;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  if (so != null) {
     so.CleanUp(); // safe
  }
}
Electric Monk
źródło
12
Co wolisz zrobić?
Electric Monk
2
Tak, brzydkie. Tak, ja też to robię.
SMBiggs
@ElectricMonk Która forma Twoim zdaniem jest lepsza, ta, którą pokazałeś lub ta pokazana tutaj w metodzie getContents (..): javapractices.com/topic/TopicAction.do?Id=126
Atom
11

Zauważ, że końcowe zmienne instancji / składowe nie są domyślnie inicjowane. Ponieważ są one ostateczne i nie można ich później zmienić w programie. To jest powód, dla którego Java nie podaje dla nich żadnej wartości domyślnej i zmusza programistę do jej zainicjowania.

Z drugiej strony, inne zmienne składowe można później zmienić. Dlatego kompilator nie pozwala im pozostać niezainicjowanymi, właśnie dlatego, że można je później zmienić. Jeśli chodzi o zmienne lokalne, zakres zmiennych lokalnych jest znacznie węższy. Kompilator wie, kiedy jest używany. Dlatego wymuszenie na programatorze zainicjowania zmiennej ma sens.

Adeel Ansari
źródło
9

Właściwa odpowiedź na twoje pytanie jest taka, że ​​zmienne metody są tworzone przez proste dodanie liczby do wskaźnika stosu. Wyzerowanie ich byłoby dodatkowym krokiem. W przypadku zmiennych klasowych są one umieszczane w zainicjowanej pamięci na stercie.

Dlaczego nie zrobić dodatkowego kroku? Cofnij się o krok - nikt nie wspomniał, że „ostrzeżenie” w tym przypadku jest bardzo dobrą rzeczą.

Nigdy nie powinieneś inicjalizować swojej zmiennej na zero lub null w pierwszym przebiegu (podczas pierwszego kodowania). Albo przypisz go do rzeczywistej wartości, albo w ogóle go nie przypisuj, ponieważ jeśli tego nie zrobisz, java może ci powiedzieć, kiedy naprawdę schrzaniłeś. Weźmy odpowiedź Electric Monk jako świetny przykład. W pierwszym przypadku zadziwiająco przydatne jest to, że mówi ci, że jeśli metoda try () nie powiedzie się, ponieważ konstruktor SomeObject rzucił wyjątek, to ostatecznie otrzymasz NPE w pliku. Jeśli konstruktor nie może zgłosić wyjątku, nie powinien znajdować się w try.

To ostrzeżenie jest niesamowitym wielościeżkowym narzędziem do sprawdzania złego programisty, który uchronił mnie przed robieniem głupich rzeczy, ponieważ sprawdza każdą ścieżkę i upewnia się, że jeśli użyłeś zmiennej w jakiejś ścieżce, musisz ją zainicjalizować w każdej ścieżce, która do niej prowadzi . Teraz nigdy jawnie nie inicjalizuję zmiennych, dopóki nie stwierdzę, że jest to właściwe.

Poza tym, czy nie lepiej jest powiedzieć wprost „int size = 0” niż „int size” i zmusić następnego programistę do zorientowania się, że zamierzasz wynosić zero?

Z drugiej strony nie mogę wymyślić ani jednego ważnego powodu, aby kompilator zainicjował wszystkie niezainicjowane zmienne na 0.

Bill K.
źródło
1
Tak, i są inne, w których mniej więcej musi być zainicjowany do wartości null z powodu sposobu, w jaki kod przepływa - nie powinienem był mówić „nigdy”, aby zaktualizować odpowiedź, aby to odzwierciedlić.
Bill K
4

Myślę, że głównym celem było zachowanie podobieństwa z C / C ++. Jednak kompilator wykrywa i ostrzega przed użyciem niezainicjowanych zmiennych, co ograniczy problem do minimum. Z punktu widzenia wydajności nieco szybciej można zadeklarować niezainicjowane zmienne, ponieważ kompilator nie będzie musiał pisać instrukcji przypisania, nawet jeśli nadpiszesz wartość zmiennej w następnej instrukcji.

Mehrdad Afshari
źródło
1
Prawdopodobnie kompilator mógłby określić, czy zawsze przypisujesz zmienną, zanim cokolwiek z nią zrobisz, i w takim przypadku pominąć automatyczne przypisywanie wartości domyślnej. Jeśli kompilator nie może określić, czy przypisanie ma miejsce przed dostępem, wygeneruje przypisanie domyślne.
Greg Hewgill
2
Tak, ale można by argumentować, że pozwala to programistom wiedzieć, czy przez pomyłkę pozostawił zmienną niezainicjowaną.
Mehrdad Afshari
1
Kompilator może to zrobić w obu przypadkach. :) Osobiście wolałbym, aby kompilator traktował niezainicjowaną zmienną jako błąd. To znaczy, że mogłem gdzieś popełnić błąd.
Greg Hewgill
Nie jestem typem Java, ale podoba mi się sposób obsługi tego języka w C #. Różnica polega na tym, że kompilator musiał wydać ostrzeżenie, które może spowodować, że otrzymasz kilkaset ostrzeżeń dla poprawnego programu;)
Mehrdad Afshari
Czy ostrzega również przed zmiennymi składowymi?
Adeel Ansari
4

(Opublikowanie nowej odpowiedzi tak długo po zadaniu pytania może wydawać się dziwne, ale pojawił się duplikat ).

Dla mnie przyczyna sprowadza się do tego: Cel zmiennych lokalnych jest inny niż cel zmiennych instancji. Zmienne lokalne mają być używane jako część obliczeń; Zmienne instancji mają zawierać stan. Jeśli używasz zmiennej lokalnej bez przypisywania jej wartości, prawie na pewno jest to błąd logiczny.

To powiedziawszy, mogłem całkowicie zostać w tyle, wymagając, aby zmienne instancji były zawsze jawnie inicjalizowane; błąd wystąpiłby w każdym konstruktorze, którego wynik zezwala na niezainicjowaną zmienną instancji (np. niezainicjowana w deklaracji, a nie w konstruktorze). Ale to nie jest decyzja Gosling, et. al., wziął się na początku lat 90-tych, więc oto jesteśmy. (I nie mówię, że wykonali zły telefon.)

Nie mogłem jednak wyjść poza domyślne zmienne lokalne. Tak, nie powinniśmy polegać na kompilatorach, aby podwójnie sprawdzać naszą logikę, a nie jest to możliwe, ale nadal jest to przydatne, gdy kompilator złapie jedną. :-)

TJ Crowder
źródło
„To powiedziawszy, mogłem całkowicie wyjść z tyłu, wymagając, aby zmienne instancji były zawsze jawnie inicjowane…” co, FWIW, jest kierunkiem, w jakim obrały one język TypeScript.
TJ Crowder
3

Bardziej efektywne jest nie inicjowanie zmiennych, aw przypadku zmiennych lokalnych jest to bezpieczne, ponieważ inicjalizację może śledzić kompilator.

W przypadkach, gdy potrzebujesz zainicjalizować zmienną, zawsze możesz to zrobić samodzielnie, więc nie stanowi to problemu.

starblue
źródło
3

Ideą zmiennych lokalnych jest to, że istnieją one tylko w ograniczonym zakresie, dla którego są potrzebne. W związku z tym nie powinno być powodów do niepewności co do wartości lub przynajmniej skąd ta wartość pochodzi. Mogłem sobie wyobrazić wiele błędów wynikających z posiadania domyślnej wartości dla zmiennych lokalnych.

Na przykład rozważmy następujący prosty kod ... ( Uwaga: załóżmy dla celów demonstracyjnych, że zmiennym lokalnym przypisywana jest wartość domyślna, jak określono, jeśli nie została ona jawnie zainicjowana )

System.out.println("Enter grade");
int grade = new Scanner(System.in).nextInt(); //I won't bother with exception handling here, to cut down on lines.
char letterGrade; //let us assume the default value for a char is '\0'
if (grade >= 90)
    letterGrade = 'A';
else if (grade >= 80)
    letterGrade = 'B';
else if (grade >= 70)
    letterGrade = 'C';
else if (grade >= 60)
    letterGrade = 'D';
else
    letterGrade = 'F';
System.out.println("Your grade is " + letterGrade);

Kiedy wszystko jest powiedziane i zrobione, zakładając , że kompilator przypisał domyślną wartość „\ 0” do letterGrade , ten kod, tak jak napisano, działałby poprawnie. A co by było, gdybyśmy zapomnieli o stwierdzeniu else?

Uruchomienie testowe naszego kodu może spowodować następujące skutki

Enter grade
43
Your grade is

Taki wynik, choć można się było spodziewać, z pewnością nie był zamiarem programisty. Rzeczywiście, prawdopodobnie w zdecydowanej większości przypadków (lub przynajmniej w znacznej ich liczbie) wartość domyślna nie byłaby wartością pożądaną , więc w zdecydowanej większości przypadków wartość domyślna powodowałaby błąd. Bardziej sensowne jest wymuszenie na koderze przypisania wartości początkowej do zmiennej lokalnej przed jej użyciem, ponieważ problemy związane z debugowaniem spowodowane zapomnieniem o = 1in for(int i = 1; i < 10; i++)znacznie przewyższają wygodę związaną z brakiem konieczności dołączania = 0in for(int i; i < 10; i++).

Prawdą jest, że bloki try-catch-final mogą się trochę zabrudzić (ale tak naprawdę nie jest to catch-22, jak wydaje się sugerować cytat), gdy na przykład obiekt zgłasza sprawdzony wyjątek w swoim konstruktorze, ale dla jednego czy z innego powodu, na końcu bloku trzeba coś zrobić z tym obiektem. Doskonałym tego przykładem jest sytuacja z zasobami, które należy zamknąć.

Jednym ze sposobów radzenia sobie z tym w przeszłości może być taki ...

Scanner s = null; //declared and initialized to null outside the block. This gives us the needed scope, and an initial value.
try {
    s = new Scanner(new FileInputStream(new File("filename.txt")));
    int someInt = s.nextInt();
} catch (InputMismatchException e) {
    System.out.println("Some error message");
} catch (IOException e) {
    System.out.println("different error message"); 
} finally {
    if (s != null) //in case exception during initialization prevents assignment of new non-null value to s.
        s.close();
}

Jednak od wersji Java 7 ta ostateczna blokada nie jest już konieczna przy użyciu zasobów próbnych.

try (Scanner s = new Scanner(new FileInputStream(new File("filename.txt")))) {
...
...
} catch(IOException e) {
    System.out.println("different error message");
}

To powiedziawszy (jak sugeruje nazwa) działa to tylko z zasobami.

I chociaż poprzedni przykład jest trochę obrzydliwy, to prawdopodobnie bardziej mówi o sposobie implementacji try-catch-final lub tych klas niż o zmiennych lokalnych i ich implementacji.

Prawdą jest, że pola są inicjowane do wartości domyślnej, ale jest to trochę inne. Kiedy mówisz, na przykład, int[] arr = new int[10];gdy tylko zainicjujesz tę tablicę, obiekt istnieje w pamięci w podanej lokalizacji. Załóżmy na chwilę, że nie ma wartości domyślnych, ale zamiast tego wartość początkowa jest dowolną serią jedynek i zer, która znajduje się w danej chwili w tej lokalizacji pamięci. W wielu przypadkach może to prowadzić do niedeterministycznych zachowań.

Załóżmy, że mamy ...

int[] arr = new int[10];
if(arr[0] == 0)
    System.out.println("Same.");
else
    System.out.println("Not same.");

Byłoby całkowicie możliwe, że Same.mogłoby to zostać wyświetlone w jednym przebiegu i Not same.w innym. Problem może stać się jeszcze poważniejszy, gdy zaczniesz mówić o zmiennych referencyjnych.

String[] s = new String[5];

Zgodnie z definicją, każdy element s powinien wskazywać na String (lub jest pusty). Jeśli jednak wartość początkowa jest dowolną serią 0 i 1, która zdarzy się w tej lokalizacji pamięci, nie tylko nie ma gwarancji, że za każdym razem otrzymasz te same wyniki, ale także nie ma gwarancji, że obiekt s [0] wskazuje to (zakładając, że wskazuje na coś znaczącego) nawet jest Stringiem (być może jest to Królik,: p )! Ten brak troski o typ byłby sprzeczny z prawie wszystkim, co czyni Java Java. Tak więc, chociaż posiadanie wartości domyślnych dla zmiennych lokalnych może być w najlepszym przypadku postrzegane jako opcjonalne, posiadanie wartości domyślnych, na przykład zmiennych, jest bliższe konieczności .

kumquatfelafel
źródło
1

jeśli się nie mylę, może być inny powód

Podanie domyślnej wartości zmiennych składowych jest częścią ładowania klasy

Ładowanie klasy jest czynnością wykonywaną w Javie, co oznacza, że ​​podczas tworzenia obiektu klasa jest ładowana z ładowaniem klasy tylko zmienne składowe są inicjowane z wartością domyślną JVM nie zajmuje czasu, aby nadać domyślną wartość zmiennym lokalnym, ponieważ niektóre metody nigdy nie będą wywoływane, ponieważ wywołanie metody może być warunkowe, więc po co poświęcać czas na nadanie im wartości domyślnej i zmniejszyć wydajność, jeśli te wartości domyślne nigdy nie będą używane.

Abhinav Chauhan
źródło
0

Eclipse ostrzega nawet przed niezainicjowanymi zmiennymi, więc i tak staje się to dość oczywiste. Osobiście uważam, że to dobrze, że jest to zachowanie domyślne, w przeciwnym razie twoja aplikacja może używać nieoczekiwanych wartości, a zamiast rzucać błąd kompilator nic nie zrobi (ale być może da ostrzeżenie) i wtedy będziesz drapać zastanawiasz się, dlaczego pewne rzeczy nie zachowują się tak, jak powinny.

Kezzer
źródło
0

Zmienne lokalne są przechowywane na stosie, ale zmienne instancji są przechowywane na stercie, więc są pewne szanse, że zostanie odczytana poprzednia wartość na stosie zamiast wartości domyślnej, jak to ma miejsce w stercie. Z tego powodu jvm nie pozwala na użycie zmiennej lokalnej bez jej zainicjowania.

David Santamaria
źródło
2
Płaski się źle ... wszystkie nie-prymitywów Java są przechowywane na stercie, niezależnie od tego, kiedy i jak są zbudowane
gshauger
Przed wersją Java 7 zmienne instancji są przechowywane na stercie, a zmienne lokalne znajdują się na stosie. Jednak każdy obiekt, do którego odwołuje się zmienna lokalna, zostanie znaleziony w stercie. Począwszy od Java 7, „Java Hotspot Server Compiler” może wykonać „analizę ucieczki” i zdecydować o przydzieleniu niektórych obiektów na stosie zamiast na stercie.
mamills
0

Zmienna instancji będzie miała wartości domyślne, ale zmienne lokalne nie mogą mieć wartości domyślnych. Ponieważ zmienne lokalne są zasadniczo w metodach / zachowaniu, ich głównym celem jest wykonanie pewnych operacji lub obliczeń. Dlatego nie jest dobrym pomysłem ustawianie wartości domyślnych dla zmiennych lokalnych. W przeciwnym razie sprawdzenie przyczyn nieoczekiwanych odpowiedzi jest bardzo trudne i czasochłonne.

Xiaogang
źródło
-1

Odpowiedź brzmi: zmienne instancji można zainicjować w konstruktorze klasy lub dowolnej metodzie klasowej, ale w przypadku zmiennych lokalnych, po zdefiniowaniu w metodzie cokolwiek, co pozostaje na zawsze w klasie.

Akash5288
źródło
-2

Mógłbym wymyślić dwa powody

  1. Jak mówi większość odpowiedzi, nakładając ograniczenie na inicjalizację zmiennej lokalnej, zapewnia się, że zmiennej lokalnej zostanie przypisana wartość zgodnie z życzeniem programisty i zapewni obliczenie oczekiwanych wyników.
  2. Zmienne instancji można ukryć, deklarując zmienne lokalne (o tej samej nazwie) - aby zapewnić oczekiwane zachowanie, zmienne lokalne są zmuszone do zainicjowania wartości. (Całkowicie tego uniknąłbym)
Mitra
źródło
Nie można nadpisywać pól. Co najwyżej można je ukryć, a ja nie widzę, jak ukrycie mogłoby przeszkodzić w sprawdzaniu inicjalizacji?
meriton
Po prawej stronie ukryte.Jeśli zdecydujesz się utworzyć zmienną lokalną o takiej samej nazwie jak instancja, z powodu tego ograniczenia, zmienna lokalna zostanie zainicjowana z odpowiednio dobraną wartością (inną niż wartość zmiennej instancji)
Mitra