Объектно-ориентированное программирование

Думаю, что количество книг по объектно-ориентированному программированию перевалило за сотни, а может и тысячи. До сих пор остается классическим труд Гради Буча «Объектно — ориентированный анализ и проектирование с примерами приложений на С++». Вы наверняка сможете найти этот труд на просторах интернета.
Также можно посмотреть в Интернете другие ссылки и статьи — их очень много. Например тут — Объектно-ориентированное программирование или тут — Введение в программирование на Java. Второй ресурс содержит введение в язык Java. Весьма неплохо читается, так что отнеситесь к нему с вниманием.

Я попробую описать идеи ООП со своей колокольни. Сама идея появления очень простая: сложность программ растет. Человек уже давно разработал весьма несложное идеологическое решение этого вопроса — декомпозицию, т.е. разделение задачи на крупные блоки, потом на более мелкие, еще мельче и т.д.
По большому счету из этого понимания и родилось процедурное программирование. Программа представляля собой последовательный вызов процедур, которые принимали какие-то параметры на вход, отдавали какие-то параметры. Когда мы были студентами, то практически все писали подрограммы числового интегрирования, построения графиков и прочая.
Но программы становились еще сложнее. Причем с катострофической скоростью. В качестве примера могу привести статистику (правда она была сделана уже после появления ООП) — за 10 лет с 1990 по 2000 сложность программ выросла в 100 раз.
И идея, которую предложили разработчики языка Simula 67 оказалась великой. Позже на этих идеях были построены самые известные объектно-ориентированные языки, такие как Java, C++, C#, Smalltalk.

Объекты

В реальном мире нас окружают объекты. Мы живем в квартирах, катаемся на машинах, заказываем товары, оплачиваем счета, делаем заказы. Все это — объекты реального мира. Если вы помните — мы с самого начала ввязались в историю с объектом «робот». Он у нас должен был передвигаться, поворачиваться. А значит писать программы, которые использовали бы именно такой подход будет удобно.
Но что важно отметить — если вы помните, в самом начале я описал две задачи, которые решает программист. (Введение). Для решения обеих задач подход ООП в огромном количестве случаев прекрасно подходит. Описать задачу на обычном языке удобно в объектах. И перевести задачу, описанную в объектах, на язык программирования, который также поддерживает объекты — опять таки удобно. Наверно это является ключевым, поэтому я повторю свою мысль еще раз — в ООП важно понимать, что во-первых, описание задачи удобно вести в объектах. И переводить описание в язык программирования с объектами — тоже удобно. Давайте порассуждаем немного в отрыве от языков программирования — просто попробуем потеоретизировать.

Классы

Развивая идею «при программировании задачи удобно использовать объекты» мы переходим к следующему шагу — объекты удобно классифицировать. В вашей программе может быть много тех же роботов — список замечательных роботов, каждый из которых умеет делать одно и тоже. Мы можем их распределить по нескольким точкам и начать рисовать сразу десятки квадратов. Еще более может впечатлять идея создать описания объектов типа «воин» и начать сражение сил добра и зла :).
Причем я не просто так привел такой пример — если вы создаете объект, то вы получаете механизмы, чтобы научить этот объект выполнять какие-то операции. Можно научить нашего «воина» оценивать ситуацию вокруг него, принимать решения и выполнять определенные действия. Нападать, защищаться, передвигаться. А теперь представьте, что вы создали описание такого объекта в виде шаблона. И под этот шаблон создали программно штук 500-600 таких «воинов». Каждый «воин» обладает набором параметров — координаты, положение рук и ног, возможно качество брони, набор оружия. Поместив их на поле боя мы теперь можем заставить их сражаться. Причем они будут это делать уже по сути независимо от нас. Не надо писать сложный программный код для управления этими «воинами». Обычный цикл — оценить, принять решение, начать двигаться, рассчитать новые координаты. И все наше поле боя рассчитывается само собой. Остается только нарисовать воинов по уже готовым координатам.
Добавление новых воинов не создаст никаких сложностей — лишь мощность процессора надо прибавить. Мы просто каждого просим делать в цикле определенный набор операций до тех пор, пока он жив. Оценили удобство ?

