Avatar ·

Разница между public, protected, package-private (default) и private в Java

Всем привет! Я только начал изучать Java и никак не могу разобраться с модификаторами доступа. Вроде бы читал теорию, но на практике постоянно путаюсь, когда писать `public`, а когда `private`. Например, у меня есть такой код: ```java public class Animal { public String name = "Unknown"; protected int age = 5; String breed = "Mixed"; // package-private private String id = "A123"; public void showInfo() { System.out.println("Name: " + name); System.out.println("Age: " + age); System.out.println("Breed: " + breed); System.out.println("ID: " + id); } } ``` В другом классе в том же пакете я могу обратиться к `name`, `age` и `breed`, но не к `id`. А если я создаю подкласс в другом пакете, то могу использовать только `name` и `age`. Почему так? И что вообще означает "package-private"? Когда какой модификатор использовать в реальных проектах? Объясните, пожалуйста, простыми словами с примерами. Заранее спасибо! <<>>

Краткая таблица видимости

В 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 — самый хитрый. Он даёт доступ двум категориям:

  1. Всем классам в том же пакете (даже не наследникам).
  2. Всем наследникам (даже в других пакетах), но только через ссылку на себя (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.

Запомните: чем меньше открыто, тем легче поддерживать код. Идеальный класс — с минимальным публичным интерфейсом и полностью скрытой реализацией.