【问题标题】:Why are local variables not initialized in Java?为什么Java中没有初始化局部变量?
【发布时间】:2010-09-29 18:20:12
【问题描述】:

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

这也会导致this comment to a blog post中解释的问题:

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

非常沮丧。

【问题讨论】:

标签: java variables initialization


【解决方案1】:

声明局部变量主要是为了做一些计算。所以设置变量的值是程序员的决定,它不应该取默认值。

如果程序员错误地没有初始化一个局部变量并且它采用了一个默认值,那么输出可能是一些意想不到的值。所以在局部变量的情况下,编译器会要求程序员在访问变量之前用一些值初始化它,以避免使用未定义的值。

【讨论】:

    【解决方案2】:

    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 将是错误的。在您获得finally 部分最终确定的资源之后,请始终输入try 部分

    so 初始化之后的try-finally用于保护SomeObject 实例,以确保无论发生什么其他情况都可以清理它。如果还有 other 需要运行的东西,但它们与 SomeObject 实例是否是属性分配无关,那么它们应该进入 another try -finally 块,可能包含我展示的那个。

    要求在使用前手动分配变量不会导致真正的问题。它只会带来一些小麻烦,但你的代码会更好。您将拥有范围更有限的变量,以及不会尝试保护太多的 try-finally 块。

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

    【讨论】:

    • 为什么不在 finally 块中添加一个 if(so!=null)...?
    • 这仍然会导致编译器警告/错误 - 我认为编译器不理解 if 检查(但我只是在内存之外执行此操作,未经测试)。
    • 在尝试将 null 检查放在 finally 子句之前,我会只放置 SomeObject so = null 。这样就不会出现编译器警告。
    • 为什么要把事情复杂化?以这种方式编写 try-finally 块,并且您知道该变量具有有效值。不需要空值检查。
    • Rob,您的示例“new SomeObject()”很简单,不应该在那里生成异常,但是如果调用可以生成异常,那么最好让它发生在 try-block 中,所以是可以处理的。
    【解决方案3】:

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

    【讨论】:

    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

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

      为什么不采取额外的步骤?退后一步——没有人提到本案中的“警告”是一件非常好的事情。

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

      这个警告是一个很棒的多路径不良程序员检查器,它使我免于做愚蠢的事情,因为它检查每个路径并确保如果您在某个路径中使用了变量,那么您必须在每个路径中初始化它导致到它。我现在从不显式初始化变量,直到我确定这是正确的做法。

      除此之外,明确地说“int size=0”而不是“int size”并让下一个程序员弄清楚你打算将它设为零不是更好吗?

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

      【讨论】:

      • 是的,并且由于代码的流动方式,还有其他一些地方或多或少必须将其初始化为 null ——我不应该说“从不”更新答案以反映这一点。
      【解决方案6】:

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

      【讨论】:

      • 可以说,编译器可以确定您是否总是在对变量进行任何操作之前对其进行赋值,并在这种情况下抑制自动默认值赋值。如果编译器无法确定访问之前是否发生了赋值,则会生成默认赋值。
      • 是的,但有人可能会争辩说,它让程序员知道他或她是否错误地未初始化变量。
      • 无论哪种情况,编译器都可以做到这一点。 :) 就个人而言,我希望编译器将未初始化的变量视为错误。这意味着我可能在某个地方犯了错误。
      • 我不是 Java 人,但我喜欢 C# 处理它的方式。不同之处在于,在这种情况下,编译器必须发出警告,这可能会让您为正确的程序收到几百条警告;)
      • 是否也对成员变量发出警告?
      【解决方案7】:

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

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

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

      【讨论】:

      • “也就是说,我完全可以落后于要求始终显式初始化实例变量...”,FWIW,这是他们在 TypeScript 中采用的方向。
      【解决方案8】:

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

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

      【讨论】:

        【解决方案9】:

        局部变量背后的想法是它们只存在于需要它们的有限范围内。因此,对于该值,或者至少该值的来源,应该没有什么不确定性的理由。我可以想象由于局部变量具有默认值而引起的许多错误。

        例如,考虑下面的简单代码...(注意,为了演示的目的,让我们假设局部变量被分配了一个默认值,如指定的,如果没有显式初始化

        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
        

        这个结果虽然在意料之中,但肯定不是编码员的本意。实际上,可能在绝大多数情况下(或至少在相当多的情况下),默认值不会是 desired 值,因此在绝大多数情况下,默认值会导致错误。强制编码器在使用局部变量之前为其分配一个初始值更有意义,因为在for(int i = 1; i < 10; i++) 中忘记= 1 引起的调试痛苦远远超过了不必将= 0 包含在其中的便利性。 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] 指向到(假设它指向任何有意义的东西)甚至 is 一个字符串(也许它是一只兔子,:p)!面对几乎所有使 Java 成为 Java 的东西,这种对类型的缺乏关注会飞起来。因此,虽然为局部变量设置默认值充其量只能被视为可选,但为实例变量设置默认值更接近必需

        【讨论】:

          【解决方案10】:

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

          赋予成员变量的默认值是类加载的一部分

          类加载在 Java 中是运行时的事情,这意味着当您创建一个对象时,该类将通过类加载进行加载。只有成员变量被初始化为默认值。

          JVM 不会花时间给你的局部变量一个默认值,因为有些方法永远不会被调用,因为方法调用可以是有条件的,那么为什么要花时间给它们一个默认值并降低性能,如果那些永远不会使用默认值?另外,给它们一个默认值意味着我们需要首先创建它们,这也意味着在类加载时,如果不使用它们,为什么要浪费内存来创建它们。

          【讨论】:

          • Re“有些方法永远不会被调用,因为一个方法调用可以是有条件的,所以为什么要花时间给它们一个默认值”:但它是 不可能在调用方法之前初始化局部变量(在堆栈上)(例如,没有任何方法可以预测要更改的内存位置),所以我认为这一点没有实际意义。
          • 随意编辑,我用粗体表示如果我没记错这只是我的想法
          【解决方案11】:

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

          【讨论】:

          【解决方案12】:

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

          【讨论】:

            【解决方案13】:

            局部变量存储在堆栈中,但实例变量存储在堆中,因此有可能会读取堆栈中的先前值而不是堆中的默认值。

            因此,JVM 不允许在未初始化的情况下使用局部变量。

            【讨论】:

            • 完全错误......所有 Java 非基元都存储在堆中,无论它们何时以及如何构造
            • 在 Java 7 之前,实例变量存储在堆上,局部变量在栈上。但是,局部变量引用的任何对象都将在堆中找到。从 Java 7 开始,“Java Hotspot Server Compiler”可能会执行“逃逸分析”并决定在堆栈而不是堆上分配一些对象。
            【解决方案14】:

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

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

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

            【讨论】:

              【解决方案15】:

              答案是实例变量可以在类构造函数或任何类方法中初始化。但是对于局部变量,一旦你在方法中定义了任何东西,它就会永远保留在类中。

              【讨论】:

                【解决方案16】:

                我可以想到以下两个原因

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

                【讨论】:

                • 字段不能被覆盖。最多可以隐藏它们,我看不出隐藏会如何干扰初始化检查?
                • 右隐藏。如果决定创建与实例同名的局部变量,由于这种约束,局部变量将被初始化为故意选择的值(除了实例变量的值)
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2014-08-23
                相关资源
                最近更新 更多