Читать книгу «Объектно-ориентированное программирование на Java. Платформа Java SE» онлайн полностью📖 — Тимура Машнина — MyBook.
image

Приведение типов


Давайте посмотрим снова на эту иерархию классов.



Легковой автомобиль и грузовик являются подклассами или производными классами класса vehicle.

Вопрос в том, если ли у нас есть объект класса car, мы можем использовать его там, где должны быть объекты класса vehicle?

Например, в переменной vehicle?



И наоборот, можем ли мы поместить объекты суперкласса там, где должны быть объекты подкласса?

И если да, то при каких обстоятельствах?

Мы говорим о кастинге или приведении при преобразовании объекта из одного класса к другому связанному классу.

Представьте себе, что у нас есть переменная vehicle, которая хранит объект vehicle, и переменная car, с сохраненным в нем объектом car.



Можем ли мы присвоить объект car переменной vehicle и наоборот?

Мы говорим о приведение к базовому типу при преобразовании объекта из класса в суперкласс.

И переход от подкласса к суперклассу всегда возможен.

Объекты подкласса наследуют все от суперкласса.

Поэтому все, что вы хотите сделать с переменной суперкласса, применимо к объекту подкласса.

Чтобы привести к базовому типу объект, вы можете указать суперкласс в круглых скобках, как вы здесь видите.

Но вы также можете не делать это, как вы видите в последней строке.

Мы говорим о понижающем приведении при конвертации объекта от класса к его подклассу.

Теперь мы хотим заставить vehicle стать car.

Мы переходим от общего класса к более конкретному классу, и это должно быть сделано явно.

В этом примере мы объявляем переменную типа vehicle, но храним в ней car.

Таким образом, мы можем явно понизить эту переменную для хранения car, который находится в переменной v.

Вы должны быть очень осторожны при кастинге вверх и вниз.

Мы объявляем переменную v, и мы храним в ней car.



Мы можем это сделать, поскольку car является vehicle.

Однако вы не можете привести v в переменную truck.

Вы не можете сделать приведение между классами, полученными из одного класса.

Вы не можете превратить car в truck или truck в car.



У них разные поля и методы.

Преобразование применимо не только для классов.

Это также возможно с примитивными типами и между примитивными типами.



Мы видели несколько примеров со строками и целыми числами.

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

При кастинге вверх мы не теряем информацию о числовом значении.

Поэтому мы можем делать это преобразование неявно.

Кастинг вниз более опасен, поскольку мы можем потерять информацию о числовом значении.

При преобразовании double в int мы получаем усеченное целочисленное значение, поэтому это преобразование нужно указывать явно.

Полиморфизм


В объектно-ориентированном программировании мы организуем объекты в классы.

Объекты в одном классе имеют одинаковые поля, и одни и те же методы.

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

Когда мы ввели наследование, мы ввели семейства связанных классов.



Класс может наследовать поля и методы из базового класса и добавить дополнительные свои поля и методы.

Теперь мы хотим настроить возможности в классах такой иерархии.

Представьте, что мы хотим иметь одни и те же методы в базовом классе и в производном классе, но мы хотим сделать что-то другое в зависимости от класса, к которому принадлежит объект.



Здесь мы видим, что в методе toString подкласса car определено другое поведение, отличное от того, которое определено в суперклассе.

Поэтому поведение считается переопределенным.

Этот же метод может делать что-то совершенно отличное от метода суперкласса, с тем же именем и теми же функциональными возможностями.

Таким образом, мы видим, что метод с тем же именем и одинаковой функциональностью может иметь разный код в разных классах иерархии.

Это называется переопределением.

Однако при необходимости можно вызвать метод суперкласса.

Для этого нам просто нужно вызвать метод с префиксом супер.



Здесь также может использоваться ключевое слово this, чтобы обратиться к методу, который определен в соответствующем классе.

Это переопределение методов называется полиморфизмом.

Слово полиморфизм происходит от греческого, что означает многие формы.

И в контексте объектно-ориентированного программирования, полиморфизм позволяет нам иметь методы с одним и тем же именем, и одинаковой функциональностью, но разным поведением в группе классов, связанных отношением наследования.

Другими словами, полиморфизм позволяет использовать наследников, как родителей. При этом, если в классе-наследнике был переопределен какой-то метод, то вызовется он.

Переопределение и перегрузка


Теперь давайте рассмотрим две концепции, которые выглядят взаимосвязанными, но на самом деле являются разными, это перегрузка и переопределение.

Обе эти концепции применяются к методам.

Ранее мы говорили о конструкторах.

Помните, что у нас был автомобиль с двумя полями, lights и color.



И мы определили в одном классе не один, а несколько конструкторов.

Имена этих конструкторов были одинаковыми, но параметры были разные.

И это важно, чтобы список параметров был другим.

Вы не можете определить два конструктора с одним и тем же именем, и одним и тем же списком параметров.

Фактически, Java понимает, какой конструктор вызвать, просматривая параметры.

И то, что мы делали для конструкторов, также применимо для методов.



Мы говорим о перегрузке, когда у нас есть разные методы с тем же именем, но разным списком параметров.

С другой стороны, мы ввели переопределение, когда мы хотели изменить поведение метода, унаследованного от суперкласса.

В этом примере метод toString суперкласса переопределяется в подклассе с помощью метода с тем же именем, и теми же параметрами, и возвращаемым типом, но другим телом метода.



Важно, чтобы параметры и возвращаемый тип были одинаковыми.

Отличалось только тело метода.

И в пределах одного класса мы можем перегрузить метод.

В этом случае имя и возвращаемый тип совпадают, но список параметров будет другим.

Компилятор будет различать, какой вызывается метод, сравнивая списки параметров.

Неправильно пытаться перегрузить метод, просто изменив возвращаемый тип.

Если мы это сделаем, мы получим ошибку компилятора.

То же самое произойдет, если мы просто изменим имена параметров.

В этом случае определенный метод не изменится вообще.

И мы также получим ошибку компилятора.

Когда мы определяем метод, мы связываем идентификатор – имя метода – с некоторым кодом – телом метода.



Всякий раз, когда мы вызываем это имя метода с некоторыми значениями, мы знаем, какой код нужно выполнить.

Например, используя объявление метода, мы связываем идентификатор sq с методом, который отображает целые числа в целые числа, возводя число в квадрат.

Идентификатор sq всегда привязан к методу в соответствующей области кода.

Во многих языках, которые не являются объектно-ориентированными, эта привязка выполняется обычно во время компиляции.

Во время выполнения эта привязка зафиксирована.

И это называется «ранним» или «статическим» связыванием.

Но этот способ не соответствует концепции полиморфизма и переопределения методов в производных классах.

Здесь мы хотим точно противоположного – чтобы часть кода была не привязана статически к имени метода, а, чтобы зависела от объекта, вызванного во время выполнения.

Поведение, которое нам нужно, называется «динамическим» связыванием.

Поэтому нам нужно различать статическое или раннее связывание, которое выполняется во время компиляции, от динамического или позднего связывания, которое выполняется во время выполнения кода.

В отличие от переопределения перегруженные методы разрешаются во время компиляции.



При этом информация предоставляется классом.

Когда код программы доходит до имени метода, компилятор знает, какое тело метода выполнить – по крайней мере в случае перегруженных методов.

Но это не относится к переопределению.

Здесь разрешение имен выполняется во время выполнения программы.

Динамическое связывание используется для переопределенных методов.

Здесь информация задается объектом, а не классом.

Предположим, мы объявили массив транспортных средств под названием «гараж» для хранения четырех автомобилей.



И предположим также, что у нас есть автомобили и грузовики, которые стоят в разных позициях.

Теперь в цикле for мы применяем методы toString,

Которые мы определили ранее, ко всем элементам массива.

Что происходит?

Свой метод применяется к каждому из этих элементов.

Таким образом, мы можем иметь единую форму объектов, но разнообразие в том, что выполняется.

Возможно даже, в случае компиляции мы не знаем классов элементов массива.

Это будет считываться во время выполнения программы.

Поэтому динамическое связывание является необходимым поведением для переопределения метода.

Теперь посмотрим на другой пример.

Давайте теперь определим несколько перегруженных методов с именем p.



У них есть один параметр, который является объектом разных классов.

И теперь мы вызываем метод p для всех элементов этого массива.

Помните, что аргумент метода p – это vehicle в массиве vehicle.



Поскольку каждый элемент является vehicle, строка будет напечатана для vehicle, так как метод p привязывается к телу во время компиляции.

Помимо примера, который мы видели, private, final, и static методы также привязываются статически.

Кроме того, атрибуты всегда привязываются статически.

Возникает вопрос, почему все не привязывать динамически?

Имеет смысл связывать идентификаторы с данными или кодом во время компиляции по двум причинам.

Во-первых, чтобы выполнить первую проверку кода и выявить ошибки, а во-вторых, оптимизировать генерируемый код.

Вот почему эта стратегия используется чаще в языках программирования.

Однако это не работает, когда мы переопределяем метод.

Во время компиляции мы можем даже не знать, какой объект мы получим.

Тогда имеет смысл применить динамическое связывание.

Динамическое связывание также называется «поздним связыванием».

Первое приближение к классу выполняется во время компиляции, но нужный класс окончательно определяется во время выполнения.

Теперь вернемся к исключениям, чтобы объяснить некоторые дополнительные исключения, которые вы должны знать и которые связаны с объектами и классами.

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

Мы уже видели три исключения: ArithmeticException, ArrayIndexOutOfBoundsException и NumberFormatException.

Следующее исключение, которое мы увидим, – это исключение NullPointerException.

Это исключение возникает при попытке программы использовать переменную, которая не имеет примитивного типа, и которая еще не была инициализирована.



Т. е. мы пытаемся использовать переменную, которая должна указывать на объект, который еще не был создан.

Представьте, что мы хотим напечатать длину массива, который мы объявили, но который мы еще не инициализировали.



Тогда мы получим такое же исключение NullPointerException.

Имейте в виду, что «length» – это метод в случае класса String, но поле в случае массива.

И, если мы попытаемся получить доступ к позиции в массиве, который не был инициализирован, программа будет генерировать исключение NullPointerException, а не исключение ArrayIndexOutOfBoundsException.

В этих примерах очень легко обнаружить, что мы пытаемся использовать переменную, которая не была инициализирована.



Однако, когда мы программируем, мы определяем методы, которые получают аргументы и которые вызываются из других объектов.

В этих случаях обнаружение переменных, которые не были инициализированы, не так очевидно.



Второе исключение, которое связано с объектами и классами, и которое мы увидим, это ClassCastException.

Чтобы проиллюстрировать это исключение, рассмотрим эту иерархию классов, где Vehicle является суперклассом, и Car и Bike – это подклассы.









1
...
...
12