Доброго времени суток, джаварашовец. Начали мне поступать вопросы о приведение ссылочных типов в Java. Что бы каждый раз не пересказывать одно и то же, решил написать маленькую статью.
Есть расширяющее и сужающее приведение.
Мы видим, что класс
Сначала разберем, что же такое приведение типов
Приведение типов (преобразование типов) — преобразование значения переменной одного типа в значение другого типа. Давайте посмотрим на примере, что это такое и с чем его едят. У нас есть некоторая иерархия классов (см. Рисунок 1). Тут видно все классы иерархии, кто кого наследует и методы каждого класса.
Cat
является наследником класса Pet
. Pet
, в свою очередь, наследник класса Animal
.
Когда мы напишем:
Animal animalCat = new Cat();
Animal animalDog = new YorkshireTerrier();
Это расширяющее приведение (или неявное). Мы расширили ссылки animalCat
и animalDog
. Они ссылаются на объекты Cat
и Dog
. При таком приведении мы не можем через ссылку animalCat/animalDog
вызвать методы, которые есть в Cat/Dog
, но которых нету в Animal
.
Сужающее приведение(или явное) происходит в обратную сторону:
Animal animalCat = new Cat();
Animal animalDog = new YorkshireTerrier();
Cat cat =(Cat)animalCat;
YorkshireTerrier dog = (YorkshireTerrier) animalDog;
Мы явно указали к какому типу хотим привести данный объект.
BUT, BE CAREFUL!!!
Если Вы сделаете вот так:
Animal animalCat = new Cat();
YorkshireTerrier dog = (YorkshireTerrier) animalCat;
компилятор пропустит этот код. А вот RunTime
выкинет вам:
Exception in thread "main" java.lang.ClassCastException: Animals.Cat cannot be cast to Animals.YorkshireTerrier
RunTime
видит, что Cat
и YorkshireTerrier
два разных класса. Что бы избежать ClassCastException при сужающем преобразовании, используйте instanceof
.
Animal animalCat = new Cat();
if (animalCat instanceof YorkshireTerrier)
{
YorkshireTerrier dog = (YorkshireTerrier) animalCat;
}
Если animalCat
является YorkshireTerrier
, тогда произойдет присвоение, если нет — ничего не произойдет.
А теперь зачем же это нужно, если мы теряем методы и можем получить такие ошибки
Рассмотрим код который я сделал по диаграмме с Рис. 1. КлассAnimal
public abstract class Animal
{
String name;
int age;
String nameOfClass = getClass().getSimpleName();
public void eat(){
System.out.println(nameOfClass + ": Omnomnom");
}
public void sleep(){
System.out.println(nameOfClass + ": Z-z-z-z");
}
}
Класс WildAnimal
который наследуется от Animal
public abstract class WildAnimal extends Animal
{
public void steelChicken()
{
System.out.println(nameOfClass+": Muhaha,I stole a chicken!");
}
}
Класс Pet
который наследуется от Animal
public abstract class Pet extends Animal
{
public void peeInTray(){
System.out.println(nameOfClass + ": Master, I peed");
}
}
Класс Fox
который наследуется от WildAnimal
public class Fox extends WildAnimal
{
public void eatColobok(){
System.out.println(nameOfClass + ": I will eat you, Colobok");
}
}
Класс Wolf
который наследуется от WildAnimal
public class Wolf extends WildAnimal
{
public void hawlAtTheMoon(){
System.out.println(nameOfClass + ": Ouuuuu!!!Ouuuu!!!");
}
}
Класс Cat
который наследуется от Pet
public class Cat extends Pet
{
public void sleepOnKeyboard(){
System.out.println(nameOfClass + ": Master, stop working!!I wanna sleep on your keyboard");
}
}
Класс YorkshireTerrier
который наследуется от Pet
public class YorkshireTerrier extends Pet
{
public void bark(){
System.out.println(nameOfClass + ": Meow!!! Meow!!!");
}
}
Представьте себе ситуацию. Нам нужно собрать всех животных в один список, накормить их и потом уложить спать. Это легко сделать, если мы создадим ArrayList
животных (Animal
). И потом у каждого животного вызовем соответствующие методы:
public class ZOO
{
public static void main(String[] args)
{
List<Animal> allAnimals = new ArrayList<>();
allAnimals.add(new Cat());
allAnimals.add(new Wolf());
allAnimals.add(new Fox());
allAnimals.add(new YorkshireTerrier());
for (Animal animal : allAnimals)
{
animal.eat();
animal.sleep();
}
}
}
Я не могу у animal
вызвать метод bark()
или sleepOnKeyboard()
. Потому что в листе allAnimals
хранятся кот, волк, йорик и лиса, но они приведены к Animal
. И у них есть только те методы, которые есть в Animal
.
Это очень хорошо, потому что, если бы мы могли вызвать все методы, то зачем нам нужен волк который спит на клавиатуре, или йорик который ворует куриц?
Спасибо за внимание. Надеюсь, эта статья для Вас будет полезна. Критика и комментарии приветствуются)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Мухи — переменные примитивного типа. У них нет ссылок, они просто занимают в памяти определённое количество байт. Тип byte занимает один байт ( 8 бит). Тип short требует два байта (16 бит). Теперь, если Вы переливаете из 8-и битного ведра в 16-и битное, то все биты перельются без потерь.
В данном случае приведение типов выполняется компилятором самостоятельно (неявно), т.е. нет необходимости писать short s = (short) b Мы преобразуем (расширяем) менее ёмкий по памяти тип до более ёмкого. Преобразования с расширением типов выполняются компилятором неявно.
В коде ниже происходит сужение типа соответственно. Часть битов из 16-и битного ведра даром пропадают при переливании в 8-и битное ведро.
Происходит сужение типа, которое мы должны обозначить явно.
"-Пол литра в дребезги? Да?!
-Да!
-Да я тебя!!!..."
Теперь разберёмся с котлетами- переменными ссылочного типа.
Переменная указывает (хранит адрес в памяти) на экземпляр объекта (конкретные данные занимающие часть памяти в зависимости от архитектуры объекта). Ссылочная переменная должна иметь тип, т.к. Java — строго типизированный язык. В самом простом варианте тип ссылочной переменной при присвоении должен соответствовать классу на основе которого создаётся экземпляр объекта:
В свете наследования классов друг от друга и полиморфизма всё усложняется. При приведении переменной типа потомок к переменной типа родитель мы теряем те поля, которые были определены только в потомке, т.е. обращаясь к объекту потомок через ссылочную переменную типа родитель, мы увидим только те переменные и методы, которые были унаследованы
Как я поняла, тут создается объект типа ChildX, полученная ссылка будет присвоена типу Parent. Т.е. объект ChildX, а тип ссылки Parent. Переменная тут р. Какой у нее тип? Обращение к p будет через тип ссылки, т.е. Раrent. Поэтому доступны будут только поля и методы класса Parent. Зачем такое объявление? Чтобы потом можно было провести сужение ссылки до типа ChildX и воспользоваться полями и методами класса ChildX?
1. Допустип есьб N количество sub классов, вам нужно их где то хранить, все они могут хранится в одной колекции Collection, вместо N количества коллекций.
2. Может так случится, что вам нет необходимости приводить к типу sub класса, т.к. вам всего лишь нужно знать значение поля parent класса.
3. Parent класс это своего рода абстрактный интерфейс, общий протокол. И при работе с другими классами интерфейсами вы будете вместо N интерфейсов создавать один интерфейс с методами где аргумент Parent, если это иммеет смысл.
4. Данный принцип програмирования зародился до появления Java.
5. Фактически данный принцип приводит к тому что вы разделяете классы/интерфейсы и работаете только с теми методами которые вам нужны.
1. При хранении в контейнере никаких преимуществ двойного объявления я не выявила. Вот немного подправленный мной код из Thinking in java
2 Если мне не нужны методы субкласса, зачем мне вообще двойное объявление? К полям Parent можно обратиться и через экземпляр самого парента и через обычно объявленного Child.
5. До понимания этого еще не доросла.
Как насчет вот такого кода.
Или вот такой код.
Еще проще, может так случится что рабоете с методом toString или hashcode, wait etc. Естественно до лампочки то какой реальный тип у обьекта в таком случае.
Откуда можно сделать логический вывод что методы суб классов не интересны, важно состояние обьекта и его универсальное поведение.
Вобщем это так сказать фича для улучшения архитектуры приложения. Однако может так случится что проще работать с обьектом с Child чем расширять ссылку на него в одном методе до Parent а потом в другом методе его сужать до Child, если нужны какие то с
И для чего это нужно отсюда:
Различаются два вида преобразований типов — upcasting и downcasting. Повышающее преобразование (upcasting) — это преобразование от типа порожденного класса (от подкласса) к базовому (суперклассу). Такое преобразование допустимо всегда. На него нет никаких ограничений и для его проведения не требуется применять никаких дополнительных синтаксических конструкций (см. предыдущий пример). Это связано с тем, что объект подкласса всегда в себе содержит как свою часть объект суперкласса.
Понижающее преобразование (downcasting) — это преобразование от суперкласса к подклассу. Такое преобразование имеет ряд ограничений. Во-первых, оно может задаваться только явно при помощи операции преобразования типов, например,
B b1 = (B)a;
Во-вторых, объект, подвергаемый преобразованию, реально должен быть того класса, к которому он преобразуется. Если это не так, то возникает исключение ClassCastException в процессе выполнения программы.
Все это выглядит странно, для тех, кто не знаком с ООП. Действительно, получается, что единственная допустимая ситуация, когда такое преобразование возможно, это когда мы построили объект класса B, где B вляется подклассом A, затем зачем-то преобразовали его к классу A, а потом, опять же непонятно для чего, преобразовали его обратно к классу B.
На самом деле это имеет смысл. Для класса A может быть создан программный код, выполняющий что-то полезное. Он имеет свои методы, и они предполагают работу с объектами класса
В лекции как раз все наоборот. И в Философии Джава тоже.
Еще такой вопрос автору — Откуда взялся объект Dog? На диаграмме его нет.
И предлагаю последнее предложение переписать так — «При таком приведении мы можем через ссылку animalCat/animalDog вызвать только те методы, которые есть в Animal, о методах Cat и YorkshireTerrier теперь ничего не известно»
Мое мнение такое — если при приведении типа можем что-то потерять (например методы подкласса) — то это сужение. По аналогии с примитивами.
Хотя странно, что в разных источниках по разному трактуют…
Прошу создалетей javarush исправить лекцию по приведению типов на 14-м уровне, а то она как минимум соответствует этой статье.
5.1.5. Widening Reference Conversion
5.1.6. Narrowing Reference Conversion
Допустим:
При расширении имеется ввиду, что идет переход от частного к общему.
но функционал объекта принимаемого ссылкой «сужается» рамками класса ссылки.
Вот.
Была собака — а стало животное. Животное может быть каким угодно(Собаки, слоны, рыбы и тп. расширение) когда собака это Лайка, Овчарка и тп(сужение, только собаки).
На примере Java. Есть класс Object(включает все классы расширение) когда есть класс String(subclass of Object сужение)
Насчет сужение функционала — это да как вариант возможен, но там согласитесь для этого и ввели так называемое наследование чтобы класс был типом предка и имел какую то свою изюминку.
Более подробно почитайте тут
Прошу внести ясность в данный вопрос. Так как в лекциях и в доп. литературе (например в Философии Джава) сужение и расширение типа подается с точностью наоборот. Кому верить?
Спасибо.
Вроде бы как раз наоборот, первое это сужающее (потому что мы взяли объект Cat и сузили его до Animal, сократив количество его вызываемых методов), а во втором случае как раз расширяющее (мы взяли ссылку на Animal и расширили ее до ссылки на Cat, добавив при этом методы, которые есть у Cat)
Но в курсе javarush говориться:
Тут нужно мнение от hubert или Diana