Обзор всех модификаторов в Java

Модификаторы это ключевые слова в Java, которые "изменяют и регулируют" работу классов, методов и переменных.

· 9 минуты на чтение

Все члены класса в языке Java имеют модификаторы. Модификаторы — это ключевые слова, которые "изменяют и регулируют" работу классов, методов и переменных.

В Java множество различных модификаторов, но они не имеют строгой структуры. Однако, из всех модификаторов принято выделять "модификаторы доступа". Их мы рассмотрим в самом конце.

Спонсор поста

Прочие модификаторы

Начнём наш разбор с модификаторов, которые нельзя объединить в какую-то группу. Этакие модификаторы одиночки :)

Модификатор final

С английского "final" можно перевести как "последний, окончательный". Этот модификатор тем или иным образом защищает от изменений переменные, методы и классы.

Переменная final

Переменная объявленная как final после инициализации становится неизменной. Это значит, что примитив изменить не выйдет, а для ссылочной переменной не удастся присвоить новую ссылку на другой объект.

Для final переменных на уровне класса значение устанавливается сразу или через конструктор.

public class Foo {

    private final int a = 10;
    private final Bar bar;

    public Foo(Bar bar) {
        this.bar = bar;
    }

}

class Bar {
    int b = 10;
}

В примере выше переменную a мы устанавливаем сразу, а переменную bar в конструкторе.

Изменить значения этих переменных не выйдет (строка 13 и 14). Однако данные внутри объекта могут быть изменены (строка 16). Таким образом, состояние полей объекта, на который ссылается final переменная, изменяемо.

Аргументы и локальные переменные метода также могут быть final. Тогда изменить их тоже не выйдет (строки 4, 5).

public void method(final int a) {
    final long b = 10;

    a = 10; // error
    b = 20; // error
}

Помните, что final переменную класса объявляют сразу или в конструкторе. Но на локальную переменную метода это правило не распространяется. Вы можете объявить её, но не инициализировать до момента ее использования.

public void method() {
    final long b;
    System.out.println(b); // error
}

Такое поведение позволяет инициализировать final переменную по условию. Например, так:

public void method(int a) {
    final long b;
    if (a > 10) {
        b = 20;
    } else {
        b = 30;
    }
    b = 50; // error
}

Метод final

Для методов final означает запрет на переопределение в наследниках.

class Foo {

    final void method() {
        System.out.println("test");
    }

}

class Bar extends Foo {

    final void method() { // error
        System.out.println("test");
    }

}

Это полезно, когда вы допускаете использование класса в наследовании, но  поведение конкретного метода хотите запретить переопределять.

Класс final

Применение final по отношению к классу объявляет класс завершённым — запрещает дальнейшее наследование от такого класса.

final class Foo {
    
}

class Bar extends Foo { // error

}

Модификатор static

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

С использованием модификатора static объявляются методы и переменные, которые не нуждаются в объекте класса. Таким образом, методы и переменные вызываются от класса, а не от объекта.

Этот модификатор не получится применить к конструктору, а также к обычному классу, но к вложенному классу применить static можно.

package p1;

class Foo {

    static String field = "Test";

    static String getString() {
        return "Test two";
    }

}

class Main {
    public static void main(String[] args) {
        System.out.println(Foo.field);
        System.out.println(Foo.getString());
    }
}

Вы можете вызывать static методы и переменные от объекта, но делать так не рекомендуется. Если мы говорим про объекты, то каждый объект обладает полями, которые содержат уникальные значения. Изменяя поле у одного объекта класса, мы не повлияем на поля другого объекта того же класса. Если вы измените static поле, то оно изменится у всех экземпляров класса.

public static void main(String[] args) {
	final Foo foo = new Foo();
	System.out.println(foo.field);
	foo.getString();
}
Так делать не стоит!

Сокрытие

Ещё один интересный момент состоит в том, что статический метод нельзя переопределить. То есть при работе со статическими методами override невозможен.

Если вы объявите одинаковый метод в классе-наследнике и в родительском классе, вы "скроете" метод родительского класса, но не переопределите. При обращении к такому методу всегда будет вызван метод исходя из типа ссылки, по которой идёт обращение к такому полю или методу. Рассмотрим проблему на примере.

У нас есть ClassOne и наследник ClassTwo. Оба этих класса имеют одинаковый статический метод. Создадим объект класса ClassTwo, но сохраним его в локальную переменную класса ClassOne classOne. И вызовем у объекта classOne статический метод. Метод какого класса будет в итоге вызван?

public class Foo {
    public static void main(String[] args) {
        final ClassOne classOne = new ClassTwo();
        System.out.println(classOne.method());
    }
}

class ClassOne {
    public static String method() {
        return "ClassOne";
    }
}

class ClassTwo extends ClassOne {
    public static String method() {
        return "ClassTwo";
    }
}

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

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

Соблюдайте тонкую грань, иначе все методы станут статическими. Бизнес-логика никогда не помещается в статик методы, а всякие утилитарные функции, например, удаление символов из строки, вполне могут быть статическими. Главное, — не злоупотреблять. Если для работы статического метода приходится передавать много аргументов, спросите себя: "А должен ли он быть статическим?".

Константы

Модификатор static используется в связке с final для создания неизменяемых переменных. Таким образом, получается константное значение, которое доступно в любом месте программы.

Утилитарные классы

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

Хороший пример такого подхода — FileUtils, который есть во множестве библиотек, Apache Commons IO в их числе. Область применения такого класса — это работа с файлами. Если заглянуть внутрь, мы обнаружим методы, наподобие boolean exists(File file), void writeToFile(List<String> data) и так далее.

В идеале такой класс ещё и финальный — final. Не к чему таким классам участвовать в наследовании.

Фабричный метод

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

Примеры таких фабричных методов в Java: String.valueOf(15), Integer.valueOf("14").

Конструктор не обладает уникальным именем. Когда у вас много конструкторов у одного класса, сложно понять какой за что отвечает. Используя фабричный метод, задавайте осмысленные имена и скрывайте вызов конструктора.

Статический блок кода

Инициализация статического блока кода выполняется на этапе загрузки класса. Благодаря этому, они используются, когда необходимо выполнить какую-то логику ещё до создания экземпляра объекта.

Выглядит синтаксис вот так:

public class UserService {
    
    static {
        // your code
    }
    
}

Если статических блоков в классе несколько, тогда их выполнение будет происходить в порядке объявления.

Статические блоки можно использовать для инициализации статических полей, например:

public class UserService {

    private static final Map<Long, String> USERS = new HashMap<>();

    static {
        USERS.put(0L, "username1");
        USERS.put(1L, "username2");
        USERS.put(2L, "username3");
    }

}

Статичный блок не может пробросить перехваченные исключения, но может выбросить не перехваченные. При возникновении исключения выбросится ExceptionInInitializerError.

Статический вложенный класс

В Java можно объявить класс внутри другого класса. Такой класс называется nested-классом. Вложенные классы делятся на статические и нестатические.

Статические вложенные классы называют внутренними классами (inner-классами). Код для иллюстрации вышесказанного:

class OuterClass {
  static NestedClass {

  }

  class InnerClass {

  }
}

Понятно, что nested классы принадлежат классу, в то время как inner классы принадлежат экземпляру объекта класса.

Модификатор abstract

Используется для создания абстрактных классов и методов. Абстрактность подразумевает некоторую незавершенность реализации класса/метода.

Класс abstract

Невозможно создать объект abstract-класса. Единственное предназначение такого класса — быть классом родителем.

abstract class Foo {

}

class Bar extends Foo {

}

class Main {
    public static void main(String[] args) {
        final Foo foo = new Foo(); // error
        final Foo bar = new Bar(); // success
    }
}

Класс не может быть одновременно abstract и final, так как класс final позволяет создавать наследников.

Абстрактный класс может содержать обычные методы, а также static-методы.

Метод abstract

Абстрактные методы тесно связаны с абстрактными классами. Абстрактный метод — это метод без реализации. Реализация такого метода должна быть описана в классе наследнике. По факту это переопределение метода. Да, это похоже на то, как работает интерфйес.

abstract class Foo {

    abstract void method();
    
}

class Bar extends Foo {

    @Override
    void method() {
        
    }
    
}

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

Абстрактный класс может содержать как обычные методы, так и абстрактные. Обычный класс не может содержать абстрактные методы. Но абстрактный класс не обязан содержать абстрактные методы.

Модификатор synchronized

Ключевое слово synchronized используется для указания того, что метод может быть доступен только одним потоком одновременно. В Java  synchronized может быть применён с любым из модификаторов уровня доступа, о которых мы поговорим ниже.

synchronized void showDetails(){
	// your code
}

Модификатор volatile

Этот модификатор тоже предназначен для многопоточной разработки. Доступ к переменной volatile синхронизирует все кэшированные переменные в оперативной памяти.

Модификатор transient

Переменная экземпляра отмеченная, как transient указывает JVM пропустить её при сериализации объекта.

transient int limit = 55;   // не будет сериализована
int b; // будет сериализована
Такой сериализацией в Java никто не пользуется. Поэтому считайте этот модификатор устаревшим.

Модификаторы доступа

Позволяют указать разрешённую область видимости для членов класса. Модификаторы доступа помогают реализовать один из принципов ООП — Инкапсуляцию. Начнем разбор с самого закрытого и дойдём до самого открытого.

Модификатор доступа private

Не позволяет использовать члены класса за пределами класса. Таким образом, модификатор позволяет защититься от изменения объекта извне.

