Tema 3-3
Tema 3-3
Pero surge una inquietud: ¿cómo logramos que diferentes animales emitan sonidos
distintos sin necesidad de crear un método específico para cada uno? Aquí es donde
entra en juego el concepto de herencia.
La herencia es un pilar fundamental de la programación orientada a objetos que permite
a una clase, denominada subclase, heredar atributos y comportamientos de otra clase,
conocida como superclase. Este principio nos posibilita la reutilización de código y la
creación de estructuras jerárquicas en nuestras clases.
Dicho de otra manera, la herencia es un mecanismo que nos permite definir una nueva
clase basándonos en una ya existente. A esta clase existente se le denomina superclase
o clase padre, mientras que la nueva clase se conoce como subclase o clase hija. La
subclase no solo hereda atributos y comportamientos (métodos) de la superclase, sino
que también puede añadir o modificar atributos y comportamientos propios.
Un método práctico y sencillo para identificar si es apropiado aplicar herencia en
programación orientada a objetos es la regla "es un" (en inglés, "is-a"). Esta regla
sugiere que si puedes describir una relación entre dos clases usando la frase "es un",
entonces la herencia puede ser adecuada. Por ejemplo, considera las clases "Perro" y
"Animal". Si podemos afirmar que "un Perro es un Animal", entonces es lógico que
"Perro" herede de "Animal". De la misma manera, al preguntarnos: ¿Una Oveja es un
Animal? La respuesta es sí, por lo que "Oveja" también debería heredar de "Animal". Sin
embargo, cuando nos preguntamos: ¿Un Animal es un Perro? La respuesta es no, ya que
"Animal" es una categoría general y "Perro" es específica. Al analizar el caso del mastín,
vemos que "Un mastín es un Animal" y también "Un mastín es un Perro", lo que indica
que "Mastín" podría ser una subclase de "Perro". Al emplear esta regla, es más fácil
determinar cuándo una subcllasificación debe heredar de una clase superior,
garantizando una estructura de código más clara y lógica.
Para representar la herencia en un diagrama de clases, empleamos una flecha con punta
hueca:
1. Implementación de Herencia
Ahora es el momento de plasmar nuestro ejemplo en código Java. Comenzaremos con la
clase Animal .
// Clase Animal.
public class Animal {
private String nombre;
private String especie;
@Override
public void sonido() {
System.out.println("guau guau");
}
}
En el código anterior, hemos sobrescrito el método sonido() en la clase Perro para
que imprima "guau guau" en lugar del mensaje genérico definido en la clase Animal .
Al usar la anotación @Override , estamos indicando explícitamente que la intención es
sobrescribir un método de la superclase, y el compilador nos advertirá si cometemos
algún error.
Por lo tanto, al emplear la herencia de métodos, no solo se facilita la reutilización de
código, sino que también se brinda la posibilidad de personalizar comportamientos. Es
importante tener en cuenta que al heredar un método, se anula completamente el
comportamiento del método original de la clase padre. Además, el método en la clase
hija debe mantener la misma firma (mismo nombre y atributos) que su contraparte en la
superclase. Por último, es altamente recomendable el uso de la etiqueta @Override al
sobrescribir.
1.3 Herencia de constructores
Por último llega el turno de los constructores. Como ya sabemos, un constructor es
similar a un método pero posee características únicas. Cuando se trata de herencia, el
proceso de manejo de constructores difiere del que hemos visto para los métodos, para
empezar no se utiliza la anotacion @Override . Evidentemente, si hemos incluido
nuevos atributos será necesario crear un constructor que permita inicializar esos
atributos. Además, de alguna forma, debemos poder ejecutar el constructor de la
superclase para inicializar también los atributos heredados. Para ello, se utiliza la
palabra clave super , que es una referencia a la superclase inmediata. super nos
permite llamar al constructor de la superclase. La sintaxis general es usar super
seguido de paréntesis que contienen los argumentos que el constructor de la superclase
espera.
Nota: En un constructor de una subclase, es obligatorio que la llamada a
super() (si se incluye) sea la primera instrucción.
Sigamos completando el ejemplo:
public class Perro extends Animal {
private String raza;
@Override
public void sonido() {
System.out.println("guau guau");
}
}
Como se puede observar en el código anterior, antes de inicializar el atributo raza
específico de la clase Perro , se realiza una llamada al constructor de la clase
Animal a través de super(nombre, especie) , asegurando que los atributos
heredados sean inicializados correctamente.
En muchas ocasiones, al trabajar con herencia, queremos que la subclase asigne un
valor por defecto específico para algunos atributos heredados, en lugar de recibirlo
como parámetro en el constructor. Esta técnica puede ser útil, por ejemplo, para
establecer valores que no cambian para una subclase en particular o para definir valores
predeterminados.
Para hacerlo, simplemente hacemos una llamada al constructor de la superclase usando
super() y, en lugar de pasar un parámetro, proporcionamos el valor predeterminado
directamente.
Por ejemplo, si deseamos que la especie del Perro siempre sea su nombre científico
"Canis lupus", podemos modificar el constructor de la siguiente manera:
public class Perro extends Animal {
private String raza;
@Override
public void sonido() {
System.out.println("guau guau");
}
}
En este código, el constructor de Perro solo acepta dos parámetros: nombre y
raza . Sin embargo, al hacer la llamada super() , asignamos directamente el nombre
científico "Canis lupus" como el valor por defecto para la especie, garantizando que
todos los objetos de la clase Perro tengan ese valor específico para el atributo
especie .
@Override
public void sonido() {
System.out.println("guau guau"); // Sonido específico de
Perro: "guau guau"
}
}
@Override
public void sonido() {
System.out.println("beee"); // Sonido específico de Oveja:
"beee"
}
}
@Override
public void sonido() {
System.out.println(nombre + " dice: guau guau"); //
Accedemos directamente al atributo 'nombre'
}
}
Al cambiar el modificador de visibilidad a
protected , le estamos dando a las
subclases un acceso directo al atributo. Esto puede ser útil si queremos que las
subclases modifiquen o lean directamente el valor del atributo sin necesidad de usar un
método getter o setter. Sin embargo, es importante tener cuidado al hacer esto porque
podría romper el principio de encapsulamiento, que es uno de los pilares de la
programación orientada a objetos. El encapsulamiento nos dice que deberíamos ocultar
los detalles internos de una clase y exponer sólo lo necesario a través de métodos
públicos.
Al hacer un atributo protected , lo estamos exponiendo a todas las subclases, lo que
podría llevar a modificaciones no deseadas o inesperadas. Por lo tanto, antes de cambiar
un modificador de visibilidad, siempre es bueno preguntarse si realmente es necesario y
si no hay otra forma de lograr el mismo objetivo sin romper el encapsulamiento.
El codigo completo quedaría así:
// Clase Animal.
public class Animal {
protected String nombre; // Modificador cambiado a 'protected'
private String especie;
@Override
public void sonido() {
System.out.println(nombre + " dice: guau guau"); //
Accedemos directamente al atributo 'nombre'
}
}
@Override
public void sonido() {
System.out.println(nombre + " dice: beee"); // Accedemos
directamente al atributo 'nombre'
}
}
@Override
public void sonido() {
System.out.println(nombre + " dice: roar"); // Accedemos
directamente al atributo 'nombre'
}
}
@Override
public void sonido() {
System.out.println(nombre + " dice: guau guau");
}
Con este nuevo diagrama, podemos apreciar una estructura jerárquica con tres niveles
de herencia. En el nivel superior, encontramos a la clase Animal , seguida por la clase
Perro en el segundo nivel. Finalmente, en el tercer nivel, están las subclases
específicas: Mastin y Chihuahua . A continuación, presentamos el código de estas
subclases:
// Clase Mastin que hereda de Perro.
public class Mastin extends Perro {
@Override
public void sonido() {
System.out.println(nombre + " dice: rrr-guau!"); // Un
sonido más grave y potente para el Mastín
}
}
@Override
public void sonido() {
System.out.println(nombre + " dice: ¡yip yip!"); // Un
sonido más agudo para el Chihuahua
}
}
En este código tenemos que:
La clase Animal es la superclase que tiene atributos básicos, como el nombre y
la especie .
La clase Perro hereda de Animal y añade un método personalizado llamado
darPatita() . Este método es exclusivo para perros, lo que significa que
cualquier subclase que derive de Perro (como Mastin o Chihuahua ) también
heredará y podrá ejecutar este método.
Las clases Mastin y Chihuahua son subclases de Perro . Ambas heredan
todas las características de la clase Perro (incluido el método darPatita() ).
Sin embargo, cada una de estas subclases redefine el método sonido() para
personalizar el ladrido según las características típicas de cada raza. El Mastín tiene
un ladrido más grave y resonante ( rrr-guau! ), mientras que el Chihuahua tiene
un ladrido más agudo ( ¡yip yip! ).
Por lo tanto, con esta estructura, todos los perros pueden "dar la patita", pero cada raza
tiene su propio ladrido distintivo que se refleja al invocar el método sonido() .
1.7 Uso de instanceof
Para emplear atributos y, especialmente, métodos personalizados de una clase en Java,
se dispone del operador instanceof . Este operador es esencial para determinar si un
objeto es una instancia de una clase específica o si pertenece a una subclase de esta.
Podemos relacionarlo con la lógica de "es un/una" al identificar el tipo de objeto con el
que trabajamos. Veámoslo con un ejemplo.
Supongamos que, en nuestra jerarquía de clases de animales, deseamos identificar qué
animales pueden volar. Podríamos tener clases como Pájaro y Murciélago que
poseen un método volar() . Sin embargo, no todas las instancias de Animal tienen
esta capacidad. Aquí es donde instanceof se vuelve valioso.
if (animal instanceof Pajaro) {
((Pajaro) animal).volar();
}
En este fragmento de código, verificamos si animal es una instancia de la clase
Pajaro . Si es así, hacemos un casting a Pajaro y llamamos al método volar() .
Nótese que ahora, además de los sonidos, los perros (incluidos el Mastín y el
Chihuahua) también muestran la acción de "dar la patita".
1.8 Clases Abstractas
En el ejemplo anterior, nos hemos centrado en crear clases sin considerar un aspecto
crucial. Si queremos representar un animal que no sea un Perro, Oveja o León, ¿es
posible? La respuesta es sí; nuestro código actual no impide hacerlo. Sin embargo, esto
introduce ambigüedades: ¿Qué tipo de animal es? ¿Qué sonido emite? ¿Cómo se
comporta?
Observemos el siguiente código:
Animal animalGenerico = new Animal("Desconocido", "Especie
Desconocida");
Aunque esta línea es sintácticamente correcta, no tiene mucho sentido en el contexto de
nuestro programa. Representa un animal genérico, del cual no sabemos nada más.
También podríamos intentar especificar un animal indicando manualmente su nombre y
especie. Sin embargo, esta aproximación trae un problema: no garantiza que el nombre
de la especie sea correcto. Veamos:
Animal avestruz = new Animal("Gertrudis", "Avistruz"); // Nota el
error en "Avistruz".
Imaginemos una clase abstracta como un molde, o un concepto general, que no tiene
sentido por sí solo pero define una estructura base para sus derivados. Por ejemplo, si
pensamos en el término "animal", sabemos que es un concepto amplio y que no nos
dice nada específico. No podemos definir cómo se ve o se comporta un "animal" sin más
detalles, pero sí podemos decir que todos los animales tienen ciertas características,
como un nombre o la capacidad de emitir un sonido.
En Java, una clase abstracta representa precisamente este concepto generalizado. No
puedes crear una instancia de una clase abstracta porque es demasiado vaga o general.
En su lugar, defines subclases concretas que heredan de la clase abstracta,
proporcionando las especificaciones necesarias. Así, en lugar de tener un genérico
"animal", tendrías clases específicas como Perro , Gato o Pájaro , cada una con
sus propios comportamientos y atributos definidos.
En resumen, las clases abstractas en Java son una forma de garantizar que ciertas
clases sirvan sólo como base para otras, sin que puedan ser instanciadas directamente.
Esto nos permite establecer un esquema claro y estructurado para la herencia,
asegurando que se respeten ciertas reglas y estructuras en el diseño de nuestro código.
Importante: Las clases abstractas nos permiten definir clases que no
pueden ser instanciadas, pero que pueden ser extendidas(heredadas).
Llega el momento de implementar y afianzar el concepto de clase abstracta. Para definir
una clase como abstracta, simplemente se utiliza la palabra clave abstract antes de
la palabra class .
abstract class NombreDeLaClaseAbstracta {
// cuerpo de la clase abstracta
}
Al igual que ocurre con una clase, también se pueden definir métodos Abstractos. Un
método abstracto es un método que se declara en la clase abstracta, pero no tiene una
implementación. Es decir, declaramos el método, pero no especificamos lo que hace
(misma idea que un prototipo en lenguaje C). Las clases concretas que heredan de la
clase abstracta deberán proporcionar una implementación para estos métodos
abstractos.
La sintaxis es la siguiente:
abstract TipoDeRetorno nombreDelMetodo(parametros);
@Override
public void sonido() {
System.out.println(nombre + " dice: guau guau");
}
2. Interfaces
En la programación orientada a objetos con Java, hemos explorado extensamente el
concepto de herencia, donde una clase puede heredar atributos y métodos de otra. Sin
embargo, existen situaciones donde la herencia no es la herramienta más adecuada. Es
aquí donde entran en juego las interfaces.
Las interfaces en Java son fundamentales cuando necesitamos implementar
comportamiento específico en múltiples clases, sin la necesidad de heredar atributos.
Sería similar a disponer de una clase abstracta pero sin atributos; en este caso, es más
conveniente utilizar una interfaz en lugar de la herencia. Las interfaces se centran en
qué debe hacer una clase, más que en cómo debe hacerlo, lo que permite una mayor
flexibilidad y reutilización del código.
Un escenario común en el que las interfaces son particularmente útiles es cuando una
clase necesita incorporar comportamientos de múltiples fuentes. A diferencia de
algunos lenguajes de programación, Java no permite la herencia múltiple (es decir, una
clase no puede heredar directamente de más de una clase). Las interfaces ofrecen una
solución elegante a este límite, permitiendo que una clase implemente múltiples
interfaces y, por lo tanto, combine varios comportamientos de manera efectiva.
Por lo tanto, una interfaz en Java es una especie de contrato que define un conjunto de
métodos abstractos (sin cuerpo). Cuando una clase implementa una interfaz, está
obligada a proporcionar implementaciones concretas de todos los métodos abstractos
declarados en la interfaz.
Esto añade una capa adicional de flexibilidad y capacidad de extensión a las interfaces,
permitiendo que definan un comportamiento más sofisticado y establezcan una jerarquía
entre ellas, similar a la herencia de clases.
Es importante señalar que una interfaz no puede contener atributos de estado (excepto
constantes estáticas y finales) ni implementaciones de métodos (aunque Java 8 y
versiones posteriores introdujeron métodos por defecto y estáticos en interfaces). Las
diferencias clave entre herencia e interfaces son:
1. Naturaleza del Contrato: Mientras que la herencia establece una relación "es un
tipo de", las interfaces definen un contrato de "tiene capacidades de". Esto significa
que las clases que implementan una interfaz pueden tener diferentes formas, pero
todas comparten ciertas capacidades.
2. Herencia Múltiple: Las interfaces permiten que una clase implemente múltiples
interfaces, lo que promueve una mayor flexibilidad y reutilización del código.
3. Métodos y Atributos: Las clases abstractas pueden tener métodos con
implementación y atributos de estado, mientras que las interfaces tradicionalmente
solo tienen métodos abstractos y constantes estáticas finales.
4. Implementación versus Extensión: Al usar interfaces, las clases implementan
métodos definidos, en contraste con las clases heredadas que extienden y pueden
modificar el comportamiento heredado.
Al implementar interfaces, las clases asumen un compromiso más enfocado y flexible,
que les permite adoptar múltiples roles y responsabilidades definidas por sus interfaces
sin las restricciones de la herencia de una sola clase.
Además, una interfaz puede:
Definir métodos abstractos, lo que significa que puede establecer la firma de un
método sin proporcionar una implementación. Cualquier clase que implemente la
interfaz deberá proporcionar su propia implementación de estos métodos.
Definir constantes static , que son variables fijas que no cambiarán con una
instancia de una clase y que pueden ser compartidas entre todas las instancias.
Implementar métodos static , lo cual es una característica introducida en Java 8,
que permite a las interfaces proporcionar implementaciones completas que no
pueden ser sobrescritas por las clases implementadoras.
Heredar de otra interfaz, permitiendo la creación de una jerarquía de interfaces. Una
interfaz puede extender otra, heredando sus métodos, lo que permite una forma de
herencia múltiple que no está disponible con las clases.
Esta flexibilidad hace que las interfaces sean una herramienta poderosa en Java,
proporcionando una forma de abstracción que facilita la separación de la definición de
las operaciones de sus implementaciones concretas, permitiendo así un diseño de
software más limpio y modular.
2.1 Implementación de Interfaces
Antes de sumergirnos en un ejemplo práctico, es importante entender la sintaxis básica
para la creación de una interfaz en Java. Una interfaz se define utilizando la palabra
clave interface , seguida del nombre de la interfaz. Los métodos dentro de una
interfaz son implícitamente abstractos y públicos. La sintaxis general es la siguiente:
public interface NombreInterfaz {
// Métodos abstractos
tipoRetorno nombreMetodo(parametros);
// ...
}
Ahora, volvamos a nuestro ejemplo del Arca de Noé, pero esta vez vamos a abordarlo
utilizando interfaces. Recordemos que las interfaces son ideales para definir
comportamientos comunes entre clases que no están necesariamente relacionadas a
través de la herencia.
Para este ejemplo, definiremos una interfaz ComportamientoAnimal que especificará
ciertos comportamientos que diferentes animales pueden tener.
// Interfaz ComportamientoAnimal.
public interface ComportamientoAnimal {
void sonido();
void mover();
}
En esta interfaz, hemos definido dos métodos abstractos, sonido() y mover() , que
cualquier clase que implemente esta interfaz deberá definir. Al implementar una interfaz,
utilizamos la palabra clave implements . Veamos ahora como implementar la interfaz
ComportamientoAnimal en la clase Perro en código:
@Override
public void sonido() {
System.out.println("Guau Guau");
}
@Override
public void mover() {
System.out.println("Corre rápidamente");
}
}
Al implementar la interfaz ComportamientoAnimal , la clase Perro ahora debe
proveer implementaciones concretas para los métodos sonido() y mover() . Esto
demuestra varios aspectos clave de las interfaces, como la especificación de
comportamiento, la flexibilidad y modularidad, la separación de responsabilidades, y el
contrato de implementación.
Nota: Al igual que en el caso de la herencia, cuando una clase en Java
implementa una interfaz pero no proporciona implementaciones concretas
para todos los métodos abstractos definidos en dicha interfaz, el entorno
de desarrollo integrado (IDE) marcará esto como un error de compilación.
El IDE puede incluso ofrecer asistencia para generar automáticamente los
cuerpos básicos de estos métodos. Es importante recordar que para evitar
errores de compilación, cada método abstracto de la interfaz debe ser
implementado en la clase que la implementa.
Sin embargo, esto provoca que hayamos perdido parte de la información que teníamos
en la clase abstracta Animal , ya que solo tenemos el comportamiento, no las
características de cada animal. Es decir, no podemos acceder a los atributos nombre ni
especie . Si queremos intentar mantenerlos, deberemos hacer un apaño creando
funcionalidades que obliguen a retornar estos valores. Como por ejemplo creando la
siguiente interfaz:
// Interfaz Animal.
public interface Animal {
String getEspecie();
String getNombre();
}
Ahora veamos cómo se implementa esta interfaz junto con ComportamientoAnimal
en la clase Perro :
public class Perro implements Animal, ComportamientoAnimal {
private String nombre;
private String especie;
private String raza;
public Perro(String nombre, String especie, String raza) {
this.nombre = nombre;
this.especie = especie;
this.raza = raza;
}
@Override
public void sonido() {
System.out.println("Guau Guau");
}
@Override
public String getEspecie() {
return especie;
}
@Override
public String getNombre() {
return nombre;
}
@Override
public void mover() {
System.out.println("Corre rápidamente");
}
@Override
public int compareTo(MiClase otro) {
return Integer.compare(this.algunValor, otro.algunValor);
}
}
Supongamos ahora que tenemos una clase Estudiante con atributos como nombre
y puntuacion . Queremos ordenar los estudiantes por su puntuación.
public class Estudiante implements Comparable<Estudiante> {
private String nombre;
private int puntuacion;
Collections.sort(estudiantes);
La llamada a
Collections.sort ordenará la lista de estudiantes en base a su
puntuación.
Mientras que Comparable define un orden natural para los objetos, a veces
necesitamos una ordenación diferente para el mismo tipo de objetos. En estos casos,
podemos usar la interface Comparator , que veremos a continuación, que permite
definir un orden específico sin alterar la implementación de Comparable en la clase.
Continuando con nuestra exploración de interfaces en Java, pasamos ahora a otra
herramienta fundamental en el ámbito de la comparación de objetos: la interface
Comparator .
Comparator es una interfaz funcional que define un método compare , que compara
dos objetos para determinar su orden. Este método devuelve:
Un valor negativo si el primer objeto es menor que el segundo.
Cero si ambos objetos son iguales.
Un valor positivo si el primer objeto es mayor que el segundo.
La sistansis de implementación de la interzar Comparator sería la siguiente:
import java.util.Comparator;
estudiantes.sort(new ComparadorNombreEstudiante());
La principal ventaja de Comparator sobre Comparable es su flexibilidad. Podemos
definir múltiples estrategias de comparación para una misma clase sin modificar la clase
en sí. Esto es especialmente útil en bibliotecas y aplicaciones donde no tenemos control
sobre las clases de los objetos que necesitamos ordenar.
2.4. Combinando Comparable y Comparator
Java ofrece la posibilidad de combinar la interfaz Comparable , que establece un orden
natural para los objetos de una clase, con la interfaz Comparator , que permite definir
estrategias de comparación personalizadas. Este enfoque híbrido es especialmente
poderoso cuando queremos ordenar objetos basándonos en múltiples atributos.
Imagina que queremos ordenar una lista de estudiantes primero por su puntuación y, en
caso de empate, por su nombre. Podemos hacerlo implementando Comparable en
nuestra clase Estudiante y usando Comparator dentro del método compareTo
para definir una lógica de comparación compuesta. Empecemos definiendo los dos
comparadores:
import java.util.Comparator;
@Override
public int compareTo(Estudiante otroEstudiante) {
Comparator<Estudiante> comparador = new
ComparadorPuntuacion().thenComparing(new ComparadorNombre());
return comparador.compare(this, otroEstudiante);
}
}
El métodothenComparing es un mecanismo refinado que permite a
Comparator
encadenar múltiples criterios de comparación. Actuando cuando dos objetos son
considerados iguales por el comparador principal, thenComparing procede a utilizar
un comparador secundario para establecer un orden definitivo. Este encadenamiento
puede ampliarse a más comparadores mediante métodos adicionales que ofrecen
Comparator , como reversed() , thenComparingInt , thenComparingLong ,
thenComparingDouble , comparing , y comparing con un comparador clave,
cada uno adaptado para distintas necesidades y escenarios de comparación.
Estos métodos no solo permiten una ordenación robusta y eficiente sino que también
contribuyen a un código más limpio y expresivo. Su habilidad para combinar múltiples
comparadores facilita el diseño de clases y algoritmos de ordenación que son claros,
mantenibles y adaptados a la lógica de negocio.
Con la implementación de compareTo en la clase Estudiante , la ordenación de una
lista de estudiantes utilizando Collections.sort resultará en una lista ordenada
primero por puntuación y, en caso de empates, por nombre.
ArrayList<Estudiante> estudiantes = new ArrayList<>();
// Agregar estudiantes a la lista
Collections.sort(estudiantes);
Combinar Comparable con Comparator de esta manera proporciona una gran
flexibilidad, permitiéndonos reutilizar comparaciones definidas y construir lógicas de
ordenación complejas sin modificar la clase subyacente. Esto es útil para mantener el
código limpio y coherente, a la vez que se ofrecen poderosas capacidades de
ordenación.
2.5. La Interfaz Iterable
La interface Iterable en Java es una parte fundamental del framework de
colecciones, ya que permite recorrer una colección de objetos de manera sencilla y
concisa. Esta interfaz es lo que hace posible el uso del bucle for-each en Java, que
puede iterar sobre cualquier objeto que implemente Iterable .
La interfaz Iterable garantiza que una clase tiene el método iterator() , que
devuelve un Iterator . El Iterator es otro objeto que proporciona métodos para
navegar a través de una colección de objetos y para eliminar elementos de dicha
colección durante la iteración.
Para que una clase sea Iterable , debe implementar el método iterator() :
public class MiColeccion<T> implements Iterable<T> {
private List<T> lista = new ArrayList<>();
@Override
public Iterator<T> iterator() {
return lista.iterator();
}
}
En este ejemplo, MiColeccion es una clase genérica que almacena sus elementos en
List<T> . Al implementar Iterable , permite a los usuarios de la clase utilizar el
bucle for-each para recorrer sus elementos.
Con Iterable , recorrer MiColeccion se convierte en una tarea sencilla:
MiColeccion<Estudiante> estudiantes = new MiColeccion<>();
// Agregar estudiantes a la colección
for (Estudiante est : estudiantes) {
// Procesar cada estudiante
}
Este bucle for-each es posible porque
MiColeccion implementa Iterable , y por
cada elemento de la colección, el bucle for-each recupera su Iterator y lo utiliza
para iterar a través de los elementos.
Además del método iterator() , Iterable incluye métodos por defecto como
forEach y spliterator :
Para habilitar la clonación de objetos en una clase, esta debe implementar la interfaz
Cloneable y sobrescribir el método clone() . Por ejemplo, una clase MiClase que
implementa Cloneable podría sobrescribir clone() de la siguiente manera:
public class MiClase implements Cloneable {
private int dato;