Парадигмы ООП
Для начала я их просто перечислю, а потом попробую описать подрообнее. Итак:
- Инкапсуляция
- Наследование
- Полиморфизм
Теперь мы рассмотрим более подробно каждую парадигму.
Инкапсуляция
Не пытайтесь найти перевод этого термина с английского — вы получите encapsulation. Зато если вы попробуйте присмотреться, вы увидите главное — «капсула». Каждый объект не должен выставлять наружу все свои параметры для изменения просто так. Например, если у нашего робота есть координаты X и Y, то не вызывает сомнений факт, что их нельзя менять прямо. Робот должен поменять свои координаты в результате передвижения. Нельзя обратиться к переменной X внутри объекта Robot и сделать самое простое присваивание. Это будет как минимум нелогично. Лучше такого вообще не позволять. Т.е. мы таким образом должны создавать описание класса, чтобы нельзя было просто так получать доступ к его внутренним переменным. Это будет похоже на то, как если бы мы при производстве телевизора давали людям доступ ко всей схеме и каждый мог переключать проводки внутри него напрямую. Обычный человек таким телевизором вряд ли бы пользовался. Конечно нашлось бы несколько энтузиастов, которые обрадовались такому положению дел. Но это скорее всего экзотика. Более правильно в случае с роботом было сделать так, чтобы можно было получить значения координат. А менять координаты можно было бы только в результате движения робота. Проехал он 10 метров — поменялись его координаты X и Y в соответствии с расстоянием и курсом. Причем автоматически.
Т.е. закрытие внутренних переменных — хорошая идея. Для правильного поведения объекта. Нам самим будет проще — при условии, что класс более-менее разумно спроектирован и реализован. Но возникает резонный вопрос — а как тогда обращаться к внутренним переменным. В какой-то момент мы будем вынуждены это сделать. Об этом мы узнаем прямо сейчас. А заодно посмотрим на класс. Перед нами достаточно простое описание робота. Рассмотрим его внимательно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public class Robot { // Текущая координата X private double x = 0; // Текущая координата Y private double y = 0; // Текущий курс (в градусах) private double course = 0; // Передвижение на дистанцию distance public void forward(int distance) { // Обращение к полю объекта X x = x + distance * Math.cos(course / 180 * Math.PI); // Обращение к полю объекта Y y = y + distance * Math.sin(course / 180 * Math.PI); } // Печать координат робота public void printCoordinates() { System.out.println(x + "," + y); } public double getX() { return x; } public double getY() { return y; } public double getCourse() { return course; } // Мы ррассмотрим этот метод несколько позже public void setCourse(double course) { this.course = course; } } |
В этом примере есть несколько моментов, на которые надо обратить внимание.
Первый — возле объявления внутренних переменных появилось ключевое слово private. Это слово определяет видимость переменной (и не только — мы это увидим). Что значит «видимость»? По сути это означает в какой части кода программы можно будет обратиться к этой переменной.
Всего в Java существует три варианта таких слов:
- private
- protected
- public
Слово «private» дает самую слабую видимость — только внутри описания/методов самого класса. Т.е. в методе forward переменные и course видимы. Но если вы попробуете обратиться к этим перменным так, как мы делали в предыдущей части из класса RobotManager, то ваша программа не скомпилируется. Вам будет указано, что у класса Robot переменная x (или y) — не доступна.
Слово «protected» позволяет видеть переменные другим классам, но не всем. Когда мы доберемся до понятие наследования и пакетов, мы рассмотрим «protected» еще раз. Пока дадим просто описание — переменные будут видны в классах-наследниках, и классах, находящихся в том же пакете. Может пока не очень понятно, но наберитесь терпения.
И наконец «public». Это описание позволяет обращаться к переменной/методу откуда угодно. Вы наверняка увидели public в коде класса Robot при объявлении методов.
Если вы никогда до этого не сталкивались с объектами и классами, то у вас может возникнуть вполне резонный вопрос «А зачем это надо — видимость какая-то?». Я попробую на него ответить.
Как мы уже говорили, было бы нелогично позволять «переносить» робота просто присваиванием новых координат — так настоящие роботы не передвигаются. Программа — это попытка отражения реальной задачи и приближение к реальности дает возможность упростить себе жизнь. Если робот «не позволяет» произвольно перемещать его куда угодно — это большое подспорье. Вы не сделаете ошибку в процессе программирования — вам это просто не позволят. Если вы думаете, что все это забавы лобастых дядек — попробуйте представить себе комнату, в которой провода с электричеством не имеют изоляции. Вообще. И теперь попробуйте представить, что вам надо сделать ремонт в этой комнате. Мне когда-то сразу стало понятно, насколько это важно.
Современные программы включают огромное количество строк кода и помнить все особенности, ограничения и прочие моменты реализации — весьма сложно. А учитывая, что большинство программ пишется командой программистов, которая меняется со временем, то «тайные знания» кода передать практически невозможно. Вы конечно можете предложить каждому вновь прибывшему программисту прочитать и понять весь код. Но вряд ли такое возможно.
Теперь если мы попробуем управлять нашим новым роботом программой, которая была написана раньше (а именно класс RobotManager), то мы не сможем ее скомпилировать. Предлагаю посмотреть код, который позволяет управлять нашим обновленным роботом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class RobotManager { public static void main(String[] args) { // Создаем объекта класса Robot Robot robot = new Robot(); // Вперед на 20 метров robot.forward(20); // Напечатать координаты robot.printCoordinates(); // Это более корректный способ менять курс. // Реализация внутри робота не сильно отличается, но // мы в любой момент сможем сделать более продвинутую версию // Но класс RobotManager об этом даже не узнает robot.setCourse(90); // Вперед на 20 метров robot.forward(20); // Напечатать координаты robot.printCoordinates(); // Курс 45 градусов robot.setCourse(45); // Вперед на 20 метров robot.forward(20); // Напечатать координаты robot.printCoordinates(); } } |
Я рекомендую посмотреть код, в нем есть важные комментарии. Но я еще раз заострю ваше внимание на моменте ограничения доступа к переменным класса Robot. Теперь ни один класс не сможет «испортить» координаты робота — мы сможем воздействовать на них только через команды роботу передвинуться или повернуться. И больше никак. (Не буду тут категоричен — на самом деле можно и туда забраться, но это уже будет низкоуровневое програмирование, которое отличается от прикладного по своим идеям). Здесь очень важным моментом является понимание, что теперь мы в большей степени приблизились к более адекватному моделирования задачи — она теперь более «живая», более настоящая. И это служит хорошим подспорьем при создании программы.
В этом приближении к реальности и есть, на мой взгляд, главная цель инкапсуляции. Проектирование программы с помощью ООП сводится таким образом к проектированию и написания классов, которые умеют делать определенную работу. По сути ваша программа превращается в набор готовых инструментов — роботы, самолеты, счета, графические окна, кнопки и т.д. И каждый инструмент «прячет» реализацию своих функций, отображая другим классам только свои «публичные» возможности. Если инструменты уже есть, то остается только написать программу, которая правильно ими управляет. А если инструмент делает не совсем то, что нам надо ? В этом случае его надо переделать. Или создать новый. Для исправления некоторых функций класса можно воспользоваться еще одной парадигмой — наследованием — о которой мы поговорим в следующей части.
Передача параметров и слово this
Прежде чем мы начнем рассмотрение следующей парадигмы ООП — наследование — мне бы очень хотелось остановиться на весьма важном моменте — передача параметров в метод и из метода. Как не странно, многие начинающие программисты, хоть и действуя интуитивно, достаточно правильно пишут программы и общаются с параметрами, в реальности не очень хорошо себе представляют механизм передачи параметров. Тем не менее понимание этого вопроса достаточно нужная вещь.
Итак, шаг первый — формальные параметры и фактические. Формальный параметр — это параметр, который вы описываете внутри метода. А фактически — это то, что вы передаете при вызове метода. Предлагаю рассмотреть все на примере. Рассмотрим упрощенный класс Robot, у которого мы оставим только одно поле course и два метода — для чтения и записи. Эту пару методов часто называют сеттер/геттер (set/get). Их очень часто создают для работы с полями класса и практически все IDE имеют редакторские возможности для создания этой пары методов. Если пойти еще немного дальше, то могу сказать. что именование этих методов используется во многих технологиях Java. Но пока давайте сосредоточимся только на параметрах. Итак у нас есть класс Robot c одним полем.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Robot { // Текущий курс (в градусах) private double course = 0; public double getCourse() { return course; } public void setCourse(double course) { this.course = course; } } |
Самое главное находится в методе setCourse. Только здесь мы объявили параметр при вызове метода. Параметр в скобках double course является формальным параметром. В этой переменной будет находится то число, которое мы будем передавать внутрь этого метода. А фактическим будет именно число. Иными словами — формальный параметр как ящик, в который мы можем положить какую-то величину (в данном случае вещественное число). А вот то, что мы положим в этот ящик и будет фактическим параметром. Думаю, что с этим разобрались. Так что давайте разбираться дальше.
Вы наверняка обратили внимание на выражение внутри нашего метода:
this.course = course
Объяснение это конструкции очень простое. Внутри нашего класса уже есть свойство course (private double course). И мы хотим именно свойству присвоить значение, которое находится в формальном параметре course. Но простое написание
course = course
привело бы нас к бесполезной операции — мы присвоили бы переменной (формальному параметру) course ее же значение. Наше цель — присвоить значение из локальной переменной (фактического параметра) в свойство объекта. И именно для этого используется ключевое слово this. Это просто ссылка на сам объект внутри его метода. В каждом методе класса можно обратиться к объекту, для которого вызывается метод. (Из этого утверждения есть исключение, но мы его рассмотрим позднее). Резонный вопрос: «А если бы формальный параметр имел бы другео имя. Например не course, а localCourse?». Очень просто — словоthisможно было бы не использовать и наш метод выглядел бы вот так
1 2 3 |
public void setCourse(double localCourse) { course = localCourse; } |
Но т.к. большинство IDE умеет генерировать такие методы, то они создаются так, как было показано ранее. Если вам не нравится — создавайте сами и не используйте this. Это дело вкуса. Однозначного мнения нет. Для облегчения жизни разработчикам IDE часто подсвечивают свойства класса и можно сразу увидеть, что переменная является не локальной переменной или формальным параметром, а именно свойством.
И теперь следующий шаг — параметры в методы передаются путем копирования. В случае передачи элементарного типа это значит, что создается копия числа и работа внутри метода идет с копией. Т.е. если мы объявили переменную внутри какого-то метода, присвоили ей число 99, а затем передали ее в другой метод, то внутри этого метода будет другая переменная (наш формальный параметр), значение которой станет 99. Но если этой переменной внутри метода присвоить число 11, то мы поменяем значение только этой переменной. Рассмотрите пример
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class TestVariable { public static void main(String[] args) { double first = 99; // Создаем экземпляр класса TestVariable tv = new TestVariable(); System.out.println("Main method:" + first); // Здесь мы увидим 99 tv.testMethod(first); System.out.println("Main method:" + first); // И здесь мы снова увидим 99 } public void testMethod(double first) { System.out.println("Test method:" + first); // Здесь мы увидим 99 first = 11; System.out.println("Test method:" + first); // Здесь мы увидим 11 } } |
Для запуска нашего класса можно использовать следующее «умение» NetBeans — вы можете запускать определенный класс. Для этого откройте в редакторе нужный класс и нажмите комбинацию Shift+F6 или выберите пункты меню Run->Run File.
Запустите наш пример и вы увидите, что внутри метода testMethod мы поменяли значение переменной first, но это никак не повлияло на значение переменной first в методе main. Как вы уже наверняка поняли переменная first в разных методах — это совершенно разные переменные. Пусть и называются одинаково. Само собой две одинаковых переменных внутри одного метода недопустимы. А в разных — не проблема.
Мы рассмотрели вариант когда в метод передаются данные элементарных типов. Теперь настало время посмотреть на объекты. С ними будет все несколько сложнее.
Давайте вспомним важный момент, который мы рассматривали ранее — при создании переменной сложного типа (класса) мы не создаем объект сразу. Мы создаем ссылку на объект, который потом надо создать с помощью оператора new.
Т.е. когда мы передаем объект в метод, на самом деле мы передаем не объект, а ссылку на него. И ссылка копируется. Т.е. у нас возникает две ссылки, которые указывают на один и тот же объект. И обе эти ссылки позволят нам обращаться к его свойствам, вызывать его методы. Что это означает? Это означает, что если мы поменяем свойство объекта при обращении к нему через одну ссылку, то и вторая ссылка позволит нам вернуть измененное свойство. Давайте рассмотрим простой пример. Используем снова наш класс Robot и класс для его управления RobotManager.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Robot { // Текущий курс (в градусах) private double course = 0; public double getCourse() { return course; } public void setCourse(double course) { this.course = course; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class RobotManager { public static void main(String[] args) { // Создаем объект для управления роботом RobotManager rm = new RobotManager(); // Создаем объекта класса Robot Robot robot = new Robot(); // Курс 45 градусов robot.setCourse(45); // Напечатать курс System.out.println(robot.getCourse()); // Здесь будет 45 // вызываем метод и передаем туда робота rm.changeCourse(robot); // Напечатать курс System.out.println(robot.getCourse()); // Здесь будет 180 } private void changeCourse(Robot robot) { robot.setCourse(180); } } |
Запускаем класс RobotManager и осознаем результат :). Он предсказуем — у нашего робота поменялось значение курса. Итак, наша ссылка на робота скопировалась. Но обе ссылки работали с одним и тем же роботом. Но что произойдет, если внутри метода changeCourse мы сделаем так:
1 2 3 4 |
private void changeCourse(Robot robot) { robot = new Robot(); robot.setCourse(180); } |
Локальная ссылка теперь будет указывать на новый объект и все действия будут производиться над новым объектом. А старый робот не будет меняться. Надеюсь, что теперь механизм передачи параметров более понятен.
Запуск проекта и метод main
Мы уже сталкивались с этим методом, когда запускали примеры и вы наверняка догадались, что именно этот метод является по сути «точкой входа» в программу. Это действительно так и есть. JVM может запустить только тот класс, который имеет такой метод. Мы пока разобрали не все слова, которые составляют описание этого метода, но не будем спешить. Мы все разберем в процессе изучения. Пока просто примите как необходимость — для запуска программы вам нужно передать JVM класс, в котором именно так и записано
1 2 3 |
public static void main(String[] args) { ... } |
И еще один момент. Только что мы с вами рассмотрели класс TestVariable и даже запускали его с помощью комбинации Shift+F6. Но также мы запускали класс RobotManager простым нажатием F6. В нашем проекте у нас одновременно существует два класса, которые можно запускать, потому что в обоих есть метод main. И здесь нет никакой путаницы. На самом деле загадка раскрывается очень просто — проект на NetBeans содержит внутри себя класс, который надо запускать по умолчанию. Вы можете в этом убедиться следующим образом: Нажмите правой кнопкой мышки на проект (Robot1) и в выпавшем меню выберите самы нижний пункт (Properties). Перед вами появиться окошко со свойствами проекта:
Выберите в левой части пункт «Run» и вы увидите, что наш проект использует по умолчанию класс RobotManager.
Если вы введете в выделенную строку другой класс, то тогда NetBeans по клавише F6 будет запускать его. Вот и весь секрет.
И теперь нас ждет следующая статья: Наследование