Herencia versus composición: cómo elegir

La herencia y la composición son dos técnicas de programación que los desarrolladores utilizan para establecer relaciones entre clases y objetos. Mientras que la herencia deriva una clase de otra, la composición define una clase como la suma de sus partes.

Las clases y los objetos creados a través de la herencia están estrechamente unidos porque cambiar el padre o la superclase en una relación de herencia corre el riesgo de romper su código. Las clases y los objetos creados a través de la composición están débilmente acoplados , lo que significa que puede cambiar más fácilmente los componentes sin romper su código.

Debido a que el código poco acoplado ofrece más flexibilidad, muchos desarrolladores han aprendido que la composición es una técnica mejor que la herencia, pero la verdad es más compleja. Elegir una herramienta de programación es similar a elegir la herramienta de cocina correcta: no usaría un cuchillo de mantequilla para cortar verduras y, de la misma manera, no debe elegir la composición para cada escenario de programación. 

En este Java Challenger aprenderá la diferencia entre herencia y composición y cómo decidir cuál es la correcta para su programa. A continuación, le presentaré varios aspectos importantes pero desafiantes de la herencia de Java: anulación de métodos, la superpalabra clave y conversión de tipos. Finalmente, probará lo que ha aprendido trabajando con un ejemplo de herencia línea por línea para determinar cuál debería ser el resultado.

Cuándo usar la herencia en Java

En la programación orientada a objetos, podemos usar la herencia cuando sabemos que existe una relación "es una" entre un hijo y su clase padre. Algunos ejemplos serían:

  • Una persona es un ser humano.
  • Un gato es un animal.
  • Un coche es un   vehículo.

En cada caso, el hijo o la subclase es una versión especializada del padre o la superclase. Heredar de la superclase es un ejemplo de reutilización de código. Para comprender mejor esta relación, dedique un momento a estudiar la Carclase, que hereda 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(); } } 

Cuando esté considerando usar la herencia, pregúntese si la subclase realmente es una versión más especializada de la superclase. En este caso, un automóvil es un tipo de vehículo, por lo que la relación de herencia tiene sentido. 

Cuando usar la composición en Java

En la programación orientada a objetos, podemos usar la composición en los casos en que un objeto "tiene" (o es parte de) otro objeto. Algunos ejemplos serían:

  • Un automóvil tiene una batería (una batería es parte de un automóvil).
  • Una persona tiene un corazón (un corazón es parte de una persona).
  • Una casa tiene una sala de estar (una sala de estar es parte de una casa).

Para comprender mejor este tipo de relación, considere la composición de un 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 { } } 

En este caso, sabemos que una casa tiene una sala de estar y un dormitorio, por lo que podemos usar los objetos BedroomLivingRoomen la composición de a House

Obtener el código

Obtenga el código fuente para ver ejemplos en este Java Challenger. Puede ejecutar sus propias pruebas siguiendo los ejemplos.

Herencia vs composición: dos ejemplos

Considere el siguiente código. ¿Es este un buen ejemplo de herencia?

 import java.util.HashSet; public class CharacterBadExampleInheritance extends HashSet { public static void main(String... badExampleOfInheritance) { BadExampleInheritance badExampleInheritance = new BadExampleInheritance(); badExampleInheritance.add("Homer"); badExampleInheritance.forEach(System.out::println); } 

En este caso, la respuesta es no. La clase hija hereda muchos métodos que nunca usará, lo que da como resultado un código estrechamente acoplado que es confuso y difícil de mantener. Si observa detenidamente, también está claro que este código no pasa la prueba "es una".

Ahora probemos el mismo ejemplo usando composición:

 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); } 

El uso de la composición para este escenario permite que la  CharacterCompositionExampleclase use solo dos de HashSetlos métodos de ', sin heredarlos todos. Esto da como resultado un código más simple y menos acoplado que será más fácil de entender y mantener.

Ejemplos de herencia en el JDK

El Java Development Kit está lleno de buenos ejemplos de herencia:

 class IndexOutOfBoundsException extends RuntimeException {...} class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {...} class FileWriter extends OutputStreamWriter {...} class OutputStreamWriter extends Writer {...} interface Stream extends BaseStream
    
      {...} 
    

Observe que en cada uno de estos ejemplos, la clase secundaria es una versión especializada de su padre; por ejemplo, IndexOutOfBoundsExceptiones un tipo de RuntimeException.

Anulación de método con herencia de Java

La herencia nos permite reutilizar los métodos y otros atributos de una clase en una nueva clase, lo cual es muy conveniente. Pero para que la herencia realmente funcione, también necesitamos poder cambiar parte del comportamiento heredado dentro de nuestra nueva subclase. Por ejemplo, es posible que queramos especializar el sonido que Cathace a:

 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 es un ejemplo de herencia de Java con anulación de métodos. Primero, ampliamos la Animalclase para crear una nueva Catclase. A continuación, anulamos el método de la Animalclase emitSound()para obtener el sonido específico que Catproduce. Aunque hemos declarado el tipo de clase como Animal, cuando lo instanciamos como Catobtendremos el maullido del gato. 

La anulación del método es polimorfismo

Es posible que recuerde de mi última publicación que la anulación de método es un ejemplo de polimorfismo o invocación de método virtual.

¿Java tiene herencia múltiple?

A diferencia de algunos lenguajes, como C ++, Java no permite la herencia múltiple con clases. Sin embargo, puede usar herencia múltiple con interfaces. La diferencia entre una clase y una interfaz, en este caso, es que las interfaces no mantienen el estado.

Si intenta una herencia múltiple como la que tengo a continuación, el código no se compilará:

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

Una solución usando clases sería heredar una por una:

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

Otra solución es reemplazar las clases con interfaces:

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

Usando 'super' para acceder a los métodos de las clases principales

When two classes are related through inheritance, the child class must be able to access every accessible field, method, or constructor of its parent class. In Java, we use the reserved word super to ensure the child class can still access its parent's overridden method:

 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"); } } } 

In this example, Character is the parent class for Moe.  Using super, we are able to access Character's  move() method in order to give Moe a beer.

Using constructors with inheritance

When one class inherits from another, the superclass's constructor always will be loaded first, before loading its subclass. In most cases, the reserved word super will be added automatically to the constructor.  However, if the superclass has a parameter in its constructor, we will have to deliberately invoke the super constructor, as shown below:

 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 } } 

If the parent class has a constructor with at least one parameter, then we must declare the constructor in the subclass and use super to explicitly invoke the parent constructor. The super reserved word won't be added automatically and the code won't compile without it.  For example:

 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"); } } } 

Type casting and the ClassCastException

Casting is a way of explicitly communicating to the compiler that you really do intend to convert a given type.  It's like saying, "Hey, JVM, I know what I'm doing so please cast this class with this type." If a class you've cast isn't compatible with the class type you declared, you will get a ClassCastException.

In inheritance, we can assign the child class to the parent class without casting but we can't assign a parent class to the child class without using casting.

Consider the following example:

 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"); } } 

When we try to cast an Animal instance to a Dog we get an exception. This is because the Animal doesn't know anything about its child. It could be a cat, a bird, a lizard, etc. There is no information about the specific animal. 

The problem in this case is that we've instantiated Animal like this:

 Animal animal = new Animal(); 

Then tried to cast it like this:

 Dog dogAnimal = (Dog) animal; 

Because we don't have a Dog instance, it's impossible to assign an Animal to the Dog.  If we try, we will get a ClassCastException

In order to avoid the exception, we should instantiate the Dog like this:

 Dog dog = new Dog(); 

then assign it to Animal:

 Animal anotherDog = dog; 

In this case, because  we've extended the Animal class, the Dog instance doesn't even need to be cast; the Animal parent class type simply accepts the assignment.

Casting with supertypes

Es posible declarar a Dogcon el supertipo Animal, pero si queremos invocar un método específico de Dog, necesitaremos convertirlo. Como ejemplo, ¿y si quisiéramos invocar el bark()método? El Animalsupertipo no tiene forma de saber exactamente qué instancia de animal estamos invocando, por lo que tenemos que lanzar Dogmanualmente antes de poder invocar el bark()método:

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

También puede utilizar la conversión sin asignar el objeto a un tipo de clase. Este enfoque es útil cuando no desea declarar otra variable:

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

¡Acepte el desafío de la herencia de Java!

Ha aprendido algunos conceptos importantes de herencia, por lo que ahora es el momento de probar un desafío de herencia. Para comenzar, estudie el siguiente código: