Warum werden lokale Variablen in Java nicht initialisiert?

103

Gab es einen Grund, warum die Entwickler von Java der Meinung waren, dass lokalen Variablen kein Standardwert zugewiesen werden sollte? Im Ernst, wenn Instanzvariablen einen Standardwert erhalten können, warum können wir dann nicht dasselbe für lokale Variablen tun?

Und es führt auch zu Problemen, wie in diesem Kommentar zu einem Blog-Beitrag erklärt :

Nun, diese Regel ist am frustrierendsten, wenn versucht wird, eine Ressource in einem finally-Block zu schließen. Wenn ich die Ressource innerhalb eines Versuchs instanziiere, aber versuche, sie innerhalb des Endes zu schließen, wird dieser Fehler angezeigt. Wenn ich die Instanziierung außerhalb des Versuchs verschiebe, wird ein weiterer Fehler angezeigt, der besagt, dass a sich innerhalb eines Versuchs befinden muss.

Sehr frustrierend.

Shivasubramanian A.
quelle
1
Tut mir leid ... diese Frage ist nicht aufgetaucht, als ich die Frage eingegeben habe. Ich denke, es gibt einen Unterschied zwischen den beiden Fragen. Ich möchte wissen, warum die Designer von Java sie so entworfen haben Die Frage, auf die Sie hingewiesen haben, stellt das nicht ...
Shivasubramanian A
Siehe auch diese verwandte C # -Frage
Raedwald
Einfach - weil es für den Compiler einfach ist, nicht initialisierte lokale Variablen zu verfolgen. Wenn es mit anderen Variablen genauso wäre, würde es. Der Compiler versucht nur, Ihnen zu helfen.
Rustyx

Antworten:

62

Lokale Variablen werden meistens deklariert, um eine Berechnung durchzuführen. Es ist also die Entscheidung des Programmierers, den Wert der Variablen festzulegen, und es sollte kein Standardwert angenommen werden. Wenn der Programmierer versehentlich keine lokale Variable initialisiert hat und den Standardwert annimmt, kann die Ausgabe ein unerwarteter Wert sein. Bei lokalen Variablen fordert der Compiler den Programmierer auf, mit einem bestimmten Wert zu initialisieren, bevor er auf die Variable zugreift, um die Verwendung undefinierter Werte zu vermeiden.

Krieger
quelle
23

Das "Problem", auf das Sie verweisen, scheint diese Situation zu beschreiben:

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

Die Beschwerde des Kommentators lautet, dass der Compiler die Zeile im finallyAbschnitt blockiert und behauptet, dass sie somöglicherweise nicht initialisiert ist. Der Kommentar erwähnt dann eine andere Art, den Code zu schreiben, wahrscheinlich ungefähr so:

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

Der Kommentator ist mit dieser Lösung unzufrieden, da der Compiler dann sagt, dass der Code "innerhalb eines Versuchs sein muss". Ich denke, das bedeutet, dass ein Teil des Codes eine Ausnahme auslösen kann, die nicht mehr behandelt wird. Ich bin mir nicht sicher. Keine der Versionen meines Codes behandelt Ausnahmen, daher sollte alles, was mit Ausnahmen in der ersten Version zu tun hat, in der zweiten Version gleich funktionieren.

Wie auch immer, diese zweite Version des Codes ist die richtige Art, ihn zu schreiben. In der ersten Version war die Fehlermeldung des Compilers korrekt. Die soVariable ist möglicherweise nicht initialisiert. Insbesondere wenn der SomeObjectKonstruktor ausfällt, sowird er nicht initialisiert, und es ist daher ein Fehler, einen Aufruf zu versuchen so.CleanUp. Geben Sie den tryAbschnitt immer ein, nachdem Sie die Ressource erhalten haben, die der finallyAbschnitt finalisiert.

Der Block try- finallynach der soInitialisierung dient nur zum Schutz der SomeObjectInstanz, um sicherzustellen, dass sie bereinigt wird, unabhängig davon, was sonst noch passiert. Wenn es andere Dinge , die Notwendigkeit zu laufen, aber sie sind nicht verwandt, ob die SomeObjectInstanz wurde Eigenschaft zugeordnet ist , dann sollten sie gehen in einen anderen try - finallyBlock, wahrscheinlich derjenige, der die eine hüllt ich gezeigt habe.

Die manuelle Zuweisung von Variablen vor der Verwendung führt nicht zu echten Problemen. Es führt nur zu geringfügigen Problemen, aber Ihr Code ist besser dafür. Sie haben Variablen mit eingeschränktem Gültigkeitsbereich und try- finallyBlöcke, die nicht versuchen, zu viel zu schützen.

Wenn lokale Variablen Standardwerte hätten, sowäre dies im ersten Beispiel der Fall gewesen null. Das hätte nichts wirklich gelöst. Anstatt einen Fehler bei der Kompilierung im finallyBlock zu erhalten, NullPointerExceptionlauert dort ein Fehler , der möglicherweise alle anderen Ausnahmen im Abschnitt "Hier etwas arbeiten" des Codes verbirgt . (Oder verketten Ausnahmen in finallyAbschnitten automatisch mit der vorherigen Ausnahme? Ich erinnere mich nicht. Trotzdem hätten Sie eine zusätzliche Ausnahme in Bezug auf die echte.)

Rob Kennedy
quelle
2
warum nicht einfach ein if (so! = null) ... im finally-Block?
izb
Das wird immer noch die Compiler-Warnung / den Compiler-Fehler verursachen - ich glaube nicht, dass der Compiler das versteht, wenn er überprüft (aber ich mache das nur aus dem Speicher, nicht getestet).
Chii
6
Ich würde nur SomeObject so = null setzen, bevor ich versuche, null check to finally-Klausel zu setzen. Auf diese Weise werden keine Compiler-Warnungen angezeigt.
Juha Syrjälä
Warum die Dinge komplizieren? Schreiben Sie den try-finally-Block auf diese Weise, und Sie wissen, dass die Variable einen gültigen Wert hat. Keine Nullprüfung erforderlich.
Rob Kennedy
1
Rob, dein Beispiel "new SomeObject ()" ist einfach und es sollten dort keine Ausnahmen generiert werden. Wenn der Aufruf jedoch Ausnahmen generieren kann, ist es besser, wenn er im try-Block auftritt, damit er verarbeitet werden kann.
Sarel Botha
12

Darüber hinaus wurde im folgenden Beispiel möglicherweise eine Ausnahme innerhalb der SomeObject-Konstruktion ausgelöst. In diesem Fall wäre die Variable 'so' null und der Aufruf von CleanUp löst eine NullPointerException aus

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

Was ich dazu neige, ist Folgendes:

SomeObject so = null;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  if (so != null) {
     so.CleanUp(); // safe
  }
}
Elektrischer Mönch
quelle
12
Was würden Sie lieber tun?
Electric Monk
2
Ja, hässlich. Ja, das mache ich auch.
SMBiggs
@ElectricMonk Welche Form ist Ihrer Meinung nach besser, die, die Sie gezeigt haben oder die hier in der Methode getContents (..) gezeigt wurde: javapractices.com/topic/TopicAction.do?Id=126
Atom
11

Beachten Sie, dass die endgültigen Instanz- / Mitgliedsvariablen nicht standardmäßig initialisiert werden. Weil diese endgültig sind und danach im Programm nicht mehr geändert werden können. Aus diesem Grund gibt Java keinen Standardwert für sie an und zwingt den Programmierer, ihn zu initialisieren.

Andererseits können nicht endgültige Mitgliedsvariablen später geändert werden. Daher lässt der Compiler sie nicht uninitialisieren, gerade weil diese später geändert werden können. In Bezug auf lokale Variablen ist der Umfang lokaler Variablen viel enger. Der Compiler weiß, wann er verwendet wird. Daher ist es sinnvoll, den Programmierer zu zwingen, die Variable zu initialisieren.

Adeel Ansari
quelle
9

Die eigentliche Antwort auf Ihre Frage lautet, dass Methodenvariablen durch einfaches Hinzufügen einer Zahl zum Stapelzeiger instanziiert werden. Sie auf Null zu setzen wäre ein zusätzlicher Schritt. Für Klassenvariablen werden sie auf dem Heap in den initialisierten Speicher gestellt.

Warum nicht den zusätzlichen Schritt machen? Machen Sie einen Schritt zurück - Niemand hat erwähnt, dass die "Warnung" in diesem Fall eine sehr gute Sache ist.

Sie sollten Ihre Variable beim ersten Durchgang (beim ersten Codieren) niemals auf Null oder Null initialisieren. Weisen Sie es entweder dem tatsächlichen Wert zu oder weisen Sie es überhaupt nicht zu, denn wenn Sie dies nicht tun, kann Java Ihnen sagen, wann Sie es wirklich vermasseln. Nehmen Sie die Antwort von Electric Monk als gutes Beispiel. Im ersten Fall ist es tatsächlich erstaunlich nützlich, dass es Ihnen sagt, dass wenn die try () fehlschlägt, weil der Konstruktor von SomeObject eine Ausnahme ausgelöst hat, Sie schließlich eine NPE erhalten würden. Wenn der Konstruktor keine Ausnahme auslösen kann, sollte dies nicht der Fall sein.

Diese Warnung ist ein großartiger Multi-Path-Bad-Programmer-Checker, der mich vor dummen Dingen bewahrt hat, da er jeden Pfad überprüft und sicherstellt, dass Sie die Variable in jedem Pfad, der zu ihr führt, initialisieren mussten, wenn Sie sie in einem Pfad verwendet haben . Ich initialisiere Variablen jetzt nie explizit, bis ich feststelle, dass dies die richtige Vorgehensweise ist.

Ist es nicht besser, explizit "int size = 0" als "int size" zu sagen und den nächsten Programmierer dazu zu bringen, herauszufinden, dass Sie beabsichtigen, Null zu sein?

Auf der anderen Seite kann ich keinen einzigen gültigen Grund finden, warum der Compiler alle nicht initialisierten Variablen auf 0 initialisiert.

Bill K.
quelle
1
Ja, und es gibt andere, bei denen es aufgrund der Art und Weise, wie der Code fließt, mehr oder weniger auf null initialisiert werden muss. Ich hätte nicht sagen sollen, dass die Antwort "nie" aktualisiert wurde, um dies widerzuspiegeln.
Bill K
4

Ich denke, der Hauptzweck war es, die Ähnlichkeit mit C / C ++ aufrechtzuerhalten. Der Compiler erkennt und warnt Sie jedoch vor der Verwendung nicht initialisierter Variablen, wodurch das Problem auf ein Minimum reduziert wird. Aus Sicht der Leistung ist es etwas schneller, nicht initialisierte Variablen deklarieren zu können, da der Compiler keine Zuweisungsanweisung schreiben muss, selbst wenn Sie den Wert der Variablen in der nächsten Anweisung überschreiben.

Mehrdad Afshari
quelle
1
Möglicherweise könnte der Compiler bestimmen, ob Sie der Variablen immer etwas zuweisen, bevor Sie etwas damit tun, und in einem solchen Fall eine automatische Standardwertzuweisung unterdrücken. Wenn der Compiler vor dem Zugriff nicht feststellen kann, ob eine Zuweisung erfolgt, wird die Standardzuweisung generiert.
Greg Hewgill
2
Ja, aber man könnte argumentieren, dass der Programmierer dadurch informiert wird, ob er die Variable versehentlich nicht initialisiert hat.
Mehrdad Afshari
1
Der Compiler könnte das auch in beiden Fällen tun. :) Persönlich würde ich es vorziehen, wenn der Compiler eine nicht initialisierte Variable als Fehler behandelt. Es bedeutet, dass ich irgendwo einen Fehler gemacht habe.
Greg Hewgill
Ich bin kein Java-Typ, aber ich mag die C # -Methode, damit umzugehen. Der Unterschied ist in diesem Fall, dass der Compiler eine Warnung ausgeben musste, wodurch Sie möglicherweise ein paar hundert Warnungen für Ihr korrektes Programm erhalten;)
Mehrdad Afshari
Warnt es auch für die Mitgliedsvariablen?
Adeel Ansari
4

(Es mag seltsam erscheinen, so lange nach der Frage eine neue Antwort zu veröffentlichen, aber es ist ein Duplikat aufgetaucht.)

Für mich liegt der Grund darin: Der Zweck lokaler Variablen unterscheidet sich vom Zweck von Instanzvariablen. Lokale Variablen sollen im Rahmen einer Berechnung verwendet werden. Instanzvariablen sollen den Status enthalten. Wenn Sie eine lokale Variable verwenden, ohne ihr einen Wert zuzuweisen, ist dies mit ziemlicher Sicherheit ein logischer Fehler.

Das heißt, ich könnte völlig dahinter kommen, dass Instanzvariablen immer explizit initialisiert werden müssen; Der Fehler würde bei jedem Konstruktor auftreten, bei dem das Ergebnis eine nicht initialisierte Instanzvariable zulässt (z. B. bei der Deklaration nicht initialisiert und nicht im Konstruktor). Aber das ist nicht die Entscheidung von Gosling et al. al., in den frühen 90ern aufgenommen, also hier sind wir. (Und ich sage nicht, dass sie falsch angerufen haben.)

Ich konnte mich jedoch nicht hinter die standardmäßigen lokalen Variablen stellen. Ja, wir sollten uns nicht darauf verlassen, dass Compiler unsere Logik überprüfen, und das tut man auch nicht, aber es ist immer noch praktisch, wenn der Compiler eine herausfindet. :-)

TJ Crowder
quelle
"Das heißt, ich könnte völlig hinter der Forderung zurückbleiben, dass Instanzvariablen immer explizit initialisiert werden ..." Dies ist, FWIW, die Richtung, in die sie in TypeScript gegangen sind.
TJ Crowder
3

Es ist effizienter, Variablen nicht zu initialisieren, und bei lokalen Variablen ist dies sicher, da die Initialisierung vom Compiler verfolgt werden kann.

In Fällen, in denen eine Variable initialisiert werden muss, können Sie dies jederzeit selbst tun, sodass dies kein Problem darstellt.

Sternenblau
quelle
3

Die Idee hinter lokalen Variablen ist, dass sie nur innerhalb des begrenzten Bereichs existieren, für den sie benötigt werden. Daher sollte es wenig Grund zur Unsicherheit hinsichtlich des Werts geben oder zumindest, woher dieser Wert stammt. Ich könnte mir viele Fehler vorstellen, die sich aus einem Standardwert für lokale Variablen ergeben.

Betrachten Sie zum Beispiel den folgenden einfachen Code ... ( Hinweis: Nehmen wir zu Demonstrationszwecken an, dass lokalen Variablen ein Standardwert zugewiesen wird, wie angegeben, wenn er nicht explizit initialisiert wird. )

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);

Wenn alles gesagt und getan ist, würde dieser geschriebene Code ordnungsgemäß funktionieren , vorausgesetzt, der Compiler hat letterGrade den Standardwert '\ 0' zugewiesen . Was aber, wenn wir die else-Anweisung vergessen haben?

Ein Testlauf unseres Codes kann Folgendes bewirken

Enter grade
43
Your grade is