Теперь давайте посмотрим внимательно на наши рассуждения — мы создавали объекты по одному образу и подобию. Мы говорим о том, что мы создали образец, чертеж, тип или КЛАСС. Именно понятие «класс» принято в ООП. Программист разрабатывает класс (Class) и потом программа создает объекты класса. Можно сказать, что мы, как инженеры, создаем чертеж машины, робота, «воина», а потом создаем объекты по этому чертежу. Вернее программа их создает.
И тут надо отметить принципиальное отличие класса и объекта. Класс — это описание, объект — это реальное воплощение.
Это различие порождает еще одну важную мысль: например робот как класс (описание, чертеж) вряд ли требует понятия местоположения (координаты робота). Но для реального робота это важно. Теперь давайте подумаем, что наш робот может быть достаточно интеллектуальным — например, он может быть заброшен на Марс. В этом случае алгоритм его работы потребует знания координат. Заметьте — алгоритм требует координат. И эти координаты будут уникальны для каждого робота, находящегося на Марсе (вдруг мы туда десант забросим).
Потихонечку мы пришли к некоторым важным выводам:

  1. Объект/класс обладает переменными, которые характеризуют его состояние (например, координаты). Не всегда параметры нужны и классу и объекту.
  2. Объект/класс умеет что-то делать (например двигаться, оценивать ситуацию и прочая)

 

Пример класса на Java

Напишем несложный класс с подробным описанием всего того, что мы в нем увидим. Давайте сразу делать это в рамках проекта на NetBeans. Для этого создаем простой проект. Назовем его Robot1. Запускаем NetBeans и делаем то, что описывалось раньше — выбираем пункт File -> New Project. Далее выбираем простой проект и устанавливаем параметры, которые показаны на рисунке.

У нас сразу будет создан класс RobotManager, который мы указали в строке «Create Main Class». Пока не будем обращать на него внимания. Он нам потребуется немного позже. Сейчас создадим новый класс Robot.
Для этого выберем пункт меню File -> New File. Вы сможете увидеть, что там есть комбинация Ctrl+N а также видна иконка кнопки на верхней панели. В следующий раз можно использовать их. Перед нами откроется окошко в котором нам надо выбрать Categories: java и File Types: Java Class. Далее нажимаем «Next».

На следующем экране устанавливаем имя класса (Robot). Обратите внимание на сообщение внизу. Оно просит о том, чтобы класс был помещен в пакет. На данный момент будем игнорировать это сообщение, но мы обязательо вернемся к нему, когда будем разбирать пакеты. А пока просто нажимаем «Finish». (Забегая немного вперед — помещать класс в пакет хорошо. А класс без пакета — не хорошо. Но на данный момент можно).

После создания класса мы увидим вот такой код.

Есть еще один способ создать новый класс. Для этого щелкните правой кнопкой мыши на проекте на панели Projects и в выпадающем меню выберите пункт New -> Java Class.
Итак, мы создали класс Robot. Он пока ничего не умеет делать и не содержит никаких параметров, но тем не менее это полноценный класс. И объекты этого класса можно создавать и что-нибудь с ними делать. (Забегая немного вперед скажу, что даже в таком состоянии класс уже многое умеет, но об этом мы поговорим несколько позже).

То, что это именно класс, говорит слово class. Слово public мы обсудим позже, но сейчас вы уже можете себе отметить, что внутри файла с именем Robot.java у нас есть (должен быть) класс Robot. И все начинается со слов public class Robot. Т.к. мы пока не обсуждали слово public, просто примите к сведению — имя файла ДОЛЖНО совпадать с именем класса, который указывается после именно двух этих слов — public class. (Мы еще узнаем, что без слова public вы можете указать несколько классов внутри одного файла. Но public class должен быть только ОДИН).

Все описание класса (поля, методы) должно находиться внутри фигурных скобок.

