Привет! Сегодняшнюю лекцию посвятим инкапсуляции и начнем ее сразу с примеров :)
Перед тобой — привычный автомат с газировкой. У меня к тебе один вопрос: а как он работает? Попробуй ответить подробно: откуда вываливается стакан, как поддерживается температура внутри, где хранится лед, как автомат понимает, какой сироп добавить и т.д.
Вероятнее всего, ответов на эти вопросы у тебя нет. Хорошо, возможно не все пользуются такими автоматами, в нынешнее время они не настолько популярны.
Попробуем привести другой пример. Что-нибудь, чем ты точно пользуешься много раз каждый день.
О, есть идея!
Расскажи, как работает поисковик Google. Как именно он ищет информацию по тем словам, которые ты ввел? Почему наверху находятся эти результаты, а не другие?
Хотя ты пользуешься гуглом каждый день, скорее всего, ты этого не знаешь. Но это не важно. Ведь тебе и не нужно этого знать.
Ты можешь вводить запросы в поисковик не задумываясь, как именно он работает. Ты можешь купить газировку в автомате, не зная как он устроен. Ты можешь водить машину, не вникая в работу двигателя внутреннего сгорания, и вообще не зная физику даже на школьном уровне.
Все это возможно благодаря одному из главных принципов объектно-ориентированного программирования — инкапсуляции.
Читая разные статьи на эту тему, наверняка ты сталкивался с тем, что в программировании есть два распространенных понятия — инкапсуляция и сокрытие. И под словом «инкапсуляция» авторы понимают то одно, то другое (так уж сложилось). Мы разберем оба термина, чтобы у тебя было полное понимание.
Изначальное значение слова «инкапсуляция» в программировании — объединение данных и методов работы с этими данными в одной упаковке («капсуле»).
В Java в роли упаковки-капсулы выступает класс. Класс содержит в себе и данные (поля класса), и методы для работы с этими данными.
Тебе это кажется очевидным, но в других концепциях программирования все устроено иначе. Например, в функциональном программировании данные строго отделены от операций над ними.
В ООП же (объектно-ориентированном программировании) программы состоят из классов-капсул, которые являются одновременно и данными, и функциями для работы с ними.
Теперь поговорим о сокрытии.
Как же так получается, что мы пользуемся всякими сложными механизмами без понимания, как они устроены и на чем основана их работа? Все просто: их создатели предоставили простой и удобный интерфейс.
На автомате с газировкой интерфейс — это кнопки на панели. Нажав одну кнопку, ты выбираешь объем стакана. Нажав вторую, выбираешь сироп. Третья отвечает за добавление льда. И это все, что тебе нужно сделать.
Неважно, как именно автомат устроен внутри. Главное — он устроен так, что для получения газировки пользователю нужно нажать три кнопки.
То же и с автомобилем. Неважно, что там происходит у него внутри. Главное — при нажатии правой педали автомобиль едет вперед, а при нажатии левой — тормозит.
Именно в этом заключается суть сокрытия. Все «внутренности» программы скрываются от пользователя. Для него эта информация является лишней, ненужной. Пользователю необходим конечный результат, а не внутренний процесс.
Давай для примера посмотрим на класс
Auto
:
public class Auto {
public void gas() {
/*внутри автомобиля происходят какие-то сложные вещи
в результате которых он едет вперед*/
}
public void brake() {
/*внутри автомобиля происходят какие-то сложные вещи
в результате которых он тормозит*/
}
public static void main(String[] args) {
Auto auto = new Auto();
//Как все выглядит для пользователя
//нажал одну педаль - поехал
auto.gas();
//нажал другую педаль - затормозил
auto.brake();
}
}
Вот как выглядит сокрытие реализации в Java-программе. Все как в реальной жизни: пользователю предоставлен интерфейс (методы). Если ему нужно, чтобы автомобиль в программе выполнил действие, достаточно вызвать нужный метод. А уж что там происходит внутри этих методов — информация лишняя, главное, чтобы все работало как надо.
Здесь мы говорили про сокрытие реализации. Кроме него в Java есть еще сокрытие данных. О нем мы писали в лекции про геттеры и сеттеры, но не будет лишним напомнить.
Например, у нас есть класс Cat
:
public class Cat {
public String name;
public int age;
public int weight;
public Cat(String name, int age, int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
public Cat() {
}
public void sayMeow() {
System.out.println("Мяу!");
}
}
Возможно, ты запомнил из прошлой лекции, в чем проблема этого класса? Если нет — давай вспомним.
Проблема в том, что его данные (поля) открыты для всех, и другой программист легко может создать в программе безымянного кота с весом 0 и возрастом -1000 лет:
public static void main(String[] args) {
Cat cat = new Cat();
cat.name = "";
cat.age = -1000;
cat.weight = 0;
}
В такой ситуации можно пристально следить за тем, не создает ли кто-то из твоих коллег объектов с неправильным состоянием, но гораздо лучше было бы исключить саму возможность создавать такие «неправильные объекты».
С сокрытием данных нам помогают:
- модификаторы доступа (private, protected, package default);
- геттеры и сеттеры.
- Контроль за корректным состоянием объекта. Примеры этому были выше: благодаря сеттеру и модификатору private, мы обезопасили нашу программу от котов с весом 0.
- Удобство для пользователя за счет интерфейса. Мы оставляем «снаружи» для доступа пользователя только методы. Ему достаточно вызвать их, чтобы получить результат, и совсем не нужно вникать в детали их работы.
- Изменения в коде не отражаются на пользователях. Все изменения мы проводим внутри методов. На пользователя это не повлияет: он как писал auto.gas() для газа машины, так и будет писать. А то, что мы поменяли что-то в работе метода gas() для него останется незаметным: он, как и раньше, просто будет получать нужный результат.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