Конструкторы

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

Итак, что же такое конструкторы? Некоторое время назад мы касались вопроса создания объектов — через служебное слово new. Вот как это выглядело:

Robot r = new Robot();

За словом new мы видим некоторое подобие вызова метода с названием Robot. Причем без параметров. И этот несколько специфический метод называется конструктор. Самое главное — это имя метода-конструктора. Он называется так же, как и сам класс. Возможно это только я придумал такое название — метод-конструктор. В нормальной литературе все просто говорят «конструктор». Обратимся к коду (в котором уже есть конструктор) для разъяснений. Это наш робот, который умеет двигаться вперед и поворачиваться.

В коде я привел вариант конструктора (который не очень пока нам полезен). Кроме названия Robot (которое обязательно должно совпадать с именем класса) можно обратить внимание, что конструктор НЕ ВОЗВРАЩАЕТ никаких данных. Причем мы не пишем слово void для этого. Отметим и такую особенность конструктора. На самом деле вы можете написать void — но тогда метод перестанет быть конструктором. Никто не запрещает написать int или double. Правда надо будет тогда обязательно сделать return. Можете потом провести эсперименты. Но продолжим наше повествование.
Наш робот уже показал свои возможности, но у него есть большой недостаток — в момент своего создания его начальные координаты всегда равны нулю. Что не может не вызывать неудовольствия. Мы можем конечно сделать методы set (сеттеры) для наших координат X и Y, но это снизит «защищенность» нашего робота. Нужен метод, который бы вызывался один раз при создании объекта и принимал нужные нам параметры X и Y. Так вот конструктор — это именно такой метод. Обратите внимание на один тонкий момент — конструктор (как метод) не создает объект. В момент вызова конструктора объект уже существует. В конструкторе можно присвоить нужные параметры и что еще приятнее — в конструктор можно передать параметры. Т.е. объявить конструктор с параметрами. Вот наш новый вариант класса Robot.

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

Класс RobotManager изменился очень мало — только строка создания робота — там я сделал соответствующий комментарий. Сейчас мы на некоторое время отойдем от конструкторов, но вернемся к ним обязательно. Спираль продолжает раскручиваться 🙂

Наследование

Итак, мы уже можем создавать описания классов. На основании описания классов создавать реальные объекты внутри программы. Причем доступ к полям объектов может быть ограничен, что создает возможность приблизить поведение програмных объектов к поведению реальных. Но давайте посмотрим еще дальше. Классификация всегда подразумеавает древообразную структуру. Наглядным примером может служить биологическая классфикация, предложенная еще в 18 веке Карлом Линнеем.
ООП прекрасно усвоило эту идею. Все представители определенного класса имеют общие признаки и общее поведение. Но должна быть возможность создать класс, который ведет себя немного не так. Т.е. должна быть возможность «переопределить» методы. Это открывает большие возможности. Представим себе, что мы получили от сторонних разработчиков замечательный набор готовых классов для графического интерфейса. В нем есть кнопки, списки, таблицы, изображения. Мы этим пользуемся и в какой-то момент времени нам настоятельно требуется несколько изменить поведение определенного класса. Например надо, чтобы стандартный выпадающий список имел возможность постраничного вывода. В процессе изучения Java мы познакомимся со стандартной библиотекой графических элементов Swing. Это не означает, что мы будем ее подробно изучать — это просто очень наглядный способ познакомиться с идеями ООП. На примерах мы посмотрим как работает парадигма наследования.

Само по себе наследование реализуется достаточно просто — при указании класса вы указываете дополнительно класс от которого вы хотите унаследоваться. Обратите внимание, что ваш новый класс сразу станет уметь делать то, что есть в классе-родителе (еще используют выражение родительский класс).
Рассмотрим немного искусственный пример — расширим возможности нашего робота. Научим его двигаться назад. Для этого нам потребуется добавить новый метод — back с параметром, сколько метров надо проехать назад. Алгоритм в данном случае простой — нам надо проехать вперед, но со знаком минус.

