Все члены класса в языке 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). Однако внутреннее состояние объекта, на который ссылается final
переменная, может быть изменено (строка 16). Таким образом, состояние полей объекта, на который ссылается final
переменная, может изменяться.
Аргументы метода и локальные переменные также могут быть объявлены как final
, что запрещает их изменение:
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
, но инициализировать её позже. Такое поведение позволяет инициализировать 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
вызываются напрямую через класс, а не через объект.
Модификатор 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());
}
}
В этом примере мы видим, что статическое поле field
и статический метод getString
вызываются напрямую через класс Foo
, без создания его экземпляра.
Хотя технически можно вызывать static методы и переменные через объект, делать это не рекомендуется. Пример:
Если говорить про обычные (нестатические) поля, то каждый объект класса имеет свои уникальные значения этих полей. Изменение значения поля одного объекта не влияет на поля других объектов того же класса. В отличие от этого, статическое поле является общим для всех экземпляров класса. Если изменить значение статического поля, это изменение отразится на всех объектах класса.
Сокрытие статических методов
Интересная особенность статических методов заключается в том, что статический метод нельзя переопределить. Это означает, что при работе со статическими методами механизм override невозможен.
Если вы объявите одинаковый статический метод в классе-наследнике и в родительском классе, вы не переопределите метод, а “скроете” его. При обращении к такому методу его вызов будет определяться типом ссылки, а не фактическим типом объекта. Рассмотрим пример:
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
и его наследник ClassTwo
. Оба класса имеют одинаковый статический метод method
. Мы создаём объект класса ClassTwo
, но сохраняем его в переменной типа ClassOne
:
final ClassOne classOne = new ClassTwo();
Затем вызываем статический метод:
System.out.println(classOne.method());
Результатом будет вывод строки "ClassOne". Это связано с тем, что для статических методов используется механизм раннего связывания. Вызов статического метода происходит на основании типа переменной (classOne
имеет тип ClassOne
), а не фактического объекта, который хранится в переменной.
Важно соблюдать баланс при использовании статических методов. Бизнес-логику никогда не стоит размещать в статических методах, так как она обычно зависит от состояния объектов. В то же время утилитарные функции, такие как преобразование строк или удаление символов, вполне могут быть статическими. Однако не злоупотребляйте этим: если для работы статического метода требуется много аргументов, стоит задать себе вопрос: “Должен ли этот метод быть статическим?”.
Константы
Модификатор static
часто используется в связке с final
для создания неизменяемых переменных — констант. Такие переменные имеют фиксированное значение, которое не может быть изменено после инициализации, и доступно в любом месте программы. Это удобно для хранения общих констант, например, математических значений или настроек приложения.
public class Constants {
public static final String APP_NAME = "MyApp";
public static final int MAX_USERS = 100;
}
В этом примере APP_NAME
и MAX_USERS
— это константы, доступные для использования в любой части программы.
Утилитарные классы
Статические методы, связанные с определённой областью задач, часто группируются в отдельные классы, называемые утилитарными классами (Utility classes). Утилитарные классы содержат статические методы, которые предоставляют вспомогательные функции для различных задач.
Пример утилитарного класса — FileUtils
, встречающийся во многих библиотеках, таких как Apache Commons IO. Его задача — работа с файлами. Внутри такого класса можно найти методы вроде boolean exists(File file)
или void writeToFile(List<String> data)
.
Хороший пример такого подхода — 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");
}
}
Здесь мы инициализируем статическое поле USERS
с помощью статического блока, который заполняет карту несколькими значениями.
Важно помнить, что статический блок не может пробрасывать перехваченные исключения. Однако он может выбросить неперехваченные исключения, такие как RuntimeException
. Если в статическом блоке возникает исключение, будет выброшена ошибка ExceptionInInitializerError
, которая указывает на проблему на этапе инициализации.
Статический вложенный класс
В Java можно объявить класс внутри другого класса. Такие классы называются вложенными (nested). Вложенные классы делятся на статические и нестатические.
Статические вложенные классы называются также inner-классами. Пример синтаксиса:
class OuterClass {
static NestedClass {
}
class InnerClass {
}
}
Статические вложенные классы (inner-классы) принадлежат внешнему классу и могут быть вызваны без создания экземпляра внешнего класса. Они похожи на обычные классы, но находятся внутри другого класса.
Нестатические вложенные классы (они также называются просто внутренними классами) принадлежат экземпляру внешнего класса и для их создания требуется объект внешнего класса.
Различие между ними заключается в том, что статические вложенные классы существуют независимо от экземпляров внешнего класса, тогда как внутренние классы связаны с экземпляром и имеют доступ к его полям и методам.
Модификатор 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
}
}
В этом примере попытка создания объекта абстрактного класса Foo
приведёт к ошибке компиляции. Однако создание объекта класса Bar
, который наследует Foo
, будет успешным.
abstract
и final
. Модификатор final
запрещает наследование, тогда как абстрактный класс предназначен именно для наследования.Абстрактный класс может содержать как обычные, так и статические методы.
Абстрактный метод
Абстрактные методы тесно связаны с абстрактными классами. Абстрактный метод — это метод без реализации. Он должен быть реализован в классе-наследнике. По сути, это форма обязательного переопределения метода, что схоже с тем, как работают интерфейсы.
abstract class Foo {
abstract void method();
}
class Bar extends Foo {
@Override
void method() {
}
}
Абстрактные методы не могут быть final
или static
, так как они требуют реализации в подклассе, а модификатор final
запрещает переопределение, а static
методы не предназначены для переопределения. Обратите внимание, что абстрактные методы заканчиваются точкой с запятой, так как они не содержат тела.
Обычный класс не может содержать абстрактные методы, потому что обычный класс должен быть полностью реализован. Однако абстрактный класс не обязан содержать абстрактные методы. Это означает, что класс может быть помечен как abstract
просто для того, чтобы его нельзя было инстанцировать, даже если он содержит только реализованные методы.
Модификатор sealed
Модификатор sealed
был добавлен в Java 15 и позволяет ограничивать наследование, обеспечивая контроль над иерархией классов.
С его помощью можно указать, какие именно классы могут наследоваться от данного класса, что усиливает безопасность и инкапсуляцию. Класс, объявленный как sealed
, должен явно указывать разрешённые классы-наследники с помощью ключевого слова permits
.
Пример использования sealed
:
public sealed class Shape permits Circle, Square {
// Общая логика для всех фигур
}
public final class Circle extends Shape {
// Логика для круга
}
public final class Square extends Shape {
// Логика для квадрата
}
В этом примере класс Shape
ограничивает возможные наследники только классами Circle
и Square
. Каждый класс-наследник sealed-класса обязан быть объявлен с модификатором final
, sealed
или non-sealed
. Попытка наследования от Shape
другим классом или отсутствие одного из обязательных модификаторов вызовет ошибку компиляции.
final
— окончательно запрещает наследование для этого класса.sealed
— позволяет классу иметь ограниченное количество наследников.non-sealed
— снимает ограничения и разрешает неограниченное наследование.
Пример использования non-sealed
:
public non-sealed class Square extends Shape {
// Square теперь может быть расширен другими классами
}
Модификатор synchronized
Ключевое слово synchronized
используется для указания, что метод или блок кода может быть выполнен только одним потоком одновременно. Это гарантирует, что несколько потоков не смогут одновременно изменить состояние объекта, что важно для обеспечения корректности многопоточных программ.
synchronized void showDetails(){
// your code
}
В Java модификатор synchronized
часто используется для синхронизации доступа к общим ресурсам в многопоточной среде, чтобы предотвратить состояние гонки (race condition).
Модификатор volatile
Модификатор volatile
также предназначен для работы в многопоточной среде. Он используется для переменных, которые могут быть изменены несколькими потоками. Доступ к переменной с модификатором volatile
гарантирует, что все потоки видят актуальное значение переменной, а не её кэшированную версию.
volatile int sharedVariable;
При использовании volatile
каждый раз, когда поток обращается к переменной, он читает её значение непосредственно из оперативной памяти, а не из кэша процессора. Это помогает избежать проблем с несогласованностью данных между потоками.
volatile
не заменяет синхронизацию в случаях, когда требуется выполнение нескольких операций атомарно.Модификатор transient
Модификатор transient
используется для переменных экземпляра, которые не должны участвовать в процессе сериализации объекта. Если переменная помечена как transient
, Java Virtual Machine (JVM) пропустит её при сериализации, и её значение не будет записано в поток данных.
transient int limit = 55; // не будет сериализована
int b; // будет сериализована
Это полезно, когда нужно исключить переменные, которые не имеют смысла сохранять (например, кэшированные данные или временные значения). Однако в современной Java сериализация часто осуществляется с помощью других библиотек, таких как Jackson или Protocol Buffers, поэтому использование transient
стало менее распространённым.
Модификаторы доступа
Модификаторы доступа в Java позволяют контролировать область видимости членов класса (переменных, методов, конструкторов и классов). Они помогают реализовать один из ключевых принципов объектно-ориентированного программирования — инкапсуляцию. Начнем с самого закрытого модификатора и дойдём до самого открытого.
Модификатор доступа private
Модификатор private
делает члены класса доступными только внутри этого класса. Это означает, что переменные и методы, объявленные с этим модификатором, не могут быть использованы за пределами класса. Такой подход позволяет защитить внутреннее состояние объекта от изменения извне и обеспечивает контроль над доступом к данным.
Приватный конструктор
Приватный конструктор запрещает создание объектов класса вне самого класса. Если у класса есть только приватные конструкторы, создать его экземпляр за пределами этого класса будет невозможно.
Это также означает, что нельзя создать наследника такого класса, так как при наследовании необходимо вызвать конструктор родительского класса.
Зачем нужен такой конструктор?
На первый взгляд, может показаться, что приватный конструктор лишает класс смысла, так как невозможно создать его объект. Однако существует два основных сценария, когда такой конструктор оказывается полезным:
- Статические фабричные методы
- Создание утилитарных классов, также упоминаемые раньше. Обычно вы не хотите, чтобы кто-то создавал объекты таких классов, так как это не имеет никакого смысла.
Класс private
Важно понимать, что модификатор доступа private
нельзя использовать для самого класса, если этот класс является верхнеуровневым. Например, для класса Foo
модификатор private
поставить не получится:
public class Foo {
private String field;
private class Bar {
}
}
Однако, вложенные классы могут быть объявлены с модификатором private
. Это позволяет скрыть их от внешних классов и контролировать их доступ только внутри родительского класса.
Приватный метод
Методы с модификатором 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
, повторяют особенности приватных методов. Они недоступны для других классов, включая классы-наследники. Это позволяет скрыть внутреннее состояние объекта и защитить его от прямого изменения извне.
Модификатор доступа “по умолчанию” (default)
Особенностью этого модификатора является отсутствие ключевого слова. Если не указать явно модификатор доступа для члена класса (поля, метода, конструктора), то он считается доступным по умолчанию.
При использовании модификатора по умолчанию доступ к членам класса возможен только внутри того же пакета. То есть классы, находящиеся в одном пакете, могут свободно обращаться к полям, методам и конструкторам с доступом по умолчанию, а классы из других пакетов — нет.
Рассмотрим пример с классами Foo
и Bar
, находящимися в одном пакете p1
, и классом DifferentPackage
, который находится в другом пакете p2
.
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
}
}
Здесь никаких ошибок не будет, так как оба класса (Foo
и Bar
) находятся в одном пакете p1
, что позволяет классу Bar
свободно использовать все поля, методы и конструкторы класса Foo
с доступом по умолчанию.
Однако если мы попробуем сделать то же самое в классе DifferentPackage
, который находится в другом пакете p2
, мы столкнемся с ошибками:
package p2;
public class DifferentPackage {
public void testMethod(Foo foo) {
foo.defaultField = 2; // error
foo.defaultMethod(); // error
foo = new Foo(1); // error
}
}
В этом примере класс DifferentPackage
из пакета p2
не имеет доступа к полям, методам и конструкторам с доступом по умолчанию в классе Foo
из пакета p1
.
Модификатор доступа protected
Модификатор доступа protected
сочетает в себе свойства модификаторов private
и default-package
. Он позволяет членам класса быть доступными как внутри того же пакета, так и в классах-наследниках, даже если они находятся в других пакетах.
Рассмотрим пример, где у нас есть класс Foo
в пакете p1
и два других класса: ChildFoo
, который является наследником Foo
и находится в пакете p2
, и DifferentPackage
(также в пакете p2
), который не является наследником.
package p1;
public class Foo {
protected int defaultField;
protected Foo(int i) {
}
protected void defaultMethod() {
}
}
Класс Foo
имеет protected
поле, конструктор и метод. Эти элементы будут доступны в пределах пакета p1
, а также в любом классе-наследнике, даже если он находится в другом пакете.
Код класса ChildFoo
в пакете p2
, который наследует класс Foo
:
package p2;
import p1.Foo;
public class ChildFoo extends Foo {
protected ChildFoo(int i) {
super(i);
}
void method() {
System.out.println(defaultField);
defaultMethod();
}
}
Класс ChildFoo
, будучи наследником Foo
, может свободно обращаться к полям, методам и конструкторам с модификатором protected
. Обратите внимание, что необходимо реализовать конструктор в классе ChildFoo
, который вызывает конструктор родителя с помощью super(i)
.
Теперь посмотрим, как ведет себя класс DifferentPackage
, который находится в том же пакете p2
, но не является наследником класса Foo
:
package p2;
import p1.Foo;
public class DifferentPackage {
public void testMethod(Foo foo) {
foo.defaultField = 2; // error: доступ к полю запрещён
foo.defaultMethod(); // error: доступ к методу запрещён
foo = new Foo(1); // error: доступ к конструктору запрещён
}
}
В данном случае класс DifferentPackage
не является наследником Foo
, поэтому он не имеет доступа к полям, методам и конструкторам с модификатором protected
, даже если он импортировал класс Foo
.
Модификатор доступа public
Модификатор public
предоставляет максимальный уровень доступа. Члены класса (переменные, методы, конструкторы), объявленные с модификатором public
, могут быть доступны из любого другого класса, независимо от пакета. Это означает, что к публичным членам можно обратиться как внутри одного пакета, так и из других пакетов.
- Если это переменная, любой класс из любого пакета может её прочитать и изменить.
- Если это метод, любой класс может его вызвать.
- Если это конструктор, любой класс может его использовать для создания объекта.
package p1;
public class Foo {
public int publicField; // Публичное поле
public Foo(int i) { // Публичный конструктор
this.publicField = i;
}
public void publicMethod() { // Публичный метод
System.out.println("Public method called");
}
}
Класс Foo
имеет публичные поле, конструктор и метод. Все они доступны из любого другого класса, даже если он находится в другом пакете.
Теперь рассмотрим пример класса из другого пакета, который может использовать публичные члены класса Foo
:
package p2;
import p1.Foo;
public class DifferentPackage {
public void testMethod() {
Foo foo = new Foo(10); // Доступ к публичному конструктору
System.out.println(foo.publicField); // Доступ к публичному полю
foo.publicMethod(); // Доступ к публичному методу
}
}
В этом примере класс DifferentPackage
из другого пакета может беспрепятственно обращаться к публичным членам класса Foo
— его конструктору, полю и методу.
Шпаргалка для модификаторов доступа переменной
Визуально модификаторы доступа переменной класса можно представить таким образом:
Резюмирую
Модификаторы — это неотъемлемая и базовая составляющая языка Java. Без полного понимания их работы изучение языка может быть затруднено. Они помогают контролировать доступ к членам класса, управлять многопоточностью, а также влияют на то, как члены класса могут взаимодействовать с другими классами.
Важно помнить, что не ко всем членам класса можно применять определённые модификаторы. Кроме того, некоторые модификаторы несовместимы друг с другом. Например, класс не может быть одновременно abstract
и final
, а методы не могут быть abstract
и static
одновременно.
Особую роль играют модификаторы доступа. Они позволяют защищать члены класса от несанкционированной модификации извне, реализуя один из ключевых принципов объектно-ориентированного программирования — инкапсуляцию. Выбор правильного модификатора доступа помогает управлять видимостью и обеспечивать безопасность данных внутри программы.