Геттеры и сеттеры в Java

Введение

Геттеры и сеттеры широко используются в программировании на Java. Однако не каждый программист понимает и реализует эти методы должным образом. В этой статье мы подробно раскроем данную тему.

1. Что это такое

В Java геттер и сеттер — это два обычных метода, которые используются для получения значения поля класса или его изменения.
Рассмотрим этот подход на примере простого класса с приватной переменной и реализованными для доступа к ней извне геттером и сеттером:
public class SimpleGetterSetter {
    private int number;
 
    public int getNumber() {
        return number;
    }
 
    public void setNumber(int number) {
        this.number = number;
    }
}
Поскольку number имеет модификатор доступа private, то обратиться к ней напрямую за пределами данного класса не получится:
SimpleGetterSetter obj = new SimpleGetterSetter();

// ошибка компиляции
obj.number = 10;   

// то же самое
int number = obj.number;
Для недопущения подобных ошибок внешний код должен вызывать геттер getNumber() и сеттер setNumber(), чтобы получить или обновить значение переменной number:
SimpleGetterSetter obj = new SimpleGetterSetter();
 
obj.setNumber(10);  // ок
int number = obj.getNumber();  // ок
Сеттер — это метод, предназначенный для изменения (установки; от слова set) значения поля. А геттер — это метод, назначение которого — возвращать (от слова get) значение поля.
Геттер иногда называют accessor (аксессор, т.к. он предоставляет доступ к полю), а сеттер mutator (мутатор, т.к. он меняет значение переменной).

2. Зачем они нужны

Представьте ситуацию, когда нам необходимо изменить состояние объекта (значение его полей) на основе некоторого условия. Как мы могли бы достичь этого без сеттера:
  • сделать поля public, protected или default
  • изменять их значения с помощью оператора точки
Перечислим последствия этих действий:
1
Нарушение инкапсуляции
Указав у полей модификаторы доступа public, protected или default, мы теряем контроль над данными и ставим под угрозу один из основополагающих принципов ООП — инкапсуляцию.
2
Изменяемость состояния
Поскольку поля не private, то кто угодно может изменить их значения за пределами класса. Это означает, что мы не сможем добиться их неизменяемости.
3
Отсутствие условия изменения состояния
Мы не можем предоставить никакой логики для изменения полей по каким-то условиям.
Пример:
public class Employee {
    public String name;
    public int retirementAge;

    public Employee(String name, int retirementAge) {
        this.name = name;
        this.retirementAge = retirementAge;
    }
}
Изменим значение retirementAge сотрудника:
public class RetirementAgeModifier {

    public static void main(String[] args) {
        Employee employee = new Employee("John", 58);
        employee.retirementAge = 18;
    }
}
Из кода видно, что кто угодно может изменить значение retirementAge. Нет никакого способа проверить корректность этого изменения.
4
Невозможность частичного ограничения к полям
А как бы мы могли ограничить доступ к полям только для чтения или только для записи извне?
Вот тут на помощь и приходят геттеры и сеттеры.
Используя геттер и сеттер, мы можем контролировать доступ к важным переменным (например, требуется изменить значение переменной в заданном диапазоне. В противном случае новое значение не будет присвоено).
Рассмотрим сеттер:
public void setNumber(int number) {
    if (number < 10 || number > 100) {
        throw new IllegalArgumentException("Ошибка диапазона.");
    }
    this.number = number;
}
Этот код гарантирует, что значение number всегда будет находиться в диапазоне от 10 до 100. Без наличия подобного сеттера пользователь может установить для number любое значение:
obj.number = 3;
Такие действия будут нарушать допустимое ограничение для значений этой переменной, что нас, конечно, не устраивает. Таким образом, установка переменной number как private и использование сеттера, устраняют все перечисленные ранее проблемы.
Что касается геттера, то он является единственным безопасным способом получения извне значения переменной number:
public int getNumber() {
    return number;
}
Следующая схема поясняет всю ситуацию:
Геттеры и сеттеры защищают значение переменной от неожиданных изменений.
Когда переменная скрыта модификатором private и доступна только через геттер и сеттер, она инкапсулирована. Поэтому реализация геттеров и сеттеров является одним из способов обеспечения инкапсуляции в коде программы.
Некоторые фреймворки такие, как Hibernate, Spring, Struts и т. д. могут проверять информацию или внедрять свой служебный код через геттеры и сеттеры. Поэтому при интеграции вашего кода с ними эти методы должны быть реализованы.
Подытоживая, выделим плюсы при использовании геттеров и сеттеров:
  • помогают достичь инкапсуляции для скрытия состояния объекта и предотвращения прямого доступа к его полям
  • при реализации только геттера (без сеттера) можно достичь неизменяемости объекта
  • могут иметь проверку корректности значения перед его присваиванием полю или обработку ошибок. Таким образом, мы можем добавить условную логику и обеспечить поведение в соответствии с нашими условиями.
