La herencia y la composición son dos técnicas de programación que utilizan los desarrolladores para establecer la relación 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.
Clases y objetos creados a través de la herencia son estrechamente acoplado porque cambiar el padre o la superclase en una relación de herencia corre el riesgo de romper su código. Las clases y objetos creados a través de la composición son débilmente acopladolo que significa que puede cambiar más fácilmente los componentes sin romper el código.
Debido a que el código débilmente acoplado ofrece más flexibilidad, muchos desarrolladores creen 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ías un cuchillo de mantequilla para cortar verduras y, de la misma manera, no debes elegir la composición para cada escenario de programación.
Herencia y composición en Java.
En este artículo, aprenderá las diferencias entre herencia y composición y cómo decidir cuál es la correcta para sus programas Java:
- Cuándo utilizar la herencia en Java
- Cuándo usar la composición en Java
- Diferencias entre herencia y composición
- Método anulado con herencia de Java
- Usando constructores con herencia
- Conversión de tipos y ClassCastException
- Errores comunes con la herencia en Java
- Qué recordar sobre la herencia en Java
Cuándo utilizar la herencia en Java
En la programación orientada a objetos, podemos usar la herencia cuando sabemos que existe una relación «es un» entre una clase secundaria y su clase principal. Algunos ejemplos serían:
- Una persona es un humano.
- Un gato es un animal.
- Un coche es un vehículo.
En cada caso, el niño o subclase es un especializado versión del padre o superclase. Heredar de la superclase es un ejemplo de reutilización de código. Para comprender mejor esta relación, tómate un momento para 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 utilizar la herencia, pregúntese si la subclase es realmente una versión más especializada de la superclase. En este caso, un coche es un tipo de vehículo, por lo que la relación sucesoria tiene sentido.
Cuándo usar la composición en Java
En la programación orientada a objetos, podemos utilizar la composición en los casos en que un objeto «tiene» (o es parte de) otro objeto. Algunos ejemplos serían:
- Un coche tiene un batería (una batería es parte de un coche).
- Una persona tiene un corazón (un corazón es parte de una persona).
- Una casa tiene un 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 una 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 el Bedroom
y LivingRoom
objetos en la composición de un House
.
Diferencias entre herencia y composición
Ahora comparemos herencia y composición en Java. ¿Es el siguiente un buen ejemplo de herencia?
import java.util.HashSet;
public class CharacterBadExampleInheritance extends HashSet
En este caso, la respuesta es no. La clase secundaria hereda muchos métodos que nunca utilizará, lo que da como resultado un código estrechamente acoplado que resulta confuso y difícil de mantener. Si observa detenidamente, también queda claro que este código no pasa la prueba de «es un».
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 CharacterCompositionExample
clase para usar sólo dos de HashSet
los métodos, sin heredarlos todos. Terminamos con un código más simple, menos acoplado y más fácil de entender y mantener.
Método anulado 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, podríamos querer especializar el sonido en un Cat
marcas:
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 nosotros extender el Animal
clase para crear una nueva Cat
clase. Luego, nosotros anular el Animal
clase emitSound()
método para obtener el sonido específico Cat
marcas. Aunque hemos declarado el tipo de clase como Animal
cuando lo instanciamos como Cat
conseguiremos el maullido del gato.
¿Java tiene herencia múltiple?
A diferencia de algunos lenguajes, como C++, Java no permite la herencia múltiple con clases. Sin embargo, puedes 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 la herencia múltiple como la que hago 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 una clase principal
Cuando dos clases están relacionadas mediante herencia, la clase secundaria debe poder acceder a todos los campos, métodos o constructores accesibles de su clase principal. En Java usamos la palabra reservada. super
para garantizar que la clase secundaria aún pueda acceder al método anulado de su padre:
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");
}
}
}
En este ejemplo, Character
es la clase principal de Moe. Usando super
podemos acceder Character
‘s move()
Método para darle una cerveza a Moe.
Usando constructores con herencia
Cuando una clase hereda de otra, el constructor de la superclase se cargará primero, antes de cargar su subclase. En la mayoría de los casos, la palabra reservada super
se agrega automáticamente al constructor. Sin embargo, si la superclase tiene un parámetro en su constructor, debemos invocar deliberadamente la super
constructor, como se muestra a continuación:
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
}
}
Si la clase principal tiene un constructor con al menos un parámetro, entonces debemos declarar el constructor en la subclase y usar super
para invocar explícitamente el constructor padre. El super
La palabra reservada no se agregará automáticamente y el código no se compilará sin ella. Por ejemplo:
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");
}
}
}
Conversión de tipos y ClassCastExceptions
La conversión es una forma de comunicar explícitamente al compilador que realmente tiene la intención de convertir un tipo determinado. Es como decir: «Oye, JVM, sé lo que estoy haciendo, así que transmite esta clase con este tipo». Si una clase que has lanzado no es compatible con el tipo de clase que declaraste, obtendrás una ClassCastException
.
En herencia, podemos asignar la clase secundaria a la clase principal sin realizar la conversión, pero no podemos asignar una clase principal a la clase secundaria sin utilizar la conversión.
Considere el siguiente ejemplo:
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"); } }
Cuando intentamos lanzar un Animal
instancia a un Dog
obtenemos una excepción. Esto se debe a que el Animal
no sabe nada sobre su hijo. Podría ser un gato, un pájaro, un lagarto, etc. No hay información sobre el animal específico.
El problema en este caso es que hemos creado una instancia Animal
como esto:
Animal animal = new Animal();
Luego intenté emitirlo así:
Dog dogAnimal = (Dog) animal;
Porque no tenemos un Dog
Por ejemplo, es imposible asignar un Animal
hacia Dog
. Si lo intentamos, obtendremos un ClassCastException
.
Para evitar la excepción, debemos crear una instancia del Dog
como esto:
Dog dog = new Dog();
luego asignarlo a Animal
:
Animal anotherDog = dog;
En este caso, debido a que hemos ampliado el Animal
clase, la Dog
La instancia ni siquiera necesita ser lanzada; el Animal
El tipo de clase principal simplemente acepta la tarea.
Casting con supertipos
Es posible declarar una Dog
con el supertipo Animal
pero si queremos invocar un método específico desde Dog
, necesitaremos lanzarlo. Como ejemplo, ¿qué pasaría si quisiéramos invocar el bark()
¿método? El Animal
supertipo no tiene manera de saber exactamente qué instancia animal estamos invocando, por lo que tenemos que lanzar Dog
manualmente antes de que podamos invocar el bark()
método:
Animal dogWithAnimalType = new Dog();
Dog specificDog = (Dog) dogWithAnimalType;
specificDog.bark();
También puedes usar 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
¡Acepta el desafío de la herencia de Java!
Has aprendido algunos conceptos importantes sobre herencia, así que ahora es el momento de probar un desafío de herencia. Para comenzar, estudie el siguiente 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");
}
}
}
¿Cuál de estos es el resultado después de ejecutar el método principal?
A)