在Java中,为什么实例变量有默认值?

23

为什么在Java中,在类中声明的变量有默认值,而在方法内部声明的变量(被称为“局部变量”)却没有默认值呢?

例如:

class abc
{
   int a;

   public static void main(String ss[])
   {
        int b;

          abc aa=new abc();
          System.out.println(aa.a);
          System.out.println(b);
    }
 }

在上面的例子中,变量 a 的默认值为0,但变量 b 会出现错误,因为它可能未被初始化。


14
int a 实际上有一个默认值为 0 - FThompson
1
对象的整个内存块总是填充为零,这就是为什么对象中的所有变量默认为0。b是一个局部变量,在那部分代码中没有被初始化。当赋值时,它会被初始化。 - x4rf41
1
但是为什么变量y和b没有默认值呢? - Shikhil Bhalla
5
a不同,b在栈中分配,出于性能考虑,它没有被清零。(更准确地说:实现不需要将其清零,因为这会限制实现的性能而带来很少的好处) - kiheru
4
请使用大写 -- 我再也不会编辑你的帖子了。 - Dexygen
显示剩余2条评论
4个回答

16

所有成员变量必须加载到堆中,因此在创建类的实例时必须使用默认值进行初始化。对于局部变量,在Java 7之前它们不会加载到堆中,而是存储在堆栈中,直到被使用,因此我们需要显式地初始化它们。现在,“Java Hotspot服务器编译器”执行“逃逸分析”,并决定将一些变量分配到堆栈而不是堆中。


本地变量加载到堆中?你注意到 OP 正在谈论一个原始类型 int 吗?o.O - Bruno Reis
是的,那不是堆,而是栈。 - Ashwani
但在Java 7之前,局部变量存储在堆栈上,实例变量存储在堆上。 - Ashwani
2
我认为这里存在一个巨大的误解。我并不是特别谈论Java 7或逃逸分析。哦,随便了。我放弃了,希望人们不会感到困惑。 - Bruno Reis
2
“在Java 7之前,将它们加载到堆栈中而未使用是没有意义的”,“因此我们需要显式初始化它们”是一个“非因果关系”。这里实际上没有回答问题。前两个句子只是重申了问题,其余部分只是一堆混乱的废话。最后一句话是无关紧要的。@BrunoReis 我完全同意。 - user207421
显示剩余4条评论

2

局部变量初始化

在方法和块中声明的变量被称为局部变量。当在方法调用时创建局部变量时,它们不会被初始化。因此,在使用局部变量之前必须显式地进行初始化。否则,当包含该方法或块被执行时,编译器将标记其为错误。

例如:

public class SomeClassName{

public static void main(String args[]){
int total;
System.out.println("The incremented total is " + total + 3); //(1)
}
}

编译器在(1)处的println语句中使用的局部变量total可能未初始化。 在使用前初始化局部变量total可以解决这个问题:
public class SomeClassName{

public static void main(String args[]){
int total = 45; //Local variable initialized with value 45 System.out.println("The incremented total is " + total+ 3); //(1)
}
}

字段初始化

如果在声明变量或初始化块中没有提供初始化,则该实例或静态变量会隐式地被初始化为其类型的默认值。每当从类创建对象时,实例变量将被初始化为其类型的默认值。 当加载类时,静态变量将被初始化为其类型的默认值


2
但问题仍然没有得到解答。即使局部变量存储在堆栈上,为什么它们不被分配默认值。 - Ankur Shanbhag
2
请参阅Java语言规范:http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5 引用:“局部变量(§14.4,§14.14)在使用之前必须明确地赋值,可以通过初始化(§14.4)或赋值(§15.26)的方式进行验证,以符合明确赋值规则(§16)的规定。” - Robert M.
@Ankur 看看最后一部分---字段初始化 - Dileep

1
作为局部变量分配在堆栈上,当局部变量被赋值时,内存块会被分配给它。
以简单的例子为例。
class Abc {
   int i = -111;
   int e;

   int doSomething() {
        int a = 10;
        int b = a + i;    
        int c = b + 100;

        Abc d = new Abc();

        e = b + c + d.a;

        return e + 1000;
    }
 }

并且从 javap -c Abc 得到的字节码

Compiled from "Abc.java"
class Abc {
  int i;
  int e;

  Abc();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        -111
       7: putfield      #2                  // Field i:I
      10: return

  int doSomething();
    Code:
       0: bipush        10
       2: istore_1
       3: iload_1
       4: aload_0
       5: getfield      #2                  // Field i:I
       8: iadd
       9: istore_2
      10: iload_2
      11: bipush        100
      13: iadd
      14: istore_3
      15: new           #3                  // class Abc
      18: dup
      19: invokespecial #4                  // Method "<init>":()V
      22: astore        4
      24: aload_0
      25: iload_2
      26: iload_3
      27: iadd
      28: aload         4
      30: getfield      #2                  // Field i:I
      33: iadd
      34: putfield      #5                  // Field e:I
      37: aload_0
      38: getfield      #5                  // Field e:I
      41: sipush        1000
      44: iadd
      45: ireturn
}

当调用一个方法时,会在堆栈中分配一个名为current frame的内存空间。
如果仔细观察,即使是int a=-111;这样的赋值也发生在隐式初始化函数Abc()中!
       int a = -111;

       5: bipush        -111
       7: putfield      #2                  // Field a:I

作为字段变量,如果未被赋值,则其为原始类型时将为0,或者是对象引用时将为null。
如果您查看doSomething()函数。
        int a = 10;
        0: bipush        10

为了使用本地变量,需要将初始值推入堆栈,例如10。如果没有这个“push”[初始化],a的值对于后续语句是不可访问的(因为该值不在堆栈上)。一旦将值推入堆栈,就可以在堆栈上执行其他操作,如iadd istore等。

下面的语句实际上在堆空间上创建一个对象并调用init方法。这是未初始化变量(如“e”)获得默认值的地方

      15: new           #3                  // class Abc
      18: dup

我将更进一步的字节码比较留给你处理 ;) 但我希望这很明显


下定决心。局部变量的“内存块”要么在分配值时分配,要么在进入方法时分配。不可能同时进行。ipush语句将值10推入堆栈槽中,紧随其后的'istore'指令将其存储到a的堆栈槽中。推送操作并不会“创建”a - user207421

0

简而言之:这更多或少是一个任意选择

如果你问我,Java为实例变量设置默认值是一个错误。编译器应该强制程序员在使用之前初始化它,就像对于局部变量的情况一样。

默认值背后的理念是安全性。当一个对象被实例化时,将为该对象分配一块内存,其中包含实例变量指向的位置等信息。Java设计者决定用零和null来清除这部分内存。这样,您永远不会读取到在对象分配之前存在的垃圾数据。他们本可以强制初始化;这个选择并没有什么根本性的东西。这可能使实现变得容易,并且对Java的设计者来说足够有意义。

对于局部变量,设计者选择强制初始化(或者也许更准确地说,当局部变量只被声明时,他们选择不进行任何初始化,因此编译器最合乎逻辑的行为是在使用之前强制初始化变量)。


编译器无法知道实例成员在赋值之前是否被使用。编译器不知道类的哪个方法会首先被调用。另一方面,对于方法的相同分析是可以进行的,因为只有一个入口点。 - user207421
@EJP Kotlin似乎做得相当不错... - Enno Shioji
Java不需要那么聪明。 - weakish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接