Full Stack Blog – C# vs Java. Инициализация статических членов класса

16 April 2023

C# vs Java. Инициализация статических членов класса

В языках Java и C# присутствуют статические члены класса. Для инициализации статических полей нам доступен механизм, называемый "static constructors" в C# и "static block" в Java.

Также, данный механизм возможно использовать если требуется только один раз что-либо выполнить. Но в этом случае стоит обратить внимание на то тогда вызывается код для статической инициализации и что в этот момент инициализировано и может быть использовано.

Java

Static initialization

Не отличается от определения с инициализацией для не статических переменных.

class Plant {
    public static long globalId = 0;
}

При наследовании статические поля инициализируются в следующей последовательности

class BasePlant {
    public static long basicId = Note.printMessage("BasePlant: 'basicId' initialization");
}
class Plant extends BasePlant {
    public static long globalId = Note.printMessage("Plant: 'globalId' initialization");
}

System.out.println("checkpoint 1");
System.out.println("basicId = " + BasePlant.basicId);
System.out.println("checkpoint 2");
System.out.println("globalId = " + Plant.globalId);
System.out.println("checkpoint 3");

// OUTPUT
// checkpoint 1
// BasePlant: 'basicId' initialization
// globalId = 77
// checkpoint 2
// Plant: 'globalId' initialization
// globalId = 77
// checkpoint 3

Изменение порядка обращения к полям приводит к предсказуемому результату

System.out.println("checkpoint 1");
System.out.println("globalId = " + Plant.globalId);
System.out.println("checkpoint 2");
System.out.println("basicId = " + BasePlant.basicId);
System.out.println("checkpoint 3");

// OUTPUT
// checkpoint 1
// BasePlant: 'basicId' initialization
// Plant: 'globalId' initialization
// globalId = 77
// checkpoint 2
// basicId = 77
// checkpoint 3

Static block

Статический блок – это блок кода, который используется для инициализации статических полей.

Пример использования:

class BasePlant {
    public static long basicId = 0;
    static {
        System.out.println("BasePlant: static block #1");
        basicId = 3;
    }
}
class Plant extends BasePlant {
    public static long globalId = 0;
    static {
        System.out.println("Plant: static block #1");
        basicId = 7;
        globalId = 7;
    }
    static {
        System.out.println("Plant: static block #2");
        basicId = 13;
        globalId = 13;
    }
}

System.out.println("checkpoint 1");
System.out.println("basicId = " + BasePlant.basicId);
System.out.println("checkpoint 2");
System.out.println("globalId = " + Plant.globalId);
System.out.println("checkpoint 3");

// OUTPUT
// checkpoint 1
// BasePlant: static block #1
// basicId = 3
// checkpoint 2
// Plant: static block #1
// Plant: static block #2
// globalId = 13
// checkpoint 3

Блоки будут выполняться в порядке их определения в классе.

Статические блоки будут выполнены только один раз при создании инстанса или при обращении к статическому полю класса.

System.out.println("checkpoint 1");
var plant = new Plant();
System.out.println("checkpoint 2");

// OUTPUT
// checkpoint 1
// BasePlant: static block #1
// Plant: static block #1
// Plant: static block #2
// checkpoint 2

Необработанные исключения в static block

При попытке использовать исключения в static block компилятор заругает нас: Initializer must be able to complete normally. Возможно возникновение RuntimeException в процессе работы приложения.

Если RuntimeException возникнет в static block, то это приведет к подобному результату

checkpoint 1
In the static block
Exception while initializing/ by zero
Exception in thread "main" java.lang.ExceptionInInitializerError
    at Main.start(Main.java:46)
    at Main.main(Main.java:38)
Caused by: java.lang.RuntimeException: / by zero
    at Main$BasePlant.<clinit>(Main.java:20)
    ... 2 more

в этом примере использовалось деление на ноль, чтобы вызвать исключение

Последовательность инициализации

- статические переменные, в порядке определения
- "static block"(s), в порядке определения
- переменные экземпляра, в порядке определения
- "instance initializer"(s), в порядке определения
- конструктор


Следующий пример показывает что при обращении к потомку будут вызваны инициализаторы класса предка

System.out.println("checkpoint 1");
System.out.println("globalId = " + Plant.globalId);
System.out.println("checkpoint 2");
System.out.println("basicId = " + BasePlant.basicId);
System.out.println("checkpoint 3");

// OUTPUT
// checkpoint 1
// BasePlant: static block #1
// Plant: static block #1
// Plant: static block #2
// globalId = 13
// checkpoint 2
// basicId = 13
// checkpoint 3

Блоки будут выполняться в порядке их определения в классе. Сначала в базовом классе, потом в классе потомке.

В интерфейсах

Допускается инициализация статических полей в интерфейсе

interface PlantInterface {
    long globalId = Note.printMessage("PlantInterface: initialization");
}

System.out.println("checkpoint 1");
System.out.println("globalId = " + PlantInterface.globalId);
System.out.println("checkpoint 2");

// OUTPUT 
// checkpoint 1
// PlantInterface: initialization
// globalId = 77
// checkpoint 2

Но! запрещено использовать static block в интерфейсах.

C#

Static initialization

Тут все аналогично Java

class Plant
{
    public static long GlobalId = Note.PrintMessage("Plant: static initialization");
}

Console.WriteLine("checkpoint 1");
Console.WriteLine("GlobalId=" + Plant.GlobalId);
Console.WriteLine("checkpoint 2");

// OUTPUT 
// checkpoint 1
// Plant: static initialization
// GlobalId=13
// checkpoint 2

Static constructors

Статический конструктор используется для инициализации любых статических данных или для выполнения определенного действия, которое необходимо выполнить только один раз. Он вызывается автоматически перед созданием первого экземпляра или ссылками на какие-либо статические элементы. Статический конструктор будет вызван не более одного раза.

class BasePlant
{
    public static long basicId = 0;
    static BasePlant()
    {
        Console.WriteLine("BasePlant: static block #1");
        basicId = 3;
    }
}
class Plant: BasePlant
{
    public static long globalId = 0;
    static Plant()
    {
        Console.WriteLine("Plant: static constructor");
        basicId = 7;
        globalId = 7;
    }
}

Console.WriteLine("checkpoint 1");
Console.WriteLine("globalId = " + BasePlant.basicId);
Console.WriteLine("checkpoint 2");
Console.WriteLine("globalId = " + Plant.globalId);
Console.WriteLine("checkpoint 3");

// OUTPUT
// checkpoint 1
// BasePlant: static block #1
// globalId = 3
// checkpoint 2
// Plant: static constructor
// globalId = 7
// checkpoint 3
  • Статический конструктор не наследуется.
  • В классе может быть только один статический конструктор
  • Статический конструктор не имеет параметров и модификатора доступа

Необработанные исключения в static block

Необработанное исключение в статическом конструкторе вызовет что-то подобное

System.TypeInitializationException: 'The type initializer for 'ConsoleApp1.Plant' threw an exception.'

В отличии от Java компилятор C# не сопротивляется использованию исключений и пропускает например такое

class BasePlant
{
    public static long basicId = 0;
    static BasePlant()
    {
        Console.WriteLine("BasePlant: static block #1");
        throw new ArgumentException();
        basicId = 3;
    }
}

Последовательность инициализации

- static constructor текущего класса
- static constructors базового класса
Console.WriteLine("checkpoint 1");
Console.WriteLine("globalId = " + Plant.globalId);
Console.WriteLine("checkpoint 2");            
Console.WriteLine("globalId = " + BasePlant.basicId);
Console.WriteLine("checkpoint 3");

// OUTPUT
// checkpoint 1
// Plant: static constructor
// BasePlant: static block #1
// globalId = 7
// checkpoint 2
// globalId = 7
// checkpoint 3

Статический конструктор будет вызван перед созданием экземпляра класса или перед первым доступом к статическому члену. Сначала вызывается статический конструктор класса наследника а потом статический конструктор базового класса.

Пример с созданием инстанса

Console.WriteLine("checkpoint 1");
var plant = new Plant();
Console.WriteLine("checkpoint 2");    

// OUTPUT
// checkpoint 1
// Plant: static constructor
// BasePlant: static block #1
// checkpoint 2

В интерфейсах

В C# допустимо использовать статический конструктор в интерфейсах для инициализации статических членов

interface IPlant
{
    static long GlobalId { get; }
    static IPlant()
    {
        GlobalId = 3;
    }
}

Console.WriteLine("checkpoint 1");
Console.WriteLine("GlobalId = " + IPlant.GlobalId);
Console.WriteLine("checkpoint 2"); 

// OUTPUT
// checkpoint 1
// GlobalId = 3
// checkpoint 2

Так как статические члены интерфейса доступны только через интерфейс, то инициализация статических членов интерфейса не будет выполняться при создании объекта класса, который наследует интерфейс

class Plant: BasePlant, IPlant
{
    ...
}

Console.WriteLine("checkpoint 1");
var plant = new Plant();
Console.WriteLine("checkpoint 2");
Console.WriteLine("GlobalId=" + IPlant.GlobalId);
Console.WriteLine("checkpoint 3");

// OUTPUT
// checkpoint 1
// Plant: static constructor
// BasePlant: static block #1
// checkpoint 2
// IPlant: static constructor
// GlobalId=3
// checkpoint 3