Слово Инкапсуляция
происходит от лат. in capsula
, capsula
- "коробочка".
Инкапсуляция
- это контроль доступа к полям и методам класса, сокрытие деталей реализации от внешних глаз.
Грамотно написанный класс должен ограничивать доступность своих членов и взаимодействовать с пользователем только с помощью своего интерфейса. Для этого необходимо четко и ясно представлять себе то, как вы представляете работу с вашим классом других частей программы, а все, что не входит в интерфейс - скрывать, т.е инкапсулировать.
Если следовать заветам Джошуа Блоха - главное правило заключается в том, чтобы сделать каждый класс или член максимально недоступным.
Почему?
Это просто понять, если представить себе, например, автомобиль. По сути, все, что является внутренностями машины - скрыто от вас, а открытое API
- это руль и педали.
Т.е все, что вам доступно - это только то, что вам необходимо для взаимодействия с объектом, в нашем случае - с автомобилем.
Если этого не было, то пользователю объекта предоставлялся бы полный доступ к реализации - ко всем составляющим объекта.
Теперь представьте какую опасность в себе таит полный доступ до составляющих для всех пользователей.
Это похоже с тем, что вы дали root
доступ на вашем сервере или компьютере для всех пользователй интернета.
Как вы думаете - как долго ваша система проживет?
Возвращаясь к примеру с машиной нарушение инкапсуляции может привести к поломке автомобиля просто потому, что водитель или пассажир по ошибке или ради интереса воткнул соломинку в двигатель. Во время движения.
Я думаю этих примеров достаточно для того, чтобы вы, как разработчик и пользователь, поняли значимость инкапсуляции
и не пренебрегали ей.
Java
, как ООП
язык предоставляет нам так называемые модификаторы доступа для обеспечения инкапсуляции
.
Модификатор доступа - это ключевые слова Java
для задания области видимости полей, методов и классов.
В Java
существует целых 4
модификатора доступа:
-
public
Доступ всем и отовсюду
.К полям, методам и классам, объявленным как
public
доступ имеет кто угодно. -
private
Доступ мне и только мне
.К полям, методам и классам, объявленным как
private
, имеет доступ толькокласс
, в котором они объявлены. -
package
Доступ мне и всем в пределах пакета, всем соседям
К полям, методам и классам, объявленным
package
, имеет доступ не толькокласс
, в котором они объявлены, но и всеклассы
, находящиеся в том же самомпакете
. Это модификатор доступа по-умолчанию, если вы не указали иного. -
protected
Доступ мне и всем наследникам
К полям, методам и классам, объявленным как
protected
, имеет доступкласс
, в котором они объявлены, всеклассы
, находящиеся в том же самом пакете и всеклассы
-потомки, все классы, унаследованные от того, где сделано объявление.Крайне важно помнить, что в
Java
модификатор доступаprotected
дает также доступ и всем в пакете! Это очень странное решение от создателей, однако так сделано и это просто надо помнить.
Сведем все в одну таблицу для наглядности:
Модификатор доступа | Границы видимости | Описание |
---|---|---|
public |
Доступ всем и отовсюду . |
К полям, методам и классам, объявленным как public доступ имеет кто угодно. |
private |
Доступ мне и только мне . |
К полям, методам и классам, объявленным как private , имеет доступ только класс , в котором они объявлены. |
package |
Доступ мне и всем в пределах пакета, всем соседям |
К полям, методам и классам, объявленным package , имеет доступ не только класс , в котором они объявлены, но и все классы , находящиеся в том же самом пакете . |
protected |
Доступ мне и всем наследникам |
К полям, методам и классам, объявленным как protected , имеет доступ класс , в котором они объявлены, все классы , находящиеся в том же самом пакете и все классы -потомки. |
Выше было сказано, что модификатор доступа package
позволяет давать доступ до полей/методов класса всем классам, которые находятся в том же пакете, что и текущий класс.
Резонный вопрос, который возникает - для чего вообще ввели понятие пакета?
Представим, что в нашем проекте несколько сотен, а то и тысяч классов.
Чаще всего в Java
в одном файле - один класс. Я думаю, вы уже начали догадываться к чему я клоню - представьте тысячу файлов в одной директории!
В таком хаосе нельзя остаться верным императору и не запутаться в тьме.
Что делать?
Логично, что надо попытаться структурировать их. Разнести по нескольким директориям, тем самым создать дерево директорий, в котором легко ориентироваться. И именно это и сделано в Java.
Только вместо директории мы говорим пакет
.
Пакеты иерархичны, эта иерархия описывается через .
, потому точка не может быть в имени пакета.
Пакет, к которому принадлежит класс, всегда описывается в самом начале исходного файла.
Если это описание есть, оно должно быть первым, только комментарии могут быть раньше него.
В пакет объединены классы, которые тесно связаны друг с другом логически.
Примерами могут служить пакеты java.swing
, где собраны классы отвечающие за работу с библиотекой swing
или же javafx.scene.control
, где можно найти все для работы с контролами - кнопки, комбо-боксы и т.д
Т.е пакеты - это некоторая структурная единица в разработке, группирующая логически классы.
И это дополнительный инструмент для инкапсуляции!
Например вы пишите библиотеку, где внутренние компоненты взаимодействуют посредством класса Event
- событий.
Т.е ваши внутренние компоненты пересылают друг другу события, вы вводите сущность некоторого служебного события.
Этот класс - внутренний для нашей библиотеки, мы не хотим и не собираемся его 'светить' наружу.
И в таком случае модификатор package
как раз то, что нам нужно!
Мы скроем наш класс от разработчиков, которые будут пользоваться нашей библиотекой, но при этом сами будем спокойно пользоваться им!
При переопределении метода у класса наследника, мы не можем изменить модификатор доступа на более закрытый.
Например, если метод public
, то в классе наследнике не получится уровень доступа изменить на private
.
Что довольно логично, раз класс-родитель предоставляет нам свой метод в качестве открытого, то и класс-потомок не может ограничивать доступ к этому методу - иначе измениться интерфейс класса.
Это сделано для того, чтобы гарантировать, что объект класса-наследника мы можем использовать везде, где можно было бы использовать объект супер-класса.
Это отсылает нас к SOLID
, а если быть точнее, то к L
- The Liskov Substitution Principle.
Наследующий класс должен дополнять, а не изменять базовый.
Главное правило, повторюсь, сделать ваш код максимально закрытым, сделать так, чтобы пользоваться можно было лишь тем, что действительно необходимо.
Если ваш класс используется только в области видимости пакета - сделайте такой класс доступным только в пакете с помощью package
.
Такой класс перестанет быть доступным извне, он станет частю реализации пакета.
В таком случае, даже если вы как-то измените логику этого класса или вообще удалите его - вы будете точно уверены, что вы не сломаете ничего у пользователей вашего кода. Оставив же такой класс открытым, вы будете обязаны поддерживать работоспособность класса, поддерживать совместимость и т.д
Может показаться, что public
обделен вниманием и дабы не обидеть его - давайте обсудим как с ним быть?
Когда мы объявляем поле или метод как public
мы по сути включаем его в API
класса.
Когда это действительно часть API
, без которого взаимодействие с классом и его объектами невозможно - это оправдано.
Но когда это не относится к API
- это уже вредная и пагубная практика, которая может привести к неподдерживаемому коду, трудноуловимым ошибкам и плохому сну.
Почему же? Давайте разбираться!
Чего надо опасаться, когда объявляем public
поле и о чем надо задумываться?
В первую очередь, это то, что большинство объектов в Java
- изменяемые.
И ссылки на объекты по умолчанию тоже изменяемые.
Это значит, что сделав повесив public
на такое поле кто-то просто может изменить не только состояние вашего объекта, но и поведение!
Давайте приведем простой пример.
Пусть у нас есть класс Person
, мы сделали его в лучших традициях начинающих разработчиков:
class Person {
public int age;
public String name;
Person(int age, String name) {
this.age = age;
this.name = name;
}
}
Теперь создадим несколько экзмепляров класса и в одном из них изменим возраст:
class Person {
public int age;
public String name;
}
// some code
public static void main(String[] args) {
Person p1 = new Person(27, "Aleksandr");
System.out.println(String.format("Возраст: %s, Имя: %s", p1.age, p1.name));
// меняем возраст
p1.age = -100;
System.out.println(String.format("Возраст: %s, Имя: %s", p1.age, p1.name));
}
И что же мы видим? А видим мы Александра в возрасте минус сто.
Да, этому Александру явно не повезло и в первую очередь - ему не повезло встретиться с таким программистом, который не учел, что возраст не может быть отрицательным.
Понятно, что для того, чтобы не дать ставить всем попало его возраст и повесить некоторые ограничения на количество лет и их качество - надо закрыть поле возраста. Однако тут же встает и еще один вопрос - закрыть то мы закроем, а как обращаться теперь к полю? И возраст менять как?
Для этого существууют так называемые getter
-ы и setter-ы
.
Давайте модернизируем наш класс, учтя ошибки:
/**
* Example of encapsulation
*/
class Person {
private int age;
private String name;
public int getAge() {
return age;
}
public String getName() {
return name;
}
/**
* We can't set age less zero.
*/
public void setAge(int age) {
if (age < 0) throw new IllegalArgumentException("Age can't be less then zero");
this.age = age;
}
}
// some code
public static void main(String[] args) {
Person p1 = new Person(27, "Aleksandr");
System.out.println(String.format("Возраст: %s, Имя: %s", p1.age, p1.name));
// меняем возраст
p1.setAge(12);
System.out.println(String.format("Возраст: %s, Имя: %s", p1.age, p1.name));
p1.setAge(-100); // если мы не отреагируем на исключение - программа упадет, возраст не изменится
}
Теперь при отрицательных числах возраста мы будем бросать исключение, тем самым не дав никому сделать Александра и не только его, с отрицательным возрастом.
А обращение к полю мы получаем через специальный метод - getter
- который является частю API
класса.
Тем самым мы, как проектировщики, подразумеваем, что пользователь может обращаться к экземплярам класса Person
и спросить у них их возраст.
При этом, если мы решим, что наши персонажи не должны сообщать никому свой возраст, то мы просто уберем этот метод.
Т.е инкапсуляция
- это еще и контроль за валидностью данных.
Не надо быть провидцем, чтобы почувствовать вопрос, который уже давно витает в воздухе.
Q: Когда же безопасно использовать public
?
A: С final
-полями и неизменяемыми объектами.
Например, при объявлении констант.
Необходимо помнить, что
final
не гарантирует нам, что вы не измените сам объект(если он модифицируемый). Т.еfinal
гарантирует лишь то, что вы не измените ссылку на объект, но сам объект может меняться.
Для объявления константы, входящей в API
класса можно использовать конструкцию вида: public static final
.
Часто константы группируют логически и выносят в отдельные классы, например, как показано вот тут.
Также, если ваш объект не изменяемый(immutable
) и должен входить в интерфейс класса, то вполне допустимо использовать public final
.
Ведь объект - неизменяемый, а значит и навредить ему или изменить его нельзя,
а благодаря ключевому слову final
вы закрываете возможность подменить этот объект на другой.
Однако, даже использование final
вас не спасет, если ваш объект может менять свое состояние, т.е является mutable
.
Как например, массивы или коллекции.
Представим, что вы объявили public static final
ссылку на массив или коллекцию в вашем классе.
Массив - это изменяемая структура данных.
В совокупности с тем, что из-за открытого доступа, ведь мы объявили массив как public
,
мы никак не контролируем добавление и удаление элементов в такой массив мы получаем серьезную проблему.
Любой желающий может добавить или удалить элемент из такого массива, при этом мы об этом во время даже не узнаем.
Как говорила моя учительница по английскому: "To sum up"!
- Чем строже уровень доступа - тем лучше.
- Проверяйте значения, которые вы присваиваете полям и соответствующим образом реагируйте на ошибки.
- Избегайте
public
полей. - Старайтесь не использовать
public
для изменяемых объектов. - Тщательно продумывайте
API
класса - его интерфейс.
Ну и таблица-напоминалка:
Модификатор доступа | В классе | В пакете | В наследнике(вне пакета) | Везде |
---|---|---|---|---|
public | + | + | + | + |
protected | + | + | + | - |
package | + | + | - | - |
private | + | - | - | - |