Разница между public, protected, package-private (default) и private в Java
Краткая таблица видимости
В Java есть 4 уровня доступа. Вот как они работают (от самого открытого к самому закрытому):
- public — виден всем и отовсюду (любой класс, любой пакет)
- protected — виден в том же пакете + наследникам (даже в других пакетах)
- package-private (default) — виден только внутри того же пакета (это когда вы не пишете никакой модификатор)
- private — виден только внутри самого класса
Разберем на вашем примере
package zoo;
public class Animal {
public String name; // Доступ: ВСЕГДА и ВЕЗДЕ
protected int age; // Доступ: zoo.* + наследники
String breed; // Доступ: только zoo.* (package-private)
private String id; // Доступ: только внутри Animal
}
1. Тот же пакет (zoo)
package zoo;
public class ZooKeeper {
public void checkAnimal(Animal a) {
System.out.println(a.name); // ✅ public
System.out.println(a.age); // ✅ protected (тот же пакет)
System.out.println(a.breed); // ✅ package-private (тот же пакет)
// System.out.println(a.id); // ❌ private — не видно!
}
}
2. Другой пакет, но наследник
package safari;
import zoo.Animal;
public class Lion extends Animal {
public void showLionInfo() {
System.out.println(name); // ✅ public — всегда видно
System.out.println(age); // ✅ protected — наследник видит
// System.out.println(breed); // ❌ package-private — другой пакет
// System.out.println(id); // ❌ private — только внутри Animal
}
}
3. Другой пакет, не наследник
package safari;
import zoo.Animal;
public class Visitor {
public void lookAtAnimal(Animal a) {
System.out.println(a.name); // ✅ public — единственный вариант
// System.out.println(a.age); // ❌ protected — не наследник
// System.out.println(a.breed); // ❌ package-private
// System.out.println(a.id); // ❌ private
}
}
Когда что использовать (практические советы)
- private — для внутренних полей и методов. Правило: делайте всё private, пока не появится веская причина открыть доступ. Это основа инкапсуляции.
- package-private — когда несколько классов в одном пакете тесно связаны и должны обмениваться данными, но для внешнего мира это не нужно (например, вспомогательные утилиты).
- protected — когда вы пишете библиотеку или фреймворк и хотите, чтобы наследники могли переопределить или использовать какой-то метод, но обычные пользователи — нет.
- public — только для API: то, что вы гарантируете как стабильный интерфейс для всех пользователей вашего класса.
Важное замечание про protected
Protected — самый хитрый. Он даёт доступ двум категориям:
- Всем классам в том же пакете (даже не наследникам).
- Всем наследникам (даже в других пакетах), но только через ссылку на себя (this). Нельзя обратиться к protected полю другого объекта-наследника, если вы не в том же пакете.
Пример ошибки:
package safari;
public class Cub extends Lion {
public void test(Lion other) {
System.out.println(this.age); // ✅ OK — this
// System.out.println(other.age); // ❌ Ошибка! Нельзя через чужую ссылку
}
}
Потому что компилятор проверяет: "А является ли класс, в котором написан код (Cub), наследником класса Animal?" — да. "А является ли тип переменной (Lion) наследником?" — да. "А находятся ли они в одном пакете?" — нет. Значит, доступ только через this.
Итог
Запомните лесенку: private → package-private → protected → public. Каждый следующий уровень добавляет новую группу видимости. Начинайте с самого строгого (private) и расширяйте доступ только по необходимости. Это сделает ваш код надёжнее и понятнее.
Дополнение: как это связано с инкапсуляцией
Коллега выше всё верно описал, но хочу добавить важный философский момент. Модификаторы доступа — это главный инструмент инкапсуляции в Java. Инкапсуляция означает, что объект сам управляет своим состоянием и не позволяет внешнему коду напрямую менять свои поля.
Правило хорошего тона:
- Все поля класса делайте private.
- Для доступа к ним используйте public геттеры и сеттеры (если нужно).
- В сеттерах можно добавить валидацию — например, запретить отрицательный возраст.
Пример:
public class Person {
private String name;
private int age;
public String getName() { return name; }
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Некорректный возраст");
}
this.age = age;
}
}
Почему это важно? Представьте, что вы сделали public int age. Любой может написать person.age = -100. А если вы потом решите переименовать поле или добавить логику — сломаете весь код, который к нему обращался. С геттерами/сеттерами вы меняете реализацию внутри класса, а внешний API остаётся тем же.
Ещё один совет: package-private часто используют в тестах (JUnit), когда тестовый класс лежит в том же пакете, что и тестируемый, но вы не хотите открывать методы наружу через public.
Запомните: чем меньше открыто, тем легче поддерживать код. Идеальный класс — с минимальным публичным интерфейсом и полностью скрытой реализацией.