【问题标题】: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?当我在类的构造函数中声明并初始化字段时,为什么我的字段被初始化为 null 或默认值零?
【发布时间】:2020-05-01 23:48:12
【问题描述】:

这是针对类似问题的规范问答,其中问题是由于阴影.


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

当我稍后查询这些字段的值时,它们返回 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 constructor field


    【解决方案1】:

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

    Java语言为每个名字定义了一个scope

    范围声明的是程序的区域,其中 声明声明的实体可以使用 简单名称,前提是它是可见的(§6.4.1)。

    换句话说,范围是一个编译时概念,它确定名称可用于指代某些程序实体的位置。

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

    private String[] elements;
    private int capacity;
    

    这些是 field 声明,也称为 instance variables,即。在 class body 中声明的一种成员。 Java 语言规范指出

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

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

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

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

    实际上是local variable declaration statements

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

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

    elements 的情况有点不同。局部变量声明语句引入了一个新名称,但是assignment expression呢?

    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];
    

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

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

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

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

    使用构造函数参数进行阴影

    与上述情况类似,您可能有 formal (constructor or method) parameters 同名隐藏字段。例如

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

    阴影规则状态

    名为nshadows 的字段或形式参数的声明d, 在 d 的整个范围内,任何其他变量的声明 命名为n,它们在d出现的范围内。

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

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

    在这种情况下,我们可以使用 primary expression this 作为 field access expression 的一部分来引用实例变量。例如

    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
    }
    

    阴影每个kind of variable、方法和类型的规则。

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

    【讨论】:

      【解决方案2】:

      int capacity = 10;在你的构造函数中声明了一个局部变量capacity,它阴影班级的领域。

      补救措施是删除int

      capacity = 10;

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

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

      【讨论】:

      • Eclipse 似乎没有。 IntelliJ 或您正在使用的任何东西都可以吗?
      • 用 setter 或构造函数参数隐藏类变量是很常见的。如果 IDE 会警告您,我会感到惊讶。
      【解决方案3】:

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

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

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

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

      int x;
      int x = 7;   
      

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

      【讨论】:

        【解决方案4】:

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

        例如带有 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 的

        【讨论】:

          猜你喜欢
          • 2020-05-01
          • 2017-06-26
          • 2016-02-22
          • 1970-01-01
          • 1970-01-01
          • 2013-08-14
          • 2020-01-18
          • 1970-01-01
          • 2011-06-22
          相关资源
          最近更新 更多