А пока давайте расширим возможности нашего класса — сделаем для нашего робота поля, которые будут хранить его координаты и курс — поля x, y и course. Сделать это несложно — мы описываем переменные как показано ниже. (Я убрал лишние комментарии и добавил более полезные).

Это весьма простой класс. Мало того, он не полный. В нем нет очень важной составляющей — наш класс ничего не умеет делать. Сейчас у него есть просто три параметра: координаты X и Y, а также курс. Мало того — все параметры изначально равны нулю.
Но здесь мы видим как можно (и нужно) определять параметры класса. А теперь попробуем немного поработать с нашим классом Robot. Откройте в редакторе описание класса RobotManager. Для этого его надо просто найти слева в панели Projects и дважды щелкнуть левой кнопкой мыши. Обратите внимание на идею — класс RobotManager будет управлять объектами Robot. Название класса не является обязательным — здесь важно «увидеть» взаимодействие объектов. Перед нами класс RobotManager.

Теперь давайте попробуем поработать с нашим классом Robot. Как мы уже говорили при обсуждении элементарных типов мы видели, что переменные любого тпа описываются в виде

<тип> имя;

Это же правило распространяется и на классы. Давайте объявим две переменные типа Robot.

И вот тут начинается путь к отличиям переменных типа класс от элементарной переменной. Когда мы с вами объявляли элементарную переменную, мы сразу получали готовый кусочек памяти, куда можно было поместить число, символ или логическое значение. С классами не так. То, что мы с вами объявили Robot r1; не создает реальный объект типа Robot. В данном случае переменные r1, r2 только ССЫЛКИ (указатели). Прежде чем использовать их, нам надо создать объект и указать на него ссылкой. Выглядит это вот так:

Robot r1 = new Robot();

Обратите внимание на эту строку — она является очень важной. Мы используем ключевое слово new для того, чтобы СОЗДАТЬ ОБЪЕКТ.
Т.е. при объявлении переменной типа Robot (или другого класса) мы не создаем объект — мы объявляем ссылку на объект. КОТОРЫЙ НАМ НАДО ЕЩЕ СОЗДАТЬ. Если этого не сделать, то ссылка будет указывать в никуда — для этого есть даже специальное слово — null.

И вот только после создания объектов наши ссылки указывают на них. И только теперь нашими объектами можно пользоваться.
Если немного углубиться в механизм ссылок и объектов, то он достаточно сильно напоминает механизм указателей в C++. В области памяти, которая называется «куча» (heap) выделяется память, в которой находятся данные объекта. А ссылка указывает на эту область памяти. У тех. кто знаком с C++ или Delphi может возникнуть вопрос: «Если память выделена, то ее надо будет очищать?». Так вот в Java существует специальный механизм «Сборщик мусора» (garbage collector), который делает это автоматически. На данный момент мы не будем погружаться в его тонкости — просто имейте в виду, что создавать объекты вам надо самим, а удалять их из памяти вам не требуется. Позже мы подробнее остановимся на этом вопросе.

Оператор обращения к полю класса

Давайте еще расширим наши знания. Только что мы с вами расширили наш класс переменными, которые содержат значения координат и курса. Нам необходим механизм, который позволит обращаться к этим полям. Выгляит он не просто, а … очень просто. Вот так: «.». Да-да, самая простая точка. Давайте рассмотрим на примере.

Как можно видеть, обращение к полю X объекта, на который указывает ссылка r1, осуществляется через обычную точку. Я специально достаточно долго расписываю «обращение к полю X объекта, на который указывает ссылка r1» — я считаю очень важным моментом понимание механизма ссылок. В принципе он не сложный, но хорошо понимать его очень важно.
А теперь рассмотрим еще один пример:

Чем он важен? Самое главное в нем — увидеть, что мы создали ДВА объекта. На один указывает сылка r1, на другой — r2. Также не менее важно увидеть, что для КАЖДОГО объекта мы отдельно устанавливаем поле X. И у КАЖДОГО объекта это поле имеет свое значение. Иными словами — каждый объект имеет свой личный набор полей, которые описаны в классе.