Dieses Ergebnis war zwar zu erwarten, aber sicherlich nicht die Absicht des Programmierers. In der Tat würde der Standardwert wahrscheinlich in den allermeisten Fällen (oder zumindest in einer signifikanten Anzahl davon) nicht der gewünschte Wert sein, sodass der Standardwert in den allermeisten Fällen zu Fehlern führen würde. Es ist sinnvoller, den Codierer zu zwingen, einer lokalen Variablen vor der Verwendung einen Anfangswert zuzuweisen, da der durch das Vergessen des = 1In verursachte Fehler beim Debuggen for(int i = 1; i < 10; i++)die Bequemlichkeit überwiegt, das In nicht einschließen = 0zu müssen for(int i; i < 10; i++).

Es ist wahr, dass Try-Catch-finally-Blöcke etwas chaotisch werden können (aber es ist eigentlich kein Catch-22, wie das Zitat zu vermuten scheint), wenn beispielsweise ein Objekt eine geprüfte Ausnahme in seinem Konstruktor auslöst, jedoch für eine Grund oder eine andere, muss etwas mit diesem Objekt am Ende des Blocks in endlich getan werden. Ein perfektes Beispiel hierfür ist der Umgang mit Ressourcen, die geschlossen werden müssen.

Eine Möglichkeit, in der Vergangenheit damit umzugehen, könnte so sein ...

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();
}

Ab Java 7 ist dieser endgültige Block jedoch nicht mehr erforderlich, wenn Sie versuchen, Ressourcen zu verwenden.

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

Das heißt, (wie der Name schon sagt) funktioniert dies nur mit Ressourcen.

Und während das erstere Beispiel ein bisschen glücklich ist, spricht dies vielleicht mehr für die Art und Weise, wie try-catch-finally oder diese Klassen implementiert werden, als für lokale Variablen und wie sie implementiert werden.

Es ist wahr, dass Felder mit einem Standardwert initialisiert werden, aber das ist etwas anders. Wenn Sie beispielsweise sagen, int[] arr = new int[10];sobald Sie dieses Array initialisiert haben, befindet sich das Objekt an einem bestimmten Ort im Speicher. Nehmen wir für einen Moment an, dass es keine Standardwerte gibt, sondern der Anfangswert eine beliebige Reihe von Einsen und Nullen ist, die sich gerade an diesem Speicherort befindet. Dies könnte in einigen Fällen zu nicht deterministischem Verhalten führen.

Angenommen, wir haben ...

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

Es wäre durchaus möglich, dass Same.dies in einem Lauf und in einem anderen Lauf Not same.angezeigt wird. Das Problem könnte noch schwerwiegender werden, wenn Sie anfangen, Referenzvariablen zu sprechen.

String[] s = new String[5];

Gemäß der Definition sollte jedes Element von s auf einen String zeigen (oder ist null). Wenn der Anfangswert jedoch eine beliebige Reihe von Nullen und Einsen ist, die an diesem Speicherort auftreten, gibt es nicht nur keine Garantie, dass Sie jedes Mal die gleichen Ergebnisse erhalten, sondern auch keine Garantie dafür, dass das Objekt s [0] zeigt zu (vorausgesetzt , es weist auf etwas Sinnvolles) selbst ist ein String (vielleicht ist es ein Kaninchen, : p )! Dieser Mangel an Sorge um den Typ würde so ziemlich allem widersprechen, was Java Java ausmacht. Während Standardwerte für lokale Variablen bestenfalls als optional angesehen werden können, sind Standardwerte für beispielsweise Variablen einer Notwendigkeit näher .

Kumquatfelafel
quelle
1

Wenn ich mich nicht irre, könnte ein anderer Grund sein

Die Angabe des Standardwerts für Elementvariablen ist Teil des Klassenladens

Das Laden von Klassen ist eine Laufzeitsache in Java. Wenn Sie ein Objekt erstellen, wird die Klasse mit dem Laden von Klassen geladen. Nur Mitgliedsvariablen werden mit dem Standardwert initialisiert. JVM benötigt keine Zeit, um Ihren lokalen Variablen einen Standardwert zuzuweisen, da einige Methoden dies niemals tun werden Wird aufgerufen, weil der Methodenaufruf bedingt sein kann. Warum sollten Sie sich also Zeit nehmen, um ihnen den Standardwert zuzuweisen und die Leistung zu verringern, wenn diese Standardeinstellungen niemals verwendet werden.

Abhinav Chauhan
quelle
0

Eclipse warnt Sie sogar vor nicht initialisierten Variablen, sodass dies ohnehin ziemlich offensichtlich wird. Persönlich denke ich, dass es eine gute Sache ist, dass dies das Standardverhalten ist, andernfalls verwendet Ihre Anwendung möglicherweise unerwartete Werte, und anstatt dass der Compiler einen Fehler auslöst, wird er nichts tun (aber möglicherweise eine Warnung geben), und dann werden Sie kratzen Ihr Kopf darüber, warum sich bestimmte Dinge nicht so verhalten, wie sie sollten.

Kezzer
quelle
0

Die lokalen Variablen werden auf einem Stapel gespeichert, aber Instanzvariablen werden auf dem Heap gespeichert, sodass die Wahrscheinlichkeit groß ist, dass ein vorheriger Wert auf dem Stapel anstelle eines Standardwerts gelesen wird, wie dies im Heap der Fall ist. Aus diesem Grund erlaubt die JVM nicht, eine lokale Variable zu verwenden, ohne sie zu initialisieren.

David Santamaria
quelle
2
Einfach falsch ... alle Java-Nicht-Primitiven werden auf dem Heap gespeichert, unabhängig davon, wann und wie sie erstellt wurden
gshauger
Vor Java 7 werden Instanzvariablen auf dem Heap und lokale Variablen auf dem Stapel gespeichert. Jedes Objekt, auf das eine lokale Variable verweist, wird jedoch im Heap gefunden. Ab Java 7 führt der "Java Hotspot Server Compiler" möglicherweise eine "Escape-Analyse" durch und beschließt, einige Objekte auf dem Stapel anstelle des Heaps zuzuweisen.
Mamills
0

Instanzvariable hat Standardwerte, aber die lokalen Variablen konnten keine Standardwerte haben. Da sich lokale Variablen im Wesentlichen in Methoden / Verhalten befinden, besteht ihr Hauptziel darin, einige Operationen oder Berechnungen durchzuführen. Daher ist es nicht ratsam, Standardwerte für lokale Variablen festzulegen. Andernfalls ist es sehr schwierig und zeitaufwändig, die Gründe für unerwartete Antworten zu überprüfen.

Xiaogang
quelle
-1

Die Antwort lautet: Instanzvariablen können im Klassenkonstruktor oder in einer beliebigen Klassenmethode initialisiert werden. Bei lokalen Variablen müssen Sie jedoch alles in der Methode definieren, was für immer in der Klasse verbleibt.

Akash5288
quelle
-2

Ich könnte mir zwei Gründe vorstellen

  1. Wie die meisten Antworten durch die Einschränkung der Initialisierung der lokalen Variablen besagen, wird sichergestellt, dass der lokalen Variablen ein vom Programmierer gewünschter Wert zugewiesen wird und sichergestellt wird, dass die erwarteten Ergebnisse berechnet werden.
  2. Instanzvariablen können durch Deklarieren lokaler Variablen (gleicher Name) ausgeblendet werden. Um das erwartete Verhalten sicherzustellen, müssen lokale Variablen einen Wert initiieren. (Würde dies aber völlig vermeiden)
Mitra
quelle
Felder können nicht überschrieben werden. Sie können höchstens ausgeblendet werden, und ich sehe nicht ein, wie das Ausblenden eine Überprüfung auf Initialisierung beeinträchtigen würde.
Meriton
Rechts ausgeblendet. Wenn Sie aufgrund dieser Einschränkung beschließen, eine lokale Variable mit demselben Namen wie die Instanz zu erstellen, wird die lokale Variable mit einem genau ausgewählten Wert (außer dem Wert der Instanzvariablen) initialisiert
Mitra,