为什么局部变量没有在Java中初始化?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么局部变量没有在Java中初始化?相关的知识,希望对你有一定的参考价值。
有没有理由为什么Java的设计者觉得局部变量不应该被赋予默认值?说真的,如果实例变量可以给出一个默认值,为什么我们不能对局部变量做同样的事情呢?
它还会导致问题,如this comment to a blog post所述:
这个规则在尝试关闭finally块中的资源时最令人沮丧。如果我在try中实例化资源,但尝试在finally中关闭它,我会收到此错误。如果我在try之外移动实例化,我会收到另一个错误,指出它必须在try中。
很沮丧。
声明局部变量主要是为了进行一些计算。因此,程序员决定设置变量的值,它不应该采用默认值。如果程序员错误地没有初始化局部变量并且它采用默认值,那么输出可能是一些意外的值。因此,在局部变量的情况下,编译器将要求程序员在访问变量之前使用某个值进行初始化,以避免使用未定义的值。
Eclipse甚至会为您提供未初始化变量的警告,因此无论如何它都变得非常明显。我个人认为这是一个好事,这是默认行为,否则你的应用程序可能会使用意外的值,而不是编译器抛出错误它不会做任何事情(但也许会发出警告)然后你会抓你的头脑为什么某些事情不应该按照他们应该的方式行事。
局部变量存储在堆栈中,但实例变量存储在堆上,因此有可能会读取堆栈上的先前值而不是堆中发生的默认值。因此,jvm不允许在不初始化的情况下使用局部变量。
实例变量将具有默认值,但局部变量不能具有默认值。由于局部变量基本上属于方法/行为,其主要目的是进行一些操作或计算。因此,为局部变量设置默认值不是一个好主意。否则,检查意外答案的原因是非常困难和耗时的。
答案是实例变量可以在类构造函数或任何类方法中初始化,但是在局部变量的情况下,一旦你在方法中定义了永远保留在类中的任何东西。
我可以想到以下两个原因
- 正如大多数答案所说的那样,通过初始化局部变量的约束来确保局部变量被分配给程序员想要的值并确保计算预期结果。
- 可以通过声明局部变量(相同名称)来隐藏实例变量 - 为了确保预期的行为,强制本地变量被赋予一个值。 (完全可以避免这种情况)
The "problem" you link to似乎在描述这种情况:
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
部分中的异常会自动链接到前一个异常吗?我不记得了。即便如此,你还会有一个额外的例外。)
此外,在下面的示例中,可能在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
}
}
请注意,默认情况下不会初始化最终的实例/成员变量。因为那些是最终的,之后无法在程序中进行更改。这就是Java没有为它们提供任何默认值并迫使程序员初始化它的原因。
另一方面,非最终成员变量可以在以后更改。因此,编译器不会让它们保持未初始化,确切地说,因为这些可以在以后更改。关于局部变量,局部变量的范围要窄得多。编译器知道它何时被使用。因此,强制程序员初始化变量是有道理的。
你的问题的实际答案是因为方法变量只是通过向堆栈指针添加一个数字来实例化。将它们归零将是一个额外的步骤。对于类变量,它们被放入堆上的初始化内存中。
为什么不采取额外的步骤?退后一步 - 没有人提到在这种情况下的“警告”是一件非常好的事情。
您永远不应该在第一次传递时将变量初始化为零或为零(当您第一次编码时)。将它分配给实际值或根本不分配它,因为如果你不这样做,那么java可以告诉你什么时候你真的搞砸了。以Electric Monk的答案为例。在第一种情况下,它实际上非常有用,它告诉你如果try()失败因为SomeObject的构造函数抛出异常,那么你最终会得到一个NPE。如果构造函数不能抛出异常,那么它不应该在try中。
这个警告是一个很棒的多路径程序员检查程序,它使我免于做愚蠢的事情,因为它检查每个路径并确保如果你在某个路径中使用该变量,那么你必须在导致它的每个路径中初始化它。我现在从未明确初始化变量,直到我确定它是正确的做法。
最重要的是,明确说“int size = 0”而不是“int size”并不是更好,并让下一个程序员弄清楚你打算将其设为零吗?
另一方面,我无法想出让编译器将所有未初始化的变量初始化为0的单一有效理由。
我认为主要目的是保持与C / C ++的相似性。但是,编译器会检测并警告您使用未初始化的变量,这会将问题减少到最小点。从性能的角度来看,让你声明未初始化的变量要快一些,因为编译器不必编写赋值语句,即使你在下一个语句中覆盖变量的值也是如此。
不初始化变量更有效,并且在局部变量的情况下这样做是安全的,因为编译器可以跟踪初始化。
如果您需要初始化变量,您可以自己完成,因此不是问题。
(在问题发布后很久就发布一个新答案似乎很奇怪,但是duplicate出现了。)
对我来说,原因归结为:局部变量的目的不同于实例变量的目的。局部变量可用作计算的一部分;实例变量包含状态。如果使用局部变量而不为其赋值,那几乎肯定是逻辑错误。
也就是说,我可以完全落后,要求实例变量始终显式初始化;错误将发生在结果允许未初始化的实例变量的任何构造函数上(例如,未在声明而不是在构造函数中初始化)。但这不是Gosling等人的决定。在90年代早期,我们就在这里。 (而且我并不是说他们打错了电话。)
不过,我无法落后于默认的局部变量。是的,我们不应该依赖编译器来仔细检查我们的逻辑,而且不会,但是当编译器捕获一个时,它仍然很方便。 :-)
局部变量背后的想法是它们只存在于需要它们的有限范围内。因此,对于价值,或者至少是价值来自哪里,应该没有什么不确定的理由。我可以想象由于拥有局部变量的默认值而产生的许多错误。
例如,考虑以下简单代码...(N.B.让我们假设为了演示目的,如果未明确初始化,则为指定的局部变量分配默认值)
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 ' '
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);
完成所有操作后,假设编译器为letterGrade分配了默认值' 0',则编写的代码将正常工作。但是,如果我们忘记了else语句怎么办?
我们的代码的测试运行可能会导致以下结果
Enter grade
43
Your grade is
这个结果虽然可以预料,但肯定不是编码人员的意图。实际上,可能在绝大多数情况下(或者至少是其中的大量情况),默认值将不是期望值,因此在绝大多数情况下,默认值将导致错误。在使用它之前强制编码器为局部变量分配初始值更有意义,因为忘记= 1
中的for(int i = 1; i < 10; i++)
导致的调试悲伤远远超过不必在= 0
中包含for(int i; i < 10; i++)
的便利性。
确实,try-catch-finally块可能会有点混乱(但它实际上并不像引号似乎暗示的那样是一个catch-22),例如,当一个对象在其构造函数中抛出一个已检查的异常时,不管是什么原因,最后必须在块的末尾对这个对象做些什么。一个完美的例子就是处理必须关闭的资源。
过去处理这种情况的一种方法可能就是这样......
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中初始化?的主要内容,如果未能解决你的问题,请参考以下文章