Только что мы посмотрели. как объявлять поля в классе. Выполнили эту фразу: «Объект/класс обладает переменными, которые характеризуют его состояние». Теперь обратим наши взоры на вторую часть: «Объект/класс умеет что-то делать».
Для наделения объектов умением что-то делать существует механизм объявления методов класса. Метод можно определить как процедура/функция, которая определена в классе. Например мы можем определить метод, который «передвигает» нашего робота на какое-то количество метров вперед в соответствии с его курсом. В упрощенном варианте — меняет его координаты в соответствии с заданной дистанцией.
Если вы постараетесь вспомнить школьный курс тригонометрии, то для вас не составит труда понять эти формулы:

Поясним: координата X будет увеличиваться на дистанцию (distance), умноженную на косинус угла курса (course). Координата Y — дистанция на синус угла курса. Надеюсь, что это не вызывает больших вопросов. А теперь давайте сразу возьмем быка за рога — посмотрим код, который передвигает нашего робота на нужную дистанцию.

 

Давайте потихонечку разбираться в нашем коде. Как можно видеть мы изменили описание нашего робота. Теперь у него появились МЕТОДЫ. До этого мы с вами рассматривали только поля.
Метод — это действие, которое может производить объект. Вы можете найти и другие определения, но мне это нравится больше всего. Форма записи метода в очень простом виде выглядит вот так:

Наш робот теперь умеет делать следующее:

  1. Печатать текущие координаты — printCoordinates
  2. «Ехать» вперед на некоторое количество метров и рассчитывать новые координаты с учетом курса — forward(int distance)

Сначала рассмотрим печать координат — void printCoordinates().
Зарезервированное слово «void» означает, что метод ничего не возвращает. Мы позже посмотрим более подробно этот вопрос. А пока просто примем этот факт — если метод не должен ничего возвращать, то он предваряется словом «void». После типа идет имя метода. Синтаксис имени подобен синтаксису имени переменной. Лучше называть методы так, чтобы было понятно, что он делает.
Теперь обратим внимание на внутренню часть методы printCoordinates. Внутри метода мы выполняем вывод данных — System.out.println(x + «,» + y);. Но на что еще надо обратить внимание — мы обращаемся к переменным x и y. Причем в самом методе мы эти переменные не объявляли. Думаю, что многие из вас догадались — это поля, которые объявлены внутри класса. Т.е. внутри метода мы можем обратиться к полям. Причем обращаемся мы к полям того объекта, у которого вызываем метод. Сам вызов метода делается так же, как и обращение к полю — через оператор «.».

 

Теперь рассмотрим метод forward(int distance). В отличии от метода printCoordinates в методе forward мы передаем данные — параметр int distance. В теле метода мы опять же обращаемся к полям объекта и делаем вычисления новых координат. И используем переданный параметр по имени.

Наш робот конечно еще далек от совершенства, но тем не менее его уже можно использовать. Можно создать объект (отметим, что при создании его координаты устанавливаются в 0), попросить его проехать вперед и напечатать его текущие координаты. Пришла пора пообщаться с нашим роботом. Смотрим код:

Думаю, что вы уже видите, что мы из одного класса — RobotManager — управляем созданием и поведением объекта класса Robot. Просто внимательно смотрите код и читайте комментарии. Если мы запустим наш проект, то увидим вот такие строки

Если вы внимательно смотрели код, то думаю, что для вас результат не является неожиданностью.

Я выложил архив нашего первого примера здесь — Robot1. Вы можете его закачать и распаковать в директорию C:\JavaLessons. Для того, чтобы открыть проект в NetBeans вам надо выбрать пункт меню File -> Open Project. Посмотрите, что это также можно сделать с помощью комбинации клавиш Ctrl+Shift+O. В появившемся окне выберите папку Robot1 и нажмите «Open Project».

