От кого наследуемся ? Класс Object

Прежде, чем мы продолжим наше рассмотрение наследования, мне бы хотелось остановиться на одном важном моменте. Как мы уже выяснили, для того, чтобы унаследоваться от какого-либо класса необходимо написать слово extends после имени нового класса и имя класса, от которого мы хотим унаследоваться. Но мы уже встречали примеры, в которых никаких слов extends у нас не было. Значит ли это, что такой класс является «основой основ». Или иными словами — не наследуется ни от какаго класса совсем. Это не так. В стандартном наборе классов Java существует та самая «основа основ» — класс Object, от которого наследуется класс, которые не имеет слова extends. Не указав наследуемый класс вы автоматически говорите компилятору, что вы наследуетесь от класса Object.
Т.е. определения класса приведенные ниже являются одинаковыми

public class SimpleClass { … }
public class SimpleClass extends Object { … }

Можно использовать обе формы, но на практике никто не пишет extends Object. Мы еще обязательно поговорим о классе Object, который имеет ряд достаточно полезных методов, но пока мы просто упомянем о его существовании и о его очень важной роли во всей иерархии классов — именно от него в конечном итоге создаются все классы — он их дедушка, прадедушка, а может и прапрапрапрадедушка.

Переопределение и перегрузка (override и overload)

Ранее мы посмотрели пример того, как можно добавлять функциональность к классу путем его расширения (наследования). Но в класс можно не только добавлять новые возможности путем создания новых методов. Методы класса можно также переопределять — сделать override. Или перегрузить — сделать overload. Давайте попробуем разобраться что означает каждый термин.

Переопределение (override)

Переопределение используется тогда, когда вы переписываете (переделываете, переопределяете) УЖЕ существующий метод. Например в классе Object есть очень популярный метод toString(), который возвращает строковое представление объекта. В реализации Object результат достаточно страшненький — имя класса с какими-то цифрами (это так называемый хеш-код). Давайте запустим очень простой пример

Наш класс Robot сейчас совсем ничего не имеет и наследуется от класса Object, который, как мы уже говорили, имеет метод toString. Во втором классе RobotManager мы вызвали этот метод и вывели на экран результаты его выполнения. Они мало впечатляют — у меня получилось вот такое:

Robot@119298d

Унаследованный метод toString сработал, но наверно нам было бы интереснее увидеть что-то более разумное и понятное — во всяком случае название метода говорит о том. что нам вернется некоторое текстовое описание объекта. Давайте переопределим этот метод и немного усложним наш класс Robot. Введем дополнительное поле с именем робота и будем возвращать из метода toString это значение. Сразу определим конструктор, который будет принимать имя при создании объекта.

Самое важное находится в описании класса Robot, а именно новый метод toString(). Рассмотрим его подробнее.
Начинается метод с загадочной конструкции @Override. Называется эта конструкция «аннотация». Служит для включения дополнительной информации, которую можно прочитать и использовать. Мы обязательно рассмотрим аннотации позже, пока просто запомним, что конструкции, которые начинаются с символа «@» являются аннотациями и их не надо боятся :). Пока просто примем к сведению, что методы, которые вы ПЕРЕОПРЕДЕЛЯЕТЕ, лучше предварять этой аннотацией. В этом случае компилятор получает возможность проверить, что вы переопределили метод, а не написали новый. Таким образом можно избежать некоторых ошибок из-за невнимательности.

ВАЖНО!!! Аннотации появились только в версии Java 1.5 и более ранние версии их не поддерживают.

Ну а дальше все очень просто — заголовок метода toString должен в точности совпадать с таким же методом у класса-предка — название, возвращаемое значение и список параметров. В данном случае у нас нет параметров и возвращаемое значение является строкой. Теперь мы получили класс, который ПЕРЕОПРЕДЕЛИЛ уже существующий метод предка. Таким образом можно изменять функциональность класса, его поведение. Причем обратите внимание, что мы не использовали исходный код класса Object — его у нас нет. Но тем не менее мы смогли поменять его поведение. Это является большим удобством при разработке программ. В обычной жизни программист на Java постоянно что-то наследует, переопределяет. Но мы должны отметить еще несколько важных моментов.

Рассмотрим пример с использованием нашего старого знакомого робота. Итак, вот наш робот, который умеет перемещаться из одной точки в другую. Мы уже делали этот пример, так что он вряд ли должен вас удивить. Единственное дополнение — теперь я использую пакеты.

Наш робот умеет перемещаться, но у него нет умения, которое может потребоваться — он не считает пройденное расстояние. Данный класс принципиально не умеет этого делать. Для создания робота, который все-таки умеет считать расстояние, можно пойти по следующему пути: унаследоваться от класса Robot, дополнить новый класс переменной, например totalDistance и при перемещении увеличивать ее на пройденную дистанцию. Т.е. нам надо переопределить метод forward, в котором надо рассчитать новые координаты и прибавить к переменной totalDistance величину входного параметра distance. Как видим все достаточно логично, за исключением одного неприятного момента — у нас УЖЕ есть алгоритм, который считает новые координаты и не использовать его было бы странно. Нужен механизм, который позволит вызывать методы предка. И этот механизм существует. Смотрим код нового класса RobotTotal.

Приступим к разбору нашего нового класса. Во-первых, можно видеть, что мы добавили новое поле totalDistance. Его назначение мы уже рассматривали. Более интересный момент — это необходимость создания конструктора. Обращаю ваше внимание на следующий факт — если бы мы не создали конструктор с параметрами у класса Robot, то нам не потребовалось бы создавать конструктор в нашем новом классе. Эту ситуацию мы рассмотрим более глубоко при рассмотрении перегрузки методов. Пока же запомним следующее — если в классе-предке нет конструктора без параметров, то класс-потомок должен определить свой конструктор. Причем совершенно не обязательно повторять набор параметров. Вы можете провести эксперимент и убрать из конструктора класса RobotTotal параметр double y. При этом можно заменить вызов super(x, y); на super(x, 0);. И это будет вполне рабочий код. Вы можете вообще убрать параметры из конструктора (и подставить два нуля в вызов super(0, 0);.
Оставим пока в покое конструктор и перейдем к переопределенному методу forward. Здесь мы видим специальную конструкцию вызова метода родительского класса, а именно зарезервированное слово super и через точку вызов метода forward. Наверно вот и весь механизм — надо просто использовать слово super. Вызов метода предка можно осуществлять в любом месте переопределенного метода потомка. Можно например сначала увеличить переменную totalDistance и только потом вызвать метод forward.

ВАЖНО !!! В конструкторе это правило не работает — в нем вы ОБЯЗАНЫ либо вызывать super первым же оператором, либо не вызывать совсем.

Думаю, что информации достаточно. На этом мы закончим описание переопределения (override) метода и перейдем ко второму термину — перегрузка (overload).

Перегрузка (overload)

Перегрузка метода заключается в следующем — вы создаете метод с таким же именем, но с другим набором параметров. Например, в классе может быть несколько методов с названием summa, но с разным набором парметров. Вот так:

Здесь необходимо добавить важное замечание — имя параметра НЕ ИМЕЕТ значения. Т.е. если вы сделаете два метода summa с двумя параметрами типа double и с разными именами, это будет ошибкой. Вот это код:

В этом примере компилятор выдаст ошибку, хотя у нас разные названия параметров — в одном случае x1 и x2, в другом — y1 и y2. А в примере ниже все будет в порядке.

Хотелось бы обратить ваше внимание на то, что перегрузить можно любой метод, в том числе и конструктор. Т.е. мы можем создать два, три, четыре и т.д. конструктора. Таким образом наш класс RobotTotal может выглядеть вот так:

Если мы вспомним о нашем главном классе Object, то у него как раз конструктор без параметров (часто используется термин «конструктор по умолчанию»). Если у класса-предка есть конструктор по умолчанию, то в классе-потомке его переопределять не надо. А вот если в классе-предке нет явного конструктора по умолчанию (как в нашем классе Robot), то приходится создавать конструктор.
В принципе мы с вами рассмотрели подавляющую часть моментов, связанных с наследованием. Пример с роботами вы можете загрузить отсюда Robot3.

Графическое приложение

И под конец этой части мы с вами создадим графическое приложение, в котором продемонстрируем возможности наследования. Это несложная программа создаст окно, на котором будет рисоваться овал. Можно видеть насколько несложным становится программирование при использовании объектного подхода. Вы просто используете уже готовые классы слегка меняя их поведение за счет переопределения методов.
Приложение состоит из трех классов. Первый класс OvalComponent- это класс, который наследуется от стандартного класса JComponent. Этот класс используется для создания графических компонентов, которые можно размещать на форме (панели, контейнере). В классе JComponent существует метод для прорисовки самого себя — paintComponent. В качестве параметра в него передается объект класса Graphics (это тоже стандартный класс, который позволяет рисовать разные графические примитивы — круги, квадраты, текст, линии и т.д. Мы вызываем его метод drawOval и передаем туда в качестве параметров координаты верхнего левого угла — в нашем случае 5, 5 и ширину и высоту. Т.к. мы хотим нарисовать овал, который зависит от ширины и высоты самого компонента, мы используем методы класса JComponent — getWidth(), getHeight() для получения ширины и высоты соответственно. Вот код нашего класса:

Второй класс — это наследник опять же стандартного класса JFrame. В нем мы переопределили конструктор, в котором создали компонент класса OvalComponent и методом add (который уже есть в классе-предкеJFrame) «положили» наш компонент на форму. После этого мы задали начальные координаты и размеры формы путем вызова метода setBounds.

Третий класс выполняет весьма простую функцию — он создает форму и делает ее видимой. Мы уже знакомились с таким кодом.

После запуска форму можно «таскать» по экрану, изменять ее размеры и всегда в ней будет рисоваться овал, который на 5 пикселей меньше размеров внутренней части окна. Исходный код проекта вы можете взять здесь — OvalApplication.
На этом мы пока завершим рассмотрения парадигмы Наследование и займемся еще одной парадигмой — Полиморфизм .