为什么局部变量没有在Java中初始化?

时间:2009-01-06 07:03:47

标签: java variables initialization

Java的设计者是否认为局部变量不应该被赋予默认值?说真的,如果实例变量可以给出一个默认值,那么为什么我们不能对局部变量做同样的事情呢?

它还会导致问题,如this comment to a blog post

中所述
  

当尝试关闭finally块中的资源时,这条规则最令人沮丧。如果我在try中实例化资源,但尝试在finally中关闭它,我会收到此错误。如果我在try之外移动实例化,我会收到另一个错误,指出它必须在try中。

     

非常令人沮丧。

16 个答案:

答案 0 :(得分:55)

声明局部变量主要用于进行一些计算。因此,程序员决定设置变量的值,它不应该采用默认值。如果程序员错误地没有初始化局部变量并且它采用默认值,那么输出可能是一些意外的值。因此,在局部变量的情况下,编译器将要求程序员在访问变量之前使用某个值进行初始化,以避免使用未定义的值。

答案 1 :(得分:22)

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实例是否已分配属性无关,那么它们应该进入另一个 { {1}} - try阻止,可能是一个包裹我已经显示的那个。

要求在使用前手动分配变量不会导致实际问题。它只会导致轻微的麻烦,但你的代码会更好。您将拥有范围更为有限的变量,并且finally - try块不会过多地保护。

如果局部变量具有默认值,则第一个示例中的finally将为so。那不会解决任何问题。你没有在null块中遇到编译时错误,而是潜伏着finally,可能隐藏“做一些工作中可能发生的任何其他异常这里“代码部分。 (或者NullPointerException部分中的异常会自动链接到上一个异常吗?我不记得了。即便如此,你还是会遇到真正的异常。)

答案 2 :(得分:12)

此外,在下面的示例中,可能在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
  }
}

答案 3 :(得分:10)

请注意,默认情况下不会初始化最终的实例/成员变量。因为那些是最终的,之后无法在程序中进行更改。这就是Java没有为它们提供任何默认值并迫使程序员初始化它的原因。

另一方面,非最终成员变量可以在以后更改。因此,编译器不会让它们保持未初始化,确切地说,因为这些可以在以后更改。关于局部变量,局部变量的范围要窄得多。编译器知道它何时被使用。因此,强制程序员初始化变量是有道理的。

答案 4 :(得分:8)

您问题的实际答案是因为方法变量只是通过向堆栈指针添加数字来实例化。将它们归零将是一个额外的步骤。对于类变量,它们被放入堆上的初始化内存中。

为什么不采取额外措施?退后一步 - 没有人提到在这种情况下的“警告”是一件非常好的事情。

在第一次传递时(第一次编码时),永远不应该将变量初始化为零或为零。将它分配给实际值或根本不分配它,因为如果你不这样做,那么java可以告诉你什么时候你真的搞砸了。以Electric Monk的答案为例。在第一种情况下,它实际上非常有用,它告诉你如果try()失败因为SomeObject的构造函数抛出异常,那么你最终会得到一个NPE。如果构造函数不能抛出异常,则它不应该在try。

这个警告是一个很棒的多路径程序员检查器,它使我免于做愚蠢的事情,因为它检查每个路径并确保如果你在某个路径中使用该变量,那么你必须在每个路径中初始化它由它决定。我现在从未明确初始化变量,直到我确定它是正确的事情。

最重要的是,明确说“int size = 0”而不是“int size”是不是更好,让下一个程序员弄清楚你打算将它设为零?

另一方面,我无法想出让编译器将所有未初始化的变量初始化为0的唯一正当理由。

答案 5 :(得分:3)

我认为主要目的是保持与C / C ++的相似性。但是,编译器会检测并警告您使用未初始化的变量,这会将问题减少到最小点。从性能的角度来看,让你声明未初始化的变量要快一些,因为编译器不必编写赋值语句,即使你在下一个语句中覆盖变量的值也是如此。

答案 6 :(得分:3)

不初始化变量更有效,在局部变量的情况下,这样做是安全的,因为编译器可以跟踪初始化。

如果您需要初始化变量,您可以自己做,所以这不是问题。

答案 7 :(得分:3)

(在问题发布后很久就发布新答案似乎很奇怪,但duplicate出现了。)

对我来说,原因归结为这个:局部变量的目的不同于实例变量的目的。局部变量可用作计算的一部分;实例变量包含状态。如果使用局部变量而不为其赋值,那几乎肯定是逻辑错误。

那就是说,我可以完全落后,要求实例变量始终显式初始化;错误将发生在结果允许未初始化的实例变量的任何构造函数上(例如,未在声明而不是在构造函数中初始化)。但这不是Gosling等人的决定。在90年代早期,我们就在这里。 (我并不是说他们做错了。)

但我可以落后于默认的局部变量。是的,我们不应该依赖编译器来仔细检查我们的逻辑,而且不会,但是当编译器捕获一个时,它仍然很方便。 : - )

答案 8 :(得分:1)

局部变量背后的想法是它们仅存在于需要它们的有限范围内。因此,应该几乎没有理由不确定该值,或者至少该值来自何处。我可以想象由于使用局部变量的默认值而引起的许多错误。

例如,考虑以下简单代码...(为了说明,让我们假设为本地变量分配默认值(如果未显式初始化的话,则指定该值)

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中的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]指向(假设它指向任何有意义的东西)甚至是 字符串(也许是Rabbit,:p )!无需担心类型,将使构成Java Java的几乎所有事物都面临。因此,虽然局部变量的默认值充其量可以看作是可选的,但实例变量的默认值更接近必要性

答案 9 :(得分:1)

如果我没记错,另一个原因可能是

提供成员变量的默认值是类加载的一部分

类加载是Java中的一个运行时事物,这意味着当您创建一个对象时,该类将通过类加载进行加载,仅使用默认值初始化成员变量JVM不需要花费时间为本地变量提供默认值,因为某些方法永远不会被调用,因为方法调用可能是有条件的,因此,如果永远不要使用这些默认值,为什么要花一些时间为它们提供默认值并降低性能。

答案 10 :(得分:0)

Eclipse甚至会为您提供未初始化变量的警告,因此无论如何它都变得非常明显。我个人认为这是一个好事,这是默认行为,否则你的应用程序可能会使用意外的值,而不是编译器抛出错误它不会做任何事情(但也许会发出警告)然后你会抓你的头脑,为什么某些事情不会按照他们应该的方式行事。

答案 11 :(得分:0)

局部变量存储在堆栈中,但实例变量存储在堆上,因此有可能会读取堆栈上的先前值而不是堆中发生的默认值。因此,jvm不允许在不初始化的情况下使用局部变量。

答案 12 :(得分:0)

实例变量将具有默认值,但局部变量不能具有默认值。由于局部变量基本上属于方法/行为,其主要目的是进行一些操作或计算。因此,为局部变量设置默认值不是一个好主意。否则,检查意外答案的原因是非常困难和耗时的。

答案 13 :(得分:0)

方法的内存堆栈是在执行时创建的。方法堆栈顺序在执行时决定。

可能存在根本无法调用的函数。因此在对象实例化时实例化局部变量将完全浪费内存。此外,对象变量在类的整个对象生命周期内都保留在内存中,而局部变量及其值在从内存堆栈中弹出时就可以进行垃圾回收。

因此,将内存分配给甚至可能不会被调用或即使被调用的方法的变量也不会在对象的生命周期内保留在内存中,这将是完全不合逻辑且值得浪费内存的

答案 14 :(得分:-1)

答案是实例变量可以在类构造函数或任何类方法中初始化,但是在局部变量的情况下,一旦你在方法中定义了任何永远存在于类中的东西。

答案 15 :(得分:-2)

我可以想到以下两个原因

  1. 正如大多数答案所说的那样,通过初始化局部变量的约束,可以确保局部变量被赋予一个程序员想要的值,并确保计算预期结果。
  2. 可以通过声明局部变量(同名)来隐藏实例变量 - 为了确保预期的行为,本地变量被强制为一个值。 (尽管会完全避免这种情况)