it-swarm.com.ru

пример явного приведения типов в Java

Я сталкивался с этим примером на http://www.javabeginner.com/learn-Java/java-object-typecasting и в части, где говорится о явном приведении типов, есть один пример, который меня смущает.

Пример:

class Vehicle {

    String name;
    Vehicle() {
        name = "Vehicle";
    }
}

class HeavyVehicle extends Vehicle {

    HeavyVehicle() {
        name = "HeavyVehicle";
    }
}

class Truck extends HeavyVehicle {

    Truck() {
        name = "Truck";
    }
}

class LightVehicle extends Vehicle {

    LightVehicle() {
        name = "LightVehicle";
    }
}

public class InstanceOfExample {

    static boolean result;
    static HeavyVehicle hV = new HeavyVehicle();
    static Truck T = new Truck();
    static HeavyVehicle hv2 = null;
    public static void main(String[] args) {
        result = hV instanceof HeavyVehicle;
        System.out.print("hV is an HeavyVehicle: " + result + "\n");
        result = T instanceof HeavyVehicle;
        System.out.print("T is an HeavyVehicle: " + result + "\n");
        result = hV instanceof Truck;
        System.out.print("hV is a Truck: " + result + "\n");
        result = hv2 instanceof HeavyVehicle;
        System.out.print("hv2 is an HeavyVehicle: " + result + "\n");
        hV = T; //Sucessful Cast form child to parent
        T = (Truck) hV; //Sucessful Explicit Cast form parent to child
    }
}

В последней строке, где T присваивается ссылка hV и typecast как (Truck), почему в комментарии говорится, что это успешное явное приведение от родителя к потомку? Как я понимаю, приведение (неявное или явное) изменит только объявленный тип объекта, но не фактический тип (который никогда не должен изменяться, если вы фактически не назначите новый экземпляр класса для ссылки на поле этого объекта). Если hv уже был назначен экземпляр класса HeavyVehicle, который является суперклассом класса Truck, то как тогда это поле может быть преобразовано в более конкретный подкласс под названием Truck, который выходит из класса HeavyVehicle?

Насколько я понимаю, приведение служит для ограничения доступа к определенным методам объекта (экземпляра класса). Следовательно, вы не можете привести объект к более конкретному классу, который имеет больше методов, чем фактически назначенный класс объекта. Это означает, что объект может быть приведен только как суперкласс или тот же класс, что и класс, из которого он был фактически создан. Это правильно или я здесь не прав? Я все еще учусь, поэтому я не уверен, что это правильный взгляд на вещи.

Я также понимаю, что это должно быть примером понижения рейтинга, но я не уверен, как это на самом деле работает, если у фактического типа нет методов класса, в который этот объект понижается. Меняет ли явное приведение каким-либо образом фактический тип объекта (не только объявленный тип), так что этот объект больше не является экземпляром класса HeavyVehicle, а теперь становится экземпляром класса Truck?

31
SineLaboreNihil

Ссылка против объекта против типов

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

Когда мы создаем объект в Java, мы объявляем его истинную природу, которая никогда не изменится. Но любой заданный объект в Java, вероятно, будет иметь несколько типов. Некоторые из этих типов, очевидно, даются благодаря иерархии классов, другие не так очевидны (например, обобщенные элементы, массивы).

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

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

Truck t = new Truck(); //or
HeavyVehicle hv = new Truck(); //or
Vehicle h = new Truck() //or
Object o = new Truck();

Таким образом, ключевым моментом здесь является осознание того, что ссылка на объект не сам объект. Природа создаваемого объекта никогда не изменится. Но мы можем использовать различные виды совместимых ссылок для получения доступа к объекту. Это одна из особенностей полиморфизма здесь. Один и тот же объект может быть доступен через ссылки разных «совместимых» типов. 

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

Повышение или расширение конверсии ссылок

Теперь, имея ссылку типа Truck, мы можем легко сделать вывод, что она всегда совместима со ссылкой типа Vehicle, потому что все Грузовики являются Транспортными средствами. Следовательно, мы могли бы отклонить ссылку, не используя явное приведение.

Truck t = new Truck();
Vehicle v = t;

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

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

Прекращение или сужение ссылки

Теперь, имея ссылку типа Vechicle, мы не можем «безопасно» сделать вывод, что она на самом деле ссылается на Truck. В конце концов это может также ссылаться на какую-то другую форму Транспортного средства. Например

Vehicle v = new Sedan(); //a light vehicle

Если вы найдете ссылку v где-то в своем коде, не зная, на какой конкретный объект он ссылается, вы не сможете «безопасно» аргументировать, указывает ли он на Truck или на Sedan или любой другой вид транспортного средства.

Компилятор хорошо знает, что он не может дать никаких гарантий относительно истинной природы объекта, на который ссылаются. Но программист, читая код, может быть уверен в том, что он/она делает. Как и в случае выше, вы можете ясно видеть, что Vehicle v ссылается на Sedan.

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

Sedan s = (Sedan) v;

Это всегда требует явного приведения, потому что компилятор не может быть уверен, что это безопасно, и поэтому это все равно, что спрашивать программиста: «Вы уверены, что делаете?». Если вы врете компилятору, вы получите ClassCastException во время выполнения, когда этот код выполняется.

Другие виды правил подтипов

Есть другие правила подтипирования в Java. Например, существует также концепция, называемая числовым продвижением, которая автоматически приводит числа в выражения. Как в

double d = 5 + 6.0;

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

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

int a = 10;
double b = a; //upcasting
int c = (int) b; //downcasting

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

Некоторые правила подтипирования могут быть не такими очевидными, как в случае массивов. Например, все ссылочные массивы являются подтипами Object[], а примитивные массивы - нет.

А в случае универсальных шаблонов, особенно с использованием подстановочных знаков, таких как super и extends, все становится еще сложнее. Как в

List<Integer> a = new ArrayList<>();
List<? extends Number> b = a;

List<Object> c = new ArrayList<>(); 
List<? super Number> d = c;

Где тип b является подтипом типа a. И тип d является подтипом типа c.

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

60
Edwin Dalorzo

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

В противном случае (в то время как вы можете иметь его в исходном коде), это приведет к исключительной ситуации ClassCastException.

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

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


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

5
MightyPork

Последняя строка кода компилируется и успешно выполняется без исключений. То, что он делает, совершенно законно.

  1. hV изначально ссылается на объект типа HeavyVehicle (назовем этот объект h1):

    static HeavyVehicle hV = new HeavyVehicle(); // hV now refers to h1.
    
  2. Позже мы заставим hV обращаться к другому объекту типа Truck (назовем этот объект t1):

    hV = T; // hV now refers to t1.
    
  3. Наконец, мы заставляем T обращаться к t1.

    T = (Truck) hV; // T now refers to t1.
    

T уже ссылался на t1, поэтому это утверждение ничего не изменило.

Если hv уже был назначен экземпляр класса HeavyVehicle, который является суперклассом класса Truck, то как тогда это поле может быть преобразовано в более конкретный подкласс под названием Truck, который выходит из класса HeavyVehicle?

К тому времени, когда мы достигнем последней строки, hV больше не относится к экземпляру HeavyVehicle. Это относится к экземпляру грузовика. Приведение экземпляра Truck к типу Truck не проблема.

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

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

Меняет ли явное приведение каким-либо образом фактический тип объекта (не только объявленный тип), так что этот объект больше не является экземпляром класса HeavyVehicle, а теперь становится экземпляром класса Truck?

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

Повторим, ничего не изменилось в последней строке. T относится к t1 до этой строки и относится к t1 после.

Так почему явное приведение (Truck) необходимо в последней строке? Мы в основном помогаем просто помогать компилятору.

Мы знаем, что к этому моменту hV относится к объекту типа Truck, поэтому можно присвоить этот объект типа Truck переменной T. Но compiler недостаточно умен, чтобы знать, что , Компилятор хочет, чтобы мы были уверены, что когда он доберется до этой строки и попытается выполнить присвоение, он найдет экземпляр Truck, ожидающий его.