Конструктор private

Если создать приватный конструктор, то создать объект такого класса не выйдет. При условии, что у класса не будет других конструкторов.

Также у вас не получится сделать наследника, так как при наследовании вызывается конструктор класса родителя.

Зачем нужен такой конструктор?

Может показаться, что это бред. Зачем тогда нужен такой класс? Выделяют два случая, когда такой конструктор оказывается полезным.

Первый — это статические фабричные методы для создания объекта. О них мы говорили в разделе модификатора static. Таким образом, мы скрываем конструктор и оставляем только фабричные методы.

public class Foo {

    private Foo(int a) {

    }

    public static Foo of(int a) {
        return new Foo(a);
    }

    public static Foo doubleCreate(int a) {
        return new Foo(a * a);
    }

}

Второй случай — это создание утилитарных классов, также упоминаемые раньше. Обычно вы не хотите, чтобы кто-то создавал объекты таких классов, так как это не имеет никакого смысла.

Класс private

Обратите внимание, что перед самим Foo стоит модификатор доступа public. Поставить private попросту не выйдет. Однако, мы сможем поставить модификатор private у вложенного класса:

public class Foo {

    private String field;
    
    private class Bar {
        
    }

}

Метод private

Приватный метод невозможно вызвать из другого класса.

class Foo {

    private void privateMethod() {

    }

    public void method() {

    }

}

class Bar {

    void method(Foo foo) {
        foo.method(); // success
        foo.privateMethod(); // error
    }

}

Приватные методы недоступны для вызова даже у наследника.

class Foo {

    private void privateMethod() {

    }

}

class ChildFoo {

    void method() {
        this.privateMethod(); // error
    }

}

Не имеет смысла объявлять метод private final так как private метод не виден в наследниках, соответственно не может быть предопределен.

Переменная private

Повторяет особенности private-метода: у других классов, в том числе у наследников, нет доступа к этому полю.

Модификатор доступа default-package

Особенностью этого модификатора — отсутствие ключевого слова. Если вы не указываете модификатор, то он применяется по умолчанию.

С таким модификатором доступ есть внутри этого класса, а также все классы, которые находятся в этом пакете, имеют доступ.

Рассмотрим пример использования. У нас есть класс Foo и Bar в одном пакете p1 и класс DifferentPackage в другом пакете.

package p1;

public class Foo {

    int defaultField;

    Foo(int i, int i2) {

    }

    void defaultMethod() {

    }

}
package p1;

public class Bar {

    public void testMethod(Foo foo) {
        foo.defaultField = 2; // success
        foo.defaultMethod(); // success
        foo = new Foo(1); // success
    }

}

В случае с классом Bar никаких ошибок не будет, мы сможем получить доступ и к конструктору, и к методу, и к полю. Но если мы попробуем то же самое сделать в классе DifferentPackage, то столкнёмся с ошибками.

package p2;

public class DifferentPackage {

    public void testMethod(Foo foo) {
        foo.defaultField = 2; // error
        foo.defaultMethod(); // error
        foo = new Foo(1); // error
    }

}

Модификатор доступа protected

Этот модификатор доступа обладает свойствами модификаторов private и default-package. А также позволяет наследникам обращаться к членам класса родителя.

Посмотрим на примере. У нас есть класс Foo в пакете p1 и пакет p2 с классом наследником ChildFoo и классом DifferentPackage.

package p1;

public class Foo {

    protected int defaultField;

    protected Foo(int i) {

    }

    protected void defaultMethod() {

    }

}

C классом DifferentPackage всё так же, как и в прошлом примере. А для ChildFoo теперь есть доступ к полям/методам/конструктору своего родителя.

package p2;

import p1.Foo;

public class ChildFoo extends Foo {

    protected ChildFoo(int i) {
        super(i);
    }

    void method() {
        System.out.println(defaultField);
        defaultMethod();
    }

}

Также необходимо у ChildFoo реализовать конструктор, который будет вызывать конструктор родителя.

Модификатор доступа public

Этот модификатор позволяет обращаться к членам класса откуда угодно. Если это переменная, то любой класс из любого пакета может её прочитать и изменить. Если это метод, то любой класс может его вызывать. Если это конструктор, то любой класс может его вызвать.

Шпаргалка для модификаторов доступа переменной

Визуально модификаторы доступа переменной класса можно представить таким образом:

Резюмирую

Модификаторы неотъемлемая базовая составляющая языка Java. Без полного понимания работы всех модификаторов будет сложно продолжать изучать этот язык.

Помните, не ко всем членам класса можно применять тот или иной модификатор. А также, что не все они сочетаются друг с другом.

Отдельным особняком стоят модификаторы доступа. Они позволяют вам защищать члены классов от модификации извне, тем самым реализуя один из принципов ООП — Инкапсуляцию.

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