Java 的设计者有什么理由认为不应该为局部变量赋予默认值?说真的,如果实例变量可以被赋予一个默认值,那么为什么我们不能对局部变量做同样的事情呢?
它还会导致问题,正如这篇对博客文章的评论中所解释的那样:
好吧,当试图在 finally 块中关闭资源时,这条规则最令人沮丧。如果我在 try 中实例化资源,但尝试在 finally 中关闭它,我会收到此错误。如果我将实例化移到 try 之外,我会收到另一个错误,指出它必须在 try 内。
非常令人沮丧。
Java 的设计者有什么理由认为不应该为局部变量赋予默认值?说真的,如果实例变量可以被赋予一个默认值,那么为什么我们不能对局部变量做同样的事情呢?
它还会导致问题,正如这篇对博客文章的评论中所解释的那样:
好吧,当试图在 finally 块中关闭资源时,这条规则最令人沮丧。如果我在 try 中实例化资源,但尝试在 finally 中关闭它,我会收到此错误。如果我将实例化移到 try 之外,我会收到另一个错误,指出它必须在 try 内。
非常令人沮丧。
声明局部变量主要是为了进行一些计算。所以设置变量的值是程序员的决定,它不应该采用默认值。
如果程序员错误地没有初始化局部变量并且它采用了默认值,那么输出可能是一些意想不到的值。因此,对于局部变量,编译器会要求程序员在访问变量之前用一些值对其进行初始化,以避免使用未定义的值。
您链接到的“问题”似乎正在描述这种情况:
SomeObject so;
try {
// Do some work here ...
so = new SomeObject();
so.DoUsefulThings();
} finally {
so.CleanUp(); // Compiler error here
}
评论者的抱怨是编译器在该finally
部分的行中犹豫不决,声称so
可能未初始化。然后评论提到了另一种编写代码的方式,可能是这样的:
// Do some work here ...
SomeObject so = new SomeObject();
try {
so.DoUsefulThings();
} finally {
so.CleanUp();
}
评论者对该解决方案不满意,因为编译器然后说代码“必须在尝试范围内”。我想这意味着某些代码可能会引发不再处理的异常。我不确定。我的代码的两个版本都没有处理任何异常,因此第一个版本中与异常相关的任何内容在第二个版本中都应该相同。
无论如何,这第二个版本的代码是正确的编写方式。在第一个版本中,编译器的错误信息是正确的。so
变量可能未初始化。特别是,如果SomeObject
构造函数失败,so
将不会被初始化,因此尝试调用so.CleanUp
. 始终在获得该部分完成的资源后try
进入该部分。finally
初始化后的try
-finally
块仅用于保护实例,以确保无论发生什么其他情况都将其清理干净。如果还有其他需要运行的东西,但它们与实例是否已分配属性无关,那么它们应该进入另一个-块,可能是包装了我展示的那个块。so
SomeObject
SomeObject
try
finally
要求在使用前手动分配变量不会导致真正的问题。它只会带来一些小麻烦,但你的代码会更好。您将拥有范围更有限的变量,并且try
-finally
不会试图保护太多的块。
如果局部变量具有默认值,那么so
在第一个示例中将是null
. 那真的解决不了任何问题。finally
您不会在块中获得编译时错误,而是NullPointerException
潜伏在那里,它可能会隐藏代码的“在这里做一些工作”部分中可能发生的任何其他异常。(或者finally
节中的异常会自动链接到前一个异常吗?我不记得了。即便如此,你还是会有一个额外的异常,就像真正的异常一样。)
请注意,最终实例/成员变量默认情况下不会被初始化。因为这些是最终的,以后不能在程序中更改。这就是Java没有为它们提供任何默认值并强制程序员对其进行初始化的原因。
另一方面,非最终成员变量可以稍后更改。因此,编译器不会让它们保持未初始化状态;确切地说,因为这些可以在以后更改。关于局部变量,局部变量的范围要窄得多;并且编译器知道它何时被使用。因此,强制程序员初始化变量是有道理的。
此外,在下面的示例中,可能在 SomeObject 构造中引发了异常,在这种情况下,“so”变量将为 null,并且对 CleanUp 的调用将引发 NullPointerException
SomeObject so;
try {
// Do some work here ...
so = new SomeObject();
so.DoUsefulThings();
} finally {
so.CleanUp(); // Compiler error here
}
我倾向于这样做:
SomeObject so = null;
try {
// Do some work here ...
so = new SomeObject();
so.DoUsefulThings();
} finally {
if (so != null) {
so.CleanUp(); // safe
}
}
您问题的实际答案是因为方法变量是通过简单地向堆栈指针添加一个数字来实例化的。将它们归零将是一个额外的步骤。对于类变量,它们被放入堆上的初始化内存中。
为什么不采取额外的步骤?退后一步——没有人提到这种情况下的“警告”是一件非常好的事情。
您永远不应该在第一次传递时将变量初始化为零或 null(当您第一次编码时)。要么将它分配给实际值,要么根本不分配它,因为如果你不这样做,那么 Java 可以告诉你什么时候你真的搞砸了。以电僧的回答为例。在第一种情况下,它实际上非常有用,它告诉您如果 try() 由于 SomeObject 的构造函数抛出异常而失败,那么您最终会在 finally 中得到NPE 。如果构造函数不能抛出异常,就不应该在 try 中。
这个警告是一个很棒的多路径错误程序员检查器,它使我免于做愚蠢的事情,因为它检查每条路径并确保如果您在某个路径中使用该变量,那么您必须在通向它的每个路径中初始化它. 我现在从不显式初始化变量,直到我确定这是正确的做法。
最重要的是,明确地说“int size=0”而不是“int size”并让下一个程序员弄清楚你打算将它设为零不是更好吗?
另一方面,我想不出一个正当的理由让编译器将所有未初始化的变量初始化为 0。
我认为主要目的是保持与 C/C++ 的相似性。但是,编译器会检测并警告您使用未初始化的变量,这会将问题减少到最小程度。从性能的角度来看,让您声明未初始化的变量要快一些,因为编译器不必编写赋值语句,即使您在下一条语句中覆盖了变量的值。
不初始化变量更有效,并且在局部变量的情况下这样做是安全的,因为编译器可以跟踪初始化。
在需要初始化变量的情况下,您总是可以自己做,所以这不是问题。
对我来说,原因归结为:局部变量的用途与实例变量的用途不同。局部变量将用作计算的一部分;实例变量用于包含状态。如果你使用一个局部变量而不给它赋值,那几乎肯定是一个逻辑错误。
也就是说,我完全可以落后于要求始终显式初始化实例变量;该错误将发生在结果允许未初始化的实例变量的任何构造函数上(例如,未在声明时初始化且未在构造函数中初始化)。但这不是 Gosling 等人的决定。al.,拍摄于 90 年代初,所以我们来了。(我并不是说他们打错了电话。)
不过,我无法落后于默认的局部变量。是的,我们不应该依赖编译器来仔细检查我们的逻辑,而且不需要,但是当编译器发现一个时它仍然很方便。:-)
局部变量背后的想法是它们只存在于需要它们的有限范围内。因此,对于该值,或者至少该值的来源,应该没有什么不确定性的理由。我可以想象由于局部变量具有默认值而引起的许多错误。
例如,考虑以下简单代码...(注意,出于演示目的,我们假设为局部变量分配了默认值,如指定的那样,如果未显式初始化)
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);
一切都说完了,假设编译器将默认值 '\0' 分配给 letterGrade,那么编写的这段代码将正常工作。但是,如果我们忘记了 else 语句怎么办?
我们的代码的测试运行可能会导致以下结果
Enter grade
43
Your grade is
这个结果虽然在意料之中,但肯定不是编码者的意图。实际上,可能在绝大多数情况下(或至少在相当多的情况下),默认值不会是所需值,因此在绝大多数情况下,默认值会导致错误。强制编码人员在使用局部变量之前为其分配初始值更有意义,因为忘记= 1
in导致的调试痛苦for(int i = 1; i < 10; i++)
远远超过了不必包含= 0
in的便利for(int i; i < 10; i++)
。
确实,try-catch-finally 块可能会有点混乱(但它实际上并不是引用似乎暗示的 catch-22),例如,当一个对象在其构造函数中抛出一个检查异常时,但对于一个出于或其他原因,必须在 finally 块的末尾对此对象执行某些操作。一个完美的例子是在处理资源时,它必须被关闭。
过去处理这个问题的一种方法可能是这样......
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();
}
但是,从 Java 7 开始,使用 try-with-resources 不再需要这个 finally 块,就像这样。
try (Scanner s = new Scanner(new FileInputStream(new File("filename.txt")))) {
...
...
} catch(IOException e) {
System.out.println("different error message");
}
也就是说,(顾名思义)这只适用于资源。
虽然前一个例子有点恶心,但这可能更多地说明了 try-catch-finally 或这些类的实现方式,而不是局部变量及其实现方式。
确实将字段初始化为默认值,但这有点不同。例如,当您说 时,int[] arr = new int[10];
一旦您初始化了这个数组,该对象就存在于内存中的给定位置。让我们暂时假设没有默认值,而是初始值是此时恰好在该内存位置中的任何 1 和 0 序列。在许多情况下,这可能会导致不确定的行为。
假设我们有...
int[] arr = new int[10];
if(arr[0] == 0)
System.out.println("Same.");
else
System.out.println("Not same.");
完全有Same.
可能在一次运行Not same.
中显示并在另一次运行中显示。一旦你开始谈论引用变量,这个问题可能会变得更加严重。
String[] s = new String[5];
根据定义,s的每个元素都应该指向一个String(或者为null)。但是,如果初始值是在这个内存位置发生的任何一系列 0 和 1,不仅不能保证每次都会得到相同的结果,而且也不能保证对象 s[0] 指向to (假设它指向任何有意义的东西)甚至是一个字符串(也许它是一只兔子,:p)!面对几乎所有使 Java 成为 Java 的东西,这种对类型的缺乏关注会飞起来。因此,虽然局部变量的默认值充其量只能被视为可选,但实例变量的默认值更接近于必要性。
如果我没记错的话,另一个原因可能是:
为成员变量提供默认值是类加载的一部分
类加载在 Java 中是运行时的事情,这意味着当您创建一个对象时,该类将通过类加载进行加载。只有成员变量被初始化为默认值。
JVM不会花时间给你的局部变量一个默认值,因为有些方法永远不会被调用,因为一个方法调用可以是有条件的,那么为什么要花时间给它们一个默认值,如果这些默认值永远不会降低性能会被使用吗?此外,给它们一个默认值意味着我们需要首先创建它们,这也意味着在类加载时,如果不使用它们,为什么要浪费内存来创建它们。
局部变量存储在堆栈中,但实例变量存储在堆中,因此有可能会读取堆栈上的先前值而不是堆中的默认值。
出于这个原因,JVM 不允许在未初始化的情况下使用局部变量。
Eclipse 甚至会为您提供未初始化变量的警告,因此无论如何它变得非常明显。我个人认为这是默认行为是一件好事,否则您的应用程序可能会使用意外的值,而不是编译器抛出错误,它不会做任何事情(但可能会发出警告)然后你会抓挠你的头脑为什么某些事情不完全按照他们应该的方式行事。
方法的内存堆栈是在执行时创建的。方法堆栈顺序在执行时决定。
可能有一个根本不会被调用的函数。所以在对象实例化的时候实例化局部变量会完全浪费内存。此外,对象变量在类的完整对象生命周期中保留在内存中,而局部变量及其值在它们从内存堆栈中弹出的那一刻就可以进行垃圾收集。
因此,将内存分配给甚至可能不会被调用或即使被调用的方法的变量,在对象的生命周期内不会保留在内存中,这将是完全不合逻辑且浪费内存的
实例变量将具有默认值,但局部变量不能具有默认值。由于局部变量基本上都在方法/行为中,它的主要目的是做一些操作或计算。因此,为局部变量设置默认值并不是一个好主意。否则,检查意外答案的原因是非常困难和耗时的。
反过来问:为什么将字段初始化为默认值?如果 Java 编译器要求您自己初始化字段而不是使用它们的默认值,那将更有效,因为在使用之前无需将内存清零。因此,如果在这方面将所有变量都视为局部变量,那将是一种明智的语言设计。
原因不是因为检查字段比检查局部变量更难。Java 编译器已经知道如何检查一个字段是否确实由构造函数初始化,因为它必须检查这个final
字段。因此,编译器将相同的逻辑应用于其他字段以确保它们在构造函数中被明确分配,这将是一点额外的工作。
原因是,即使对于final
编译器证明该字段在构造函数中明确赋值的字段,其赋值前的值仍然可以从其他代码中看到:
class A {
final int x;
A() {
this.x = calculate();
}
int calculate() {
System.out.println(this.x);
return 1;
}
}
在这段代码中,构造函数肯定会赋值给this.x
,但即便如此,该字段的默认初始值在打印点0
的方法中也是可见的。如果在调用构造函数之前该字段未清零,则该方法将能够观察未初始化内存的内容,这将是非确定性行为并存在潜在的安全问题。calculate
this.x
calculate
calculate()
另一种方法是在尚未明确分配字段的代码中此时禁止方法调用。但这很不方便;能够像这样从构造函数调用方法很有用。能够做到这一点的便利性比在调用构造函数之前将字段的内存清零的微小性能成本更有价值。
请注意,这种推理不适用于局部变量,因为方法的未初始化局部变量在其他方法中是不可见的;因为他们是本地人。
答案是实例变量可以在类构造函数或任何类方法中初始化。但是对于局部变量,一旦您在方法中定义了任何内容,它就会永远保留在类中。
我可以想到以下两个原因