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

Введение
Геттеры и сеттеры широко используются в программировании на Java. Однако не каждый программист понимает и реализует эти методы должным образом. В этой статье мы подробно раскроем данную тему.
1. Что такое геттеры и сеттеры?
В Java геттер и сеттер — это два обычных метода, которые используются для получения значения поля класса или его изменения.
Следующий код является примером простого класса с private-переменной и реализованных, для доступа к ней извне, геттера и сеттера:

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();

// возникнет ошибка компиляции, т.к. number является private
obj.number = 10;   

// то же самое
int number = obj.number;
Чтобы таких проблем не было, внешний код должен вызывать геттер getNumber() и сеттер setNumber(), чтобы получить или обновить значение переменной:

SimpleGetterSetter obj = new SimpleGetterSetter();
 
obj.setNumber(10);  // ок
int number = obj.getNumber();  // ок
Итак, сеттер — это метод, который изменяет (устанавливает; от слова set) значение поля. А геттер — это метод, который возвращает (от слова get) нам значение какого-то поля.
Геттер иногда называют accessor (аксессор, т.к. он предоставляет доступ к полю), а сеттер mutator (мутатор, т.к. он меняет значение переменной).
2. Зачем нужны геттеры и сеттеры?
Представьте ситуацию, когда нам необходимо изменить состояние объекта (значение его полей) на основе некоторого условия. Как мы могли бы достичь этого без сеттера:
  • сделать поля public, protected или default
  • изменять их значения с помощью оператора точки (.)
Давайте посмотрим на последствия этих действий:
Во-первых, сделав поля public, protected или default, мы теряем контроль над данными и ставим под угрозу один из основополагающих принципов ООП — инкапсуляцию.
Во-вторых, поскольку поля не private, то кто угодно может изменить их за пределами класса. Это значит, что мы не сможем добиться их неизменяемости.
В-третьих, мы не можем предоставить никакой логики для изменения полей по каким-то условиям.
Давайте предположим, что у нас есть класс Employee с полем retirementAge:

public class Employee {
    public String name;
    public int retirementAge;

    public Employee(String name, int retirementAge) {
        this.name = name;
        this.retirementAge = retirementAge;
    }
}
Обратите внимание, что в классе мы установили поля как public, чтобы разрешить к ним доступ извне. Теперь давайте изменим retirementAge сотрудника:

public class RetirementAgeModifier {

    public static void main(String[] args) {
        Employee employee = new Employee("John", 58);
        employee.retirementAge = 18;
    }
}
Как мы видим, любой, кто обратится к классу Employee, может легко делать то, что он хочет с полем retirementAge. Нет никакого способа проверить корректность этого изменения.
В-четвертых, а как бы мы могли ограничить доступ к полям только для чтения или только для записи извне?
Вот тут и появляются геттеры и сеттеры.
Используя геттер и сеттер, мы можем контролировать доступ к важным переменным и их обновление (например, требуется изменить значение переменной в заданном диапазоне. В противном случае новое значение не будет присвоено).
Рассмотрим следующий код сеттера:

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 и т. д. могут проверять информацию или внедрять свой служебный код через геттер и сеттер. Поэтому при интеграции вашего кода с ними необходимо иметь эти методы.
Подытоживая, можно выделить ряд плюсов при использовании геттеров и сеттеров:
  • они помогают достичь инкапсуляции для скрытия состояния объекта и предотвращения прямого доступа к его полям
  • при реализации только геттера (без сеттера) можно достичь неизменяемости объекта
  • они могут предоставлять дополнительные функции: проверка корректности значения перед его присваиванием полю или обработка ошибок. Таким образом, мы можем добавить условную логику и обеспечить поведение в соответствии с потребностями (если сеттер не имеет подобной логики, а лишь присваивает полю какое-то значение, то его наличие не обеспечивает инкапсуляцию. А его присутствие становится фиктивным)
  • можем предоставить полям разные уровни доступа: например, get (доступ для чтения) может быть public, в то время как set (доступ для записи) может быть 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: «Используйте в открытых классах методы доступа, а не открытые поля».
Ошибка № 2: Присваивание ссылки на объект напрямую в сеттере.
Пусть есть такой сеттер:

private int[] scores;
 
public void setScores(int[] scores) {
    this.scores = scores;
}
Ниже приведен код, демонстрирующий проблему:

int[] myScores = {5, 5, 4, 3, 2, 4}; // 1
 
setScores(myScores); // 2
displayScores(); // 3   
 
myScores[1] = 1; // 4
displayScores(); // 5
Массив целых чисел myScores, проинициализированный шестью значениями (строка 1), передается в метод setScores() (строка 2). Затем метод displayScores() выводит все значения этого массива:

