Java的设计者是否有任何理由认为不应为局部变量提供默认值?认真地讲,如果实例变量可以被赋予默认值,那为什么我们不能对局部变量做同样的事情呢?
并且还会导致出现问题,如博客文章的此评论所述:
那么,当试图在finally块中关闭资源时,此规则最令人沮丧。如果我在try中实例化资源,但最后尝试关闭它,则会收到此错误。如果我将实例化移动到try之外,则会收到另一个错误,指出它必须在try内。
非常沮丧。
Java的设计者是否有任何理由认为不应为局部变量提供默认值?认真地讲,如果实例变量可以被赋予默认值,那为什么我们不能对局部变量做同样的事情呢?
并且还会导致出现问题,如博客文章的此评论所述:
那么,当试图在finally块中关闭资源时,此规则最令人沮丧。如果我在try中实例化资源,但最后尝试关闭它,则会收到此错误。如果我将实例化移动到try之外,则会收到另一个错误,指出它必须在try内。
非常沮丧。
Answers:
您链接到的“问题”似乎正在描述这种情况:
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没有为它们提供任何默认值并迫使程序员对其进行初始化的原因。
另一方面,非最终成员变量可以在以后更改。因此,正是因为以后可以更改它们,编译器才允许它们保持未初始化状态。关于局部变量,局部变量的范围要窄得多。编译器知道何时使用它。因此,强制程序员初始化变量是有意义的。
您问题的实际答案是因为方法变量是通过将数字简单地添加到堆栈指针来实例化的。将它们清零将是一个额外的步骤。对于类变量,它们被放入堆中的初始化内存中。
为什么不采取额外的步骤?退后一步-没有人提到这种情况下的“警告”是一件非常好的事情。
第一次进行编码时,绝对不要将变量初始化为零或null。要么将其分配给实际值,要么根本不分配它,因为如果您不这样做,那么java会告诉您何时真正搞砸了。以电和尚的答案为例。在第一种情况下,它告诉您如果try()因为SomeObject的构造函数引发了异常而失败,那么实际上非常有用,最后您将得到一个NPE。如果构造函数无法引发异常,则不应尝试。
此警告是一个了不起的多路径错误程序员检查器,它使我免于做任何愚蠢的事情,因为它检查每个路径,并确保如果在某个路径中使用了变量,则必须在导致该路径的每个路径中对其进行初始化。 。现在,在确定这是正确的操作之前,我永远不会显式初始化变量。
最重要的是,显式说“ 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);
说完这些后,假设编译器为letterGrade分配了默认值'\ 0',那么编写的代码将可以正常工作。但是,如果我们忘记了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),例如,当一个对象在其构造函数中引发了一个检查异常时,原因或其他原因,最后必须在该块的末尾对该对象执行某些操作。一个完美的例子是在处理必须关闭的资源时。
过去处理此问题的一种方法可能是这样的...
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];
根据定义,的每个元素应指向一个String(或为null)。但是,如果初始值是在此存储位置上偶然发生的一系列0和1,则不仅不能保证每次都会得到相同的结果,也不能保证对象s [0]指向to(假设它指向任何有意义的东西)甚至是一个String(也许是Rabbit,:p)!无需担心类型,将使构成Java Java的几乎所有事物都面临。因此,尽管具有局部变量的默认值充其量可以看作是可选的,但具有实例变量的默认值更接近必要性。
如果我没有记错,另一个原因可能是
提供成员变量的默认值是类加载的一部分
在Java中,类加载是运行时的事情,这意味着在创建对象时,类加载时将类加载,仅使用默认值初始化成员变量JVM不会花时间为本地变量提供默认值,因为某些方法永远不会之所以被调用,是因为方法调用可能是有条件的,因此如果永远不要使用这些默认值,为什么要花一些时间为它们提供默认值并降低性能。
局部变量存储在堆栈上,而实例变量存储在堆上,因此有可能读取堆栈上的先前值,而不是读取堆中的默认值。因此,jvm不允许在未初始化的情况下使用局部变量。