Veamos el orden en el que se ejecuta el código en los bloques de inicialización (estáticos y no estáticos), los constructores y la inicialización de los campos estáticos y no estáticos. Investigaremos lo que sucede en la práctica ejecutando código.

Para empezar, tenemos una clase con un conjunto completo de todos los elementos posibles:

public class MyClass {
    static {
        System.out.println("Static Block #1.");
    }

    public static String staticField = setStaticField();

    public MyClass() {
        System.out.println("Constructor.");
    }

    static {
        System.out.println("Static Block #2.");
    }

    {
        System.out.println("Initialization Block #1.");
    }

    public String nonStaticField = setNonStaticField();

    {
        System.out.println("Initialization Block #2.");
    }

    private String setNonStaticField() {
        System.out.println("Non-static field.");
        return "nonStaticField";
    }

    private static String setStaticField() {
        System.out.println("Static field.");
        return "staticField";
    }

    public static void print() {
        System.out.println("print() method.");
    }
}

Ahora, junto con esta clase, crearemos otra con un método main y lo ejecutaremos:

public class Solution {
    public static void main(String args[]) {
        System.out.println("hello");
    }
}

El resultado no incluye nada de la clase MyClass. No hubo llamadas a los métodos de MyClass, por lo que la clase ni siquiera se cargó. Ahora intentemos llamar al método estático print() de la clase MyClass. Dos veces.

public class Solution {
    public static void main(String args[]) {
        MyClass.print();
        MyClass.print();
    }
}

Resultado:

Static Block #1.
Static field.
Static Block #2.
print() method.
print() method.

Solo los bloques de inicialización estáticos se ejecutaron y el campo estático se inicializó. Y esto solo sucedió una vez. Esto se debe a que la clase ya estaba cargada cuando hicimos la segunda llamada al método print(). Recuerda: los campos estáticos se inicializan y los bloques de inicialización se ejecutan una vez en la primera interacción con la clase.

Ten en cuenta que los bloques estáticos se ejecutan y los campos se inicializan en el orden en que se declaran.

A continuación, en lugar de llamar a un método estático, intentemos crear dos objetos de nuestra clase:

public class Solution {
    public static void main(String args[]) {
        new MyClass();
        System.out.println();
        new MyClass();
    }
}

Resultado:

Static Block #1.
Static field.
Static Block #2.
Initialization Block #1.
Non-static field.
Initialization Block #2.
Constructor.

Initialization Block #1.
Non-static field.
Initialization Block #2.
Constructor.

Primero, los bloques estáticos se ejecutan y los campos estáticos se inicializan una vez. Después de eso, cada vez que se crea un objeto, se procesan los bloques no estáticos, los campos y un constructor. Los campos y bloques de inicialización se procesan en el orden de su declaración, pero el constructor se procesa al final, independientemente de donde esté declarado.

Vamos a complicar el ejemplo: tomaremos dos clases, donde una hereda de la otra:

public class ParentClass {
    static {
        System.out.println("Static Block #1 of the parent class.");
    }

    public static String parentStatic = setParentStatic();

    static {
        System.out.println("Static Block #2 of the parent class.");
    }

    {
        System.out.println("Initialization Block #1 of the parent class.");
    }

    public String parentNonStatic = setParentNonStatic();

    {
        System.out.println("Initialization Block #2 of the parent class.");
    }

    public ParentClass() {
        System.out.println("Constructor of the parent class.");
    }

    private String setParentNonStatic() {
        System.out.println("Non-static field of the parent class.");
        return "parentNonStatic";
    }

    private static String setParentStatic() {
        System.out.println("Static field of the parent class.");
        return "parentStatic";
    }

    public String setChildNonStatic1() {
        System.out.println("Non-static field of the child class #1.");
        return "childNonStatic2" + parentNonStatic;
    }
}

public class ChildClass extends ParentClass {
    static {
        System.out.println("Static Block #1 of the child class.");
    }

    public static String childStatic = setChildStatic();

    static {
        System.out.println("Static Block #2 of the child class.");
    }

    public String childNonStatic1 = setChildNonStatic1();

    {
        System.out.println("Initialization Block #1 of the child class.");
    }

    public String childNonStatic2 = setChildNonStatic2();

    {
        System.out.println("Initialization Block #2 of the child class.");
    }

    public ChildClass() {
        System.out.println("Constructor of the child class.");
    }

    private String setChildNonStatic2() {
        System.out.println("Non-static field of the child class #2.");
        return "childNonStatic";
    }

    private static String setChildStatic() {
        System.out.println("Static field of the child class.");
        return "childStatic";
    }
}

Creemos dos objetos de la clase hija:

public class Solution {
    public static void main(String[] args) {
        new ChildClass();
        System.out.println();
        new ChildClass();
    }
}

Resultado:

Static Block #1 of the parent class.
Static field of the parent class.
Static Block #2 of the parent class.
Static Block #1 of the child class.
Static field of the child class.
Static Block #2 of the child class.
Initialization Block #1 of the parent class.
Non-static field of the parent class.
Initialization Block #2 of the parent class.
Constructor of the parent class.
Non-static field of the child class #1.
Initialization Block #1 of the child class.
Non-static field of the child class #2.
Initialization Block #2 of the child class.
Constructor of the child class.

Initialization Block #1 of the parent class.
Non-static field of the parent class.
Initialization Block #2 of the parent class.
Constructor of the parent class.
Non-static field of the child class #1.
Initialization Block #1 of the child class.
Non-static field of the child class #2.
Initialization Block #2 of the child class.
Constructor of the child class.

En el nuevo resultado, vemos que los bloques y variables estáticas de la clase padre se procesan antes que los bloques y variables estáticas de la clase hija. Lo mismo sucede con los bloques y variables no estáticos y los constructores: primero la clase padre y luego la clase hija. Para entender por qué esto es necesario, veamos el ejemplo del campo childNonStatic1 de la clase hija. Se utiliza un método de la clase padre para inicializarlo, y ese método a su vez utiliza una variable de la clase padre. Eso significa que cuando se inicializa el campo childNonStatic1, la clase padre y sus métodos ya deben estar cargados y las variables de la clase padre deben estar inicializadas.

En la práctica, es posible que no encuentres clases que incluyan todos estos elementos al mismo tiempo, pero será útil recordar qué se inicializa antes que qué. Ah, y también se pregunta mucho sobre esto en las entrevistas de trabajo.