Все члены класса в языке 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
поле, то оно изменится у всех экземпляров класса.
Сокрытие
Ещё один интересный момент состоит в том, что статический метод нельзя переопределить. То есть при работе со статическими методами 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
используется в связке с 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; // будет сериализована
Модификаторы доступа
Позволяют указать разрешённую область видимости для членов класса. Модификаторы доступа помогают реализовать один из принципов ООП — Инкапсуляцию. Начнем разбор с самого закрытого и дойдём до самого открытого.
Модификатор доступа 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. Без полного понимания работы всех модификаторов будет сложно продолжать изучать этот язык.
Помните, не ко всем членам класса можно применять тот или иной модификатор. А также, что не все они сочетаются друг с другом.
Отдельным особняком стоят модификаторы доступа. Они позволяют вам защищать члены классов от модификации извне, тем самым реализуя один из принципов ООП — Инкапсуляцию.