Геттеры и сеттеры в Java: концепция JavaBeans
Класс с двумя открытыми полями выглядит безобидно — до первого вызова вроде этого:
CircleWrong circle = new CircleWrong();
circle.diam = 25;
circle.radius = 10; // компилятор молчит, объект в невозможном состоянии Диаметр 25 при радиусе 10 — геометрически невозможно, но Java такое рассогласование не заметит: она проверяет типы, а не смысл. Именно от подобных молчаливых поломок инвариантов и защищают геттеры и сеттеры.
Геттер — это public метод, который возвращает значение поля класса. Сеттер — это public метод, который присваивает полю новое значение. Поля при этом объявляют с модификатором private, поэтому обратиться к ним напрямую извне класса нельзя — только через эти методы. Такой класс, где приватные поля закрыты парами get/set, называют JavaBean.
Что такое геттеры и сеттеры
Если поле объявлено как private, оно видно только внутри своего класса. Но данные объекта всё равно нужно читать и изменять снаружи, иначе такое поле бессмысленно. Поэтому доступ открывают через специальные public методы:
- геттер (getter, метод доступа) — возвращает значение поля;
- сеттер (setter, метод изменения) — записывает в поле новое значение.
Поле, к которому есть такая пара методов, называют свойством (property) класса. Важно, что имя свойства определяется именно именами методов, а не именем поля: для фреймворков вроде Jackson или Hibernate класс — это набор его геттеров и сеттеров, а как называется поле внутри, они не знают.
Правила именования: get, set и is
Имя метода строится по строгой схеме: берётся имя свойства, его первая буква переводится в верхний регистр и к результату добавляется префикс.
| Тип свойства | Префикс геттера | Префикс сеттера | Пример поля | Пример методов |
|---|---|---|---|---|
| Любой, кроме boolean | get | set | name | getName() / setName(String) |
boolean (примитив) | is или get | set | printed | isPrinted() / setPrinted(boolean) |
Boolean (обёртка) | только get | set | active | getActive() / setActive(Boolean) |
| Коллекция | get | set | roles | getRoles() / setRoles(List<String>) |
Требования к сигнатурам:
- Геттер объявляется как
public, не принимает параметров и возвращает значение типа свойства. - Сеттер объявляется как
public, возвращаетvoidи принимает ровно один параметр типа свойства.
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
} Ключевое слово this в сеттере обязательно: параметр age перекрывает поле age, и без this. строка age = age присвоит параметр самому себе, а поле останется нулевым. Компилятор такую строку пропустит — предупреждение выдаст только IDE или статический анализатор.
Важно
Префикс is разрешён спецификацией JavaBeans только для примитивного boolean. Если поле имеет тип обёртки Boolean, метод isActive() стандартный интроспектор свойством не признает — нужен getActive(). Именно поэтому свойство иногда «пропадает» из JSON после замены boolean на Boolean ради поддержки null.
Концепция JavaBeans: требования к классу
JavaBean — это обычный Java-класс, оформленный по соглашению, которое позволяет внешним инструментам работать с ним, ничего не зная о его устройстве. Соглашение состоит из четырёх пунктов.
| Требование | Зачем нужно | Насколько строго на практике |
|---|---|---|
public класс | Чтобы фреймворк мог его загрузить | Обязательно |
public конструктор без аргументов | Фреймворк создаёт экземпляр через рефлексию, не зная про параметры | Обязательно для Hibernate и для десериализации Jackson по умолчанию |
Поля private (или protected) | Инкапсуляция: состояние меняется только через методы класса | Соглашение, но нарушать его смысла нет |
| Геттеры и сеттеры по правилам именования | По ним вычисляется имя свойства | Обязательно |
implements Serializable | Сохранение состояния объекта в поток | По спецификации требуется, в веб-разработке почти не используется |
На это соглашение опирается практически весь Java-экосистемный инструментарий: Jackson и Gson при работе с JSON, JPA/Hibernate при отображении объектов на таблицы БД, Spring при внедрении зависимостей через сеттеры, JSP и Expression Language при обращении ${person.fullName}, JavaFX при привязке свойств к элементам интерфейса. Подробности описаны в документации класса Introspector.
Пример: класс Person
Класс Person с тремя приватными полями и полным набором методов доступа. Обратите внимание на isRetired(): поле имеет примитивный тип boolean, поэтому префикс is здесь допустим.
import java.io.Serializable;
public class Person implements Serializable {
private String fullName;
private int age;
private boolean retired;
public Person() {
}
public Person(String fullName, int age, boolean retired) {
this.fullName = fullName;
this.age = age;
this.retired = retired;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isRetired() {
return retired;
}
public void setRetired(boolean retired) {
this.retired = retired;
}
} public class PersonExample {
public static void main(String[] args) {
Person person = new Person();
person.setFullName("Петров Иван Иванович");
person.setAge(56);
person.setRetired(false);
System.out.println("Полное имя: " + person.getFullName());
System.out.println("Возраст: " + person.getAge());
System.out.println("Пенсионер? " + person.isRetired());
}
} Полное имя: Петров Иван Иванович
Возраст: 56
Пенсионер? false Для чего нужны геттеры и сеттеры
Геттеры и сеттеры делают код многословнее, поэтому вопрос «зачем они, если можно объявить поле public» возникает у всех. Ответ виден на примере класса, где два поля связаны между собой.
public class CircleWrong {
int radius; // модификатор не указан: поле видно всему пакету
int diam;
} Любой класс из того же пакета может изменить эти поля напрямую. Значения должны быть согласованы — диаметр вдвое больше радиуса, — но ничто этого не гарантирует:
public class CircleWrongExample {
public static void main(String[] args) {
CircleWrong circle = new CircleWrong();
circle.diam = 25;
circle.radius = 10;
System.out.println("Диаметр: " + circle.diam); // 25
System.out.println("Радиус: " + circle.radius); // 10 — противоречие
}
} Объект хранит бессмысленную комбинацию, и все дальнейшие вычисления по нему будут неверными. Причём ошибка проявится не здесь, а где-нибудь в отчёте через три модуля — искать её придётся долго.
Перепишем класс по концепции JavaBeans и добавим в сеттеры логику: каждый из них пересчитывает второе поле, а заодно отсекает отрицательные значения.
public class Circle {
private double radius;
private double diam;
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
if (radius < 0) {
throw new IllegalArgumentException("Радиус не может быть отрицательным: " + radius);
}
this.radius = radius;
this.diam = radius * 2;
}
public double getDiam() {
return diam;
}
public void setDiam(double diam) {
if (diam < 0) {
throw new IllegalArgumentException("Диаметр не может быть отрицательным: " + diam);
}
this.diam = diam;
this.radius = diam / 2;
}
} public class CircleExample {
public static void main(String[] args) {
Circle circle = new Circle();
circle.setDiam(25);
System.out.println(circle.getDiam()); // 25.0
System.out.println(circle.getRadius()); // 12.5 — согласовано
}
} Теперь состояние объекта нельзя привести в противоречивый вид: какой бы сеттер ни вызвали, второе поле пересчитается автоматически. Это и есть главный смысл сеттера — не «присвоить значение», а сохранить инвариант класса.
Неочевидный момент
Здесь поля намеренно имеют тип double, а не int. С типом int вызов setDiam(25) выполнил бы целочисленное деление 25 / 2 и записал в радиус 12, после чего radius * 2 дало бы 24 при диаметре 25 — инвариант нарушен снова, только теперь тише и незаметнее. Сеттер защищает данные лишь настолько, насколько корректна арифметика внутри него.
Логика внутри геттера
Геттер тоже не обязан быть однострочником return field;. Он может отдавать вычисленное или урезанное представление данных. Классический пример — пароль, который нельзя показывать целиком: возвращаем первый символ, остальное заменяем звёздочками.
public class User {
private String login;
private String password;
public User(String login, String password) {
this.login = login;
this.password = password;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getMaskedPassword() {
if (password == null || password.isEmpty()) {
return "";
}
return password.charAt(0) + "*****";
}
public void setPassword(String password) {
this.password = password;
}
} public class UserExample {
public static void main(String[] args) {
User user = new User("mylogin", "mypassword");
System.out.println("Логин: " + user.getLogin());
System.out.println("Пароль: " + user.getMaskedPassword());
}
} Логин: mylogin
Пароль: m***** Почему метод называется getMaskedPassword, а не getPassword
Геттер обязан возвращать значение свойства. Если назвать маскирующий метод getPassword(), любой инструмент, работающий по соглашению JavaBeans, решит, что свойство password действительно равно m*****: Jackson положит эту строку в JSON, а Hibernate запишет её в базу вместо настоящего пароля. Маскирование — это отдельная операция, и имя метода должно об этом говорить.
IDE и Lombok: не писать это руками
Методы доступа никто не набирает вручную. В IntelliJ IDEA их генерирует Alt + Insert → Getter and Setter, в Eclipse — Source → Generate Getters and Setters. IDE сама расставит префиксы, включая is для примитивного boolean.
Второй вариант — библиотека Lombok, которая генерирует методы на этапе компиляции по аннотациям:
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class Person {
private String fullName;
private int age;
private boolean retired;
} В байт-коде появятся getFullName(), setFullName(String), isRetired() и остальные — обращаться к ним можно как к обычным методам. Аннотации можно ставить и над отдельным полем, если нужен, например, только геттер.
| Аннотация Lombok | Что генерирует | Когда применять |
|---|---|---|
@Getter / @Setter | Методы доступа для всех полей класса или для одного поля | Базовый вариант, полный контроль над тем, что открыто |
@Data | @Getter, @Setter, toString(), equals(), hashCode() и конструктор для обязательных полей | Простые DTO. Для JPA-сущностей нежелательно: сгенерированные equals/hashCode ломаются на ленивых связях |
@Value | Неизменяемый класс: поля final, только геттеры | Объекты-значения там, где недоступны records |
Records вместо JavaBeans
Начиная с Java 16, для классов, единственная задача которых — хранить данные, есть record:
public record Person(String fullName, int age, boolean retired) {
} Компилятор сам создаёт приватные final поля, конструктор со всеми параметрами, equals(), hashCode() и toString(), а также методы доступа. Но обращение к ним выглядит иначе:
Person person = new Person("Петров Иван Иванович", 56, false);
System.out.println(person.fullName()); // не getFullName()
System.out.println(person.retired()); // не isRetired() | Признак | JavaBean | Record |
|---|---|---|
| Имя метода чтения | getFullName(), isRetired() | fullName(), retired() |
| Изменяемость | Изменяемый: есть сеттеры | Неизменяемый: сеттеров нет в принципе |
| Конструктор без аргументов | Есть | Невозможен |
| Наследование | Можно расширять | final, наследование запрещено |
| Где уместен | Сущности JPA, формы, объекты с изменяемым состоянием | DTO, ответы API, ключи, результаты запросов |
Формально record не является JavaBean: у него нет ни сеттеров, ни конструктора без аргументов, а имена методов не соответствуют соглашению. Современные библиотеки научились с этим работать напрямую (Jackson поддерживает records с версии 2.12), а вот Hibernate использовать record как сущность не позволяет — там по-прежнему нужен изменяемый класс с конструктором без аргументов. Подробности — в JEP 395.
На чём чаще всего ошибаются
- Геттер возвращает изменяемую коллекцию. Метод
getRoles(), отдающий самList, полностью сводит инкапсуляцию на нет: вызывающий код может сделатьperson.getRoles().clear()и опустошить внутреннее состояние объекта. ВозвращайтеList.copyOf(roles)илиCollections.unmodifiableList(roles). - Поле начинается с одной строчной буквы перед заглавной. Для поля
sNameIDE сгенерируетgetsName(), и стандартный интроспектор увидит свойствоsNameвместо ожидаемого. Соглашение декапитализации: если первые два символа имени метода после префикса — заглавные (getURL()), имя свойства остаётся как есть —URL; во всех остальных случаях первая буква переводится в нижний регистр. - Сеттер, который только присваивает. Пара пустых
get/setнад каждым полем ничем не лучшеpublicполя — только длиннее. Смысл появляется, когда внутри есть валидация, пересчёт связанных полей или защитное копирование. Если для поля не нужно ни того, ни другого и меняться оно не должно — сеттер не пишите вовсе. - Вызов сеттера из конструктора. Если наследник переопределит сеттер, он выполнится до инициализации полей подкласса и увидит их со значениями по умолчанию. В конструкторе присваивайте полям значения напрямую.
- Забытый конструктор без аргументов. Как только в классе появляется конструктор с параметрами, конструктор по умолчанию исчезает — и Hibernate падает с
InstantiationException, а Jackson сInvalidDefinitionException. Объявляйте его явно, как в примере сPerson.
Часто задаваемые вопросы
Нужно ли писать геттер и сеттер для каждого поля?
Нет. Методы доступа создают только для тех свойств, которые действительно должны быть видны или изменяемы снаружи. Для поля, которое задаётся один раз в конструкторе и дальше не меняется, пишут только геттер — получается свойство «только для чтения». А для чисто служебных полей, например кэшированного значения или счётчика попыток, не нужен ни геттер, ни сеттер.
Почему префикс is не работает с типом Boolean?
Спецификация JavaBeans разрешает префикс is только для примитивного типа boolean. Класс java.beans.Introspector, на который опираются JSP EL, JavaFX и часть библиотек, метод isActive() с возвращаемым типом Boolean свойством не считает. Поэтому после замены boolean на Boolean ради поддержки null свойство может исчезнуть из JSON или из EL-выражения. Правильное имя в этом случае — getActive().
Чем record отличается от JavaBean и что выбрать?
Record неизменяем, его методы доступа называются по имени компонента без префикса get, у него нет сеттеров и нет конструктора без аргументов. Выбирайте record для DTO, ответов API и любых объектов, состояние которых не меняется после создания. JavaBean с геттерами и сеттерами остаётся нужен там, где объект должен изменяться или где этого требует фреймворк — прежде всего для сущностей Hibernate и JPA.
Может ли геттер нарушить инкапсуляцию?
Да, и это частый вопрос на собеседовании. Если геттер возвращает ссылку на изменяемый объект — список, массив, Date — вызывающий код получает доступ к внутреннему состоянию и может его изменить: вызов getRoles().add("ADMIN") добавит роль в обход всякой проверки. Решение — возвращать защитную копию через List.copyOf или неизменяемую обёртку Collections.unmodifiableList.
Зачем JavaBean нужен конструктор без аргументов?
Фреймворк создаёт объект через рефлексию и не знает, какие параметры принимает ваш конструктор. Он вызывает конструктор без аргументов, а затем заполняет свойства сеттерами. Так работают Hibernate при загрузке сущности из базы и Jackson при разборе JSON в объект. Если в классе объявлен только конструктор с параметрами, конструктор по умолчанию не создаётся и приложение падает уже во время выполнения.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии