Инкапсуляция (иногда называемая сокрытием информации) — ключевая концепция работы с объектами. Формально инкапсуляция — это просто объединение данных и поведения в одну единицу кода (пакет/модуль/класс и т.д.) и сокрытие деталей реализации (в нашем случае реализации полей и методов) от пользователей объекта. Конкретный объект, являющийся экземпляром класса, будет иметь определённые значения своих полей. Набор этих значений представляет собой текущее состояние объекта. При каждом вызове метода объекта его состояние может измениться.
Обычно ключ к эффективной инкапсуляции при работе с классами состоит в том, чтобы запретить напрямую обращаться к полям объекта класса и разрешить обращаться к полям класса только через его методы. То есть программы должны взаимодействовать с данными объекта только через методы этого объекта. Инкапсуляция — это способ придать объекту поведение "черного ящика", что является ключом к повторному использованию и надежности.
Почему это важно? Рассмотрим следующую программу:
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 41);
tom.age = -18;
tom.print(); // Name: Tom Age: -18
}
}
class Person
{
String name;
int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
void print() {
System.out.printf("Name: %s \tAge: %d\n", name, age);
}
}
Здесь в классе Person определены два поля - name и age, которые представляют соответственно имя и возраст и которые доступны в любой точке программы. Если другой класс имеет прямой доступ к этому полю (как, например, в коде выше класс Program), то есть вероятность, что в процессе работы программы ему будет передано некорректное значение, например, отрицательное число:
tom.age = -18;
Подобное изменение данных не является желательным. Либо же мы хотим, чтобы некоторые данные были достуны напрямую, чтобы их можно было вывести на консоль или просто узнать их значение. В этой связи рекомендуется как можно больше ограничивать доступ к данным, чтобы защитить их от нежелательного доступа извне (как для получения значения, так и для его изменения).
Использование различных модификаторов гарантирует, что данные не будут искажены или изменены не надлежащим образом:
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 41);
// tom.age = -18; // напрямую обратиться к полю нельзя
tom.print(); // Name: Tom Age: 41
}
}
class Person
{
private String name; // поле недоступно вне класса
private int age; // поле недоступно вне класса
Person(String name, int age){
this.name = name;
this.age = age;
}
void print() {
System.out.printf("Name: %s \tAge: %d\n", name, age);
}
}
Однако возникает другая проблема: если мы все таки захотим изменить значения полей на корретные значения, то мы не сможем это сделать. И для этой цели используют методы доступа. Например:
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", 41);
tom.print(); // Name: Tom Age: 41
// изменяем возраст
tom.setAge(22);
tom.print(); // Name: Tom Age: 22
// снова пытаемся изменить возраст
tom.setAge(-123); // некорректное значение - возраст не изменяется
tom.print(); // Name: Tom Age: 22
// получаем значения
String name = tom.getName();
int age = tom.getAge();
System.out.printf("Name: %s; Age: %d\n", name, age);
}
}
class Person
{
private String name; // поле недоступно вне класса
private int age; // поле недоступно вне класса
Person(String name, int age){
this.name = name;
this.age = age;
}
void print() {
System.out.printf("Name: %s \tAge: %d\n", name, age);
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
public void setAge(int age){
if(age > 0 && age < 110)
this.age = age;
}
}
Теперь вместо непосредственной работы с полями name и age в классе Person мы будем работать с методами, которые устанавливают и возвращают значения этих полей.
Для изменения возраста предназначен метод setAge(), который принимает новое значение и устанавливает его, если оно соответствует некоторым ограничениям:
public void setAge(int age){
if(age > 0 && age < 110)
this.age = age;
}
Как правило, методы, которые предназначены для изменения поля, называются по шаблону set[имя_поля] (имя поля с большой буквы как в "setAge") и называются мьютейтерами (mutator) или сеттерами, так как они изменяют значения поля.
И если мы хотим изменить значение поля, то используем эти методы:
tom.setAge(22); tom.setAge(-123);
Для поля name такой метод отсутствует, так как я предполагаю, что в программе не требуется менять имя. Однако при необходимости и для поля name можно было бы добавить аналогичный метод.
Для полученияз начений полей предназначены методы getName() и getAge()
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
Такие методы еще называют аксессерами (accessor) или геттерами, так как с их помощью мы получаем значение поля. Они обычно называются по шаблону get[Имя_поля]. И в программе мы можем их использовать для
получения значений полей:
String name = tom.getName(); int age = tom.getAge();
Выше добавленные методы позволяют обезопасить доступ к полям класса, однако тем не менее у нас продолжает иметься проблема, так как мы можем передать в конструктор некорректные данные:
Person tom = new Person("Tom", -123);
И здесь нет единого решения. Можно, например, в этом случае генерировать ошибку (в последующих статьях будет показано, как это делать), либо можно использовать какие-то другие способы. Но одно из решений является - определение единой точки доступа к полям:
public class Program {
public static void main(String[] args) {
Person tom = new Person("Tom", -123);
tom.print(); // Name: Tom Age: 1
}
}
class Person
{
private String name;
private int age = 1; // поле имеет значение по умолчанию
Person(String name, int age){
this.setName(name);
this.setAge(age);
}
void print() {
System.out.printf("Name: %s \tAge: %d\n", this.getName(), this.getAge());
}
public String getName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
public int getAge(){
return this.age;
}
public void setAge(int age){
if(age < 0 && age > 110)
this.age = age;
}
}
Теперь даже внутри класса для установки полей надо обращаться к методам setName/setAge, а для получения - к методам getName/getAge. В случае, если полю передано некорректное значение, мы можем установить значение по умолчанию, как в примере выше число 1 для поля age.
В примере выше рассматривались нестатические компоненты - поля и методы. Но все то же самое касается и статических полей и методов. Например:
public class Program {
public static void main(String[] args) {
// создаем ряд объектов
Person tom = new Person("Tom");
Person bob = new Person("Bob");
Person sam = new Person("Sam");
// получаем количество созданных объектов
int count = Person.getCount();
System.out.printf("Создано объектов: %d\n", count); // Создано объектов: 3
}
}
class Person
{
private String name;
private static int count; // счетчик созданных объектов
Person(String name){
this.name = name;
count++; // при создании нового объекта увеличиваем счетчик
}
public static int getCount(){
return count;
}
}
Здесь в классе Person определена статическая переменная count, которая хранит количество созданных объектов Person. Поскольку она относится ко всему классу в целом, она определена как статическая. Она приватная, поэтому мы не можем обратиться к ней вне класса. Если бы мы могли бы это сделать, то ей можно было бы присвоить любое произвольное значение. Соответственно тогда корректность счетчика была бы под вопросом. В данном же случае она приватная, а для ее получения ее значения определен специальный статический метод getCount