Модификаторы доступа private, public, protected: разбор

Когда вы проектируете класс, каждый его член - поле, метод, конструктор - живёт в одной из трёх зон видимости: доступен всем, только потомкам или только самому классу. Именно это и задают модификаторы доступа private, protected и public. Без них код компилируется, но быстро превращается в запутанный клубок: любой файл меняет любое состояние, и баг найти невозможно. Ниже разберём, что именно скрывает каждый модификатор, как это выглядит в диаграмме наследования и как посчитать степень инкапсуляции вашего класса - покрутите слайдеры калькулятора и сразу увидите.
Что означает каждый модификатор
Три ключевых слова отвечают на один вопрос: «кто имеет право обратиться к этому члену?» Этот вопрос кажется простым, но именно от ответа на него зависит, насколько легко будет поддерживать, тестировать и расширять код через год-два.
private - только сам класс. Ни подклассы, ни внешний код не видят private-членов: это «внутренности» объекта, детали реализации, которые могут меняться без оглядки на внешний мир. Если переменная balance объявлена private, никакой злоумышленник не напишет account.balance = 99999 снаружи. Компилятор не только отклонит такой код, но и явно скажет: «поле недоступно». Это статическая защита, работающая ещё до запуска программы - принципиальное отличие от проверки условий в рантайме.
protected - класс и все его потомки (подклассы). Такой член скрыт от «внешнего мира», но доступен в иерархии наследования. В Java и C# protected-доступ обычно ещё и распространяется на пакет/сборку, в Python - это просто соглашение (одно подчёркивание _name). Ключевая идея: protected - это контракт с наследниками. Вы говорите «я планирую расширяемость здесь, и потомки могут опереться на это поле или метод», но внешним пользователям знать о нём не нужно.
public - всё и все. Открытый интерфейс класса: именно эти члены образуют API, которым пользуются клиенты. Менять сигнатуру public-метода - это ломать обратную совместимость. В библиотеках и фреймворках это критично: даже переименование public-параметра может сломать чужой код, если его передают через named arguments (Kotlin, Python, C#). Поэтому грамотный дизайн public-части - одно из главных умений разработчика API.
В большинстве языков (Java, C++, C#, PHP) модификатор пишется явно. В Python - соглашение: __name (двойное подчёркивание) манглирует имя и имитирует private, _name сигнализирует «не трогать снаружи» (protected-дух), а name - public.
Что видно в каждой точке кода
Рассмотрим конкретный класс:
class Person {
private int age; // только Person
protected String name; // Person и подклассы
public String getId() {} // все
}
class Employee extends Person {
void greet() {
System.out.println(name); // OK - protected
// System.out.println(age); - ошибка, age - private
}
}
Таблица видимости даёт точную картину:
| Контекст | private | protected | public |
|---|---|---|---|
| Внутри самого класса | да | да | да |
| В подклассе | нет | да | да |
| Во внешнем коде | нет | нет* | да |
* В Java protected виден в том же пакете даже без наследования - особенность языка.

Три концентрических зоны наглядно показывают принцип «наименьшего привилегия»: чем меньше код знает о реализации соседа, тем проще изменять каждый компонент в отдельности.
Зачем нужна инкапсуляция
Инкапсуляция - не самоцель. Она решает три практических проблемы, с которыми сталкивается любая команда при работе над нетривиальным проектом.
Контроль инварианта. private-поле balance в банковском счёте можно изменить только через deposit() и withdraw(), которые проверяют допустимость операции. Если бы balance был public, любой код мог написать account.balance = -1000 без всяких проверок. Инвариант «баланс не может быть отрицательным» соблюдался бы только по доброй воле - а это ненадёжно. Аналогичный принцип работает для любого ограничения: возраст , скорость максимально допустимой, дата конца даты начала.
Свобода рефакторинга. Пока интерфейс (public API) не меняется, внутреннюю реализацию можно переписать полностью: сменить структуру данных, алгоритм, тип поля - клиентский код ничего не заметит. Именно поэтому правило «по умолчанию private, открывай только необходимое» ускоряет поддержку кода в разы. Классический пример: коллекция сначала реализована через массив, потом переписана на хэш-таблицу для производительности - если внутренние детали скрыты, это «бесплатный» рефакторинг.
Читаемость. Когда public API минимален, пользователь класса сразу понимает, что с ним можно делать. Класс с 15 public-методами и 1 private-полем - сигнал, что дизайн размыт. Закон Деметра («говори только с прямыми соседями») напрямую вытекает из принципа инкапсуляции: чем меньше объект знает о внутренностях других объектов, тем ниже связность и тем легче вносить изменения.
Protected: когда и зачем
protected - компромисс между изоляцией и расширяемостью. Слишком часто его выбирают «по инерции», не понимая, что он открывает доступ всей иерархии наследования. Правильные сценарии выглядят так:
- Шаблонный метод (Template Method): базовый класс определяет алгоритм в
public-методе, а отдельные шаги выносит вprotected-методы для переопределения подклассами. Клиентский код вызывает толькоpublic-фасад, не зная о деталях алгоритма. - Общее состояние иерархии: поле, которое нужно нескольким подклассам, но не должно утекать наружу - хороший кандидат для
protected. Например,protected Logger loggerв базовом сервисе: каждый подкласс пишет в тот же логгер, но внешние классы не должны управлять им напрямую. - Конструкторы фабричных классов:
protected-конструктор запрещает прямойnew, но позволяет фабричному методу или подклассу вызвать его. Паттерн используется в JDK:Calendar.getInstance()создаёт объект, а прямойnew Calendar()недоступен. - Unit-тестирование без разрушения инкапсуляции: некоторые команды делают вспомогательные методы
protected(а неprivate), чтобы тестовый подкласс мог их вызвать. Это спорная практика, но иногда предпочтительнее рефлексии.
Злоупотребление protected - когда поле делают protected «на всякий случай, вдруг подкласс понадобится». Правило: начинайте с private, переводите в protected только тогда, когда конкретный подкласс этого требует. Чем меньше protected-членов, тем свободнее вы при рефакторинге базового класса.
Модификаторы в разных языках
Ядро концепции одно, но детали различаются.
Java добавляет четвёртый уровень - «package-private» (без ключевого слова): видно всем классам в том же пакете. protected в Java означает «пакет + потомки».
C++ разрешает указывать тип наследования (public, protected, private): class B : private A делает все члены A недоступными через B из внешнего кода, даже если они были public в A.
C# добавляет internal (видно в сборке) и protected internal (видно в сборке или в подклассах за её пределами), а также private protected (пересечение: потомок в той же сборке).
Python не принуждает, а сигнализирует: __name вызывает name mangling (_ClassName__name) - технически доступно, но явный «заходить нельзя». Без подчёркиваний - public.
Пример проектирования: класс Stack
Проектируем простой стек. Какой модификатор у чего?
class Stack {
private Object[] data; // внутренний массив - деталь реализации
private int top; // указатель вершины - деталь реализации
public void push(Object o) { ... } // API
public Object pop() { ... } // API
public boolean isEmpty() { ... } // API
private void resize() { ... } // вспомогательный - скрыт
}
Клиенту нужно три действия: push, pop, isEmpty. Всё остальное - внутреннее. Если завтра мы поменяем Object[] на LinkedList, ни один клиентский файл не изменится. В этом и ценность private.
Теперь расширим иерархию: нужен BoundedStack с ограничением по размеру. Если бы поле data было protected, подкласс мог бы напрямую манипулировать массивом, обойдя проверку размера. Правильное решение - оставить data как private в Stack, а BoundedStack переопределит push() через public API базового класса, добавив проверку isFull(). Инвариант сохранён, иерархия расширяема, каждый класс ограничен своей зоной ответственности.
Частые ошибки
- Сделать всё
public«для простоты». Быстрый путь в начале оборачивается часами отладки: любой код меняет состояние объекта, цепочку вызовов не отследить. - Путать
protectedиprivateв контексте подкласса.privateнедоступен даже в подклассе того же файла - стандартная ловушка для новичков в Java и C#. - Оставлять
protected-поля там, где достаточноprivate+ геттер. Полеprotectedоткрывает прямой доступ к состоянию подклассу - нарушает инкапсуляцию так же, какpublic, только в рамках иерархии. - Не учитывать package-private в Java. Класс без модификатора уровня
publicвиден только в пакете - это не то же самое, чтоprivate-класс. - Python: не ставить
__там, где нужен настоящий барьер. Одно подчёркивание - соглашение, два - реальное манглирование имени. Дляprivate-семантики нужны именно__.
FAQ
Чем отличается private от protected в Java?
private виден только внутри объявляющего класса. protected виден всем классам в том же пакете и во всех подклассах, даже если они в другом пакете. Подкласс в другом пакете НЕ видит private-членов родителя.
Можно ли переопределить private-метод в подклассе?
Технически - нет переопределения (override): private-метод не виден подклассу, поэтому подкласс создаёт новый, независимый метод с тем же именем. Полиморфизм через @Override не работает.
Когда делать конструктор private?
Когда нужно запретить прямой new: паттерны Singleton (один экземпляр) и Factory Method (создание только через статический фабричный метод). private-конструктор сигнализирует: «этот класс нельзя инстанцировать произвольно».
Коротко
private скрывает член от всех, кроме самого класса; protected открывает его подклассам и (в Java) пакету; public делает его частью открытого API. Золотое правило: начинай с самого ограничительного модификатора и открывай доступ ровно настолько, насколько необходимо. Инкапсуляция защищает инвариант объекта, упрощает рефакторинг и делает API понятным. Калькулятор выше поможет оценить баланс видимости в вашем классе.
Читайте также

Абстрактный класс и интерфейс: в чём отличие
Абстрактный класс и интерфейс: чем отличаются в ООП, когда наследовать поведение, а когда задавать контракт, как выбрать на примерах Java, C# и Python.

Наследование классов в ООП: пример и разбор
Наследование классов в ООП на примере: базовый класс, наследник, переопределение методов и вызов super. Разбираем виды наследования, синтаксис в разных языках и частые ошибки.

Полиморфизм в ООП: пример на простом коде и разбор
Полиморфизм в ООП на примере: один вызов метода работает по-разному для разных классов. Разбираем перегрузку, переопределение, виртуальные методы и утиную типизацию на коде.