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 super
palabra 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 Car
clase, 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 Bedroom
y LivingRoom
en 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 CharacterCompositionExample
clase use solo dos de HashSet
los 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, IndexOutOfBoundsException
es 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 Cat
hace 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 Animal
clase para crear una nueva Cat
clase. A continuación, anulamos el método de la Animal
clase emitSound()
para obtener el sonido específico que Cat
produce. Aunque hemos declarado el tipo de clase como Animal
, cuando lo instanciamos como Cat
obtendremos 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 Dog
con 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 Animal
supertipo no tiene forma de saber exactamente qué instancia de animal estamos invocando, por lo que tenemos que lanzar Dog
manualmente 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: