为什么不使用Java初始化局部变量?


103

Java的设计者是否有任何理由认为不应为局部变量提供默认值?认真地讲,如果实例变量可以被赋予默认值,那为什么我们不能对局部变量做同样的事情呢?

并且还会导致出现问题,如博客文章的此评论所述

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

非常沮丧。



1
抱歉...当我键入问题时此问题没有弹出..但是,我想这两个问题之间存在差异...我想知道为什么Java的设计师如此设计它,而您所指出的问题不会问...
Shivasubramanian

另见本相关的C#问题:stackoverflow.com/questions/1542824/...
Raedwald

简单-因为编译器很容易跟踪未初始化的局部变量。如果它可以与其他变量相同,那就可以。编译器只是试图为您提供帮助。
rustyx

Answers:


62

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


23

您链接到的“问题”似乎正在描述这种情况:

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。那真的不会解决任何事情。finallyNullPointerException可能会潜伏在代码中,而不是在代码块中出现编译时错误,从而可能会隐藏代码的“在这里做一些工作”部分中可能发生的任何其他异常。(或者finally部分中的异常会自动链接到先前的异常吗?我不记得了。即使如此,您还是会遇到一个真正的异常。)


2
为什么不只是在finally块中添加if(so!= null)...?
2009年

仍然会导致编译器警告/错误-我不认为编译器会理解是否检查(但我只是在内存之外执行此操作,未经测试)。
CHII

6
我只将SomeObject so = null放在尝试之前,然后将null检查放到finally子句中。这样就不会有编译器警告。
2009年

为什么使事情复杂化?用这种方式编写try-finally块,然后您知道该变量具有有效值。无需空检查。
罗伯·肯尼迪

1
Rob,您的示例“ new SomeObject()”很简单,不应该在此生成任何异常,但是如果调用可以生成异常,则最好将它出现在try块中,以便可以对其进行处理。
Sarel Botha

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
  }
}

12
您想做什么?
电和尚

2
是的,丑陋。是的,我也是。
SMBiggs

@ElectricMonk您认为哪种形式更好,您在getContents(..)方法中显示的形式还是此处显示的形式:javapractices.com/topic/TopicAction.do?Id=126
Atom

11

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

另一方面,非最终成员变量可以在以后更改。因此,正是因为以后可以更改它们,编译器才允许它们保持未初始化状态。关于局部变量,局部变量的范围要窄得多。编译器知道何时使用它。因此,强制程序员初始化变量是有意义的。


9

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

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

第一次进行编码时,绝对不要将变量初始化为零或null。要么将其分配给实际值,要么根本不分配它,因为如果您不这样做,那么java会告诉您何时真正搞砸了。以电和尚的答案为例。在第一种情况下,它告诉您如果try()因为SomeObject的构造函数引发了异常而失败,那么实际上非常有用,最后您将得到一个NPE。如果构造函数无法引发异常,则不应尝试。

此警告是一个了不起的多路径错误程序员检查器,它使我免于做任何愚蠢的事情,因为它检查每个路径,并确保如果在某个路径中使用了变量,则必须在导致该路径的每个路径中对其进行初始化。 。现在,在确定这是正确的操作之前,我永远不会显式初始化变量。

最重要的是,显式说“ int size = 0”而不是“ int size”并让下一个程序员确定您希望它为零不是更好吗?

另一方面,我无法提出一个有效的理由使编译器将所有未初始化的变量初始化为0。


1
是的,还有其他一些人由于代码的流向或多或少必须将其初始化为null-我不应该说“从不”更新答案来反映这一点。
Bill K

4

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


1
可以说,编译器可以确定是否在对变量进行任何操作之前始终将其赋值,并在这种情况下取​​消自动默认值赋值。如果编译器无法确定分配是否在访问之前发生,它将生成默认分配。
Greg Hewgill

2
是的,但是可能有人争辩说,它可以让程序员知道他或她是否未正确地将变量初始化。
Mehrdad Afshari

1
无论哪种情况,编译器都可以这样做。:)就个人而言,我希望编译器将未初始化的变量视为错误。这意味着我可能在某个地方犯了一个错误。
2009年

我不是Java专家,但是我喜欢C#的处理方式。所不同的是,在这种情况下,编译器必须发出警告,这可能会使您为正确的程序收到几百个警告;)
Mehrdad Afshari 2009年

它也警告成员变量吗?
Adeel Ansari

4

(在问题问了这么长时间后发布新答案似乎很奇怪,但是出现了一个重复的问题。)

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

就是说,我完全落后于要求始终显式初始化实例变量。该错误将在结果允许未初始化实例变量的任何构造函数中发生(例如,未在声明时初始化且不在构造函数中)。但这不是Gosling等人的决定。Al。,摄于90年代初期,所以我们来了。(我并不是说他们打错了电话。)

但是,我无法落后于默认局部变量。是的,我们不应该依赖编译器来仔细检查我们的逻辑,一个也不应该,但是当编译器发现一个逻辑时,它仍然很方便。:-)


“也就是说,我完全落后于要求总是显式初始化实例变量……” FWIW,是他们在TypeScript中采取的方向。
TJ Crowder

3

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

在需要初始化变量的情况下,您始终可以自己进行操作,因此这不是问题。


3

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

例如,考虑下面的简单代码...(注意,为演示起见,假设未明确初始化,则为局部变量分配默认值)。

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

尽管可以预料,但这种结果肯定不是编码人员的意图。确实,在绝大多数情况下(或至少在相当数量的情况下),默认值不是期望值,因此在绝大多数情况下,默认值都会导致错误。强制编码器在使用之前为局部变量分配初始值更有意义,因为忘记= 1in 引起的调试麻烦for(int i = 1; i < 10; i++)远远超过了不必包含= 0in 的便利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的几乎所有事物都面临。因此,尽管具有局部变量的默认值充其量可以看作是可选的,但具有实例变量的默认值更接近必要性


1

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

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

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


0

Eclipse甚至会向您发出未初始化变量的警告,因此无论如何它都变得非常明显。我个人认为这是默认行为,这是一件好事,否则您的应用程序可能会使用意外的值,而不是编译器抛出错误,它什么也不会做(但可能会给出警告),然后您就会抓挠为什么某些事情表现得不尽如人意?


0

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


2
弄清楚错误...所有Java非基元都存储在堆中,无论它们何时以及如何构造
gshauger 2010年

在Java 7之前,实例变量存储在堆中,而局部变量在堆栈中找到。但是,将在堆中找到任何局部变量引用的对象。从Java 7开始,“ Java Hotspot Server编译器”可能会执行“转义分析”,并决定在堆栈而不是堆上分配一些对象。
mamills 2013年

0

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


-1

答案是实例变量可以在类构造函数或任何类方法中初始化,但是在使用局部变量的情况下,一旦在方法中定义了永久保留在类中的任何内容。


-2

我可以想到以下两个原因

  1. 正如大多数答案通过限制初始化局部变量所说的那样,可以确保按照程序员的要求为局部变量分配一个值,并确保计算出预期的结果。
  2. 可以通过声明局部变量(相同的名称)来隐藏实例变量-为了确保预期的行为,强制将局部变量的值设为intail。(虽然会完全避免这种情况)

字段不能被覆盖。最多,它们可以被隐藏,而我看不到隐藏将如何干扰初始化检查?
meriton

右隐藏。如果决定创建与实例同名的局部变量,由于这一限制,局部变量将使用适当选择的值(实例变量的值除外)进行初始化
Mitra
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.