Примечание: если сеттер не имеет подобной логики, а лишь присваивает полю какое-то значение, то его наличие не обеспечивает инкапсуляцию. А его присутствие становится фиктивным.
  • можем предоставить полям разные уровни доступа: например, геттер (доступ для чтения) может быть public, в то время как сеттер (доступ для записи) может быть protected
  • с их помощью мы достигаем еще одного ключевого принципа ООП — абстракции, которая скрывает детали реализации, чтобы никто не мог использовать поля непосредственно в других классах или модулях

3. Правила именования

Принцип именования геттеров и сеттеров должен соответствовать конвенции Java об именовании: getXXX() и setXXX(),
где ХХХ — это имя переменной для которой реализуются эти методы. Например, для следующей переменной:
private String name;
имена геттера и сеттера будут такие:
public void setName(String name)
 
public String getName()
Если переменная имеет тип boolean, то геттер будет содержать префикс isXXX(). Например:
private boolean single;
 
public String isSingle()

public void setSingle(boolean single)
Пример именования геттеров и сеттеров:

4. Типичные ошибки

Этот раздел описывает типичные ошибки при реализации геттеров и сеттеров, а также пути их устранения:
1
Использование геттеров и сеттеров для public-полей
Рассмотрим фрагмент кода:
public String firstName;
 
public void setFirstName(String firstName) {
    this.firstName = firstName;
}
 
public String getFirstName() {
    return firstName;
}
Переменная firstName объявлена с модификатором public, следовательно, доступ к ней может быть получен напрямую, делая геттер и сеттер бесполезными. Для решения этой проблемы необходимо использовать модификатор с более ограниченным доступом, как protected или private:
private String firstName;
В третьем издании книги Джошуа Блоха «Java. Эффективное программирование» об этой проблеме говорится в главе 4.2:
«Используйте в открытых классах методы доступа, а не открытые поля».

Joshua J. Bloch
Software engineer
2
Присваивание ссылки на объект напрямую в сеттере
Пусть есть сеттер:
private int[] scores;
 
public void setScores(int[] scores) {
    this.scores = scores;
}
Проблемный код:
int[] scores = {5, 5, 4, 3, 2, 4}; // 1
 
setScores(scores); // 2
displayScores(); // 3   
 
scores[1] = 1; // 4
displayScores(); // 5
Массив целых чисел scores, проинициализированный шестью значениями (строка 1), передается в метод setScores() (строка 2). Затем метод displayScores() выводит значения массива:
public void displayScores() {
    for (int number : scores) {
        System.out.print(number + " ");
    }
    System.out.println();
}
Вывод в консоль:
5 5 4 3 2 4
Строка scores[1] = 1; меняет значение 2-го элемента (не забываем, что в массивах нумерация начинается с нуля).
В строке 5 снова вызывается метод displayScores(), который выводит следующие значения:
5 1 4 3 2 4
Как видим значение 2-го элемента изменилось с 5 на 1 в результате присвоения в строке 4. Почему это важно? Потому что на наших глазах данные, которые якобы хранятся в private-переменной и не должны бесконтрольно меняться извне, изменились, что нарушает идею инкапсуляции.
Давайте взглянем на реализацию setScores():
public void setScores(int[] scores) {
    this.scores = scores;
}
После присваивания обе переменные (поле scores и пришедший в метод параметр int[] scores) ссылаются на один и тот же объект в памяти — на массив scores. Таким образом, изменения, которые могут быть внесены в this.scores, либо в scores, фактически вносятся в один и тот же объект.
Решением проблемы является создание копии исходного массива и ее присвоение this.scores:
public void setScores(int[] scores) {
    this.scores = new int[scores.length];
    System.arraycopy(scores, 0, this.scores, 0, scores.length);
}
В этом случае переменная this.scores больше не ссылается на тот же объект, на который ссылается scores. Вместо этого, массив this.scores инициализируется новым массивом с размером, равным размеру массива scores. Затем мы копируем все элементы из массива scores в this.scores, используя метод System.arraycopy().
Снова запускаем наш пример и получаем такой вывод:
5 5 4 3 2 4
5 5 4 3 2 4
Теперь два вызова метода displayScores() выводят один и тот же результат. Это означает, что массив this.scores независим и отличается от scores, переданного в сеттер. Таким образом, присваивание:
myScores[1] = 1;
не влияет на массив this.scores.
Вывод: если вы передаете ссылку на объект в сеттер, то не копируйте ее во внутреннюю переменную напрямую. Вместо этого делайте ее копию и только тогда присваивайте ее полю.
Все то же самое можно сделать, используя метод copyOf() или copyOfRange():
public void setScores(int[] scores) {
    this.scores = Arrays.copyOf(scores, scores.length);
}
Прочитать о разнице между способами копирования можно в этой статье.
3
Возврат геттером ссылки на объект
Рассмотрим следующий геттер:
private int[] scores;
 
public int[] getScores() {
    return scores;
}
И следующий фрагмент кода:
int[] scores = {5, 5, 4, 3, 2, 4}; // 1
 
setScores(scores) // 2
displayScores();  // 3
 
int[] copyScores = getScores(); // 4
copyScores[1] = 1; // 5
displayScores(); // 6
Тогда мы получим следующий вывод:
5 5 4 3 2 4
5 1 4 3 2 4
2-й элемент поля scores изменяется вне сеттера в строке 5. Поскольку геттер возвращает ссылку на этот массив, внешний код с ее помощью может вносить в него изменения.
Для решения проблемы геттеру необходимо возвращать копию объекта, а не ссылку на оригинал. Модифицируем вышеупомянутый геттер:
public int[] getScores() {
    int[] copyScores = new int[scores.length];
    System.arraycopy(scores, 0, copyScores, 0, copyScores.length);
    return copyScores;
}
То же самое для copyOf():
public int[] getScores() {
    return Arrays.copyOf(scores, scores.length);
}
Вывод: не возвращайте в геттере ссылку на исходный объект. Возвращайте копию.

5. Реализация для примитивных типов

Переменные примитивных типов вы можете свободно передавать/возвращать прямо в сеттере/геттере, поскольку Java автоматически копирует их значения. Это позволяет избежать разобранных выше ошибок.
Например, следующий код безопасен, потому что сеттер и геттер работают с примитивным типом float:
private float amount;
 
public void setAmount(float amount) {
    this.amount = amount;
}
 
public float getAmount() {
    return amount;
}
Таким образом, примитивные типы не наделены проблемами объектов.

6. Реализация для объектов системных классов

1
Геттеры и сеттеры для String
String — неизменяемый (immutable) тип. Это означает, что после создания объекта этого типа, его значение нельзя изменить. Любые изменения будут приводить к созданию нового объекта String. Таким образом, вы можете безопасно реализовать геттер и сеттер для переменной String:
private String address;
 
public void setAddress(String address) {
    this.address = address;
}
 
public String getAddress() {
    return address;
}
2
Геттеры и сеттеры для объектов типа Date
Т.к. объекты класса java.util.Date являются изменяемыми, то внешние классы не должны иметь доступ к их оригиналам. Данный класс реализует метод clone() из класса Object, который возвращает копию объекта, но использовать его для этих целей не стоит.
По этому поводу Джошуа Блох пишет следующее:
«Поскольку Date не является окончательным классом, нет гарантии, что метод clone() возвратит объект, класс которого именно java.util.Date: он может вернуть экземпляр ненадежного подкласса, созданного специально для нанесения ущерба. Такой подкласс может, например, записывать ссылку на каждый экземпляр в момент создания последнего в закрытый статический список, а затем предоставить злоумышленнику доступ к этому списку. В результате злоумышленник получит полный контроль над всеми экземплярами копий. Чтобы предотвратить атаки такого рода, не используйте метод clone() для создания копии параметра, тип которого позволяет ненадежным сторонам создавать подклассы».

