Материал предоставлен https://it.rfei.ru

Преобразование типов значений ссылок

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

  • присваивания,
  • передачи параметров,
  • явного преобразования типов.

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

Приведение типов значений ссылок во время присваивания

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

Пример 6.11. Присваивание и передача ссылочных значений

interface IStack    {/*...*/}

interface ISafeStack extends IStack   {/*...*/}
class StackImpl implements IStack     {/*...*/}
class SafeStackImpl extends StackImpl

              implements ISafeStack   {/*...*/}

public class ReferenceConversion {

    public static void main(String[] args) {
        // Объявления ссылок
        Object        objRef;
        StackImpl     stackRef;
        SafeStackImpl safeStackRef;
        IStack        iStackRef;
        ISafeStack    iSafeStackRef;

        // ТипИсточника - это тип класса
        safeStackRef  = new SafeStackImpl(10);
        objRef        = safeStackRef;// (1) Всегда возможно
        stackRef      = safeStackRef;// (2) Присваивание подкласса в суперкласс
        iStackRef     = stackRef;    // (3) StackImpl реализует IStack
        iSafeStackRef = safeStackRef;// (4) SafeStackImpl реализует ISafeStack

        // ТипИсточника - это тип интерфейса
        objRef    = iStackRef;       // (5) Always possible
        iStackRef = iSafeStackRef;   // (6) Sub- to super-interface assignment

        // ТипИсточника - это тип массива
        Object[]        objArray        = new Object[3];
        StackImpl[]     stackArray      = new StackImpl[3];
        SafeStackImpl[] safeStackArray  = new SafeStackImpl[5];
        ISafeStack[]    iSafeStackArray = new ISafeStack[5];
        int[]           intArray        =  new int[10];

        // Присваивается ссылочное значение
        objRef     = objArray;       // (7) Всегда возможно
        objRef     = stackArray;     // (8) Всегда возможно
        objArray   = stackArray;     // (9) Всегда возможно
        objArray   = iSafeStackArray;// (10) Всегда возможно
        objRef     = intArray;       // (11) Всегда возможно
    //  objArray   = intArray;       // (12) Ошибка компиляции
        stackArray = safeStackArray; // (13) Массив подкласс в массив суперкласса
        iSafeStackArray =
                safeStackArray;      // (14) SafeStackImpl реализует ISafeStack

        // Преобразование параметров
        System.out.println("First call:");
        sendParams(stackRef, safeStackRef, iStackRef,
                   safeStackArray, iSafeStackArray);                    // (15)
    //  Вызов сигнатуры: sendParams(StackImpl, SafeStackImpl, IStack,
    //                             SafeStackImpl[], ISafeStack[]);

        System.out.println("Second call:");
        sendParams(iSafeStackArray, stackRef, iSafeStackRef,
                   stackArray, safeStackArray);                         // (16)
    //  Вызов сигнатуры: sendParams(ISafeStack[], StackImpl, ISafeStack,
    //                             StackImpl[], SafeStackImpl[]);
    }

    public static void sendParams(Object objRefParam, StackImpl stackRefParam,
            IStack iStackRefParam, StackImpl[] stackArrayParam,
            final IStack[] iStackArrayParam) {                          // (17)
    //  Сигнатура: sendParams(Object, StackImpl, IStack, StackImpl[], IStack[])
    //  Вывод имени класса объекта, обозначенного ссылкой во время выполнения
        System.out.println(objRefParam.getClass());
        System.out.println(stackRefParam.getClass());
        System.out.println(iStackRefParam.getClass());
        System.out.println(stackArrayParam.getClass());
        System.out.println(iStackArrayParam.getClass());
    }
}

Вывод программы:

