Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
134 views
in Technique[技术] by (71.8m points)

java - 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?

This is meant to be a canonical question and answer for similar questions where the issue is a result of shadowing.


I've defined two fields in my class, one of a reference type and one of a primitive type. In the class' constructor, I try to initialize them to some custom values.

When I later query for those fields' values, they come back with Java's default values for them, null for the reference type and 0 for the primitive type. Why is this happening?

Here's a reproducible example:

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

I expected getCapacity() to return the value 10 and getElements() to return a properly initialized array instance.

Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Entities (packages, types, methods, variables, etc.) defined in a Java program have names. These are used to refer to those entities in other parts of a program.

The Java language defines a scope for each name

The scope of a declaration is the region of the program within which the entity declared by the declaration can be referred to using a simple name, provided it is visible (§6.4.1).

In other words, scope is a compile time concept that determines where a name can be used to refer to some program entity.

The program you've posted has multiple declarations. Let's start with

private String[] elements;
private int capacity;

These are field declarations, also called instance variables, ie. a type of member declared in a class body. The Java Language Specification states

The scope of a declaration of a member m declared in or inherited by a class type C (§8.1.6) is the entire body of C, including any nested type declarations.

This means you can use the names elements and capacity within the body of StringArray to refer to those fields.

The two first statements in your constructor body

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

are actually local variable declaration statements

A local variable declaration statement declares one or more local variable names.

Those two statements introduce two new names in your program. It just so happens that those names are the same as your fields'. In your example, the local variable declaration for capacity also contains an initializer which initializes that local variable, not the field of the same name. Your field named capacity is initialized to the default value for its type, ie. the value 0.

The case for elements is a little different. The local variable declaration statement introduces a new name, but what about the assignment expression?

elements = new String[capacity];

What entity is elements referring to?

The rules of scope state

The scope of a local variable declaration in a block (§14.4) is the rest of the block in which the declaration appears, starting with its own initializer and including any further declarators to the right in the local variable declaration statement.

The block, in this case, is the constructor body. But the constructor body is part of the body of StringArray, which means field names are also in scope. So how does Java determine what you're referring to?

Java introduces the concept of Shadowing to disambiguate.

Some declarations may be shadowed in part of their scope by another declaration of the same name, in which case a simple name cannot be used to refer to the declared entity.

(a simple name being a single identifier, eg. elements.)

The documentation also states

A declaration d of a local variable or exception parameter named n shadows, throughout the scope of d, (a) the declarations of any other fields named n that are in scope at the point where d occurs, and (b) the declarations of any other variables named n that are in scope at the point where d occurs but are not declared in the innermost class in which d is declared.

This means that the local variable named elements takes priority over the field named elements. The expression

elements = new String[capacity];

is therefore initializing the local variable, not the field. The field is initialized to the default value for its type, ie. the value null.

Inside your methods getCapacity and getElements, the names you use in the in their respective return statements refer to the fields since their declarations are the only ones in scope at that particular point in the program. Since the fields were initialized to 0 and null, those are the values returned.

The solution is to get rid of the local variable declarations altogether and therefore have the names refer to the instance variables, as you originally wanted. For example

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

Shadowing with constructor parameters

Similar to the situation described above, you may have formal (constructor or method) parameters shadowing fields with the same name. For example

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

Shadowing rules state

A declaration d of a field or formal parameter named n shadows, throughout the scope of d, the declarations of any other variables named n that are in scope at the point where d occurs.

In the example above, the declaration of the constructor parameter capacity shadows the declaration of the instance variable also named capacity. It's therefore impossible to refer to the instance variable with its simple name. In such cases, we need to refer to it with its qualified name.

A qualified name consists of a name, a "." token, and an identifier.

In this case, we can use the primary expression this as part of a field access expression to refer to the instance variable. For example

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
}

There are Shadowing rules for every kind of variable, method, and type.

My recommendation is that you use unique names wherever possible so as to avoid the behavior altogether.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...