Hi! In a past lesson, we discussed casting primitive types. Let's briefly recall what was discussed. Widening and narrowing of reference types - 1We imagined primitive types (in this case, numeric types) as nesting dolls that vary in size according to the amount of memory they occupy. As you'll remember, putting a smaller doll inside a larger one is simple both in real life and in Java programming.
public class Main {

   public static void main(String[] args) {

       int bigNumber = 10000000;
       byte littleNumber = 16;
       bigNumber = littleNumber;
       System.out.println(bigNumber);
   }

}
This is an example of automatic conversion or widening. It happens by itself, so you don't need to write additional code. In the end, we aren't doing anything unusual: we're just putting a smaller doll into a bigger doll. It's another matter if we try to do the opposite and put a larger Russian doll into a smaller doll. You can't do that in real life, but in programming you can. But there's one nuance. If we try to put an int into a short variable, things don't go so smoothly for us. After all, the short variable only holds 16 bits of information, but an int occupies 32 bits! As a result, the passed value is distorted. The compiler will give us an error ('Dude, you're doing something suspicious!'). But if we explicitly indicate the type that we're converting our value to, it will go ahead and perform the operation.
public class Main {

   public static void main(String[] args) {

       int bigNumber = 10000000;

       bigNumber = (short) bigNumber;

       System.out.println(bigNumber);

   }

}
That's just what we did in the example above. The operation was performed, but because the short variable can accommodate only 16 of the 32 bytes, the final value is distorted and we get the number -27008. Such an operation is called an explicit conversion, or narrowing.

Examples of widening and narrowing of reference types

Now let's talk about the same operators applied not to primitive types, but to objects and reference variables! How does this work in Java? It's actually quite simple. There are objects that are unrelated. It would be logical to assume that they cannot be converted to each other, neither explicitly nor automatically:
public class Cat {
}

public class Dog {
}

public class Main {

   public static void main(String[] args) {

       Cat cat = new Dog(); // Error!

   }

}
Here, of course, we get an error. The Cat and Dog classes are not related to each other, and we have not written a 'converter' to move from one to the other. It makes sense that we can't do this: the compiler has no idea how to convert these objects from one type to the other. If the objects are related, well, that's another matter! Related how? Above all, through inheritance. Let's try using inheritance to create a small system of classes. We'll have a common class to represent animals:
public class Animal {

   public void introduce() {

       System.out.println("I'm Animal");
   }
}
Everybody knows that animals can be domesticated (pets) or wild:
public class WildAnimal extends Animal {

   public void introduce() {

       System.out.println("I'm WildAnimal");
   }
}

public class Pet extends Animal {

   public void introduce() {

       System.out.println("I'm Pet");
   }
}
For example, take canines — we have domestic dogs and coyotes:
public class Dog extends Pet {

   public void introduce() {

       System.out.println("I'm Dog");
   }
}



public class Coyote extends WildAnimal {

   public void introduce() {

       System.out.println ("I'm Coyote");
   }
}
We specifically chosen the most basic classes to make them easier to understand. We don't really need any fields, and one method is enough. Let's try executing this code:
public class Main {

   public static void main(String[] args) {

       Animal animal = new Pet();
       animal.introduce();
   }
}
What do you think will be displayed on the console? Will the introduce method of the Pet class or the Animal class be invoked? Try to justify your answer before you continue reading. And here's the result! I'm Pet Why did we get that? It's all simple. We have a parent variable and a descendant object. By writing,
Animal animal = new Pet();
we widened a Pet reference and assigned it to an Animal variable. As with primitive types, reference types are automatically widened in Java. You don't need to write additional code to make it happen. Now we have a descendant object assigned to a parent reference. As a result, we see that the method call is made on the descendant class. If you still don't fully understand why this code works, rewrite it in plain language:
Animal animal = new DomesticatedAnimal();
There's no problem with this, right? Imagine that this is real life, and the reference is simply a paper label with 'Animal' written on it. If you take that piece of paper and attach it to the collar of any pet, everything will be correct. After all, any pet is an animal! The reverse process — moving down the inheritance tree to descendants — is narrowing:
public class Main {

   public static void main(String[] args) {

       WildAnimal wildAnimal = new Coyote();

       Coyote coyote = (Coyote) wildAnimal;

       coyote.introduce();
   }
}
As you can see, here we clearly indicate the class that we want to convert our object to. We previously had a WildAnimal variable, and now we have a Coyote, which is lower on the inheritance tree. It makes sense that without an explicit indication the compiler won't allow such an operation, but if we indicate the type in parentheses, then everything works. Widening and narrowing of reference types - 2Consider another more interesting example:
public class Main {

   public static void main(String[] args) {

       Pet pet = new Animal(); // Error!
   }
}
The compiler generates an error! But why? Because you are trying to assign a parent object to a descendant reference. In other words, you're trying to do something like this:
DomesticatedAnimal domesticatedAnimal = new Animal();
Well, maybe everything will work if we explicitly specify the type that we're trying to convert to? That worked with numbers — Let's give it a try! :)
public class Main {

   public static void main(String[] args) {

       Pet pet = (Pet) new Animal();
   }
}
Exception in thread "main" java.lang.ClassCastException: Animal cannot be cast to Pet Error! The compiler didn't yell at us this time, but we ended up with an exception. We already know the reason: we're trying to assign a parent object to a descendant reference. But why exactly can't you do that? Because not all Animals are DomesticatedAnimals. You created an Animal object and are trying to assign it to a Pet variable. A coyote is also an Animal, but it is not a Pet. In other words, when you write
Pet pet = (Pet) new Animal();
new Animal() could represent any animal, not necessarily a pet! Naturally, your Pet pet variable is only suitable for storing Pets (and their descendants) and not any type of animal. That's why a special Java exception, ClassCastException, was created for cases where an error occurs while casting classes. Let's review it again to make things clearer. A parent reference can point to instances of a descendant class:
public class Main {

   public static void main(String[] args) {

       Pet pet = new Pet();
       Animal animal = pet;

       Pet pet2 = (Pet) animal;
       pet2.introduce();
   }
}
For example, here we have no problems. We have a Pet object referenced by a Pet variable. Later, an Animal reference pointed at the very same object. After that, we convert animal to a Pet. By the way, why did that work for us? Last time we got an exception! Because this time our original object is a Pet!
Pet pet = new Pet();
But in the last example, it was an Animal object:
Pet pet = (Pet) new Animal();
You cannot assign an ancestor object to a descendant variable. You can do the opposite.