Мы с вами посмотрели как создавать класс, как создавать объект. Заодно освоили (я надеюсь, что вы вместе со мной сделали это) как это делать в NetBeans. Наш робот весьма простой и далек от совершенства — в нем нет еще многих элементов ООП, но тем не менее это уже рабочий код — с чем вас и поздравляю.

Ссылки на объекты

Возможно, что кому-то сразу стала очевидна природа ссылок на объекты. Но судя по вопросам, которые я регулярно слышал, слышу и скорее всего буду слышать, далеко не всем это сразу становится понятно и очевидно. Для тех, кто не понял и написана эта часть.

ВАЖНО !!! Если вы не поняли природу ссылок — это нормально, не думайте, что вы безнадежны. Что гораздо важнее — в этом вопросе необходимо обязательно разобраться.

Итак, мы с вами чуть выше говорили, что описание переменной автоматически НЕ СОЗДАЕТ объект нужного нам типа. Для создания объекта вам надо это сделать явно. Еще раз повторим это утверждение на примере:

Здесь важно отметить, что переменная r1 НЕ ЯВЛЯЕТСЯ объектом. Она является ССЫЛКОЙ на объект, который располагается в “куче” (heap — очень часто говорят — “находится в хипе”).
Проводя бытовую аналогию, переменная это как бумажная форма для записи адреса. Она может быть пустой (незаполненной) — тогда мы говорим, что переменная не инициализирована. Для локальной переменной компилятор будет просто напросто ругаться, если вы ей ничего не присвоите. Вы можете написать так:

И этот код не будет даже компилироваться. Если же вы напишите так, то компилироваться будет.

Если же вы ее заполнили (создали объект и присвоили его переменной), то форма теперь УКАЗЫВАЕТ на адрес, ССЫЛАЕТСЯ на него. Вот почему переменная типа класса является ССЫЛКОЙ. Повторим еще раз пример создания объекта

Вот только теперь ссылка r1 ссылается на объект типа Robot.

Посмотрим еще один пример кода

А теперь попробуйте ответить на следующий вопрос: “На какой объект указывает переменная r2 ?”
Если вы предположили, что обе переменные указывают НА ОДИН и ТОТ ЖЕ объект — то вы совершенно правы.
Теперь важно “увидеть” эту ситуацию — у вас ДВЕ ссылки, но только ОДИН объект. По сути у вас две формы, которые указывают на один и тот же адрес.
Фактически мы можем работать с одним и тем же объектом через ДВЕ ссылки — r1 и r2. Через ОБЕ.

Теперь давайте рассмотрим такой код:

Посмотрите внимательно на две последние строки. Мы поменяли поле x у объекта через переменную-ссылку r2, а печатаем поле x через переменную-ссылку r1. Вы уже скорее всего догадались, что на экране мы увидим 888.
Вы точно поняли, почему это происходит ? Если снова не поняли, то проговорите про себя еще раз — ОБЕ ССЫЛКИ УКАЗЫВАЮТ НА ОДИН И ТОТ ЖЕ ОБЪЕКТ.

Чтобы я еще хотел отметить — сам объект через присваивание НЕ КОПИРУЕТСЯ, но ссылки КОПИРУЮТСЯ. Т.е. у нас ДВЕ сылки. Они являются копиями друг друга. Их даже можно сравнить — и если они указывают на один и тот же объект — они равны.

С одной стороны, понимание ссылок не является каким-то сложным построением. С другой стороны — если вы не понимаете их природу, то вам будет непросто двигаться дальше. Так что постарайтесь четко осознать эту тему.

И вот теперь высказав все необходимые на мой взгляд идеи, которые стоят за ООП, я собираюсь переходить к тому, что называется парадигмами ООП. Не сомневаюсь, что некоторые из вас слышали о них. Но мне бы хотелось донести до вас истоки появления этих парадигм — они появились не на пустом месте — именно этому были посвящены несколько предыдущих абзацев.
Теперь мы можем перейти к следующей части нашего повествования: Инкапсуляция.