6. Programación Orientada a Objetos
Programación Orientada a Objetos
Section titled “Programación Orientada a Objetos”La Programación Orientada a Objetos (POO) es un paradigma de programación que utiliza “objetos” para modelar entidades del mundo real. Java es un lenguaje fundamentalmente orientado a objetos, diseñado desde sus inicios con este paradigma en mente. La POO se basa en cuatro pilares principales: encapsulamiento, herencia, polimorfismo y abstracción.
4.1 Clases y objetos
Section titled “4.1 Clases y objetos”En Java, las clases son plantillas o “planos” que definen la estructura y comportamiento de los objetos. Los objetos son instancias de estas clases, representaciones concretas que ocupan espacio en memoria.
Definición de una clase
Section titled “Definición de una clase”Una clase en Java se define utilizando la palabra clave class, seguida del nombre de la clase y un bloque de código delimitado por llaves {} que contiene sus miembros (atributos y métodos).
public class Persona { // Atributos (variables de instancia) String nombre; int edad; double altura;
// Constructor public Persona(String nombre, int edad, double altura) { this.nombre = nombre; this.edad = edad; this.altura = altura; }
// Métodos public void saludar() { System.out.println("Hola, mi nombre es " + nombre); }
public void cumplirAnios() { edad++; System.out.println(nombre + " ahora tiene " + edad + " años."); }}Creación de objetos
Section titled “Creación de objetos”Para crear un objeto (instancia) de una clase, se utiliza el operador new seguido del constructor de la clase:
// Creación de objetos PersonaPersona persona1 = new Persona("Ana", 25, 1.65);Persona persona2 = new Persona("Carlos", 30, 1.78);
// Uso de los objetospersona1.saludar(); // Imprime: Hola, mi nombre es Anapersona2.saludar(); // Imprime: Hola, mi nombre es Carlos
persona1.cumplirAnios(); // Imprime: Ana ahora tiene 26 años.Constructores
Section titled “Constructores”Los constructores son métodos especiales que se llaman automáticamente cuando se crea un objeto. Su propósito principal es inicializar los atributos del objeto.
public class Estudiante { String nombre; int edad; String carrera;
// Constructor por defecto (sin parámetros) public Estudiante() { nombre = "Sin nombre"; edad = 18; carrera = "No especificada"; }
// Constructor con parámetros public Estudiante(String nombre, int edad, String carrera) { this.nombre = nombre; this.edad = edad; this.carrera = carrera; }
// Constructor con algunos parámetros (sobrecarga de constructores) public Estudiante(String nombre, int edad) { this.nombre = nombre; this.edad = edad; this.carrera = "No especificada"; }}La palabra clave this
Section titled “La palabra clave this”La palabra clave this se refiere al objeto actual y se utiliza principalmente para:
- Diferenciar entre variables de instancia y parámetros con el mismo nombre
- Llamar a otros constructores de la misma clase
- Pasar el objeto actual como argumento a otro método
public class Rectangulo { double ancho; double alto;
// Uso de this para diferenciar variables public Rectangulo(double ancho, double alto) { this.ancho = ancho; // this.ancho se refiere al atributo, ancho al parámetro this.alto = alto; }
// Uso de this para llamar a otro constructor public Rectangulo() { this(1.0, 1.0); // Llama al constructor con parámetros }
// Uso de this para pasar el objeto actual public void compararCon(Rectangulo otro) { Comparador.comparar(this, otro); }}Ciclo de vida de los objetos
Section titled “Ciclo de vida de los objetos”El ciclo de vida de un objeto en Java consta de tres fases principales:
- Creación: Se asigna memoria para el objeto cuando se usa el operador
new - Uso: El objeto se utiliza a través de sus referencias
- Destrucción: El recolector de basura de Java (Garbage Collector) libera la memoria cuando el objeto ya no es accesible
public class EjemploCicloVida { public static void main(String[] args) { // Creación Persona persona = new Persona("Juan", 28, 1.75);
// Uso persona.saludar(); persona.cumplirAnios();
// Destrucción (implícita) persona = null; // Elimina la referencia al objeto // El recolector de basura eventualmente liberará la memoria }}4.2 Métodos y atributos
Section titled “4.2 Métodos y atributos”Los atributos (también llamados campos o variables de instancia) representan el estado de un objeto, mientras que los métodos definen su comportamiento.
Atributos
Section titled “Atributos”Los atributos pueden tener diferentes modificadores de acceso y pueden ser inicializados en su declaración o en constructores.
public class Producto { // Atributos con diferentes modificadores de acceso public String nombre; // Accesible desde cualquier clase private double precio; // Accesible solo dentro de esta clase protected String categoria; // Accesible en esta clase y sus subclases int stock; // Accesible en el mismo paquete (default)
// Atributo constante (final) public final String codigo;
// Atributo de clase (static) public static int cantidadProductos = 0;
public Producto(String nombre, double precio, String categoria, int stock, String codigo) { this.nombre = nombre; this.precio = precio; this.categoria = categoria; this.stock = stock; this.codigo = codigo; cantidadProductos++; }}Tipos de atributos
Section titled “Tipos de atributos”| Tipo | Descripción | Ejemplo |
|---|---|---|
| Variables de instancia | Pertenecen a cada objeto individual | |
| Variables de clase (static) | Compartidas por todas las instancias de la clase | |
| Constantes (final) | No pueden cambiar después de su inicialización | |
| Constantes de clase (static final) | Constantes compartidas por todas las instancias | |
Métodos
Section titled “Métodos”Los métodos definen el comportamiento de los objetos y pueden tener diferentes modificadores, parámetros y valores de retorno.
public class Calculadora { // Método simple sin parámetros ni valor de retorno public void mostrarMensaje() { System.out.println("Calculadora lista para usar"); }
// Método con parámetros y valor de retorno public int sumar(int a, int b) { return a + b; }
// Método con parámetros variables (varargs) public double promedio(double... numeros) { double suma = 0; for (double num : numeros) { suma += num; } return numeros.length > 0 ? suma / numeros.length : 0; }
// Método estático (de clase) public static double elevarAlCuadrado(double numero) { return numero * numero; }
// Método sobrecargado (mismo nombre, diferentes parámetros) public double multiplicar(double a, double b) { return a * b; }
public double multiplicar(double a, double b, double c) { return a * b * c; }}Tipos de métodos
Section titled “Tipos de métodos”| Tipo | Descripción | Ejemplo |
|---|---|---|
| Métodos de instancia | Operan sobre una instancia específica | |
| Métodos de clase (static) | Pertenecen a la clase, no a instancias específicas | |
| Métodos de acceso (getters) | Devuelven el valor de un atributo | |
| Métodos modificadores (setters) | Modifican el valor de un atributo | |
| Constructores | Inicializan objetos cuando se crean | |
4.2.3 Métodos void (sin retorno)
Section titled “4.2.3 Métodos void (sin retorno)”Los métodos void son aquellos que no devuelven ningún valor. Se utilizan principalmente para realizar acciones o modificar el estado de un objeto sin necesidad de retornar información.
public class Impresora { private String modelo; private int nivelTinta; private boolean encendida;
public Impresora(String modelo) { this.modelo = modelo; this.nivelTinta = 100; this.encendida = false; }
// Método void que modifica el estado del objeto public void encender() { encendida = true; System.out.println("Impresora " + modelo + " encendida."); }
// Método void que modifica el estado del objeto public void apagar() { encendida = false; System.out.println("Impresora " + modelo + " apagada."); }
// Método void con parámetros public void imprimir(String documento) { if (!encendida) { System.out.println("Error: La impresora está apagada."); return; // Salida temprana del método }
if (nivelTinta <= 0) { System.out.println("Error: No hay tinta suficiente."); return; }
System.out.println("Imprimiendo: " + documento); nivelTinta -= 10; // Reduce el nivel de tinta }
// Método con retorno para comparación public int obtenerNivelTinta() { return nivelTinta; }}Características de los métodos void
Section titled “Características de los métodos void”- Declaración: Se declaran con la palabra clave
voidcomo tipo de retorno - No usan
returncon valor: Si utilizanreturn, lo hacen sin valor para salir del método prematuramente - Propósito: Realizar acciones, modificar estado o mostrar información
- Uso común: Setters, métodos de inicialización, métodos de visualización
4.2.4 Métodos static
Section titled “4.2.4 Métodos static”Los métodos static (o métodos de clase) pertenecen a la clase en lugar de a instancias específicas. Pueden ser llamados sin necesidad de crear un objeto de la clase.
public class Matematicas { // Constante estática public static final double PI = 3.14159;
// Método estático public static double calcularAreaCirculo(double radio) { return PI * radio * radio; }
// Método estático public static double calcularPerimetroCirculo(double radio) { return 2 * PI * radio; }
// Método estático con múltiples parámetros public static double calcularDistancia(double x1, double y1, double x2, double y2) { return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); }
// Método estático que utiliza otros métodos estáticos public static double calcularAreaSector(double radio, double angulo) { return (angulo / 360) * calcularAreaCirculo(radio); }}Uso de métodos static
Section titled “Uso de métodos static”// Llamada a métodos estáticos sin crear instanciasdouble area = Matematicas.calcularAreaCirculo(5);double perimetro = Matematicas.calcularPerimetroCirculo(5);double distancia = Matematicas.calcularDistancia(0, 0, 3, 4); // 5.0
// Acceso a constantes estáticasSystem.out.println("El valor de PI es: " + Matematicas.PI);
// Ejemplo de métodos estáticos de la clase Mathdouble raiz = Math.sqrt(16); // 4.0int maximo = Math.max(10, 20); // 20double aleatorio = Math.random(); // Número aleatorio entre 0.0 y 1.0Características de los métodos static
Section titled “Características de los métodos static”- Pertenecen a la clase: No requieren una instancia para ser llamados
- No pueden acceder a variables de instancia: Solo pueden acceder directamente a otros miembros estáticos
- No pueden usar
thisosuper: Ya que no operan sobre una instancia específica - Memoria: Se cargan en memoria cuando la clase se carga, no cuando se crean objetos
- Uso común: Funciones de utilidad, operaciones independientes del estado del objeto, fábricas de objetos
public class EjemploStatic { private int contador = 0; // Variable de instancia private static int contadorGlobal = 0; // Variable de clase
public void incrementarContador() { contador++; contadorGlobal++; }
// Método estático correcto public static void mostrarContadorGlobal() { System.out.println("Contador global: " + contadorGlobal); // Error de compilación: System.out.println(contador); }
// Método estático que recibe un objeto para acceder a sus variables de instancia public static void mostrarContadorInstancia(EjemploStatic objeto) { System.out.println("Contador de instancia: " + objeto.contador); }}Casos de uso comunes para métodos static
Section titled “Casos de uso comunes para métodos static”- Funciones de utilidad: Métodos que realizan operaciones independientes del estado del objeto
- Operaciones matemáticas: Como en la clase
Mathde Java - Fábricas de objetos: Métodos que crean y devuelven instancias de la clase
- Constantes compartidas: Combinados con
finalpara definir constantes de clase - Contadores globales: Para llevar un registro compartido entre todas las instancias
Paso de parámetros en Java
Section titled “Paso de parámetros en Java”Java utiliza el paso de parámetros por valor, lo que significa que:
- Para tipos primitivos, se pasa una copia del valor
- Para objetos, se pasa una copia de la referencia (no el objeto en sí)
public void incrementar(int numero) { numero += 10; // Modifica la copia local, no el original}
int x = 5;incrementar(x);System.out.println(x); // Imprime 5, no 15public void modificarPersona(Persona p) { p.nombre = "Nuevo nombre"; // Modifica el objeto original}
Persona persona = new Persona("Original", 30, 1.75);modificarPersona(persona);System.out.println(persona.nombre); // Imprime "Nuevo nombre"4.3 Encapsulamiento
Section titled “4.3 Encapsulamiento”El encapsulamiento es uno de los principios fundamentales de la POO que consiste en ocultar los detalles de implementación de una clase y exponer solo lo necesario. Esto se logra principalmente mediante el uso de modificadores de acceso y métodos getter y setter.
Modificadores de acceso
Section titled “Modificadores de acceso”Java proporciona cuatro niveles de control de acceso:
| Modificador | Clase | Paquete | Subclase | Cualquier lugar |
|---|---|---|---|---|
| ✓ | ✗ | ✗ | ✗ |
| ✓ | ✓ | ✗ | ✗ |
| ✓ | ✓ | ✓ | ✗ |
| ✓ | ✓ | ✓ | ✓ |
Implementación del encapsulamiento
Section titled “Implementación del encapsulamiento”El patrón común para implementar el encapsulamiento es:
- Declarar los atributos como
private - Proporcionar métodos
publicgetter y setter para acceder y modificar los atributos
public class CuentaBancaria { // Atributos encapsulados (privados) private String numeroCuenta; private double saldo; private String titular;
// Constructor public CuentaBancaria(String numeroCuenta, String titular) { this.numeroCuenta = numeroCuenta; this.titular = titular; this.saldo = 0.0; }
// Getters (métodos de acceso) public String getNumeroCuenta() { return numeroCuenta; }
public double getSaldo() { return saldo; }
public String getTitular() { return titular; }
// Setters (métodos modificadores) public void setTitular(String titular) { this.titular = titular; }
// No hay setter para numeroCuenta (no se puede cambiar) // El saldo solo se modifica a través de métodos específicos
// Métodos específicos para operaciones public void depositar(double cantidad) { if (cantidad > 0) { saldo += cantidad; System.out.println("Depósito de " + cantidad + " realizado. Nuevo saldo: " + saldo); } else { System.out.println("La cantidad a depositar debe ser positiva"); } }
public void retirar(double cantidad) { if (cantidad > 0) { if (saldo >= cantidad) { saldo -= cantidad; System.out.println("Retiro de " + cantidad + " realizado. Nuevo saldo: " + saldo); } else { System.out.println("Saldo insuficiente"); } } else { System.out.println("La cantidad a retirar debe ser positiva"); } }}Beneficios del encapsulamiento
Section titled “Beneficios del encapsulamiento”- Control de acceso: Limita qué partes del programa pueden acceder a los datos
- Validación de datos: Permite verificar los datos antes de modificar atributos
- Flexibilidad: Permite cambiar la implementación interna sin afectar el código cliente
- Mantenimiento: Facilita la depuración y el mantenimiento del código
4.4 Herencia
Section titled “4.4 Herencia”La herencia es un mecanismo que permite a una clase adquirir propiedades y comportamientos de otra clase. La clase que hereda se denomina “subclase” o “clase hija”, mientras que la clase de la que se hereda se llama “superclase” o “clase padre”.
Sintaxis básica
Section titled “Sintaxis básica”En Java, la herencia se implementa utilizando la palabra clave extends:
// Clase padrepublic class Animal { protected String nombre; protected int edad;
public Animal(String nombre, int edad) { this.nombre = nombre; this.edad = edad; }
public void comer() { System.out.println(nombre + " está comiendo."); }
public void dormir() { System.out.println(nombre + " está durmiendo."); }}
// Clase hija que hereda de Animalpublic class Perro extends Animal { private String raza;
public Perro(String nombre, int edad, String raza) { super(nombre, edad); // Llama al constructor de la clase padre this.raza = raza; }
public void ladrar() { System.out.println(nombre + " está ladrando: ¡Guau, guau!"); }}La palabra clave super
Section titled “La palabra clave super”La palabra clave super se utiliza para:
- Llamar al constructor de la clase padre
- Acceder a métodos o atributos de la clase padre que han sido sobrescritos en la clase hija
public class Gato extends Animal { private int vidasRestantes;
public Gato(String nombre, int edad, int vidasRestantes) { super(nombre, edad); // Llama al constructor de Animal this.vidasRestantes = vidasRestantes; }
// Sobrescribe el método dormir de la clase padre @Override public void dormir() { // Llama al método dormir de la clase padre super.dormir(); System.out.println(nombre + " ronronea mientras duerme."); }
public void maullar() { System.out.println(nombre + " está maullando: ¡Miau!"); }}Tipos de herencia
Section titled “Tipos de herencia”| Tipo | Descripción | Soporte en Java |
|---|---|---|
| Herencia simple | Una clase hereda de una sola clase | Soportado |
| Herencia múltiple | Una clase hereda de múltiples clases | No soportado directamente (se puede simular con interfaces) |
| Herencia multinivel | Una clase hereda de otra clase que a su vez hereda de otra | Soportado |
| Herencia jerárquica | Múltiples clases heredan de una sola clase | Soportado |
Ejemplo de herencia multinivel
Section titled “Ejemplo de herencia multinivel”public class Animal { protected String nombre;
public void comer() { System.out.println("El animal come."); }}
public class Mamifero extends Animal { public void amamantar() { System.out.println("El mamífero amamanta a sus crías."); }}
public class Perro extends Mamifero { public void ladrar() { System.out.println("El perro ladra."); }}
// UsoPerro miPerro = new Perro();miPerro.nombre = "Rex"; // Heredado de AnimalmiPerro.comer(); // Heredado de AnimalmiPerro.amamantar(); // Heredado de MamiferomiPerro.ladrar(); // Definido en PerroModificador final
Section titled “Modificador final”El modificador final puede aplicarse a clases, métodos y atributos:
- Clase final: No puede ser heredada
- Método final: No puede ser sobrescrito en subclases
- Atributo final: No puede ser modificado después de su inicialización
// Clase que no puede ser heredadapublic final class Utilidades { // Método que no puede ser sobrescrito public final static double calcularImpuesto(double monto) { return monto * 0.16; }}
public class Matematicas { // Constante que no puede ser modificada public final double PI = 3.14159;
// Método que no puede ser sobrescrito public final double calcularAreaCirculo(double radio) { return PI * radio * radio; }}Herencia vs. Composición
Section titled “Herencia vs. Composición”Existen dos formas principales de reutilizar código en POO: herencia y composición.
public class Vehiculo { protected String marca; protected String modelo;
public void arrancar() { System.out.println("El vehículo arranca."); }}
public class Coche extends Vehiculo { private int numeroPuertas;
public void acelerar() { System.out.println("El coche acelera."); }}public class Motor { private String tipo;
public Motor(String tipo) { this.tipo = tipo; }
public void encender() { System.out.println("Motor " + tipo + " encendido."); }}
public class Coche { private String marca; private String modelo; private Motor motor; // Composición
public Coche(String marca, String modelo, String tipoMotor) { this.marca = marca; this.modelo = modelo; this.motor = new Motor(tipoMotor); }
public void arrancar() { System.out.println("Coche arrancando..."); motor.encender(); }}Cuándo usar herencia
Section titled “Cuándo usar herencia”La herencia es más apropiada cuando:
- Existe una relación “es un” clara entre las clases (un perro “es un” animal)
- La subclase es una versión especializada de la superclase
- La subclase reutiliza y extiende el comportamiento de la superclase sin modificarlo drásticamente
4.5 Polimorfismo
Section titled “4.5 Polimorfismo”El polimorfismo es la capacidad de los objetos de diferentes clases para responder al mismo mensaje o método de diferentes maneras. En Java, el polimorfismo se implementa principalmente a través de la sobrescritura de métodos y el uso de referencias de tipo superclase para objetos de tipo subclase.
Sobrescritura de métodos (Override)
Section titled “Sobrescritura de métodos (Override)”La sobrescritura de métodos ocurre cuando una subclase proporciona una implementación específica para un método que ya está definido en su superclase.
public class Figura { public double calcularArea() { return 0; // Implementación por defecto }
public void dibujar() { System.out.println("Dibujando una figura"); }}
public class Circulo extends Figura { private double radio;
public Circulo(double radio) { this.radio = radio; }
@Override // Anotación que indica sobrescritura public double calcularArea() { return Math.PI * radio * radio; }
@Override public void dibujar() { System.out.println("Dibujando un círculo"); }}
public class Rectangulo extends Figura { private double base; private double altura;
public Rectangulo(double base, double altura) { this.base = base; this.altura = altura; }
@Override public double calcularArea() { return base * altura; }
@Override public void dibujar() { System.out.println("Dibujando un rectángulo"); }}Polimorfismo en acción
Section titled “Polimorfismo en acción”El polimorfismo permite tratar objetos de diferentes clases a través de una interfaz común (una superclase o interfaz):
public class EjemploPolimorfismo { public static void main(String[] args) { // Creación de objetos Figura figura1 = new Circulo(5.0); Figura figura2 = new Rectangulo(4.0, 6.0);
// Polimorfismo en acción System.out.println("Area de figura1: " + figura1.calcularArea()); // Llama a Circulo.calcularArea() System.out.println("Area de figura2: " + figura2.calcularArea()); // Llama a Rectangulo.calcularArea()
figura1.dibujar(); // Llama a Circulo.dibujar() figura2.dibujar(); // Llama a Rectangulo.dibujar()
// Array polimórfico Figura[] figuras = new Figura[3]; figuras[0] = new Circulo(3.0); figuras[1] = new Rectangulo(2.0, 4.0); figuras[2] = new Circulo(7.0);
// Procesamiento polimórfico for (Figura f : figuras) { System.out.println("Area: " + f.calcularArea()); f.dibujar(); } }}Enlace dinámico (Dynamic Binding)
Section titled “Enlace dinámico (Dynamic Binding)”El enlace dinámico es el mecanismo por el cual Java determina en tiempo de ejecución qué método invocar cuando hay sobrescritura. Java utiliza el tipo real del objeto, no el tipo de la referencia, para decidir qué método llamar.
Figura f = new Circulo(5.0);f.calcularArea(); // Llama a Circulo.calcularArea(), no a Figura.calcularArea()Casting de objetos
Section titled “Casting de objetos”A veces es necesario convertir una referencia de un tipo a otro:
Figura f = new Circulo(5.0);
// Downcasting (de superclase a subclase)if (f instanceof Circulo) { Circulo c = (Circulo) f; // Casting seguro // Ahora podemos acceder a métodos específicos de Circulo}
// Upcasting (de subclase a superclase)Circulo c = new Circulo(3.0);Figura f2 = c; // Upcasting implícito, no requiere sintaxis especialSobrecarga vs. Sobrescritura
Section titled “Sobrecarga vs. Sobrescritura”| Característica | Sobrecarga (Overloading) | Sobrescritura (Overriding) |
|---|---|---|
| Definición | Múltiples métodos con el mismo nombre pero diferentes parámetros | Redefinir un método heredado en una subclase |
| Ocurre en | Misma clase o clase hija | Clase hija |
| Parámetros | Diferentes (tipo, número o ambos) | Iguales |
| Tipo de retorno | Puede ser diferente | Debe ser igual o un subtipo (covariante) |
| Modificadores de acceso | Pueden ser diferentes | Igual o menos restrictivo |
| Excepciones | Pueden ser diferentes | Iguales o subtipos de las declaradas en el método original |
| Resolución | En tiempo de compilación | En tiempo de ejecución (enlace dinámico) |
Ejemplo completo de polimorfismo
Section titled “Ejemplo completo de polimorfismo”// Clase basepublic abstract class Empleado { protected String nombre; protected double salarioBase;
public Empleado(String nombre, double salarioBase) { this.nombre = nombre; this.salarioBase = salarioBase; }
// Método que será sobrescrito public abstract double calcularSalario();
public void mostrarDetalles() { System.out.println("Nombre: " + nombre); System.out.println("Salario: " + calcularSalario()); }}
// Subclase 1public class EmpleadoTiempoCompleto extends Empleado { private double bono;
public EmpleadoTiempoCompleto(String nombre, double salarioBase, double bono) { super(nombre, salarioBase); this.bono = bono; }
@Override public double calcularSalario() { return salarioBase + bono; }
@Override public void mostrarDetalles() { super.mostrarDetalles(); System.out.println("Tipo: Tiempo Completo"); System.out.println("Bono: " + bono); }}
// Subclase 2public class EmpleadoTiempoParcial extends Empleado { private int horasTrabajadas; private double tarifaPorHora;
public EmpleadoTiempoParcial(String nombre, double salarioBase, int horasTrabajadas, double tarifaPorHora) { super(nombre, salarioBase); this.horasTrabajadas = horasTrabajadas; this.tarifaPorHora = tarifaPorHora; }
@Override public double calcularSalario() { return salarioBase + (horasTrabajadas * tarifaPorHora); }
@Override public void mostrarDetalles() { super.mostrarDetalles(); System.out.println("Tipo: Tiempo Parcial"); System.out.println("Horas trabajadas: " + horasTrabajadas); System.out.println("Tarifa por hora: " + tarifaPorHora); }}
// Uso del polimorfismopublic class SistemaNomina { public static void main(String[] args) { // Array polimórfico Empleado[] empleados = new Empleado[3]; empleados[0] = new EmpleadoTiempoCompleto("Juan Pérez", 2000, 500); empleados[1] = new EmpleadoTiempoParcial("María López", 1000, 20, 15); empleados[2] = new EmpleadoTiempoCompleto("Carlos Gómez", 2500, 300);
// Cálculo de nómina usando polimorfismo double totalNomina = 0; for (Empleado emp : empleados) { emp.mostrarDetalles(); // Llamada polimórfica System.out.println("-------------------"); totalNomina += emp.calcularSalario(); // Llamada polimórfica }
System.out.println("Total nómina: " + totalNomina); }}4.6 Abstracción
Section titled “4.6 Abstracción”La abstracción es el proceso de ocultar los detalles de implementación y mostrar solo la funcionalidad al usuario. En Java, la abstracción se logra mediante clases abstractas e interfaces.
Clases abstractas
Section titled “Clases abstractas”Una clase abstracta es una clase que no puede ser instanciada directamente y que puede contener métodos abstractos (métodos sin implementación) y métodos concretos (con implementación).
// Clase abstractapublic abstract class Vehiculo { // Atributos protected String marca; protected String modelo; protected int anio;
// Constructor public Vehiculo(String marca, String modelo, int anio) { this.marca = marca; this.modelo = modelo; this.anio = anio; }
// Método abstracto (sin implementación) public abstract void acelerar();
// Método abstracto public abstract void frenar();
// Método concreto (con implementación) public void encender() { System.out.println("El vehículo " + marca + " " + modelo + " ha sido encendido."); }
// Método concreto public void apagar() { System.out.println("El vehículo " + marca + " " + modelo + " ha sido apagado."); }
// Getters y setters public String getMarca() { return marca; }
public String getModelo() { return modelo; }
public int getAnio() { return anio; }}Implementación de clases abstractas
Section titled “Implementación de clases abstractas”Las clases que heredan de una clase abstracta deben implementar todos sus métodos abstractos, a menos que la subclase también sea abstracta.
// Subclase concreta que implementa la clase abstracta Vehiculopublic class Coche extends Vehiculo { private int numeroPuertas;
public Coche(String marca, String modelo, int anio, int numeroPuertas) { super(marca, modelo, anio); this.numeroPuertas = numeroPuertas; }
// Implementación de métodos abstractos @Override public void acelerar() { System.out.println("El coche " + marca + " " + modelo + " está acelerando."); }
@Override public void frenar() { System.out.println("El coche " + marca + " " + modelo + " está frenando."); }
// Método específico de Coche public void abrirPuertas() { System.out.println("Abriendo las " + numeroPuertas + " puertas del coche."); }}Características de las clases abstractas
Section titled “Características de las clases abstractas”- No pueden ser instanciadas directamente (
new Vehiculo()no está permitido) - Pueden tener constructores y bloques de inicialización
- Pueden tener métodos abstractos y concretos
- Pueden tener atributos, métodos estáticos y finales
- Una clase abstracta puede extender otra clase y puede implementar interfaces
- Si una clase tiene al menos un método abstracto, la clase debe ser declarada como abstracta
Ejemplo de uso de clases abstractas
Section titled “Ejemplo de uso de clases abstractas”public abstract class Forma { // Método abstracto public abstract double calcularArea();
// Método concreto public void mostrarArea() { System.out.println("El área es: " + calcularArea()); }}
public class Cuadrado extends Forma { private double lado;
public Cuadrado(double lado) { this.lado = lado; }
@Override public double calcularArea() { return lado * lado; }}
public class Triangulo extends Forma { private double base; private double altura;
public Triangulo(double base, double altura) { this.base = base; this.altura = altura; }
@Override public double calcularArea() { return (base * altura) / 2; }}
// Usopublic class PruebaFormas { public static void main(String[] args) { Forma cuadrado = new Cuadrado(5); Forma triangulo = new Triangulo(4, 3);
cuadrado.mostrarArea(); // El área es: 25.0 triangulo.mostrarArea(); // El área es: 6.0 }}4.7 Interfaces
Section titled “4.7 Interfaces”Una interfaz en Java es una colección de métodos abstractos (sin implementación) y constantes. A partir de Java 8, las interfaces también pueden incluir métodos default y estáticos con implementación.
Definición de una interfaz
Section titled “Definición de una interfaz”public interface Dibujable { // Constante (implicitamente public, static y final) String HERRAMIENTA = "Lápiz";
// Método abstracto (implícitamente public y abstract) void dibujar();
// Método abstracto void cambiarColor(String color);
// Método default (Java 8+) default void mostrarInformacion() { System.out.println("Dibujando con " + HERRAMIENTA); }
// Método estático (Java 8+) static void describirInterfaz() { System.out.println("Esta interfaz define objetos que pueden ser dibujados."); }}Implementación de interfaces
Section titled “Implementación de interfaces”Una clase implementa una interfaz utilizando la palabra clave implements. La clase debe proporcionar implementaciones para todos los métodos abstractos de la interfaz.
public class Circulo implements Dibujable { private double radio; private String color;
public Circulo(double radio) { this.radio = radio; this.color = "Negro"; // Color por defecto }
// Implementación de los métodos de la interfaz @Override public void dibujar() { System.out.println("Dibujando un círculo de radio " + radio + " en color " + color); }
@Override public void cambiarColor(String color) { this.color = color; System.out.println("Color cambiado a " + color); }
// No es necesario sobrescribir mostrarInformacion() porque tiene una implementación default}Múltiples interfaces
Section titled “Múltiples interfaces”Una clase puede implementar múltiples interfaces, lo que permite una forma de “herencia múltiple” de comportamiento.
public interface Redimensionable { void redimensionar(double factor);}
public interface Rotable { void rotar(double grados);}
// Implementación de múltiples interfacespublic class Rectangulo implements Dibujable, Redimensionable, Rotable { private double ancho; private double alto; private String color; private double rotacion;
public Rectangulo(double ancho, double alto) { this.ancho = ancho; this.alto = alto; this.color = "Negro"; this.rotacion = 0; }
// Implementación de Dibujable @Override public void dibujar() { System.out.println("Dibujando un rectángulo de " + ancho + "x" + alto + " en color " + color + " con rotación " + rotacion + " grados"); }
@Override public void cambiarColor(String color) { this.color = color; }
// Implementación de Redimensionable @Override public void redimensionar(double factor) { ancho *= factor; alto *= factor; System.out.println("Rectángulo redimensionado a " + ancho + "x" + alto); }
// Implementación de Rotable @Override public void rotar(double grados) { this.rotacion = (this.rotacion + grados) % 360; System.out.println("Rectángulo rotado a " + rotacion + " grados"); }}Interfaces vs. Clases abstractas
Section titled “Interfaces vs. Clases abstractas”| Característica | Interfaz | Clase abstracta |
|---|---|---|
| Métodos | Abstractos, default, estáticos | Abstractos y concretos |
| Variables | Solo constantes (public static final) | Cualquier tipo de variable |
| Constructores | No permitidos | Permitidos |
| Herencia múltiple | Una clase puede implementar múltiples interfaces | Una clase solo puede extender una clase abstracta |
| Acceso a miembros | Implícitamente public | Cualquier modificador de acceso |
| Uso principal | Definir comportamientos que pueden ser implementados por clases no relacionadas | Proporcionar una base común para subclases relacionadas |
Cuándo usar interfaces vs. clases abstractas
Section titled “Cuándo usar interfaces vs. clases abstractas”Usa interfaces cuando:
- Quieres definir un contrato que múltiples clases no relacionadas deben cumplir
- Necesitas simular herencia múltiple
- Esperas que las clases que implementan la interfaz tengan implementaciones completamente diferentes
- Quieres especificar el comportamiento de un tipo particular, pero no te preocupa quién lo implementa
Usa clases abstractas cuando:
- Quieres compartir código entre varias clases estrechamente relacionadas
- Necesitas acceso a modificadores que no sean public
- Quieres declarar campos no estáticos o no finales
- Necesitas proporcionar una implementación base común para todos los métodos
Interfaces funcionales y expresiones lambda (Java 8+)
Section titled “Interfaces funcionales y expresiones lambda (Java 8+)”Una interfaz funcional es una interfaz que contiene exactamente un método abstracto. Estas interfaces pueden ser implementadas mediante expresiones lambda.
// Interfaz funcional (anotación opcional pero recomendada)@FunctionalInterfacepublic interface Calculadora { // Único método abstracto double operar(double a, double b);
// Métodos default no cuentan para la definición de interfaz funcional default double operarAbsoluto(double a, double b) { return Math.abs(operar(a, b)); }}
public class PruebaLambda { public static void main(String[] args) { // Implementación tradicional con clase anónima Calculadora suma = new Calculadora() { @Override public double operar(double a, double b) { return a + b; } };
// Implementación con expresión lambda Calculadora resta = (a, b) -> a - b; Calculadora multiplicacion = (a, b) -> a * b; Calculadora division = (a, b) -> a / b;
// Uso System.out.println("Suma: " + suma.operar(5, 3)); // 8.0 System.out.println("Resta: " + resta.operar(5, 3)); // 2.0 System.out.println("Multiplicación: " + multiplicacion.operar(5, 3)); // 15.0 System.out.println("División: " + division.operar(6, 3)); // 2.0
// Uso de método default System.out.println("Resta absoluta: " + resta.operarAbsoluto(3, 5)); // 2.0 }}Herencia entre interfaces
Section titled “Herencia entre interfaces”Las interfaces pueden extender una o más interfaces utilizando la palabra clave extends.
public interface Vehiculo { void acelerar(); void frenar();}
public interface VehiculoElectrico extends Vehiculo { void cargar(); int getNivelBateria();}
public class CocheElectrico implements VehiculoElectrico { private int nivelBateria = 100;
@Override public void acelerar() { System.out.println("Acelerando coche eléctrico"); nivelBateria -= 5; }
@Override public void frenar() { System.out.println("Frenando coche eléctrico"); nivelBateria -= 1; }
@Override public void cargar() { nivelBateria = 100; System.out.println("Batería cargada al 100%"); }
@Override public int getNivelBateria() { return nivelBateria; }}Ejemplo completo: Combinando interfaces y clases abstractas
Section titled “Ejemplo completo: Combinando interfaces y clases abstractas”// Interfaz basepublic interface Reproducible { void reproducir(); void pausar(); void detener();}
// Clase abstracta que implementa parcialmente la interfazpublic abstract class DispositivoMultimedia implements Reproducible { protected String nombre; protected boolean encendido;
public DispositivoMultimedia(String nombre) { this.nombre = nombre; this.encendido = false; }
// Métodos concretos public void encender() { encendido = true; System.out.println(nombre + " encendido."); }
public void apagar() { encendido = false; System.out.println(nombre + " apagado."); }
// Implementación parcial de la interfaz @Override public void pausar() { if (encendido) { System.out.println(nombre + " en pausa."); } else { System.out.println("El dispositivo está apagado."); } }
@Override public void detener() { if (encendido) { System.out.println(nombre + " detenido."); } else { System.out.println("El dispositivo está apagado."); } }
// Método abstracto adicional public abstract void mostrarInformacion();}
// Interfaz adicionalpublic interface Conectable { void conectar(); void desconectar(); boolean estaConectado();}
// Clase concreta que extiende la clase abstracta e implementa otra interfazpublic class ReproductorMP3 extends DispositivoMultimedia implements Conectable { private boolean conectado; private String[] canciones; private int cancionActual;
public ReproductorMP3(String nombre, String[] canciones) { super(nombre); this.canciones = canciones; this.cancionActual = 0; this.conectado = false; }
// Implementación del método abstracto de DispositivoMultimedia @Override public void mostrarInformacion() { System.out.println("Reproductor MP3: " + nombre); System.out.println("Estado: " + (encendido ? "Encendido" : "Apagado")); System.out.println("Conexión: " + (conectado ? "Conectado" : "Desconectado")); System.out.println("Canciones disponibles: " + canciones.length); }
// Implementación del método restante de Reproducible @Override public void reproducir() { if (encendido) { System.out.println("Reproduciendo: " + canciones[cancionActual]); } else { System.out.println("El dispositivo está apagado."); } }
// Implementación de los métodos de Conectable @Override public void conectar() { conectado = true; System.out.println(nombre + " conectado al sistema."); }
@Override public void desconectar() { conectado = false; System.out.println(nombre + " desconectado del sistema."); }
@Override public boolean estaConectado() { return conectado; }
// Métodos adicionales específicos public void siguienteCancion() { if (encendido) { cancionActual = (cancionActual + 1) % canciones.length; System.out.println("Siguiente canción: " + canciones[cancionActual]); } else { System.out.println("El dispositivo está apagado."); } }
public void cancionAnterior() { if (encendido) { cancionActual = (cancionActual - 1 + canciones.length) % canciones.length; System.out.println("Canción anterior: " + canciones[cancionActual]); } else { System.out.println("El dispositivo está apagado."); } }}
// Usopublic class PruebaMultimedia { public static void main(String[] args) { String[] canciones = {"Canción 1", "Canción 2", "Canción 3", "Canción 4"}; ReproductorMP3 reproductor = new ReproductorMP3("Mi Reproductor", canciones);
reproductor.encender(); reproductor.conectar(); reproductor.mostrarInformacion(); reproductor.reproducir(); reproductor.siguienteCancion(); reproductor.reproducir(); reproductor.pausar(); reproductor.siguienteCancion(); reproductor.reproducir(); reproductor.detener(); reproductor.desconectar(); reproductor.apagar(); }}