Ключевые слова static и final

Это занятие начнем с рассмотрения довольно значимой конструкции ООП – статических полей и методов класса. Возможно, вы уже обращали внимание, на ключевое слово static перед функцией main:

public static void main(String[] args) { ... }

Сейчас мы разберемся, что оно означает. В любом классе при объявлении переменных можно перед их типами прописывать это ключевое слово, например, так:

class Point {
    static int cnt;
    int x, y;
}

Спрашивается: в чем отличие между полями x, y и статическим полем cnt? Лучше всего это продемонстрировать на следующем рисунке.

Когда создаются объекты класса Point:

Point pt1 = new Point();
Point pt2 = new Point();

то в каждый из них будут помещены копии полей x, y, но поле cnt скопировано не будет, оно останется на уровне класса Point и станет общим для всех экземпляров этого класса. То есть, при обращении к статической переменной cnt из любого экземпляра класса, например, вот так:

class Point {
    static int cnt;
    int x, y;
 
    {
        x = y = -1;
    }
 
    Point() {
        cnt++;
    }
}

Мы при создании нового объекта в конструкторе будем увеличивать общую для всех экземпляров переменную cnt на единицу. И, так как эта переменная общая, то при создании двух объектов этот счетчик будет равен двум. В этом можно убедиться, если вывести его значение в консоль:

System.out.println(Point.cnt);

Обратите внимание, мы обращаемся к статическому полю cnt напрямую через класс Point, т.к. оно хранится непосредственно в нем. То же самое будет, если обратиться к этой переменной через ссылку pt1:

System.out.println(pt1.cnt);

Мы здесь обращаемся к той же самой статической переменной cnt. Думаю, этот момент с общим полем понятен, давайте теперь выполним его начальную инициализацию. Для обычных переменных мы использовали инициализатор в виде фигурных скобок. Статические тоже можно помещать в этот блок, но тогда они будут инициализироваться заново каждый раз при создании нового объекта:

    {
        cnt = 0;
        x = y = -1;
    }

Если выполнить программу, то в консоли вместо двух увидим 1, так как наш счетчик cnt обнуляется каждый раз при создании нового объекта. Вместе с тем, логика статических полей подразумевает их одноразовую инициализацию. Это можно сделать двумя способами. В первом, самом простом, мы присваиваем начальное значение сразу в момент объявления статической переменной:

static int cnt = 0;

Второй способ подразумевает использование статического инициализатора:

    static {
        cnt = 0;
    }

Такой инициализатор срабатывает только один раз в момент создания класса Point в памяти устройства и используется исключительно для статических полей и методов класса.

Вот так ведут себя статические переменные. Чтобы лучше понять логику их функционирования, отмечу, что ключевое слово static указывает компилятору создавать переменную один раз в процессе работы программы в определенной области памяти и не перемещать ее куда бы то ни было (в другую область памяти), в том числе не создавать ее копии. Поэтому, статическая переменная объявленная в классе Point создается внутри него и не переходит в экземпляры этого класса, а остается общей для всех.

Статические методы

Похожим образом ведут себя и статические методы классов: они принадлежат непосредственно классу и не дублируются в его экземпляры. Давайте для примера добавим статический метод в класс Point:

class Point {
    private static int cnt = 0;
    int x, y;
 
    ...
 
    public static int getCounter() {
        return cnt;
    }
}

Мы здесь сделали поле cnt приватным, а доступ к нему – через геттер getCounter. И обратите внимание в какой последовательности записываются модификаторы доступа, слово static и тип поля или метода:

[модификатор] static <тип> <имя поля или метода>

Соответственно, в функции main мы теперь должны прописать этот геттер вместо прямого обращения к cnt:

System.out.println(Point.getCounter());

В итоге картина расположения статических и нестатических полей будет следующей:

Здесь статический метод getCounter() общий для всех экземпляров класса Point. Фактически, он принадлежит только классу Point, но также может вызываться из его объектов. Именно поэтому, мы можем обращаться к нему непосредственно через класс Point, используя конструкцию:

Point.getCounter()

Так можно делать с любыми статическими методами и полями класса. Но статичность налагает и определенные ограничения. В частности, метод getCounter() может получать доступ к статической переменной cnt, но не может работать с динамическими полями x, y. Действительно, если написать что-то вроде:

    public static int getCounter() {
        return x;
    }

то возникнет ошибка, так как статический метод на уровне класса просто «не видит» переменные x, y в экземплярах. Все что ему доступно – это другие статические методы и поля класса Point. То есть, из статических методов можно вызывать только другие статические методы и обращаться исключительно к статическим переменным. Или же, создавать объекты и переменные непосредственно внутри статического метода:

    public static int getCounter() {
        int x = 5;
        return x;
    }

Отчасти это мы делаем в статическом методе main().

Статический импорт

Наконец, есть еще такое понятие как статический импорт. Что это такое? Все достаточно просто. Если в пакете имеются классы со статическими элементами, например, в пакете System мы обращаемся к классу out со статическим методом println(), то простой импорт:

import java.lang.System.*;

не импортирует такой класс, только классы без статических данных. А вот если добавить ключевое слово static:

import static java.lang.System.*;

то получим статический импорт и класс out появится в нашем текущем модуле.

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

Ключевое слово final

В заключении этого занятия я расскажу об еще одном ключевом слове final. Оно позволяет задавать константы в языке Java. Например, если у поля cnt добавить это ключевое слово, то дальнейшее изменение переменной cnt станет невозможным:

private static final int cnt = 0;

Опять же, обратите внимание на очередность ключевых слов. Конечно, это странный пример, поэтому давайте оставим переменную cnt как обычную статическую, а в класс Point добавим еще одно поле с указанием final:

final int MAX_COORD = 10;

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

final int MAX_COORD;

Ключевое слово final можно использовать и у методов. Но особенность их поведения будет проявляться только в момент наследования. Поэтому я буду говорить об этом в теме наследования классов.

Путь кодера

Подвиг 1. Объявите класс ShopItem для представления продуктов в магазине с полями: id (идентификатор – целое число), название товара, габариты, вес, цена. Поле id должно быть уникальным для каждого объекта класса. Это следует реализовать через статическую переменную, которая подсчитывает количество создаваемых экземпляров.

Подвиг 2. Реализовать класс Rect для описания прямоугольника с полями: x1, y1, x2, y2 – координат вершин верхнего правого и нижнего левого углов. Прописать два статических метода для вычисления ширины и высоты прямоугольника. В качестве параметра этим методам передавать ссылку на экземпляр класса Rect, для которого выполняется вычисление.

Подвиг 3. Реализовать класс Singleton, в котором определить статический метод getInstance(). Этот метод должен возвращать экземпляр класса, если он еще не создавался. Иначе, возвращается ссылка на ранее созданный экземпляр. Также следует запретить создание объектов класса Singleton напрямую через оператор new. (Полученная реализация будет гарантировать существование только одного экземпляра класса в процессе работы программы и, фактически, является примером известного паттерна singleton).

Видео по теме