public void displayScores() {
    for (int number : scores) {
        System.out.print(number + " ");
    }
    System.out.println();
}
В консоли отобразится следующий результат:
Строка myScores[1] = 1; меняет значение 2-го элемента (не забываем, что в массивах нумерация начинается с нуля).
В строке 5 снова вызывается метод displayScores(), который выводит следующие значения:
Как видим значение 2-го элемента изменилось с 5 на 1 в результате присвоения в строке 4. Почему это важно? Потому что на наших глазах данные, которые якобы хранятся в private-переменной и не должны бесконтрольно меняться извне, изменились, что нарушает идею инкапсуляции. Кстати, почему это происходит?
Давайте снова посмотрим на setScores():

public void setScores(int[] scores) {
    this.scores = scores;
}
После присваивания обе переменные ссылаются на один и тот же объект в памяти — на массив myScores. Таким образом, изменения, которые могут быть внесены в this.scores, либо в myScores, фактически вносятся в один и тот же объект.
Решением этой проблемы является создание копии исходного массива и ее присвоение 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().
Снова запускаем наш пример и получаем такой вывод:
Теперь два вызова метода displayScores() выводят один и тот же результат. Это означает, что массив this.scores независим и отличается от scores, переданного в сеттер. Таким образом, присваивание:

myScores[1] = 1;
не влияет на массив this.scores.
Итак, правило такое: если вы передаете ссылку на объект в сеттер, то не копируйте ее во внутреннюю переменную напрямую. Вместо этого делайте ее копию и только тогда присваивайте ее полю.
Все то же самое можно сделать, используя метод copyOf или copyOfRange:

public void setScores(int[] scores) {
    this.scores = Arrays.copyOf(scores, scores.length);
}
Прочитать о разнице между arraycopy и copyOf можно в этой статье.
Ошибка № 3: Возврат геттером ссылки на объект.
Рассмотрим следующий геттер:

private int[] scores;
 
public int[] getScores() {
    return scores;
}
И следующий фрагмент кода:

int[] myScores = {5, 5, 4, 3, 2, 4}; // 1
 
setScores(myScores) // 2
displayScores();  // 3
 
int[] copyScores = getScores(); // 4
copyScores[1] = 1; // 5
displayScores(); // 6
Тогда мы получим следующий вывод:
Как вы заметили, 2-й элемент массива scores изменяется вне сеттера (в строке 5). Поскольку геттер возвращает ссылку на scores, внешний код, имея эту ссылку, может вносить изменения в массив.
Решение этой проблемы заключается в том, что геттеру необходимо возвращать копию объекта, а не ссылку на оригинал. Модифицируем вышеупомянутый геттер следующим образом:

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 автоматически копирует их значения. Таким образом, ошибок № 2 и № 3 можно избежать.
Например, следующий код безопасен, потому что сеттер и геттер работают с примитивным типом float:

private float amount;
 
public void setAmount(float amount) {
    this.amount = amount;
}
 
public float getAmount() {
    return amount;
}
Таким образом, примитивные типы не наделены проблемами объектов.
6. Реализация геттеров и сеттеров для объектов системных классов
Геттеры и сеттеры для String
String — это immutable-тип. Это означает, что после создания объекта этого типа, его значение нельзя изменить. Любые изменения будут приводить к созданию нового объекта String. Таким образом, как и для примитивных типов, вы можете безопасно реализовать геттер и сеттер для переменной String:

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

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> listTitles;
 
public void setListTitles(List<String> listTitles) {
    this.listTitles = listTitles;
}
 
public List<String> getListTitles() {
    return listTitles;
}
Рассмотрим следующую программу:

import java.util.*;
 
public class CollectionGetterSetter {
    private List<String> listTitles;
 
    public void setListTitles(List<String> listTitles) {
        this.listTitles = listTitles;
    }
 
    public List<String> getListTitles() {
        return listTitles;
    }
 
    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.setListTitles(titles1);
        System.out.println("Titles 1: " + titles1); 
        titles1.set(2, "Habilitation");
 
        List<String> titles2 = app.getListTitles();
        System.out.println("Titles 2: " + titles2);
        titles2.set(0, "Full name");
 
        List<String> titles3 = app.getListTitles();
        System.out.println("Titles 3: " + titles3);
    }
}
Мы можем ожидать, что в консоли будут отображены три одинаковых результата. Однако при запуске программа выдает следующее:
Это означает, что коллекция может быть изменена из кода, находящегося за пределами геттера и сеттера.
Для коллекции из String'ов одним из решений является использование конструктора, который принимает другую коллекцию в качестве аргумента. Например, мы можем изменить код вышеупомянутого геттера и сеттера следующим образом:

public void setListTitles(List<String> listTitles) {
    this.listTitles = new ArrayList<>(listTitles);
}
 
public List<String> getListTitles() {
    return new ArrayList<>(listTitles);   
}
Повторно скомпилируем и запустим программу CollectionGetterSetter. Она выдаст желаемый результат:
Обратите внимание: описанный выше подход с конструктором работает только с коллекциями из String-элементов, но он не будет работать для других объектов коллекций.

Рассмотрим следующий пример для коллекции объектов Person:

import java.util.*; 
   
public class CollectionGetterSetterObject { 
    private List<Person> listPeople; 
   
    public void setListPeople(List<Person> listPeople) { 
        this.listPeople = new ArrayList<>(listPeople); 
    } 
   
    public List<Person> getListPeople() { 
        return new ArrayList<>(listPeople); 
    } 
   
    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.setListPeople(list1); 
   
        System.out.println("List 1: " + list1); 
   
        list1.get(2).setName("Maryland"); 
   
        List<Person> list2 = app.getListPeople(); 
        System.out.println("List 2: " + list2); 
   
        list1.get(0).setName("Peter Crouch"); 
   
        List<Person> list3 = app.getListPeople(); 
        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; 
    } 
}
При запуске мы получим следующий вывод:
При создании новой коллекции на основе имеющейся, объекты, размещенные в оригинальной коллекции, не будут скопированы. Вместо этого в новую коллекцию будут скопированы только ссылки на эти объекты. И хоть в итоге две коллекции и различны, но содержат одни и те же объекты.
Также мы можем обнаружить, что ArrayList, HashMap, HashSet и т. д. реализуют свои собственные методы clone(). Эти методы якобы возвращают копии, но на самом деле копируют лишь ссылки (происходит неглубокое копирование). Об этом прямо написано в Javadoc метода clone() класса ArrayList:

Returns a shallow copy of this ArrayList instance. (The elements themselves are not copied)
public Object clone()
Что переводится, как «возвращает копию этого экземпляра ArrayList (сами элементы не копируются)».
Таким образом, мы не можем использовать метод clone() этих классов коллекций. Нам самостоятельно необходимо реализовать метод clone() в Person. Реализация будет выглядеть так:

public Object clone() {
    return new Person(name);
}
Сеттер для listPeople модицифируем следующим образом:

public void setListPeople(List<Person> listPeople) {
    for (Person person : listPeople) {
        this.listPeople.add((Person) person.clone());
    }
}
А геттер модифицируем так:

public List<Person> getListPeople() {
    List<Person> listPeople = new ArrayList<>();
    for (Person person : this.listPeople) {
        listPeople.add((Person) person.clone());
    }
    return listPeople;
}
Новая версия класса CollectionGetterSetterObject:

import java.util.*;
 
public class CollectionGetterSetterObject {
    private List<Person> listPeople = new ArrayList<>();
 
    public void setListPeople(List<Person> listPeople) {
        for (Person person : listPeople) {
            this.listPeople.add((Person) person.clone());
        }
    }
 
    public List<Person> getListPeople() {
        List<Person> listPeople = new ArrayList<>();
        for (Person person : this.listPeople) {
            listPeople.add((Person) person.clone());
        }
        return listPeople;
    }
 
    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.setListPeople(list1);
 
        System.out.println("List 1: " + list1);
 
        list1.get(2).setName("Maryland");
 
        List<Person> list2 = app.getListPeople();
        System.out.println("List 2: " + list2);
 
        list1.get(0).setName("Peter Crouch");
 
        List<Person> list3 = app.getListPeople();
        System.out.println("List 3: " + list3);
 
    }
}
Запустив код выше, мы получим следующий вывод:
Таким образом, ключевыми моментами для реализации геттера и сеттера для коллекций являются:
  • Для коллекций из элементов типа 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;
    }
 
    public String toString() {
        return name;
    }
 
    public Object clone() {
        return new Person(name);
    }
}
Как мы видим, класс Person реализует свой метод clone() для возврата клонированной версии самого себя.
При этом сеттер должен быть реализован следующим образом:

public void setFriend(Person friend) {
    this.friend = (Person) friend.clone();
}
А геттер таким образом:

public Person getFriend() {
    return (Person) friend.clone();
}
Итак, правила реализации геттера и сеттера для вашего собственного класса такие:
  • Реализуйте метод clone()
  • Возвращайте клонированный объект из геттера
  • Используйте клонированный объект в сеттере
Заключение
В этой статье мы подробно разобрали, на первый взгляд, простую, но очень важную тему, которую многие программисты пробегают по верхам, не вдаваясь в детали. Мы рассмотрели на примерах возникающие проблемы при неверном использовании геттеров и сеттеров, а также выяснили способы их решения.
Оцените статью, если она вам понравилась!