Геттеры и сеттеры в Java
Безопасный доступ к приватным полям в Java через методы-геттеры и сеттеры с валидацией входных данных.
Геттер — это метод, возвращающий значение поля; сеттер — метод, записывающий в него значение. Вместе они представляют стандартный способ предоставить управляемый доступ к приватному полю. В предыдущей главе об инкапсуляции объяснялось зачем — эта глава посвящена как, включая соглашения об именовании, шаблоны валидации и случаи, когда от них можно отказаться.
Базовый шаблон
Поле, геттер и сеттер:
public class User {
private String email;
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}По соглашению:
- Геттер называется
get<FieldName>для всего, кроме boolean. - Для поля типа
booleanгеттер называетсяis<FieldName>(isActive,hasPermission). - Сеттер называется
set<FieldName>и принимает один параметр типа поля. - Оба метода являются
public, если нет причин для иного.
Эти правила также являются соглашением JavaBeans — многие инструменты (библиотеки сериализации, шаблонизаторы, рефакторинг в IDE) опираются на них и просто не увидят поля, чьи акцессоры названы иначе.
Валидация в сеттерах
Сеттер — это единственная возможность отклонить некорректные данные до записи в поле:
public class User {
private String email;
public void setEmail(String email) {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("not a valid email: " + email);
}
this.email = email;
}
}Сеттер, который просто присваивает значение, мало чем лучше публичного поля. Смысл размещения сеттера между вызывающим кодом и полем — возможность добавить туда проверку. Если вы пишете десятки сеттеров вида this.x = x;, стоит задуматься, действительно ли класс должен быть изменяемым — объект, значения которого задаются через конструктор и доступны только для чтения (immutable класс или record), нередко подходит лучше.
Вычисляемые и производные геттеры
Геттер не обязан отображаться один к одному на поле. Он может вычислять возвращаемое значение:
public class Rectangle {
private final double width, height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
public double getWidth() { return width; }
public double getHeight() { return height; }
public double getArea() { return width * height; } // derived
}Для вызывающего кода getArea() выглядит точно так же, как getWidth() — оба просто возвращают double. То, что один читает сохранённое значение, а другой вычисляет его — деталь реализации, которую можно изменить незаметно для остального кода.
Геттеры только для чтения
Геттер без сеттера открывает поле для чтения, но блокирует запись:
public class Order {
private final long id;
private final long createdAt;
public Order(long id, long createdAt) {
this.id = id;
this.createdAt = createdAt;
}
public long getId() { return id; }
public long getCreatedAt() { return createdAt; }
}Внешний код может спросить «какой ID?», но не может его изменить. Именно так на практике работают поля, неизменяемые после конструирования.
Именование boolean: is, has, should
Геттеры для boolean читаются естественнее с префиксом is/has/can/should:
public boolean isActive() { return active; }
public boolean hasPermission() { return permission != null; }
public boolean canRetry() { return retries < maxRetries; }В сочетании с именем поля вызывающий код читается как обычное предложение: if (user.isActive()) { ... }. Сеттер для таких полей — просто setActive(boolean), без префикса is.
Одна тонкость: инструменты JavaBeans выводят имя свойства из акцессора, поэтому isActive() и getActive() оба ссылаются на свойство active — однако инструменты, как правило, ищут акцессоры с префиксом is только для примитивного типа boolean. Для поля типа Boolean (обёртка) лучше оставить getActive(), чтобы оставаться видимым для инструментов.
Защитные копии (снова)
Если геттер возвращает изменяемый внутренний объект, верните копию или немодифицируемое представление:
private final List<String> tags = new ArrayList<>();
public List<String> getTags() {
return List.copyOf(tags);
}То же самое при получении данных через сеттер:
public void setTags(List<String> tags) {
this.tags.clear();
this.tags.addAll(tags); // copy in
}Без этих копий вызывающий код мог бы изменить внутренний список tags в обход всей остальной логики класса.
Современная Java: более короткие акцессоры
Новый Java-код (особенно в библиотеках, не привязанных к инструментарию JavaBeans) нередко опускает префикс get для симметрии с тем, как именуются акцессоры record:
public class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
}Если вы не связаны с JavaBeans (нет Hibernate, нет настроек Jackson по умолчанию, нет JSP), такой стиль вполне допустим — и соответствует тому, что автоматически генерирует record Point(int x, int y) {}. Выберите один стиль для всей кодовой базы и придерживайтесь его. Подробнее о подходе в стиле record см. в разделе современные records.
Не генерируйте их машинально
Современные IDE с лёгкостью генерируют геттер и сеттер для каждого поля одним нажатием клавиши. Сопротивляйтесь этому порыву. Каждая сгенерированная пара — это проектное решение, которое вы принимаете:
- Геттер открывает поле — действительно ли вы хотите, чтобы вызывающий код его читал?
- Сеттер открывает путь записи — действительно ли вы хотите, чтобы вызывающий код его менял?
- Если оба ответа «да» без оговорок, зачем вообще делать поле приватным?
Правильный ответ нередко — «ни то, ни другое»: замените setBalance(int) на deposit(int) и withdraw(int), которые выражают реальные операции.
Полный пример
Что дальше
На этом заканчивается изучение основ отдельного класса — как его объявлять, как управлять его членами, как открывать ровно столько, сколько нужно внешнему коду. Следующая глава начинает вторую большую идею ООП: наследование, при котором один класс строится на основе другого, а не с нуля. Перейдите к наследованию в Java.