Herança e composição são duas técnicas de programação usadas pelos desenvolvedores para estabelecer o relacionamento entre classes e objetos. Enquanto a herança deriva uma classe de outra, a composição define uma classe como a soma de suas partes.

Classes e objetos criados por herança são fortemente acoplado porque alterar o pai ou a superclasse em um relacionamento de herança corre o risco de quebrar seu código. Classes e objetos criados por meio de composição são fracamente acopladao que significa que você pode alterar mais facilmente as partes dos componentes sem quebrar o código.

Como o código fracamente acoplado oferece mais flexibilidade, muitos desenvolvedores acreditam que a composição é uma técnica melhor do que a herança, mas a verdade é mais complexa. Escolher uma ferramenta de programação é semelhante a escolher o utensílio de cozinha correto: você não usaria uma faca de manteiga para cortar vegetais e, da mesma forma, não deveria escolher a composição para cada cenário de programação.

Herança e composição em Java

Neste artigo, você aprenderá as diferenças entre herança e composição e como decidir qual é a correta para seus programas Java:

  • Quando usar herança em Java
  • Quando usar composição em Java
  • Diferenças entre herança e composição
  • Substituição de método com herança Java
  • Usando construtores com herança
  • Transmissão de tipo e ClassCastException
  • Erros comuns com herança em Java
  • O que lembrar sobre herança em Java

Quando usar herança em Java

Na programação orientada a objetos, podemos usar herança quando sabemos que existe um relacionamento “é um” entre um filho e sua classe pai. Alguns exemplos seriam:

  • Uma pessoa é um humano.
  • Um gato é um animal.
  • Um carro é um veículo.

Em cada caso, o filho ou subclasse é um especializado versão do pai ou superclasse. Herdar da superclasse é um exemplo de reutilização de código. Para entender melhor essa relação, reserve um momento para estudar o Car classe, que herda de Vehicle:


class Vehicle {

    String brand;
    String color;
    double weight;
    double speed;

    void move() {
        System.out.println("The vehicle is moving");
    }
}

public class Car extends Vehicle {
    String licensePlateNumber;
    String owner;
    String bodyStyle;

    public static void main(String... inheritanceExample) {
        System.out.println(new Vehicle().brand);
        System.out.println(new Car().brand);
        new Car().move();
    }
}

Ao considerar o uso de herança, pergunte-se se a subclasse é realmente uma versão mais especializada da superclasse. Nesse caso, carro é um tipo de veículo, então a relação de herança faz sentido.

Quando usar composição em Java

Na programação orientada a objetos, podemos usar composição nos casos em que um objeto “tem” (ou faz parte de) outro objeto. Alguns exemplos seriam:

  • Um carro tem um bateria (uma bateria é parte de um carro).
  • Uma pessoa tem um coração (um coração é parte de uma pessoa).
  • Uma casa tem um sala de estar (uma sala de estar é parte de uma casa).

Para entender melhor esse tipo de relacionamento, considere a composição de um House:


public class CompositionExample {

    public static void main(String... houseComposition) {
        new House(new Bedroom(), new LivingRoom());
        // The house now is composed with a Bedroom and a LivingRoom
    }

    static class House {

        Bedroom bedroom;
        LivingRoom livingRoom;

        House(Bedroom bedroom, LivingRoom livingRoom) {
            this.bedroom = bedroom;
            this.livingRoom = livingRoom;
        }
    }
    static class Bedroom { }
    static class LivingRoom { }
}

Neste caso, sabemos que uma casa possui uma sala e um quarto, então podemos utilizar o Bedroom e LivingRoom objetos na composição de um House.

Diferenças entre herança e composição

Agora vamos comparar herança e composição em Java. O seguinte é um bom exemplo de herança?


import java.util.HashSet;

public class CharacterBadExampleInheritance extends HashSet

Neste caso, a resposta é não. A classe filha herda muitos métodos que nunca usará, resultando em um código fortemente acoplado que é confuso e difícil de manter. Se você olhar com atenção, também ficará claro que esse código não passa no teste “é um”.

Agora vamos tentar o mesmo exemplo usando composição:


import java.util.HashSet;
import java.util.Set;

public class CharacterCompositionExample {
    static Set set = new HashSet<>();

    public static void main(String... goodExampleOfComposition) {
        set.add("Homer");
        set.forEach(System.out::println);
    }

Usar a composição para este cenário permite que o CharacterCompositionExample classe para usar apenas dois dos HashSetmétodos, sem herdar todos eles. Acabamos com um código mais simples, menos acoplado e mais fácil de entender e manter.

Substituição de método com herança Java

A herança nos permite reutilizar métodos e outros atributos de uma classe em uma nova classe, o que é muito conveniente. Mas para que a herança realmente funcione, também precisamos ser capazes de alterar alguns dos comportamentos herdados em nossa nova subclasse. Por exemplo, podemos querer especializar o som de um Cat faz:


class Animal {
    void emitSound() {
        System.out.println("The animal emitted a sound");
    }
}
class Cat extends Animal {
    @Override
    void emitSound() {
        System.out.println("Meow");
    }
}
class Dog extends Animal {
}

public class Main {
    public static void main(String... doYourBest) {
        Animal cat = new Cat(); // Meow
        Animal dog = new Dog(); // The animal emitted a sound
        Animal animal = new Animal(); // The animal emitted a sound
        cat.emitSound();
        dog.emitSound();
        animal.emitSound();
    }
}

Este é um exemplo de herança Java com substituição de método. Nós primeiro ampliar o Animal classe para criar um novo Cat aula. Em seguida nós sobrepor o Animal aula emitSound() método para obter o som específico do Cat faz. Mesmo que tenhamos declarado o tipo de classe como Animalquando instanciamos isso como Cat vamos ouvir o miado do gato.

Java tem herança múltipla?

Ao contrário de algumas linguagens, como C++, Java não permite herança múltipla com classes. Entretanto, você pode usar herança múltipla com interfaces. A diferença entre uma classe e uma interface, neste caso, é que as interfaces não mantêm o estado.

Se você tentar herança múltipla como fiz abaixo, o código não será compilado:


class Animal {}
class Mammal {}
class Dog extends Animal, Mammal {}

Uma solução usando classes seria herdar uma por uma:


class Animal {}
class Mammal extends Animal {}
class Dog extends Mammal {}

Outra solução é substituir as classes por interfaces:


interface Animal {}
interface Mammal {}
class Dog implements Animal, Mammal {}

Usando 'super' para acessar os métodos de uma classe pai

Quando duas classes estão relacionadas por herança, a classe filha deve ser capaz de acessar todos os campos, métodos ou construtores acessíveis de sua classe pai. Em Java, usamos a palavra reservada super para garantir que a classe filha ainda possa acessar o método substituído do pai:


public class SuperWordExample {
    class Character {
        Character() {
            System.out.println("A Character has been created");
        }
        void move() {
            System.out.println("Character walking...");
        }
    }
    class Moe extends Character {
        Moe() {
            super();
        }
        void giveBeer() {
            super.move();
            System.out.println("Give beer");
        }
    }
}

Neste exemplo, Character é a classe pai de Moe. Usando superpodemos acessar Characterde move() método para dar uma cerveja a Moe.

Usando construtores com herança

Quando uma classe herda de outra, o construtor da superclasse será carregado primeiro, antes de carregar sua subclasse. Na maioria dos casos, a palavra reservada super é adicionado automaticamente ao construtor. Entretanto, se a superclasse tiver um parâmetro em seu construtor, devemos invocar deliberadamente o parâmetro super construtor, conforme mostrado abaixo:


public class ConstructorSuper {
    class Character {
        Character() {
            System.out.println("The super constructor was invoked");
        }
    }
    class Barney extends Character {
        // No need to declare the constructor or to invoke the super constructor
        // The JVM will to that
    }
}

Se a classe pai tiver um construtor com pelo menos um parâmetro, então devemos declarar o construtor na subclasse e usar super para invocar explicitamente o construtor pai. O super a palavra reservada não será adicionada automaticamente e o código não será compilado sem ela. Por exemplo:


public class CustomizedConstructorSuper {
    class Character {
        Character(String name) {
            System.out.println(name + "was invoked");
        }
    }
    class Barney extends Character {
        // We will have compilation error if we don't invoke the constructor explicitly
        // We need to add it
        Barney() {
            super("Barney Gumble");
        }
    }
}

Transmissão de tipo e ClassCastExceptions

Casting é uma forma de comunicar explicitamente ao compilador que você realmente pretende converter um determinado tipo. É como dizer: “Ei, JVM, eu sei o que estou fazendo, então, por favor, lance esta classe com este tipo”. Se uma classe que você lançou não for compatível com o tipo de classe que você declarou, você receberá um ClassCastException.

Na herança, podemos atribuir a classe filha à classe pai sem lançar, mas não podemos atribuir uma classe pai à classe filha sem usar a conversão.

Considere o seguinte exemplo:


public class CastingExample {
    public static void main(String... castingExample) {
        Animal animal = new Animal();
        Dog dogAnimal = (Dog) animal; // We will get ClassCastException
        Dog dog = new Dog();
        Animal dogWithAnimalType = new Dog();
        Dog specificDog = (Dog) dogWithAnimalType;
        specificDog.bark();
        Animal anotherDog = dog; // It's fine here, no need for casting
        System.out.println(((Dog)anotherDog)); // This is another way to cast the object
    }
}
class Animal { }
class Dog extends Animal { void bark() { System.out.println("Au au"); } }

Quando tentamos lançar um Animal instância para um Dog obtemos uma exceção. Isso ocorre porque o Animal não sabe nada sobre seu filho. Pode ser um gato, um pássaro, um lagarto, etc. Não há informações sobre o animal específico.

O problema neste caso é que instanciamos Animal assim:


Animal animal = new Animal();

Então tentei lançar assim:


Dog dogAnimal = (Dog) animal;

Porque não temos um Dog por exemplo, é impossível atribuir um Animal para o Dog. Se tentarmos, conseguiremos ClassCastException.

Para evitar a exceção, devemos instanciar o Dog assim:


Dog dog = new Dog();

então atribua-o a Animal:


Animal anotherDog = dog;

Neste caso, porque estendemos o Animal classe, o Dog a instância nem precisa ser lançada; o Animal o tipo de classe pai simplesmente aceita a atribuição.

Casting com supertipos

É possível declarar um Dog com o supertipo Animalmas se quisermos invocar um método específico de Dog, precisaremos lançá-lo. Por exemplo, e se quiséssemos invocar o bark() método? O Animal supertype não tem como saber exatamente qual instância de animal estamos invocando, então temos que lançar Dog manualmente antes de podermos invocar o bark() método:


Animal dogWithAnimalType = new Dog();
Dog specificDog = (Dog) dogWithAnimalType;
specificDog.bark();

Você também pode usar a conversão sem atribuir o objeto a um tipo de classe. Esta abordagem é útil quando você não deseja declarar outra variável:


System.out.println(((Dog)anotherDog)); // This is another way to cast the object

Aceite o desafio da herança Java!

Você aprendeu alguns conceitos importantes de herança, então agora é hora de experimentar um desafio de herança. Para começar, estude o seguinte código:


public class InheritanceCompositionChallenge {
    private static int result;
    public static void main(String... doYourBest) {
        Character homer = new Homer();
        homer.drink();
        new Character().drink();
        ((Homer)homer).strangleBart();
        Character character = new Character();
        System.out.println(result);
        ((Homer)character).strangleBart();
    }
    static class Character {
        Character() {
            result++;
        }
        void drink() {
            System.out.println("Drink");
        }
    }
    static class Homer extends Character {
        Lung lung = new Lung();
        void strangleBart() {
            System.out.println("Why you little!");
        }
        void drink() {
            System.out.println("Drink beer");
            lung.damageLungs();
        }
    }
    static class Lung {
        void damageLungs() {
            System.out.println("Soon you will need a transplant");
        }
    }
}

Qual destes é o resultado após executar o método principal?

A)