繁体   English   中英

当我在类的构造函数中声明并初始化它们时,为什么我的字段被初始化为 null 或默认值零?

[英]Why are my fields initialized to null or to the default value of zero when I've declared and initialized them in my class' constructor?

这是一个规范的问题和类似问题的答案,其中问题是阴影的结果。


我在 class 中定义了两个字段,一个是引用类型,一个是原始类型。 在类的构造函数中,我尝试将它们初始化为一些自定义值。

当我稍后查询这些字段的值时,它们会返回 Java 的默认值, null用于引用类型,0 用于原始类型。 为什么会这样?

这是一个可重现的示例:

public class Sample {
    public static void main(String[] args) throws Exception {
        StringArray array = new StringArray();
        System.out.println(array.getCapacity()); // prints 0
        System.out.println(array.getElements()); // prints null
    }
}

class StringArray {
    private String[] elements;
    private int capacity;
    public StringArray() {
        int capacity = 10;
        String[] elements;
        elements = new String[capacity];
    }
    public int getCapacity() {
        return capacity;
    }
    public String[] getElements() {
        return elements;
    }
}

我希望getCapacity()返回值 10 和getElements()返回正确初始化的数组实例。

Java 程序中定义的实体(包、类型、方法、变量等)都有名称 这些用于引用程序其他部分中的那些实体。

Java 语言为每个名称定义了一个范围

声明的范围是程序的区域,在该区域内可以使用简单名称引用声明所声明的实体,前提是它是可见的(第 6.4.1 节)。

换句话说,作用域是一个编译时概念,它决定了一个名称可以用来指代某个程序实体的位置。

您发布的程序有多个声明。 让我们开始

private String[] elements;
private int capacity;

这些是字段声明,也称为实例变量,即。 类体中声明的一种成员。 Java 语言规范指出

的成员的声明的范围m中声明或由类继承的类型C (§8.1.6)是整个身体C ,包括任何嵌套类型声明。

这意味着您可以使用StringArray主体中的名称elementscapacity来引用这些字段。

构造函数体中的前两个语句

public StringArray() {
    int capacity = 10;
    String[] elements;
    elements = new String[capacity];
}

实际上是局部变量声明语句

局部变量声明语句声明一个或多个局部变量名称。

这两个语句在您的程序中引入了两个新名称。 碰巧这些名称与您的字段相同。 在您的示例中, capacity的局部变量声明还包含初始化该局部变量的初始化程序,而不是同名的字段。 您的名为capacity字段被初始化为其类型的默认值,即。 0

elements的情况略有不同。 局部变量声明语句引入了一个新名称,但是赋值表达式呢?

elements = new String[capacity];

elements指的是什么实体?

范围状态规则

块中局部变量声明的范围(第 14.4 节)是该声明出现的块的其余部分,从它自己的初始值设定项开始,并包括局部变量声明语句右侧的任何其他声明符。

在这种情况下,块是构造函数体。 但是构造函数体是StringArray体的一部分,这意味着字段名称也在范围内。 那么 Java 如何确定您所指的是什么?

Java 引入了Shadowing的概念来消除歧义。

某些声明可能在其部分范围内被另一个同名声明遮蔽,在这种情况下,不能使用简单名称来引用声明的实体。

(一个简单的名字是一个单一的标识符,例如。 elements 。)

该文件还指出

的声明d名为局部变量或异常参数的n阴影,整个的范围d中,(a)指定的任何其他字段的声明n在范围上在其中点d发生,和(b)所述的任何声明其他名为n变量在d出现的点的范围内,但未在声明d的最里面的类中声明。

这意味着名为elements的局部变量优先于名为elements的字段。 表达方式

elements = new String[capacity];

因此正在初始化局部变量,而不是字段。 该字段被初始化为其类型的默认值,即。 null

在您的方法getCapacitygetElements ,您在它们各自的return语句中使用的名称指的是字段,因为它们的声明是程序中特定点范围内的唯一声明。 由于字段被初始化为0null ,这些是返回的值。

解决方案是完全摆脱局部变量声明,因此让名称引用实例变量,正如您最初想要的那样。 例如

public StringArray() {
    capacity = 10;
    elements = new String[capacity];
}

带构造函数参数的阴影

与上述情况类似,您可能有形式(构造函数或方法)参数隐藏具有相同名称的字段。 例如

public StringArray(int capacity) {
    capacity = 10; 
}

阴影规则状态

一个声明d字段或命名的形式参数的n阴影,整个范围d ,任何其他变量的声明命名n在范围上的点在哪里d发生。

在上面的例子中,构造函数参数capacity的声明影响了同样命名为capacity的实例变量的声明。 因此不可能用简单的名称来引用实例变量。 在这种情况下,我们需要用它的限定名来引用它。

限定名称由名称、“.”组成。 令牌和标识符。

在这种情况下,我们可以使用主表达式this作为字段访问表达式的一部分来引用实例变量。 例如

public StringArray(int capacity) {
    this.capacity = 10; // to initialize the field with the value 10
    // or
    this.capacity = capacity; // to initialize the field with the value of the constructor argument
}

每种变量、方法和类型都有阴影规则。

我的建议是尽可能使用唯一的名称,以完全避免这种行为。

int capacity = 10; 在您的构造函数中声明了一个局部变量capacity ,它隐藏了类的字段。

补救措施是删除int

capacity = 10;

这将更改字段值。 班级中的其他领域也是如此。

您的 IDE 没有警告您这种阴影吗?

另一个被广泛接受的约定是在类成员中添加一些前缀(或后缀 - 无论您喜欢什么),以将它们与局部变量区分开来。

例如带有m_前缀的类成员:

class StringArray {
  private String[] m_elements;
  private int      m_capacity;

  public StringArray(int capacity) {
    m_capacity = capacity;
    m_elements = new String[capacity];
  }

  public int getCapacity() {
    return m_capacity;
  }

  public String[] getElements() {
    return m_elements;
  }
}


大多数 IDE 已经支持这种表示法,下面是针对 Eclipse 的

在此处输入图片说明

在 java/c/c++ 中使用变量有两个部分。 一种是声明变量,另一种是使用变量(无论是赋值还是在计算中使用)。

当你声明一个变量时,你必须声明它的类型。 所以你会使用

int x;   // to declare the variable
x = 7;   // to set its value

使用变量时不必重新声明变量:

int x;
int x = 7;   

如果变量在同一范围内,您将收到编译器错误; 但是,正如您所发现的,如果变量在不同的范围内,您将屏蔽第一个声明。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM