ООП ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-08-04

Модификаторы доступа в 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

Кратко о каждом уровне:

  1. public (открытый) — член доступен из любого кода, который видит сам класс.
  2. protected (защищённый) — член доступен внутри своего пакета и дополнительно классам-наследникам в других пакетах.
  3. package-private (уровень доступа по умолчанию) — член доступен только коду из того же пакета. Отдельного ключевого слова у этого уровня нет.
  4. private (закрытый) — член доступен только внутри тела своего класса верхнего уровня, то есть в самом классе и в его вложенных классах.

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

Четыре уровня доступа: сводная таблица

Эту таблицу удобно держать перед глазами: она отвечает на вопрос «откуда виден член класса» для всех четырёх уровней сразу.

Уровень доступа Тот же класс Другой класс в том же пакете Наследник в другом пакете Любой класс в другом пакете
public Да Да Да Да
protected Да Да Да, но только через ссылку своего типа Нет
package-private (по умолчанию) Да Да Нет Нет
private Да Нет Нет Нет

Уровни доступа упорядочены по строгости: private → package-private → protectedpublic. Каждый следующий включает всё, что разрешал предыдущий.

Важно

Начиная с 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 код попадает в безымянный модуль, где эта проверка не ограничивает ничего.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

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