【问题标题】: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 或默认值零?
【发布时间】:2017-06-26 07:44:36
【问题描述】:

这是针对类似问题的规范问答,其中问题是由遮蔽造成的。


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

当我稍后查询这些字段的值时,它们会返回 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/c/c++ 中使用变量有两个部分。一种是声明变量,另一种是使用变量(无论是赋值还是在计算中使用)。

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

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

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

    int x;
    int x = 7;   
    

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

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

        【解决方案3】:

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

        补救方法是删除int

        capacity = 10;

        这将更改字段值。类中的其他字段也是如此。

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

        【讨论】:

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

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

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

        声明的范围是程序所在的区域 声明所声明的实体可以使用 简单的名称,只要它是可见的(§6.4.1)。

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

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

        private String[] elements;
        private int capacity;
        

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

        成员 m 的声明范围在 a 中声明或继承 类类型 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的异常参数 shadows,在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; 
        }
        

        阴影规则状态

        一个名为n shadows 的字段或形参的声明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、方法和类型都有阴影规则。

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

        【讨论】:

          猜你喜欢
          • 2020-05-01
          • 2020-05-01
          • 1970-01-01
          • 2016-02-04
          • 1970-01-01
          • 2016-02-22
          • 1970-01-01
          • 1970-01-01
          • 2013-03-05
          相关资源
          最近更新 更多