2
dshiga

Когда вы делаете приведение из объекта Грузовик к HeavyVehicle, как это: 

Truck truck = new Truck()
HeavyVehicle hv = truck;

Объект все еще грузовик, но у вас есть доступ только к методам и полям heavyVehicle, используя ссылку HeavyVehicle. Если вы снова опуститесь на грузовик, вы можете снова использовать все методы и поля грузовика.

Truck truck = new Truck()
HeavyVehicle hv = truck;
Truck anotherTruckReference = (Truck) hv; // Explicit Cast is needed here

Если фактический объект, который вы понижаете, не является грузовиком, будет выдано исключение ClassCastException, как в следующем примере:

HeavyVehicle hv = new HeavyVehicle();
Truck tr = (Truck) hv;  // This code compiles but will throw a ClasscastException

Исключение выдается, потому что фактический объект не принадлежит правильному классу, это объект суперкласса (HeavyVehicle)

2
David SN

Приведенный выше код скомпилируется и будет работать нормально. Теперь измените приведенный выше код и добавьте следующую строку System.out.println (T.name);

Это гарантирует, что вы не используете объект T после понижения класса hV как Грузовик.

В настоящее время в вашем коде вы не используете T после downcast, поэтому все в порядке и работает. 

Это связано с тем, что, явно указав hV в качестве Truck, complier жалуется, считая, что программист произвел кастинг объекта и знает, какой объект был приведен к чему.

Но во время выполнения JVM не может оправдать кастинг и выдает исключение ClassCastException «HeavyVehicle не может быть приведен к Truck».

1
Prashant Thakkar

Чтобы лучше проиллюстрировать некоторые моменты, изложенные выше, я изменил рассматриваемый код и добавил к нему дополнительные коды со встроенными комментариями (включая фактические результаты) следующим образом: 

class Vehicle {

        String name;
        Vehicle() {
                name = "Vehicle";
        }
}

class HeavyVehicle extends Vehicle {

        HeavyVehicle() {
                name = "HeavyVehicle";
        }
}

class Truck extends HeavyVehicle {

        Truck() {
                name = "Truck";
        }
}

class LightVehicle extends Vehicle {

        LightVehicle() {
                name = "LightVehicle";
        }
}

public class InstanceOfExample {

        static boolean result;
        static HeavyVehicle hV = new HeavyVehicle();
        static Truck T = new Truck();
        static HeavyVehicle hv2 = null;
        public static void main(String[] args) {

                result = hV instanceof HeavyVehicle;
                System.out.print("hV is a HeavyVehicle: " + result + "\n"); // true

                result = T instanceof HeavyVehicle;
                System.out.print("T is a HeavyVehicle: " + result + "\n"); // true
//      But the following is in error.              
//      T = hV; // error - HeavyVehicle cannot be converted to Truck because all hV's are not trucks.                               

                result = hV instanceof Truck;
                System.out.print("hV is a Truck: " + result + "\n"); // false               

                hV = T; // Sucessful Cast form child to parent.
                result = hV instanceof Truck; // This only means that hV now points to a Truck object.                            
                System.out.print("hV is a Truck: " + result + "\n");    // true         

                T = (Truck) hV; // Sucessful Explicit Cast form parent to child. Now T points to both HeavyVehicle and Truck. 
                                // And also hV points to both Truck and HeavyVehicle. Check the following codes and results.
                result = hV instanceof Truck;                             
                System.out.print("hV is a Truck: " + result + "\n");    // true 

                result = hV instanceof HeavyVehicle;
                System.out.print("hV is a HeavyVehicle: " + result + "\n"); // true             

                result = hV instanceof HeavyVehicle;
                System.out.print("hV is a HeavyVehicle: " + result + "\n"); // true 

                result = hv2 instanceof HeavyVehicle;               
                System.out.print("hv2 is a HeavyVehicle: " + result + "\n"); // false

        }

}
0
S. W. Chi