"Olá, amigo! O tópico da lição de hoje é ampliar e restringir conversões de tipos. Você aprendeu sobre ampliar e restringir tipos primitivos há muito tempo. No nível 10. Hoje vamos falar sobre como funciona para tipos de referência, ou seja, instâncias de classes”.

Na verdade, é tudo bem simples. Imagine a cadeia de herança de uma classe: a classe, seu pai, o pai do pai etc., até a classe Object. Como uma classe contém todos os métodos membros da classe que ela herda , uma instância da classe pode ser salva em uma variável cujo tipo é o de qualquer um de seus pais.

Aqui está um exemplo:

Código Descrição
class Animal
{
public void doAnimalActions();
}class Cat extends Animal
{
public void doCatActions();
}class Tiger extends Cat
{
public void doTigerActions();
}
Aqui temos três declarações de classe: Animal, Cat e Tiger. Gato herda Animal. E Tiger herda Cat.
public static void main(String[] args)
{
Tiger tiger = new Tiger();
Cat cat = new Tiger();
Animal animal = new Tiger();
Object obj = new Tiger();
}
Um objeto Tiger sempre pode ser atribuído a uma variável cujo tipo é o de um de seus ancestrais. Para a classe Tiger, são Cat, Animal e Object.

Agora vamos dar uma olhada nas conversões de ampliação e restrição.

Se uma operação de atribuição nos faz mover para cima na cadeia de herança (em direção à classe Object), então estamos lidando com uma conversão de ampliação (também conhecida como upcasting). Se descermos na cadeia em direção ao tipo do objeto, será uma conversão de estreitamento (também conhecida como downcasting).

Subir na cadeia de herança é chamado de alargamento, porque leva a um tipo mais geral. No entanto, ao fazer isso, perdemos a capacidade de invocar os métodos adicionados à classe por meio de herança.

Código Descrição
public static void main(String[] args)
{
Object obj = new Tiger();
Animal animal = (Animal) obj;
Cat cat = (Cat) obj;
Tiger tiger = (Tiger) animal;
Tiger tiger2 = (Tiger) cat;
}
Ao restringir o tipo, você precisa usar um operador de conversão de tipo, ou seja, realizamos uma conversão explícita.

Isso faz com que a máquina Java verifique se o objeto realmente herda o tipo para o qual queremos convertê-lo.

Essa pequena inovação produziu uma redução múltipla no número de erros de conversão de tipos e aumentou significativamente a estabilidade dos programas Java.

Código Descrição
public static void main(String[] args)
{
Object obj = new Tiger();
if (obj instanceof Cat)
{
Cat cat = (Cat) obj;
cat.doCatActions();
}}
Melhor ainda, use uma  instância de verificação
public static void main(String[] args)
{
Animal animal = new Tiger();
doAllAction(animal);

Animal animal2 = new Cat();
doAllAction(animal2);

Animal animal3 = new Animal();
doAllAction(animal3);
}

public static void doAllAction(Animal animal)
{
if (animal instanceof Tiger)
{
Tiger tiger = (Tiger) animal;
tiger.doTigerActions();
}

if (animal instanceof Cat)
{
Cat cat = (Cat) animal;
cat.doCatActions();
}

animal.doAnimalActions();
}
E aqui está o porquê. Dê uma olhada no exemplo à esquerda.

Nós (nosso código) nem sempre sabemos com que tipo de objeto estamos trabalhando. Pode ser um objeto do mesmo tipo da variável (Animal), ou qualquer tipo descendente (Gato, Tigre).

Considere o método doAllAction. Funciona corretamente, independentemente do tipo de objeto passado.

Em outras palavras, funciona corretamente para todos os três tipos: Animal, Gato e Tigre.

public static void main(String[] args)
{
Cat cat = new Tiger();
Animal animal = cat;
Object obj = cat;
}
Aqui temos três operações de atribuição. Todos eles são exemplos de conversões ampliadas.

O operador type cast não é necessário aqui, porque nenhuma verificação é necessária. Uma referência de objeto sempre pode ser armazenada em uma variável cujo tipo é um de seus ancestrais.

"Ah, o penúltimo exemplo deixou tudo claro: por que a verificação é necessária e por que a conversão de tipo é necessária."

"Espero que sim. Quero chamar sua atenção para este fato:"

Nada disso faz com que um objeto mude de forma alguma! A única coisa que muda é o número de métodos disponíveis para serem chamados em uma determinada variável de referência.

Por exemplo, uma variável Cat permite chamar os métodos doAnimalActions e doCatActions. Ele não sabe nada sobre o método doTigerActions, mesmo que aponte para um objeto Tiger.

"Sim, eu entendo. Foi mais fácil do que eu pensei que seria."