Static Initializers And Static Methods In Java

2020-02-12 03:13发布

问题:

Does calling a static method on a class in Java trigger the static initalization blocks to get executed?

Empirically, I'd say no. I have something like this:

public class Country {
    static {
        init();
        List<Country> countries = DataSource.read(...); // get from a DAO
        addCountries(countries);
    }

    private static Map<String, Country> allCountries = null;

    private static void init() {
        allCountries = new HashMap<String, Country>();
    }

    private static void addCountries(List<Country> countries) {
        for (Country country : countries) {
            if ((country.getISO() != null) && (country.getISO().length() > 0)) {
                allCountries.put(country.getISO(), country);
            }
        }
    }

    public static Country findByISO(String cc) {
        return allCountries.get(cc);
    }
}

In the code using the class, I do something like:

Country country = Country.findByISO("RO");

The problem is that I get a NullPointerException because the map (allCountries) is not initialized. If I set up breakpoints in the static block I can see the map getting populated correctly, but it's as if the static method has no knowledge of the initializer being executed.

Can anyone explain this behavior?


Update: I've added more detail to the code. It's still not 1:1 (there are several maps in there and more logic), but I've explicitly looked at the declarations/references of allCountries and they are as listed above.

You can see the full initialization code here.

Update #2: I tried to simplify the code as much as possible and wrote it down on the fly. The actual code had the static variable declaration after the initializer. That caused it to reset the reference, as Jon pointed out in the answer below.

I modified the code in my post to reflect this, so it's clearer for people who find the question. Sorry about the confusion everyone. I was just trying to make everyone's life easier :).

Thanks for your answers!

回答1:

Does calling a static method on a class in Java trigger the static initalization blocks to get executed?

Empirically, I'd say no.

You're wrong.

From the JLS section 8.7:

A static initializer declared in a class is executed when the class is initialized (§12.4.2). Together with any field initializers for class variables (§8.3.2), static initializers may be used to initialize the class variables of the class.

Section 12.4.1 of the JLS states:

A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

  • T is a class and an instance of T is created.

  • T is a class and a static method declared by T is invoked.

  • A static field declared by T is assigned.

  • A static field declared by T is used and the field is not a constant variable (§4.12.4).

  • T is a top level class (§7.6), and an assert statement (§14.10) lexically nested within T (§8.1.3) is executed.

This is easily shown:

class Foo {
    static int x = 0;
    static {
        x = 10;
    }

    static int getX() {
        return x;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println(Foo.getX()); // Prints 10
    }
}

Your problem is in some part of the code that you didn't show us. My guess is that you're actually declaring a local variable, like this:

static {
    Map<String, Country> allCountries = new HashMap<String, Country>();
    // Add entries to the map
}

That hides the static variable, leaving the static variable null. If this is the case, just change it to an assignment instead of a declaration:

static {
    allCountries = new HashMap<String, Country>();
    // Add entries to the map
}

EDIT: One point worth noting - although you've got init() as the very first line of your static initializer, if you're actually doing anything else before then (possibly in other variable initializers) which calls out to another class, and that class calls back into your Country class, then that code will be executed while allCountries is still null.

EDIT: Okay, now we can see your real code, I've found the problem. Your post code has this:

private static Map<String, Country> allCountries;
static {
    ...
}

But your real code has this:

static {
    ...
}
private static Collection<Country> allCountries = null;

There are two important differences here:

  • The variable declaration occurs after the static initializer block
  • The variable declaration includes an explicit assignment to null

The combination of those is messing you up: the variable initializers aren't all run before the static initializer - initialization occurs in textual order.

So you're populating the collection... and then setting the reference to null.

Section 12.4.2 of the JLS guarantees it in step 9 of the initialization:

Next, execute either the class variable initializers and static initializers of the class, or the field initializers of the interface, in textual order, as though they were a single block.

Demonstration code:

class Foo {

    private static String before = "before";

    static {
        before = "in init";
        after = "in init";
        leftDefault = "in init";
    }

    private static String after = "after";
    private static String leftDefault;

    static void dump() {
        System.out.println("before = " + before);
        System.out.println("after = " + after);
        System.out.println("leftDefault = " + leftDefault);
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Foo.dump();
    }
}

Output:

before = in init
after = after
leftDefault = in init

So the solution is either to get rid of the explicit assignment to null, or to move the declarations (and therefore initializers) to before the static initializer, or (my preference) both.



回答2:

The static initializer will get called when the class is loaded, which is normally when it is first 'mentioned'. So calling a static method would indeed trigger the initializer if this is the first time that the class gets referenced.

Are you sure the null pointer exception is from the allcountries.get(), and not from a null Country returned by get()? In other words, are you certain which object is null?



回答3:

Theoretically, static block should get executed by the time classloader loads the class.

Country country = Country.findByISO("RO");
^

In your code, it is initialized the first time you mention the class Country (probably the line above).

I ran this:

public class Country {
    private static Map<String, Country> allCountries;
    static {
        allCountries = new HashMap<String, Country>();
        allCountries.put("RO", new Country());
    }

    public static Country findByISO(String cc) {
        return allCountries.get(cc);
    }
}

with this:

public class Start
{
    public static void main(String[] args){
        Country country = Country.findByISO("RO");
        System.out.println(country);
    }
}

and everything worked correctly. Can you post the stack trace of the error?

I would say that the problem lies in the fact that the static block is declared before the actual field.



回答4:

Do you have allCountries = new HashMap(); in your static initializer block? The static initializer block is actually called upon class initialization.