Модификаторы доступа в Java
Класс Child в пакете oop.p2 наследует Parent из пакета oop.p1. Внутри Child вызов this.protectedMethod() компилируется, а вызов other.protectedMethod(), где other объявлен как Parent, — нет. Тот же самый метод, тот же самый класс-наследник, а результат разный. Это не баг компилятора: protected открывает доступ не «для наследников вообще», а только через ссылку своего типа. Разберём модель доступа Java целиком, чтобы такие случаи перестали быть сюрпризом.
Что такое модификаторы доступа
Модификаторы доступа в Java — это ключевые слова public, protected и private, которые управляют видимостью класса, поля, метода или конструктора из другого кода. Модификаторов три, а уровней доступа четыре: четвёртый, package-private (уровень доступа по умолчанию), получается, когда модификатор не указан вообще.
Модификатор доступа принято записывать первым в объявлении члена класса, перед остальными модификаторами и типом (порядок модификаторов язык не фиксирует — public static и static public для компилятора равнозначны, но общепринятый стиль — первый вариант):
public int i;
private static double j;
private int myMethod(int a, char b) { /* ... */ }
int defaultField; // модификатор не указан - package-private Кратко о каждом уровне:
- public (открытый) — член доступен из любого кода, который видит сам класс.
- protected (защищённый) — член доступен внутри своего пакета и дополнительно классам-наследникам в других пакетах.
- package-private (уровень доступа по умолчанию) — член доступен только коду из того же пакета. Отдельного ключевого слова у этого уровня нет.
- private (закрытый) — член доступен только внутри тела своего класса верхнего уровня, то есть в самом классе и в его вложенных классах.
Ограничение доступа к членам класса — это основной технический механизм инкапсуляции: поля закрываются модификатором private, а работа с ними ведётся через публичные методы, которые могут проверять данные и сохранять объект в согласованном состоянии.
Четыре уровня доступа: сводная таблица
Эту таблицу удобно держать перед глазами: она отвечает на вопрос «откуда виден член класса» для всех четырёх уровней сразу.
| Уровень доступа | Тот же класс | Другой класс в том же пакете | Наследник в другом пакете | Любой класс в другом пакете |
|---|---|---|---|---|
public | Да | Да | Да | Да |
protected | Да | Да | Да, но только через ссылку своего типа | Нет |
| package-private (по умолчанию) | Да | Да | Нет | Нет |
private | Да | Нет | Нет | Нет |
Уровни доступа упорядочены по строгости: private → package-private → protected → public. Каждый следующий включает всё, что разрешал предыдущий.
Важно
Начиная с Java 9 четыре уровня доступа — уже не вся картина. Модульная система JPMS добавляет внешний слой: public-класс не виден за пределами своего модуля, если пакет не экспортирован директивой exports в module-info.java. Поэтому формулировка «public — значит доступен всем» верна только внутри одного модуля или в проектах без module-info.java.
public, private и уровень доступа по умолчанию
Рассмотрим отличие public, private и package-private на примере. В классе Modifiers объявлены три поля с разными уровнями доступа. Внутри самого класса доступны все три, что видно в методе toString():
package oop.p1;
public class Modifiers {
public int publicVar; // открытый уровень доступа
private int privateVar; // закрытый уровень доступа
int defaultVar; // package-private, модификатор не указан
@Override
public String toString() {
return "Modifiers{"
+ "publicVar=" + publicVar
+ ", privateVar=" + privateVar
+ ", defaultVar=" + defaultVar
+ '}';
}
} Класс ModifiersExample1 лежит в том же пакете oop.p1. Из него доступны public-поле и package-private поле, а обращение к private-полю не компилируется:
package oop.p1;
public class ModifiersExample1 {
public static void main(String[] args) {
Modifiers object = new Modifiers();
object.defaultVar = 10; // OK: тот же пакет
object.publicVar = 20; // OK
// object.privateVar = 100; // Ошибка компиляции: private вне класса Modifiers
}
} Теперь создадим похожий класс, но в другом пакете — oop.p2. Здесь недоступно уже и package-private поле: пакет другой, а вложенность имён (oop.p1 и oop.p2) роли не играет — в Java пакеты не вкладываются друг в друга с точки зрения доступа.
package oop.p2;
import oop.p1.Modifiers;
public class ModifiersExample2 {
public static void main(String[] args) {
Modifiers object = new Modifiers();
// object.defaultVar = 10; // Ошибка компиляции: другой пакет
object.publicVar = 20; // OK
// object.privateVar = 100; // Ошибка компиляции
}
} Неочевидный момент
Модификаторы доступа применимы только к членам класса и к самим классам. Локальная переменная и параметр метода не могут быть private или public — их область видимости и так ограничена блоком кода. Единственный модификатор, допустимый для локальной переменной, — final.
Модификатор protected
protected — самый неочевидный из уровней. Он делает две вещи одновременно: даёт доступ всему своему пакету (как package-private) и дополнительно открывает член классам-наследникам в других пакетах. Поэтому распространённая формулировка «protected — это для наследников» неполна: наследование добавляет доступ, но не заменяет пакетный.
Объявим в классе Parent три метода с разными уровнями доступа:
package oop.p1;
public class Parent {
public void publicAccessMethod() {
}
void defaultAccessMethod() {
}
protected void protectedAccessMethod() {
}
} Определим наследника в другом пакете. Ему доступны public- и protected-методы, но не package-private:
package oop.p2;
import oop.p1.Parent;
public class Child extends Parent {
public void someMethod() {
publicAccessMethod(); // OK
// defaultAccessMethod(); // Ошибка компиляции: другой пакет
protectedAccessMethod(); // OK: унаследованный protected-метод
}
} А вот тот самый случай из начала урока. Обращение к protected-члену из наследника в другом пакете разрешено только через ссылку типа самого наследника (или его подтипа) — так требует спецификация языка Java, раздел 6.6.2.1:
package oop.p2;
import oop.p1.Parent;
public class Child extends Parent {
public void compare(Parent other, Child sibling) {
this.protectedAccessMethod(); // OK: ссылка типа Child
super.protectedAccessMethod(); // OK
sibling.protectedAccessMethod(); // OK: тип Child
// other.protectedAccessMethod(); // Ошибка компиляции: тип Parent
}
} Смысл ограничения такой: наследник получает право работать со своей унаследованной частью, а не со всеми объектами родительского типа в системе.
Теперь класс AccessClass в пакете oop.p2, который не наследует Parent. Ему доступны только public-методы:
package oop.p2;
import oop.p1.Parent;
public class AccessClass {
public static void main(String[] args) {
Parent parent = new Parent();
parent.publicAccessMethod(); // OK
// parent.defaultAccessMethod(); // Ошибка компиляции
// parent.protectedAccessMethod();// Ошибка компиляции
}
} Перенесём AccessClass в пакет oop.p1, где лежит Parent, — и без всякого наследования получим доступ и к protected, и к package-private членам:
package oop.p1;
public class AccessClass {
public static void main(String[] args) {
Parent parent = new Parent();
parent.publicAccessMethod(); // OK
parent.defaultAccessMethod(); // OK: тот же пакет
parent.protectedAccessMethod(); // OK: тот же пакет
}
} Уровни доступа для класса
Для класса верхнего уровня (не вложенного) доступны только два варианта из четырёх:
- package-private — класс без модификатора виден только коду из своего пакета.
- public — класс виден отовсюду (с оговоркой про JPMS выше).
Модификаторы private и protected к классу верхнего уровня применить нельзя — компилятор выдаст ошибку. А вот вложенному классу доступны все четыре уровня, потому что он является членом внешнего класса.
Когда мы говорим, что класс A имеет доступ к классу B, это значит, что A может:
- создать экземпляр класса
B; - объявить переменную типа
Bи унаследовать классB; - обратиться к тем членам
B, которые ему открыты.
Если класса не видно, недоступно ничего, включая его public-члены. Ниже — попытка унаследовать package-private класс HotBeverage из другого пакета; она не компилируется, причём ошибка возникнет уже на строке import:
package oop.p1;
class HotBeverage {
} package oop.p2;
// import oop.p1.HotBeverage; // Ошибка компиляции: класс не public
public class Tea {
// extends HotBeverage - невозможно из другого пакета
} И правило про файлы: public-класс должен быть единственным public-типом верхнего уровня в файле, а имя файла обязано совпадать с именем этого класса. Рядом с ним в том же файле можно объявить сколько угодно package-private классов — но обратите внимание: это независимые классы верхнего уровня, и private-члены друг друга они не видят:
// файл Beverage.java
public class Beverage {
}
class HotBeverage {
}
class ColdBeverage {
} Модификаторы при переопределении и в интерфейсах
При переопределении метода сужать уровень доступа запрещено, расширять — можно. Если в родителе метод protected, в наследнике он может стать protected или public, но не package-private и не private:
package oop.p1;
public class Parent {
protected void hook() {
}
} package oop.p1;
public class Child extends Parent {
@Override
public void hook() { // OK: доступ расширен с protected до public
}
// @Override
// void hook() {} // Ошибка: попытка ослабить доступ (attempting to assign weaker access privileges)
} Причина в принципе подстановки Лисков: код, работающий с переменной типа Parent, обязан иметь возможность вызвать hook(), какой бы конкретный подкласс туда ни подставили.
private-метод не наследуется и потому не переопределяется. Метод с той же сигнатурой в наследнике — это просто новый, независимый метод, и аннотация @Override на нём вызовет ошибку компиляции.
В интерфейсах правила свои:
- абстрактные методы неявно
public abstract, поля — неявноpublic static final; - писать
publicу метода интерфейса разрешено, но это избыточно и обычно считается лишним шумом; - с Java 8 доступны
default- иstatic-методы с телом (ключевое словоdefaultздесь — про реализацию по умолчанию, а не про уровень доступа); - с Java 9 в интерфейсе разрешены
private-методы — для вынесения общего кода изdefault-методов.
Полезно знать
Область действия private — это тело объемлющего класса верхнего уровня, а не только тот класс, где объявлен член: вложенный класс видит private-поля внешнего и наоборот. Важно не путать это с файлом: два независимых класса верхнего уровня в одном .java-файле — чужие друг другу, их private-члены взаимно недоступны. До Java 11 доступ между вложенными классами компилятор реализовывал через синтетические методы-мосты, а начиная с Java 11 работает механизм nestmates — JVM проверяет принадлежность классов к одному «гнезду» (nest) напрямую, без сгенерированных методов.
На чём чаще всего спотыкаются
- Считают, что вложенные пакеты наследуют доступ. Пакет
oop.p1.internalне «внутри» пакетаoop.p1: для правил доступа это два совершенно разных пакета. - Путают
protectedи package-private.protectedстрого шире: пакет плюс наследники. - Обращаются к
protected-члену через ссылку родительского типа из наследника в другом пакете — и не понимают ошибку компиляции. - Делают поля
public«на время». Публичное поле становится частью контракта класса: убрать его потом нельзя без поломки чужого кода. - Пишут
publicу методов интерфейса и думают, что без него метод будет package-private. Нет — он всё равноpublic. - Забывают про конструкторы. Конструктор тоже принимает любой из четырёх уровней:
private-конструктор запрещает создание экземпляров снаружи (утилитные классы, синглтоны, фабричные методы).
Рабочее правило при проектировании: начинайте с самого строгого уровня и ослабляйте его только тогда, когда это действительно потребовалось. Поля — private; вспомогательные методы — private или package-private; protected — только для того, что вы сознательно предлагаете расширять наследникам; public — исключительно для продуманного API класса.
Часто задаваемые вопросы
Почему у уровня доступа по умолчанию нет ключевого слова default?
Потому что package-private задаётся отсутствием модификатора: если написать int x;, поле уже видно всему пакету. Ключевое слово default в Java существует, но означает совсем другое — ветку в switch и метод с реализацией по умолчанию в интерфейсе. Писать default int x; нельзя, это ошибка компиляции.
Почему наследник в другом пакете не видит protected-член через ссылку типа родителя?
Спецификация языка (JLS 6.6.2.1) разрешает такой доступ только через выражение, тип которого совпадает с классом-наследником или является его подтипом. Идея в том, что наследник имеет право работать со своей унаследованной частью, а не со всеми объектами родительского типа в программе. Внутри класса помогают this и super, а также ссылки, объявленные типом самого наследника.
Можно ли при переопределении метода сузить уровень доступа?
Нет. Разрешено только сохранить или расширить уровень: package-private можно поднять до protected или public, protected — до public. Попытка сузить даёт ошибку компиляции attempting to assign weaker access privileges. Иначе объект наследника нельзя было бы безопасно использовать через ссылку родительского типа.
Отменяют ли модули Java 9 привычные модификаторы доступа?
Нет, модули работают поверх них. Сначала JVM проверяет, экспортирует ли модуль пакет директивой exports, и только потом действуют обычные правила public, protected, package-private и private. Практический итог: класс может быть public и при этом недоступен из другого модуля. В проектах без module-info.java код попадает в безымянный модуль, где эта проверка не ограничивает ничего.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии