Слово Инкапсуляция
происходит от лат. in capsula
, capsula
- "коробочка".
Инкапсуляция
- механизм, позволяющий объединить данные и методы, работающие с этими данными, в единый объект и скрыть детали реализации от пользователя.
Грамотно написанный класс должен ограничивать доступность своих членов и взаимодействовать с пользователем только с помощью своего интерфейса. Для этого необходимо четко и ясно понимать то, как вы представляете взаимодействие вашего класса и других частей программы, а все, что не входит в интерфейс скрывать.
Если следовать заветам Джошуа Блоха: главное правило заключается в том, чтобы сделать каждый класс или член максимально недоступным.
Почему?
Представьте себе автомобиль. По сути, все, что является внутренностями машины, скрыто от вас, а открытое API
- это руль и педали.
Т.е все, что вам доступно - это только то, что вам необходимо для взаимодействия с объектом, в нашем случае - с автомобилем.
Если этого не было, то пользователю объекта предоставлялся бы полный доступ к реализации, ко всем составляющим объекта.
Теперь представьте, какую опасность в себе таит полный доступ до составляющих для любого пользователя.
Эта ситуация похожа на ту, где вы дали root
доступ на вашем сервере или компьютере для всех пользователй интернета.
Как вы думаете: как долго ваша система проживет?
Возвращаясь к примеру с машиной: нарушение инкапсуляции может привести к поломке автомобиля просто потому, что водитель или пассажир по ошибке, а может и ради интереса, воткнут соломинку в двигатель. Во время движения.
Я думаю этих примеров достаточно для того, чтобы вы, как разработчик и пользователь, поняли значимость инкапсуляции
и не пренебрегали ей.
Java
, как ООП
язык, предоставляет нам несколько инструментов для обеспечения инкапсуляции
: модификаторы доступа и пакеты.
В начале давайте поговорим про то, что такое пакет
.
Представим, что в нашем проекте несколько сотен, а то и тысяч классов.
В большинстве случаев в Java
каждый класс определяется в отдельном файле.
Я думаю, вы уже начали догадываться к чему я клоню: представьте тысячу файлов в одной директории! А если классов более тысячи? Или же вы подключили стороннюю библиотеку, в которой есть классы, с такими же именами, что и у вас в проекте?
В таком хаосе нельзя остаться верным императору и не запутаться во тьме.
Что делать?
Логично, что надо попытаться структурировать их: разнести по нескольким директориям, тем самым создать дерево директорий, в котором легко ориентироваться.
Именно это и сделано в Java
.
Только вместо директории мы говорим пакет
.
Для описания того, что класс относится к пакету используется ключевое слово package
.
Пакеты иерархичны, эта иерархия описывается через .
, поэтому точка не может быть в имени пакета.
Пакет, которому принадлежит класс, всегда описывается в самом начале исходного файла.
Это описание, если оно есть, должно быть первым, только комментарии могут быть раньше него.
// some comments
package aarexer.utils.hash;
public final class HashUtils {
// some code
}
Таким образом класс HashUtils
принадлежит пакету hash
, который является вложенным в пакет utils
, который в свою очередь также вложен в aarexer
.
Существует также некоторое джентльменское соглашение: для обеспечения уникальности имен пакетов принято в начало добавлять реверсированный домен второго уровня разработчика пакета.
Т.е если у вас есть домен aarexer.ru
, то имена пакетов начинаться должны с ru.aarexer
.
Этому джентльменскому соглашению придерживается большинство компаний и разработчиков, например, класс FileUtils
у apache-commons
лежит в пакете:
package org.apache.commons.io;
Имя пакета является частью полного имени класса, это весьма важный момент. Полное имя класса, содержащее имя пакета, решает еще одну важную задачу — уникальность имен классов. И потому появляется возможность создавать классы с одним именем, но в разных пакетах.
В качестве примера можно привести: java.awt.List
и java.util.List
.
Это два разных класса с одним названием, однако, благодаря тому, что они логически разделены пакетами, а пакет является частью полного имени класса, все становится на свои места.
В пакет объединяются классы, которые тесно связаны друг с другом логически.
Примерами могут служить пакет java.swing
, где собраны классы отвечающие за работу с библиотекой swing
или же пакет javafx.scene.control
, где можно найти все для работы с контролами - кнопки, комбо-боксы и т.д
Т.е пакеты - это некоторая структурная единица в разработке, группирующая логически классы. И это дополнительный инструмент для инкапсуляции!
Например, вы пишите библиотеку, где внутренние компоненты взаимодействуют посредством класса Event
- событий.
Т.е ваши внутренние компоненты пересылают друг другу события, вы вводите сущность некоторого служебного события.
Этот класс - внутренний для нашей библиотеки, мы не хотим и не собираемся его 'светить' наружу, так как он нужен нам только для внутреннего использования.
В таком случае мы выносим такой класс в пакет и применяем модификатор доступа package
.
Доступ мне и всем в пределах пакета, всем соседям.
Скроем наш класс от разработчиков, которые будут пользоваться нашей библиотекой, но при этом в использовании не ограничим.
Теперь поговорим про модификаторы доступа.
Модификатор доступа - это ключевое слово Java
для задания области видимости полей, методов и классов.
В Java
существует целых 4
модификатора доступа:
-
public
Доступ всем и отовсюду
.К полям, методам и классам, объявленным как
public
доступ имеет кто угодно. -
private
Доступ мне и только мне
.К полям, методам и классам, объявленным как
private
, имеет доступ толькокласс
, в котором они объявлены. -
package
Доступ мне и всем в пределах пакета, всем соседям
К полям, методам и классам, объявленным
package
, имеет доступ не толькокласс
, в котором они объявлены, но и всеклассы
, находящиеся в том же самомпакете
. Это модификатор доступа по-умолчанию, если вы не указали иного. -
protected
Доступ мне и всем наследникам
К полям, методам и классам, объявленным как
protected
, имеет доступкласс
, в котором они объявлены, всеклассы
, находящиеся в том же самом пакете и всеклассы
-потомки, все классы, унаследованные от того, где сделано объявление.Крайне важно помнить, что в
Java
модификатор доступаprotected
дает также доступ и всем в пакете! Это очень странное решение от создателей, однако так сделано и это просто надо помнить.
Сведем все в одну таблицу для наглядности:
Модификатор доступа | Границы видимости | Описание |
---|---|---|
public |
Доступ всем и отовсюду . |
К полям, методам и классам, объявленным как public доступ имеет кто угодно. |
private |
Доступ мне и только мне . |
К полям, методам и классам, объявленным как private , имеет доступ только класс , в котором они объявлены. |
package |
Доступ мне и всем в пределах пакета, всем соседям |
К полям, методам и классам, объявленным package , имеет доступ не только класс , в котором они объявлены, но и все классы , находящиеся в том же самом пакете . |
protected |
Доступ мне и всем наследникам |
К полям, методам и классам, объявленным как protected , имеет доступ класс , в котором они объявлены, все классы , находящиеся в том же самом пакете и все классы -потомки. |
При переопределении метода у класса наследника, мы не можем изменить модификатор доступа на более закрытый.
Например, если метод public
, то в классе наследнике не получится уровень доступа изменить на private
.
Что довольно логично, раз класс-родитель предоставляет нам свой метод в качестве открытого, то и класс-потомок не должен ограничивать доступ к этому методу, так как иначе изменится интерфейс класса. Это сделано для того, чтобы гарантировать, что объект класса-наследника мы можем использовать везде, где можно было бы использовать объект супер-класса.
Это отсылает нас к SOLID, а если быть точнее, то к L
- The Liskov Substitution Principle.
Класс наследник должен дополнять, а не изменять базовый.
Главное правило, повторюсь, сделать ваш код максимально закрытым, сделать так, чтобы пользоваться можно было лишь тем, что действительно необходимо.
Если ваш класс используется только в области видимости пакета - сделайте такой класс доступным только в пакете с помощью package
.
Тогда такой класс перестанет быть доступным извне, он станет частью реализации пакета.
В таком случае, даже если вы как-то измените логику этого класса или вообще удалите его, вы будете точно уверены, что не сломаете ничего у пользователей вашего кода. Оставив такой класс открытым, вы будете обязаны поддерживать работоспособность класса, поддерживать совместимость и прочее.
Модификатор доступа protected
используется в основном тогда, когда вы планируете использовать это поле/метод в наследовании.
После public
это наиболее открытый модификатор.
Соответсвенно, исходя из описания, не следует применять его вне того, для чего он обычно используется.
Наиболее часто злоупотребляют использованием модификатора public
, поэтому разберем его подробнее.
Когда вы объявляете поле или метод как public
вы по сути включаете его в API
класса.
В случае, если это действительно часть API
, без которого взаимодействие с классом и его объектами невозможно - это оправдано.
Но в тех случаях, когда это не относится к API
- это уже вредная и пагубная практика, которая может привести к неподдерживаемому коду, трудноуловимым ошибкам и плохому сну(как вашему, так и людей, которые используют ваш код).
Почему? Давайте разбираться!
В случае, когда мы делаем метод public
, надо понимать, что мы даем возможность его использования всем.
Тем самым мы вносим такой метод в интерфейс
класса.
Давайте опять сядем в автомобиль и посмотрим на приборную панель. Огромное многообразие кнопок и рычагов - это интерфейс.
Грубо говоря, каждой кнопке и рычагу соответствует какой-то метод.
Если вы добавляете функцию автоматических дворников - логично, что этому должна соответствовать какая-то кнопка, т.е это public
-метод.
А вот если вы делаете функцию подачи горючего в двигатель, то выносить это на приборную панель - губительная практика.
Так как вам, как создателю, наверняка ясно когда эту кнопку нажимать, а вот обыкновенному пользователю совершенно нет. И вероятность того, что он ее нажмет не в то время и не в том месте приближается к катастрофическим 100%.
Поэтому отдавайте себе отчет: что выносить в интерфейс
класса, а что нет.
Чего надо опасаться, когда объявляем 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 than 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
и спросить у них их возраст.
При этом, если мы решим, что наши персонажи не должны сообщать никому свой возраст, то мы просто уберем этот метод, закрыв поле в классе и не дав к нему доступа.
Т.е инкапсуляция
- это еще и контроль за валидностью данных.
Не надо быть провидцем, чтобы почувствовать вопрос, который уже давно витает в воздухе.
Вопрос:
Когда же безопасно использовать public
?
Ответ:
С final
-полями и неизменяемыми объектами.
Например, при объявлении констант.
Необходимо помнить, что
final
не гарантирует нам, что вы не измените сам объект(если он модифицируемый). Т.еfinal
гарантирует лишь то, что вы не измените ссылку на объект, но сам объект может меняться.
Для объявления константы, входящей в API
класса можно использовать конструкцию вида: public static final
.
Про константы и их оформление можно прочесть тут.
Также, если ваш объект не изменяемый(immutable
) и должен входить в интерфейс класса, то вполне допустимо использовать public final
.
Ведь объект - неизменяемый, а значит и навредить ему или изменить его нельзя,
а благодаря ключевому слову final
вы закрываете возможность подменить этот объект на другой.
Однако, даже использование final
не спасет, если объект может менять свое состояние, т.е является изменяемым, mutable
-объектом.
Как например, массивы или коллекции.
Представим, что вы объявили public static final
ссылку на массив или коллекцию в вашем классе.
Массив - это изменяемая структура данных.
В совокупности с тем, что из-за открытого доступа, ведь мы объявили массив как public
, мы никак не контролируем добавление и удаление элементов в такой массив мы получаем серьезную проблему.
Любой желающий может добавить или удалить элемент из такого массива, при этом мы об этом во время даже не узнаем.
Если все, что вы прочли выше для вас не явилось новинкой, вы прекрасно все это знаете и считаете, что тут не может быть подводных камней, то давайте ответим на пару вопросов в порядке возрастания сложности?
Вопрос 0:
Есть ли ограничения на то, кому какой модификатор доступа поставить? Какие модификаторы доступа можно ставить полям класса? Методам? Самим классам?
Ответ:
У членов класса(полей, методов и конструкторов) ограничений нет, могут быть любые.
Сами классы могут быть public
и package
. Внутренние классы могут быть объявлены с любым модификатором доступа, так как в этом плане не отличаются от членов класса.
Вопрос 1:
Какой модификатор доступа будет у конструктора по умолчанию?
Ответ:
Тут все просто - модификатор доступа для конструктора по умолчанию будет такой же, какой модификатор доступа у класса.
Вопрос 2:
Зачем нужен private
конструктор? А protected
конструктор?
Ответ:
Напомним, что конструктор - это способ инициализации экземпляра создаваемого объекта. У класса может быть несколько конструкторов, это просто несколько способов создания. Логично, что и на эти способы хотелось бы иметь возможность навешивать какие-то ограничения.
С private
конструктором все довольно прозрачно. Это средство создания объектов класса, зарезервированное для нас самих и только для нас. Соответственно, в классе-наследнике вы его не вызовете.
С protected
конструктором дело обстоит чуть интереснее.
Он может использоваться в двух случаях.
-
protected
конструктор может вызываться в качестве конструктора родительского класса. Т.е через вызовsuper(...)
. -
protected
конструктор может вызываться в простой конструкцииnew
, но только внутри пакета, где определен класс.
Вопрос 3:
Разрешен ли доступ к private
методам и полям в одном экземпляре класса к другому экземпляру того же класса?
Например:
public class Test {
private int field = 0;
public void testAccess(Test other) {
other.field = 1;
System.out.println(String.format("Private method other.getField()=%s", other.getField()));
System.out.println(String.format("Private field other.field=%s", other.field));
}
private int getField() {
return field;
}
}
Будет ли валиден такой код?
Ответ:
Да! Будет!
Такой код валиден и разрешен.
Ограничение private
не ограничивает доступ экземплярам класса друг к другу.
Более того, это распространяется и на статические методы – т.е. если в статический метод класса A
передать экземпляр класса A
, то внутри этого метода разрешен доступ ко всем private
-членам переданного экземпляра.
Вопрос 4:
А что будет, если у нас есть еще и класс наследник и он в другом пакете?
package examples.modifiers.parent;
public class TestParent {
protected int a = 0;
protected int getA() {
return a;
}
}
Наследник:
package examples.modifiers.child;
import examples.modifiers.parent.TestParent;
public class TestChild extends TestParent {
public void testAccess(TestParent other) {
other.protectedField = 3;
System.out.println(String.format("other.getField()=%s", other.getField()));
System.out.println(String.format("other.field=%s", other.field));
}
}
Будет ли валиден такой код?
Обратите внимание на пакеты и модификаторы доступа!
Ответ:
Нет! Такой код будет не валиден.
При этом, если мы изменим тип аргумента other с TestParent
на TestChild
код уже будет валидным.
Т.е доступ будет к полю или методу чужого экземпляра только если чужой экземпляр является экземпляром того же класса наследника (или унаследованного в свою очередь от него). А вот к полю или методу чужого экземпляра и при этом родительского класса – нет.
Как говорила моя учительница по английскому: "To sum up"!
Инкапсуляция
- механизм, позволяющий объединить данные и методы, работающие с этими данными, в единый объект и скрыть детали реализации от пользователя.
Т.е данные и методы связаны в едином объекте. Концепция подразумевает использование объекта путем предоставления интерфейса взаимодействия и сокрытия деталей его реализации.
При помощи модификаторов доступа уже осуществляется понимание какие члены и методы класса будут являться его интерфейсом взаимодействия, а какие будут являться деталью реализации.
В Java
существует четыре модификатора доступа: public
, protected
, package
и private
.
Основное правило их применения в том, что чем строже уровень доступа - тем лучше.
Так как public
дает доступ до поля/метода всем, то старайтесь не использовать его для изменяемых объектов, либо контролируйте изменения.
Помните, что public
добавляет поле/метод в API
класса!
Ну и таблица-напоминалка:
Модификатор доступа | В классе | В пакете | В наследнике(вне пакета) | Везде |
---|---|---|---|---|
public | + | + | + | + |
protected | + | + | + | - |
package | + | + | - | - |
private | + | - | - | - |
- Инкапсуляция
- Инкапсуляция в ООП
- Статья Евгения Матюшкина aka Skipy
- Разница между модификаторами доступа StackOverFlow
- Модификатор доступа у конструктора StackOverFlow
- Официальная документация
- Java Courses With Kovalevskyi Модуль 2. Урок 4. Пакеты в Java
Также стоит познакомиться с SOLID