Как видим, пока не все так сложно — мы просто написали слово extends (англ. расширять, раздвигать) и далее имя класса Robot. Также в коде видно обращение к методу forward внутри нашего нового методаback. Думаю, что все достаточно очевидно. Но даже в этом коде у нас есть достаточно много важных моментов, на которых мы сосредоточимся.
Во-первых, в коде класса Robot мы не видим наш замечательный конструктор с парамметрами. Во-вторых — нам повезло, что метод back не требует прямого обращения к координатам. Но наверняка такие действия нам могут потребоваться. Например, если мы попробуем написать еще более расширенный класс, который умеет двигаться по кругу. Нам точно потребуется обращаться к координатам, которые закрыты для доступа.
Давайте сначала разберем первый вопрос. Вы можете попробовать просто добавить в описание класса Robot наш конструктор с параметрами. И мы получим ошибку компиляции. Текст ее вот такой:

error: constructor Robot in class Robot cannot be applied to given types;

Проблема в том, что создавать новый класс-наследник можно только либо имея такой же конструктор, что и у родительского класса, либо создать собственный конструктор (возможно с другим набором параметров) — но об этом мы еще поговорим. Пока продолжим — т.к. у нашего родительского класса Robot есть конструктор только с параметрами, то нам нужно создать такой же. Ну что же — сделаем так, как требуется. Теперь наш код будет выглядеть вот так:

Но здесь мы встречаем еще один новый поворот — внутри конструктора класса RobotExt мы видим специальное слово super с параметрами. Это слово означает, что мы вызываем конструктор родительского класса, который принимает два наших параметра. Несколько позже мы познакомимся еще с некоторыми особенностями вызова конструктора родителя.
Теперь снова попробуем внести изменения в наш замечательный класс RobotExt. В нашем классе Robot есть небольшой недостаток — при его создании мы не задаем ему курс. Т.к. у нас есть метод setCourse, то кажется, что проблема не так уж и велика. Но на самом деле все не так безоблачно. В отличии от изначального класса Robot, который у нас может разворачиваться на месте, может появиться робот, для которого это невозможно. Как обычная машина не может развернуться прямо вокруг своей оси, так и робот «нового поколения» может не уметь вытворять такие фокусы. Значит нам важно иметь возможность устанавливать курс тоже. Давайте сделаем новый конструктор не с двумя, а с тремя параметрами — X, Y и курс.

Упс. Не получается. Компилятор опять ругается. Что мы сделали не так? Да очень просто — мы обратились к закрытому полю. Но у нас же есть прекрасный метод setCourse. Да, в данный момент это выход, можно воспользоваться. Сделаем так:

Это решение работает — компилятор не выдает ошибку. Но на самом деле все не так просто, но об этом мы поговорим в другой раз. А пока отметим еще один момент — ключевое слово super ДОЛЖНО СТОЯТЬ ПЕРВОЙ ОПЕРАЦИЕЙ В КОНСТРУКТОРЕ. Вы можете попробовать поменять строки местами — вам будет выдано сообщение об ошибке.

call to super must be first statement in constructor

Так что будьте внимательны.

Но вернемся к ситуации, когда нам надо обратиться к полям родительского класса. Хоть мы и вывернулись, но тем не менее это не всегда возможно. К тому же я обещал вас убедить в том, что такое решение тоже не корректно. Доказательство некорректности мы отложим, а вот попробовать обратиться к закрытому полю нам надо.
Нам действительно не удастся обратиться к полю, которое объявлено как private. Но я предлагаю вам вспомнить, что в разделе об инкапсуляции мы говорили о вариантах доступа, среди которых упоминалось слово protected. Время этого слова пришло. Тогда я написал, что «переменные будут видны в классах-наследниках, и в классах, находящихся в том же пакете». про пакеты мы пока не говорили, но классы-наследники у нас появились. Так что теперь самое время использовать наши знания. Смотрим код наших классов и обращаем внимание на определение свойства course в классе Robot. Все остальное мы оставили без изменений.

Теперь вы можете попробовать обращаться к нашему более расширенному роботу из класса RobotManager. Обратите внимание, что мы можем вызывать методы класса Robot — forward, setCourse для объекта класса RobotExt. Мы унаследовали эти методы и можем ими пользоваться.

Исходные коды проекта можно взять здесь: Robot2.

И теперь нас ждет следующая статья: Пакеты