Joshua J. Bloch
Software engineer
Что тогда делать? Все очень просто — создавать каждый раз новый экземпляр и работать с ним:
private Date birthDate;
 
public void setBirthDate(Date birthDate) {
    this.birthDate = new Date(birthDate.getTime());
}
 
public Date getBirthDate() {
    return new Date(birthDate.getTime();
}
Вы можете узнать об этом больше в книге Джошуа Блоха «Java. Эффективное программирование» в главе 8.2: «При необходимости создавайте защитные копии».

7. Реализация для коллекций

Написание следующего кода является не самой лучшей идеей, поскольку он подвержен ошибкам 2 и 3, описанных ранее:
private List<String> titles;
 
public void setListTitles(List<String> titles) {
    this.titles = titles;
}
 
public List<String> getTitles() {
    return titles;
}
Рассмотрим следующую программу:
import java.util.*;
 
public class CollectionGetterSetter {
    private List<String> titles;
 
    public void setTitles(List<String> titles) {
        this.titles = titles;
    }
 
    public List<String> getTitles() {
        return titles;
    }
 
    public static void main(String[] args) {
        CollectionGetterSetter app = new CollectionGetterSetter();
        List<String> titles1 = new ArrayList();
        titles1.add("Name");
        titles1.add("Address");
        titles1.add("Email");
        titles1.add("Job");
 
        app.setTitles(titles1);
        System.out.println("Titles 1: " + titles1); 
        titles1.set(2, "Habilitation");
 
        List<String> titles2 = app.getTitles();
        System.out.println("Titles 2: " + titles2);
        titles2.set(0, "Full name");
 
        List<String> titles3 = app.getTitles();
        System.out.println("Titles 3: " + titles3);
    }
}
Мы можем ожидать, что в консоли будут отображены три одинаковых результата. Однако при запуске программа выдает следующее:
Titles 1: [Name, Address, Email, Job]
Titles 2: [Name, Address, Habilitation, Job]
Titles 3: [Full name, Address, Habilitation, Job]
Это означает, что коллекция может быть изменена из кода, находящегося за пределами геттера и сеттера.
Для коллекции String одним из решений этой проблемы является использование конструктора, который принимает другую коллекцию в качестве аргумента. Например, мы можем изменить код вышеупомянутого геттера и сеттера следующим образом:
public void setListTitles(List<String> listTitles) {
    this.listTitles = new ArrayList<>(listTitles);
}
 
public List<String> getListTitles() {
    return new ArrayList<>(listTitles);   
}
Теперь программа выдет желаемый результат:
Titles 1: [Name, Address, Email, Job]
Titles 2: [Name, Address, Email, Job]
Titles 3: [Name, Address, Email, Job]
Примечание: описанный выше подход с конструктором работает только с коллекциями из String.
Рассмотрим следующий пример для коллекции объектов Person:
import java.util.*; 
   
public class CollectionGetterSetterObject { 
    private List<Person> people; 
   
    public void setPeople(List<Person> people) { 
        this.people = new ArrayList<>(people); 
    } 
   
    public List<Person> getPeople() { 
        return new ArrayList<>(people); 
    } 
   
    public static void main(String[] args) { 
        CollectionGetterSetterObject app = new CollectionGetterSetterObject(); 
   
        List<Person> list1 = new ArrayList<>(); 
        list1.add(new Person("Peter")); 
        list1.add(new Person("Alice")); 
        list1.add(new Person("Mary")); 
   
        app.setPeople(list1); 
   
        System.out.println("List 1: " + list1); 
   
        list1.get(2).setName("Maryland"); 
   
        List<Person> list2 = app.getPeople(); 
        System.out.println("List 2: " + list2); 
   
        list1.get(0).setName("Peter Crouch"); 
   
        List<Person> list3 = app.getPeople(); 
        System.out.println("List 3: " + list3); 
   
    } 
} 
   
class Person { 
    private String name; 
   
    public Person(String name) { 
        this.name = name; 
    }    
   
    public void setName(String name) { 
        this.name = name; 
    } 
   
    public String toString() { 
        return name; 
    } 
}
При запуске получим следующий вывод:
List 1: [Peter, Alice, Mary]
List 2: [Peter, Alice, Maryland]
List 3: [Peter Crouch, Alice, Maryland]
При создании новой коллекции на основе имеющейся, объекты, размещенные в оригинальной коллекции, не будут скопированы. Вместо этого в новую коллекцию будут скопированы только ссылки на эти объекты. И хоть в итоге две коллекции и различны, но содержат одни и те же объекты.
Также мы можем обнаружить, что ArrayList, HashMap, HashSet и т. д. реализуют свои собственные методы clone(). Эти методы якобы возвращают копии, но на самом деле копируют лишь ссылки (происходит неглубокое копирование). Об этом прямо написано в Javadoc метода clone() класса ArrayList:
Returns a shallow copy of this ArrayList instance. (The elements themselves are not copied.)
Что переводится, как «возвращает копию этого экземпляра ArrayList (сами элементы не копируются)».
Таким образом, мы не можем использовать метод clone() классов коллекций. Нам самостоятельно необходимо его реализовать в Person:
public Object clone() {
    return new Person(name);
}
Сеттер для people модифицируем следующим образом:
public void setPeople(List<Person> people) {
    for (Person person : people) {
        this.people.add((Person) person.clone());
    }
}
А геттер модифицируем так:
public List<Person> getPeople() {
    List<Person> people = new ArrayList<>();
    for (Person person : this.people) {
        people.add((Person) person.clone());
    }
    return people;
}
Новая версия класса CollectionGetterSetterObject:
import java.util.*;
 
public class CollectionGetterSetterObject {
    private List<Person> people = new ArrayList<>();
 
    public void setPeople(List<Person> people) {
        for (Person person : people) {
            this.people.add((Person) person.clone());
        }
    }
 
    public List<Person> getPeople() {
        List<Person> people = new ArrayList<>();
        for (Person person : this.people) {
            people.add((Person) person.clone());
        }
        return people;
    }
 
    public static void main(String[] args) {
        CollectionGetterSetterObject app = new CollectionGetterSetterObject();
 
        List<Person> list1 = new ArrayList<>();
        list1.add(new Person("Peter"));
        list1.add(new Person("Alice"));
        list1.add(new Person("Mary"));
 
        app.setPeople(list1);
 
        System.out.println("List 1: " + list1);
 
        list1.get(2).setName("Maryland");
 
        List<Person> list2 = app.getPeople();
        System.out.println("List 2: " + list2);
 
        list1.get(0).setName("Peter Crouch");
 
        List<Person> list3 = app.getPeople();
        System.out.println("List 3: " + list3);
 
    }
}
Запустив код выше, мы получим следующий вывод:
List 1: [Peter, Alice, Mary]
List 2: [Peter, Alice, Mary]
List 3: [Peter, Alice, Mary]
Ключевые моменты реализации геттера и сеттера для коллекций:
  • Для коллекций из элементов типа String не требуется специальной реализации, так как объекты этого типа неизменяемы (immutable)
  • Для коллекций пользовательских типов данных необходимо:
    • реализовать метод clone()
    • в сеттере добавить клонирование элементов из исходной коллекции в конечную
    • в геттере создать новую возвращаемую коллекцию, используя клонирование элементов из исходной коллекции в новую

8. Реализация для пользовательского класса

Для создания объекта пользовательского класса необходимо реализовать метод clone().
Пример:
class Person {
    private String name;
 
    public Person(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @Override
    public String toString() {
        return name;
    }

    @Override 
    public Object clone() {
        return new Person(name);
    }
}
Класс Person реализует свой метод clone() для возврата своей копии.
При этом сеттер класса, использующего Person, должен быть реализован следующим образом:
public void setFriend(Person friend) {
    this.friend = (Person) friend.clone();
}
А геттер таким образом:
public Person getFriend() {
    return (Person) friend.clone();
}
Ключевые моменты реализации геттера и сеттера для ваших классов:
  • Реализуйте метод clone()
  • Возвращайте клонированный объект из геттера
  • Используйте клонированный объект в сеттере

Заключение

В этой статье мы подробно разобрали простую, но очень важную тему, которую многие программисты пробегают по верхам, не вдаваясь в детали. Мы рассмотрели на примерах возникающие проблемы при неверном использовании геттеров и сеттеров, а также выяснили способы их решения.
Оцените статью, если она вам понравилась!