First call:
class SafeStackImpl
class SafeStackImpl
class SafeStackImpl
class [LSafeStackImpl;
class [LSafeStackImpl;
Second call:
class [LSafeStackImpl;
class SafeStackImpl
class SafeStackImpl
class [LSafeStackImpl;
class [LSafeStackImpl;

Правила для присваивания ссылочного значения даются на основании следующего кода.

SourceType srcRef;
// srcRef соответствующим образом проинициализирована
DestinationType destRef = srcRef;

Если присваивание допустимо, то говорят, что ссылочное значение srcRef может быть присваиваемым (или совместимо по присваиванию) ссылке DestinationType. Правила показаны на реальных примерах в упражнении 6.11.

Если SourceType (ТипИсточника) — это тип класса, то ссылочное значение в srcRef может быть присвоено ссылке destRef, если только тип DestinationType является одним из следующих:

  • DestinationType — это суперкласс подкласса SourceType;
  • DestinationType — это интерфейсный тип, который реализуется классом SourceType.
objRef        = safeStackRef;  // (1) Всегда возможно
stackRef      = safeStackRef;  // (2) Присваивание подкласса суперклассу
iStackRef     = stackRef;      // (3) StackImpl реализует IStack
iSafeStackRef = safeStackRef;  // (4) SafeStackImpl реализует ISafeStack

Если SourceType является интерфейсным типом, то ссылочное значение в srcRef может быть присвоено ссылке destRef, только если тип DestinationType является одним из следующих:

  • DestinationType — это Object;
  • DestinationType — это суперинтерфейс для подынтерфейса SourceType.
objRef    = iStackRef;     // (5) Всегда возможно
iStackRef = iSafeStackRef; // (6) Присваивание подынтерфейса интерфейсу

Если SourceType является типом массива, то ссылочное значение в srcRef может быть присвоено ссылке destRef, только если тип DestinationType является одним из следующих:

  • DestinationType — это Object;
  • DestinationType является типом массива, для которого элемент типа SourceType может быть присвоен элементу типа DestinationType.
   objRef     = objArray;       // (7) Всегда возможно
   objRef     = stackArray;     // (8) Всегда возможно
   objArray   = stackArray;     // (9) Всегда возможно
   objArray   = iSafeStackArray;// (10) Всегда возможно
   objRef     = intArray;       // (11) Всегда возможно
// objArray   = intArray;       // (12) Ошибка компиляции
   stackArray = safeStackArray; // (13) Массив подкласса в массив суперкласса
   iSafeStackArray =
           safeStackArray;      // (14) SafeStackImpl реализует ISafeStack

Правила присваивания проводятся в жизнь компилятором, что гарантирует, что во время выполнения не произойдет ошибка приведения типов. Такое преобразование типов безопасно. Причина, по которой правила поддерживаются компилятором, заключается в том, что они касаются типа ссылки (который всегда известен на этапе компиляции), а не реального типа объекта, на который будут ссылаться (известного на этапе выполнения).

Приведения типов во время передачи параметров

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

В примере 6.11 метод sendParams() из строки (17) содержит сигнатуру, показывающую типы формальных параметров.

sendParams(Object, Stacklmpl, IStack, Stacklmpl[], Istack[]);

Вызов метода в строке (15) имеет следующую сигнатуру, показывая типы реальных параметров:

sendParams(StackImpl, SafeStackImpl, IStack, SafeStackImpl[], ISafeStack[]);

Обратите внимание, что присваивание значений реальных параметров в соответствующие формальные параметры допустимо согласно правилам присваивания, рассмотренным выше. Вызов метода в строке (16) дает другой пример преобразования типов при передаче параметров. Он имеет следующую сигнатуру:

sendParams(ISafeStack[], StackImpl ISafeStack, StackImpl[], SafeStackImpl[]);

По аналогии с присваиванием правила преобразования для передачи параметров основываются на ссылочном типе параметров и проводятся в жизнь компилятором. Вывод из примера 6.11 показывает класс реальных объектов, на которые ссылаются формальные параметры во время выполнения, ими могут оказаться в нашем случае или SafeStackImpl, или SafeStackImpl[]. Символ L в выводе указывает, что массив одномерный и имеет тип класса или интерфейса.

Правила преобразования типов при передаче параметров являются полезными в случае создания общих типов данных, которые могут использоваться для обработки объектов произвольных типов. Классы из пакета java.util интенсивно используют тип Object как тип параметров в своих методах, для того чтобы реализовывать коллекции, которые могут содержать произвольные объекты.

Приведение типа для ссылок и оператор instanceof

Выражение для приведения типа <ссылки> из <исходного типа> в <целевой тип> следующий синтаксис:

(<целевой тип>) <ссылка>

Выражение приведения типов проверяет, что ссылочное значение объекта, обозначенного ссылкой <ссылка>, может быть присвоено ссылке <целевого типа>, т.е. что <исходный тип> совместим с <целевой тип>. Если это не так, то будет выброшено исключение ClassCastException. Ссылочное значение null может быть преобразовано к любому ссылочному типу.

Бинарный оператор instanceof имеет следующий синтаксис (обратите внимание, что оператор составлен из букв нижнего регистра):

<ссылка> instanceof <целевой тип>

Оператор instanceof возвращает true, если тип операнда слева (<ссылка>) можно преобразовать к типу операнда справа (<целевой тип>), но в случае, если значение операнда слева null, то оператор всегда возвращает false. Если оператор instanceof возвращает true, то соответствующее выражение с оператором приведения типов всегда допустимо. И для оператора приведения, и для оператора instanceof выполняется проверка на этапе компиляции и выполнения, как объясняется ниже.

На этапе компиляции определяется, могут ли ссылка <исходного типа> и ссылка <целевого типа> обозначать объекты ссылочного типа, которые являются общим под типом и <исходного типа> и <целевого типа> в иерархии типов. Если это не так, то очевидно, что не существует никакой связи между типами и ни оператор приведения, ни оператор instanceof не будут допустимы. На этапе выполнения результат операции определяет тип реального объекта, на который указывает <ссылка>.

В случае, если <исходный тип> и <целевой тип> — это классы Light и String соответственно, то не существует отношения подтип-супертип между <исходным типом> и <целевым типом>. Компилятор отклонит преобразование ссылки типа Light к типу String или применение оператора instanceof, как показано в строках (2) и (3) в примере 6.12. В случае, если <исходный тип> и <целевой тип> — это классы Light и TubeLight соответственно, то ссылки Light и TubeLight могут обозначать объекты класса TubeLight (или его подклассов) в иерархии наследования, изображенной на рис. 3.6. Поэтому имеет смысл применить оператор instanceof или ссылку типа Light преобразовать к типу TubeLight, как показано в строках (4) и соответственно в примере 6.12.

Во время выполнения результатом работы оператора instanceof из строки (4) будет false, потому что ссылка lightl типа Light будет реально обозначать объект подкласса LightBulb, который не может быть обозначен ссылкой класса этого же уровня в иерархии наследования TubeLight. По этой же причине попытка приведенному в строке (5) повлечет выбрасывание исключения ClassCastException. Именно поэтому говорят, что преобразования приведения небезопасны, так как они могут выбросить исключение ClassCastException во время выполнения. Обратите внимание, что если результатом оператора instanceof будет false, то приведение, включающее операнды, выбрасывает ClassCastException.

В примере 6.12 в результате выполнения оператора instanceof из строки (6) также получается false, потому что ссылка light1 будет все еще обозначать объект класса LightBulb, а экземпляры этого класса не могут быть обозначены ссылкам типа его подкласса SpotLightBulb. Таким образом, приведение в строке (7) выбрасывает на этапе выполнения ClassCastException.

Ситуация, показанная в строках (8), (9) и (10), демонстрирует типичное использование оператора instanceof для определения того, какой объект обозначается ссылкой, чтобы далее можно было выполнить целевое приведение типов для выполнения какого-либо конкретного действия. Ссылка light1 типа Light в строке (8) специализируется объектом подкласса NeonLight. Результат работы оператора instanceof в строке (9) будет true, потому что ссылка light1 будет обозначать объект подкласса NeonLight, на экземпляр которого может также указывать ссылка типа суперкласса TubeLight. По этой же причине преобразование в строке (10) также допустимо. Если результатом применения оператора instanceof является true, то приведение включенных в оператор операндов будет всегда допустимо.

Пример 6.12. instanceof и оператор приведения типов

class Light { /* ... */ }
class LightBulb extends Light { /* ... */ }
class SpotLightBulb extends LightBulb { /* ... */ }
class TubeLight extends Light { /* ... */ }
class NeonLight extends TubeLight { /* ... */ }

public class WhoAmI {
    public static void main(String[] args) {
        boolean result1, result2, result3, result4, result5;
        Light light1 = new LightBulb();                // (1)
    //  String str = (String) light1;                  // (2) Ошибка компиляции.
    //  result1 = light1 instanceof String;            // (3) Ошибка компиляции.
        result2 = light1 instanceof TubeLight;         // (4) false. Одноуровневый класс.
    //  TubeLight tubeLight1 = (TubeLight) light1;     // (5) ClassCastException.

        result3 = light1 instanceof SpotLightBulb;     // (6) false: Суперкласс
    //  SpotLightBulb spotRef = (SpotLightBulb) light1;// (7) ClassCastException

        light1 = new NeonLight();                      // (8)
        if (light1 instanceof TubeLight) {             // (9) true
            TubeLight tubeLight2 = (TubeLight) light1; // (10) OK
            // Теперь можно использовать tubeLight2 для доступа к объекту суперкласса NeonLight.
        }
    }
}

Как мы видели, оператор instanceof эффективно определяет, можно ли присвоить иное значение объекта, обозначенного ссылкой слева, ссылочному типу справа. Обратите внимание, что экземпляр подтипа является частным случаем экземпляра супертипа. Во время выполнения реальный тип объекта, обозначенного ссылкой, сравнивается с типом, задаваемым правосторонним операндом. Другими словами, во время выполнения имеет значение тип реального объекта, а не тип ссылки. В примере 6.13 даются примеры оператора instanceof. Поучительно пройтись всем операторам print и понять выводимый ими результат. Константа null не является экземпляром ссылочного типа, как показывают операторы print в строках (1). (2) (16) Экземпляр суперкласса не является экземпляром своего подкласса, как показано операторе print из (4). Экземпляр класса не является экземпляром совершенно неродственного класса, как показано в операторе print строки (10). Экземпляр класса не является экземпляром интерфейсного типа, который класс не реализует, как показано в операторе print в (6). Любой массив непримитивного типа является экземпляром и типа Object и типа Object[], как показано в операторе print в строках (14) и (15) соответственно.

Пример 6.13. Использование оператора instanceof

interface IStack                   { /*...*/ }
interface ISafeStack extends IStack   { /*...*/ }
class StackImpl implements IStack     { /*...*/ }
class SafeStackImpl extends StackImpl
              implements ISafeStack   { /*...*/ }

public class Identification {
    public static void main(String[] args) {
        Object obj = new Object();
        StackImpl stack = new StackImpl(10);
        SafeStackImpl safeStack = new SafeStackImpl(5);
        IStack iStack;
        System.out.println("(1): " +
            (null instanceof Object));       // Всегда false.
        System.out.println("(2): " +
            (null instanceof IStack));       // Всегда false.

        System.out.println("(3): " +         // true: экземпляр подкласса
            (stack instanceof Object));      //       Object.
        System.out.println("(4): " +
            (obj instanceof StackImpl));     // false: Нисходящее преобразование
        System.out.println("(5): " +
            (stack instanceof StackImpl));   // true: экземпляр класса StackImpl.

        System.out.println("(6): " +         // false: Object не реализует
             (obj instanceof IStack));       //        IStack.
        System.out.println("(7): " +         // true: SafeStackImpl реализует
             (safeStack instanceof IStack)); //       IStack.

        obj = stack;                         // Присваивание подкласса суперклассу
        System.out.println("(8): " +
            (obj instanceof StackImpl));     // true: экземпляр StackImpl.
        System.out.println("(9): " +         // true: StackImpl реализует
            (obj instanceof IStack));        //       IStack.
        System.out.println("(10): " +
             (obj instanceof String));       // false: нет отношения

        iStack = (IStack) obj; // Требуется приведение. Суперкласс присваивается подклассу
        System.out.println("(11): " +        // true: экземпляр подкласса
            (iStack instanceof Object));     //        Object.
        System.out.println("(12): " +
            (iStack instanceof StackImpl));  // true: экземпляр StackImpl.

        String[] strArray = new String[10];
    //  System.out.println("(13): " +        // Ошибка компиляции,
    //      (strArray instanceof String);    // нет отношения.
        System.out.println("(14): " +
            (strArray instanceof Object));   // true: подкласс Object.
        System.out.println("(15): " +
            (strArray instanceof Object[])); // true: подкласс Object[].
        System.out.println("(16): " +
            (strArray[0] instanceof Object));// false: strArray[0]==null.
        strArray[0] = "Amoeba strip";
        System.out.println("(17): " +
            (strArray[0] instanceof String));// true: экземпляр String.
    }
}

Вывод программы:

(1): false
(2): false
(3): true
(4): false
(5): true
(6): false
(7): true
(8): true
(9): true
(10): false
(11): true
(12): true
(14): true
(15): true
(16): false
(17): true

Преобразование ссылок типов класса и интерфейса

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

IStack    istackOne = new StackImpl(5);              // Восходящее приведение
StackImpl stackTwo  = (StackImpl) istackOne;         // Нисходящее преобразование

С помощью ссылки istackOne интерфейсного типа IStack метод интерфейса IStack можно вызвать для объектов класса StackImpl, которые реализуют этот интерфейс. Однако дополнительные члены класса StackImpl не будут доступны по ссылке без предварительного преобразования ее к типу StackImpl.

bject obj1 = istackOne.pop();        // OK. Метод интерфейса IStack.
Object obj2 = istackOne.peek();      // Не верно. Метод не из интерфейса IStack.
Object obj3 = ((StackImpl) istackOne).peek(); // OK. метод класса StackImpl.
Иерархия типовПолиморфизм и динамический поиск метода