10 Ejercicios resueltos de Interfaces y Clases Abstractas en Java
Domina la programación orientada a objetos con estos 10 ejercicios resueltos de interfaces y clases abstractas en Java. Aprenderás a estructurar tu código paso a paso mediante ejemplos prácticos y cotidianos, diseñados para afianzar tus conocimientos intermedios de forma clara y directa.
Aprendizaje teórico
¿Qué es una Clase Abstracta?
Una clase abstracta es una plantilla general que no puede instanciarse directamente. Sirve para agrupar características y comportamientos comunes (métodos normales) junto con reglas que las clases hijas deben cumplir obligatoriamente (métodos abstractos). Es ideal cuando tus objetos comparten un «qué son»; por ejemplo, un Animal que tiene un nombre, pero donde cada especie se mueve de forma distinta.
¿Qué es una Interfaz?
Una interfaz es como un contrato 100% abstracto. Define «qué puede hacer» un objeto, sin importar «qué es». Por ejemplo, la interfaz Volador obliga a implementar el método volar(), y puede ser usada tanto por un Pájaro como por un Avión. A diferencia de las clases, en Java puedes implementar múltiples interfaces a la vez en un solo objeto.
¿Cuándo usar cada una?
Usa clases abstractas cuando exista una jerarquía clara («es un») y código común que desees reutilizar. Usa interfaces cuando necesites garantizar que diferentes objetos compartan una misma habilidad («puede hacer»), sin importar si tienen relación entre sí.
Ejercicio 1: Creando tu primera clase abstracta
Crea una clase abstracta llamada Instrumento con un método abstracto tocar(). Luego, crea una clase hija Guitarra que herede de Instrumento e implemente el método para imprimir «La guitarra está sonando». En la clase principal, instancia la guitarra y hazla sonar.
Pista
Recuerda usar la palabra clave abstract tanto en la clase padre como en la firma de su método. Las clases abstractas no llevan llaves {} en sus métodos abstractos.
abstract class Instrumento {
// Método abstracto, sin cuerpo
abstract void tocar();
}
class Guitarra extends Instrumento {
@Override
void tocar() {
System.out.println("La guitarra está sonando: ¡Strum strum!");
}
}
public class Main {
public static void main(String[] args) {
Instrumento miGuitarra = new Guitarra();
miGuitarra.tocar();
}
}
terminal Resultado esperado:
La guitarra está sonando: ¡Strum strum!
Ejercicio 2: Implementando una interfaz sencilla
Crea una interfaz llamada Nadador que defina el método nadar(). Crea una clase Pez que implemente esta interfaz y muestre un mensaje indicando que el pez está nadando. Pruébalo en la función principal.
Pista
Las interfaces utilizan la palabra interface para definirse y las clases usan implements para adoptar su comportamiento.
interface Nadador {
void nadar();
}
class Pez implements Nadador {
@Override
public void nadar() {
System.out.println("El pez nada rápidamente bajo el agua.");
}
}
public class Main {
public static void main(String[] args) {
Nadador nemo = new Pez();
nemo.nadar();
}
}
terminal Resultado esperado:
El pez nada rápidamente bajo el agua.
Ejercicio 3: Mezclando métodos normales y abstractos
Crea una clase abstracta CuentaBancaria con un atributo saldo, un constructor para inicializarlo, un método normal mostrarSaldo() y un método abstracto retirar(double monto). Crea una clase CuentaAhorro que implemente el retiro, asegurándose de no permitir retiros si el saldo queda en negativo.
Pista
Usa super en el constructor de la clase hija para pasarle el saldo inicial a la clase abstracta padre.
abstract class CuentaBancaria {
protected double saldo;
public CuentaBancaria(double saldoInicial) {
this.saldo = saldoInicial;
}
public void mostrarSaldo() {
System.out.println("Saldo actual: $" + saldo);
}
abstract void retirar(double monto);
}
class CuentaAhorro extends CuentaBancaria {
public CuentaAhorro(double saldoInicial) {
super(saldoInicial);
}
@Override
void retirar(double monto) {
if (saldo >= monto) {
saldo -= monto;
System.out.println("Retiro exitoso de $" + monto);
} else {
System.out.println("Fondos insuficientes para retirar $" + monto);
}
}
}
public class Main {
public static void main(String[] args) {
CuentaBancaria miCuenta = new CuentaAhorro(100.0);
miCuenta.mostrarSaldo();
miCuenta.retirar(40.0);
miCuenta.mostrarSaldo();
miCuenta.retirar(80.0);
}
}
terminal Resultado esperado:
Saldo actual: $100.0 Retiro exitoso de $40.0 Saldo actual: $60.0 Fondos insuficientes para retirar $80.0
Ejercicio 4: Implementación de múltiples interfaces
En Java no hay herencia múltiple de clases, pero sí de interfaces. Crea dos interfaces: Lector (con método leer()) y Escritor (con método escribir()). Crea una clase ArchivoTexto que implemente ambas.
Pista
Para implementar más de una interfaz, separa sus nombres con una coma después de la palabra reservada implements.
interface Lector {
void leer();
}
interface Escritor {
void escribir(String contenido);
}
class ArchivoTexto implements Lector, Escritor {
private String textoGuardado = "";
@Override
public void escribir(String contenido) {
this.textoGuardado = contenido;
System.out.println("Guardando texto...");
}
@Override
public void leer() {
System.out.println("Leyendo archivo: " + textoGuardado);
}
}
public class Main {
public static void main(String[] args) {
ArchivoTexto documento = new ArchivoTexto();
documento.escribir("Hola, mundo interfaces!");
documento.leer();
}
}
terminal Resultado esperado:
Guardando texto... Leyendo archivo: Hola, mundo interfaces!
Ejercicio 5: Interfaces con métodos por defecto (Default methods)
Desde Java 8, las interfaces pueden tener métodos implementados usando default. Crea una interfaz Cargable con un método abstracto cargar() y un método por defecto mostrarNivelBateria(). Crea una clase Smartphone que implemente la interfaz pero solo sobrescriba cargar().
Pista
El método default permite que las clases que implementan la interfaz lo usen sin necesidad de reescribirlo.
interface Cargable {
void cargar();
// Método por defecto con implementación real
default void mostrarNivelBateria() {
System.out.println("Nivel de batería: Consultando estado...");
}
}
class Smartphone implements Cargable {
@Override
public void cargar() {
System.out.println("El smartphone está conectado y cargando.");
}
}
public class Main {
public static void main(String[] args) {
Smartphone miTelefono = new Smartphone();
miTelefono.mostrarNivelBateria(); // Usa el método default
miTelefono.cargar(); // Usa el método implementado
}
}
terminal Resultado esperado:
Nivel de batería: Consultando estado... El smartphone está conectado y cargando.
Ejercicio 6: Combinando clases abstractas e interfaces
Crea una clase abstracta Vehiculo con el atributo marca y un constructor. Crea una interfaz Volador con el método despegar(). Luego, crea una clase Avion que herede de Vehiculo e implemente Volador.
Pista
El orden de las palabras clave en la clase hija siempre debe ser primero extends (para la clase abstracta) y luego implements (para la interfaz).
abstract class Vehiculo {
protected String marca;
public Vehiculo(String marca) {
this.marca = marca;
}
public void mostrarMarca() {
System.out.println("Marca del vehículo: " + marca);
}
}
interface Volador {
void despegar();
}
class Avion extends Vehiculo implements Volador {
public Avion(String marca) {
super(marca);
}
@Override
public void despegar() {
System.out.println("El avión de marca " + marca + " está despegando.");
}
}
public class Main {
public static void main(String[] args) {
Avion miAvion = new Avion("Boeing");
miAvion.mostrarMarca();
miAvion.despegar();
}
}
terminal Resultado esperado:
Marca del vehículo: Boeing El avión de marca Boeing está despegando.
Ejercicio 7: Polimorfismo con clases abstractas en arreglos
Crea una clase abstracta Trabajador con un método abstracto calcularPagoMensual(). Implementa dos clases: Programador (gana 3000) y Disenador (gana 2500). Crea un arreglo polimórfico de trabajadores e imprime el pago de cada uno recorriéndolo en un bucle.
Pista
Define un arreglo del tipo de la clase padre abstracta (Trabajador[]). Esto te permitirá almacenar objetos de cualquiera de sus clases hijas.
abstract class Trabajador {
protected String nombre;
public Trabajador(String nombre) {
this.nombre = nombre;
}
abstract double calcularPagoMensual();
}
class Programador extends Trabajador {
public Programador(String nombre) {
super(nombre);
}
@Override
double calcularPagoMensual() {
return 3000.0;
}
}
class Disenador extends Trabajador {
public Disenador(String nombre) {
super(nombre);
}
@Override
double calcularPagoMensual() {
return 2500.0;
}
}
public class Main {
public static void main(String[] args) {
Trabajador[] equipo = {
new Programador("Carlos"),
new Disenador("Laura"),
new Programador("Javier")
};
for (Trabajador t : equipo) {
System.out.println(t.nombre + " cobra: $" + t.calcularPagoMensual());
}
}
}
terminal Resultado esperado:
Carlos cobra: $3000.0 Laura cobra: $2500.0 Javier cobra: $3000.0
Ejercicio 8: Polimorfismo usando únicamente interfaces
Crea una interfaz Notificable con el método enviarMensaje(String texto). Implementa dos clases: Email y SMS. Crea una lista o arreglo de tipo Notificable y envía un mensaje de alerta masivo simulando que usas diferentes canales.
Pista
A diferencia del ejercicio anterior, aquí el arreglo será del tipo de la interfaz. Esto demuestra que las interfaces también sirven como tipos de datos polimórficos.
interface Notificable {
void enviarMensaje(String texto);
}
class Email implements Notificable {
@Override
public void enviarMensaje(String texto) {
System.out.println("Enviando correo: " + texto);
}
}
class SMS implements Notificable {
@Override
public void enviarMensaje(String texto) {
System.out.println("Enviando mensaje de texto (SMS): " + texto);
}
}
public class Main {
public static void main(String[] args) {
Notificable[] canales = { new Email(), new SMS() };
String alerta = "¡El servidor ha sido actualizado!";
for (Notificable canal : canales) {
canal.enviarMensaje(alerta);
}
}
}
terminal Resultado esperado:
Enviando correo: ¡El servidor ha sido actualizado! Enviando mensaje de texto (SMS): ¡El servidor ha sido actualizado!
Ejercicio 9: Constantes en las interfaces
Toda variable definida en una interfaz es implícitamente public static final (una constante). Crea una interfaz OperacionMatematica que defina una constante PI = 3.1416 y un método abstracto calcularArea(double radio). Impleméntala en una clase Circulo.
Pista
No necesitas declarar que la variable es final, Java lo hace por ti al estar dentro de la interfaz. Simplemente úsala en el cálculo del área.
interface OperacionMatematica {
// Es equivalente a: public static final double PI = 3.1416;
double PI = 3.1416;
double calcularArea(double radio);
}
class Circulo implements OperacionMatematica {
@Override
public double calcularArea(double radio) {
// Usamos la constante PI de la interfaz
return PI * (radio * radio);
}
}
public class Main {
public static void main(String[] args) {
OperacionMatematica miCirculo = new Circulo();
double area = miCirculo.calcularArea(5.0);
System.out.println("El área del círculo es: " + area);
}
}
terminal Resultado esperado:
El área del círculo es: 78.54
Ejercicio 10: Integración completa de jerarquías y contratos
Crea un sistema pequeño: una clase abstracta Producto (con nombre y precio), y dos interfaces Descontable (método aplicarDescuento(double porcentaje)) e Impuestable (método calcularImpuesto()). Crea la clase ArticuloFisico que herede de la clase y adopte ambas interfaces para calcular un precio final.
Pista
Combina todo. La clase concreta deberá manejar sus datos mediante la herencia del padre y establecer el comportamiento matemático gracias a las reglas de las interfaces.
abstract class Producto {
protected String nombre;
protected double precioBase;
public Producto(String nombre, double precioBase) {
this.nombre = nombre;
this.precioBase = precioBase;
}
public void mostrarInfo() {
System.out.println("Producto: " + nombre + " | Base: $" + precioBase);
}
}
interface Descontable {
double aplicarDescuento(double porcentaje);
}
interface Impuestable {
double calcularImpuesto();
}
class ArticuloFisico extends Producto implements Descontable, Impuestable {
public ArticuloFisico(String nombre, double precioBase) {
super(nombre, precioBase);
}
@Override
public double aplicarDescuento(double porcentaje) {
return precioBase - (precioBase * (porcentaje / 100));
}
@Override
public double calcularImpuesto() {
// Impuesto fijo del 15%
return precioBase * 0.15;
}
public void imprimirPrecioFinal(double porcentajeDescuento) {
double conDescuento = aplicarDescuento(porcentajeDescuento);
double conImpuesto = calcularImpuesto();
double total = conDescuento + conImpuesto;
System.out.println("Total a pagar (con dto e impuesto): $" + total);
}
}
public class Main {
public static void main(String[] args) {
ArticuloFisico monitor = new ArticuloFisico("Monitor 24 pulgadas", 200.0);
monitor.mostrarInfo();
System.out.println("Impuesto aplicado: $" + monitor.calcularImpuesto());
monitor.imprimirPrecioFinal(10.0); // 10% de descuento
}
}
terminal Resultado esperado:
Producto: Monitor 24 pulgadas | Base: $200.0 Impuesto aplicado: $30.0 Total a pagar (con dto e impuesto): $210.0
Preguntas frecuentes
¿Puedo instanciar una clase abstracta o una interfaz?
No. Ninguna de las dos se puede instanciar directamente usando new. Ambas sirven como plantillas. Para utilizarlas, debes crear clases «hijas» concretas que hereden de ellas o las implementen, definiendo así su comportamiento exacto.
¿Cuántas clases abstractas e interfaces puede heredar una clase?
En Java, una clase solo puede heredar de una única clase padre (sea abstracta o normal). Sin embargo, puede implementar múltiples interfaces al mismo tiempo, separándolas por comas. Esto brinda una gran flexibilidad al diseñar tus sistemas.
¿Qué son los métodos «default» en las interfaces?
Desde Java 8, las interfaces pueden incluir métodos con la palabra reservada default. Estos métodos tienen un bloque de código con un comportamiento predeterminado, permitiendo agregar nuevas funciones a la interfaz sin romper las clases que ya la utilizaban.
Conclusión
Las interfaces y clases abstractas son pilares fundamentales para crear arquitecturas de software robustas en Java. Ahora sabes cómo estructurar tus proyectos forzando reglas de comportamiento mediante abstracciones, y cómo combinar múltiples habilidades en un solo objeto gracias a las interfaces. Dominar estas herramientas te permitirá escribir código mucho más flexible, reutilizable y fácil de mantener en equipos de trabajo. Sigue practicando estos conceptos diseñando tus propios sistemas cotidianos; la experimentación es el mejor camino para afianzar lo aprendido de manera permanente.
Sigue aprendiendo
Documentación oficial de Oracle donde se detalla cómo funcionan las clases y métodos abstractos.
Guía completa sobre el uso de interfaces, su implementación y beneficios dentro de la jerarquía en Java.
Herramientas recomendadas para practicar
Java es un lenguaje de programación orientado a objetos, tipado y multiplataforma, respaldado por la JVM. Su principio de «escribe una vez, ejecuta en cualquier lugar» lo hace útil para sistemas empresariales, microservicios en la nube, escritorio y Android.
Sigue practicando
Otros ejercicios prácticos recomendados
10 Ejercicios resueltos de Genéricos y Colecciones en Java
Aprende a dominar los genéricos y las colecciones en Java con esta guía práctica. Descubre cómo crear código más seguro, flexible y reutilizable a través de 10 ejercicios resueltos paso a paso, ideales para afianzar tus...
10 Ejercicios resueltos de Archivos y Directorios en Java
Aprende a gestionar archivos y directorios en Java con estos 10 ejercicios resueltos. Descubre cómo crear, leer, escribir y organizar carpetas paso a paso. Práctica ideal para afianzar tus conocimientos intermedios y apl...
10 Ejercicios resueltos de Manejo de Excepciones en Java
Descubre cómo crear programas a prueba de fallos con estos 10 ejercicios resueltos de manejo de excepciones en Java. Aprenderás a usar try-catch, prever errores comunes y construir aplicaciones estables y profesionales....