Introducción
La Programación Orientada a Objetos (POO) es un paradigma que organiza el software como una colección de objetos que combinan datos y comportamiento. En lugar de escribir una lista de instrucciones, modelas el mundo real en clases que luego se instancian como objetos.
Java es uno de los lenguajes más populares para aprender POO porque fuerza este paradigma desde el primer archivo: todo en Java vive dentro de una clase.
¿Qué vas a aprender en este lab?
- Definir clases y crear objetos
- Proteger el estado interno con encapsulamiento
- Reutilizar código con herencia
- Escribir código flexible con polimorfismo
- Usar clases abstractas e interfaces
- Identificar y corregir errores comunes
Requisitos previos: conocimiento básico de Java (variables, tipos de datos, condicionales y bucles). No necesitas experiencia previa con POO.
1. Clases y Objetos
Una clase es la plantilla. Un objeto es una instancia concreta de esa plantilla.
Piénsalo así: Persona es la clase (el molde), y julian es un objeto (una persona real hecha con ese molde).
Sintaxis de una clase en Java
public class Persona {
// Atributos (estado)
String nombre;
int edad;
// Método (comportamiento)
public void saludar() {
System.out.println("Hola, soy " + nombre + " y tengo " + edad + " años.");
}
}
Crear e instanciar un objeto
public class Main {
public static void main(String[] args) {
// Crear un objeto de tipo Persona
Persona julian = new Persona();
julian.nombre = "Julian";
julian.edad = 25;
julian.saludar(); // Hola, soy Julian y tengo 25 años.
}
}
La palabra clave new reserva espacio en memoria y devuelve una referencia al nuevo objeto.
Ejercicio 1
Crea una clase Producto con los atributos nombre (String), precio (double) y stock (int). Añade un método mostrarInfo() que imprima los tres valores. Instancia dos productos distintos en main y llama al método en cada uno.
2. Encapsulamiento
El encapsulamiento consiste en ocultar el estado interno de un objeto y exponer solo lo necesario a través de métodos. Esto evita que cualquier parte del código modifique los datos de forma incorrecta.
Modificadores de acceso
| Modificador | Visible desde |
|---|---|
private |
Solo la misma clase |
public |
Cualquier clase |
protected |
La clase y sus subclases |
| (sin modificador) | El mismo paquete |
Getters y Setters
public class Persona {
private String nombre;
private int edad;
// Getter
public String getNombre() {
return nombre;
}
// Setter con validación
public void setEdad(int edad) {
if (edad < 0 || edad > 150) {
System.out.println("Edad no válida.");
return;
}
this.edad = edad;
}
public int getEdad() {
return edad;
}
}
La palabra clave this diferencia el atributo de la clase (this.edad) del parámetro del método (edad).
Persona p = new Persona();
p.setEdad(25); // OK
p.setEdad(-5); // Imprime "Edad no válida."
// p.edad = -5; // Error de compilación: edad es private
Ejercicio 2
Modifica la clase Producto del ejercicio anterior: declara todos los atributos como private. Añade getters para los tres y un setter para precio que rechace valores negativos. Prueba que no puedes asignar precio directamente desde main.
3. Constructores
Un constructor es un método especial que se llama automáticamente al crear un objeto con new. No tiene tipo de retorno y lleva el mismo nombre que la clase.
Constructor por defecto
Si no declaras ningún constructor, Java crea uno vacío automáticamente. En cuanto declares uno propio, el automático desaparece.
Constructor parametrizado
public class CuentaBancaria {
private String titular;
private double saldo;
// Constructor parametrizado
public CuentaBancaria(String titular, double saldoInicial) {
this.titular = titular;
this.saldo = saldoInicial;
}
public void depositar(double monto) {
if (monto > 0) saldo += monto;
}
public void mostrarSaldo() {
System.out.println(titular + " — Saldo: $" + saldo);
}
}
CuentaBancaria cuenta = new CuentaBancaria("Julian", 1000.0);
cuenta.depositar(500.0);
cuenta.mostrarSaldo(); // Julian — Saldo: $1500.0
Sobrecarga de constructores
Puedes tener múltiples constructores con diferentes parámetros:
public class CuentaBancaria {
private String titular;
private double saldo;
public CuentaBancaria(String titular) {
this(titular, 0.0); // llama al otro constructor
}
public CuentaBancaria(String titular, double saldoInicial) {
this.titular = titular;
this.saldo = saldoInicial;
}
}
Ejercicio 3
Añade dos constructores a Producto: uno que reciba solo nombre (precio y stock quedan en 0) y otro que reciba los tres atributos. Crea instancias usando ambos constructores y verifica que los valores sean correctos.
4. Herencia
La herencia permite que una clase hija reutilice atributos y métodos de una clase padre, y los extienda o modifique según sus necesidades.
public class Animal {
protected String nombre;
public Animal(String nombre) {
this.nombre = nombre;
}
public void hacerSonido() {
System.out.println(nombre + " hace un sonido.");
}
}
public class Perro extends Animal {
public Perro(String nombre) {
super(nombre); // llama al constructor del padre
}
@Override
public void hacerSonido() {
System.out.println(nombre + " ladra: ¡Guau!");
}
}
public class Gato extends Animal {
public Gato(String nombre) {
super(nombre);
}
@Override
public void hacerSonido() {
System.out.println(nombre + " maúlla: ¡Miau!");
}
}
Animal miPerro = new Perro("Rex");
Animal miGato = new Gato("Luna");
miPerro.hacerSonido(); // Rex ladra: ¡Guau!
miGato.hacerSonido(); // Luna maúlla: ¡Miau!
Puntos clave:
extendsdeclara la relación de herenciasuper()llama al constructor del padre (debe ser la primera línea)@Overrideindica que se está redefiniendo un método del padre
Ejercicio 4
Crea una clase padre Vehiculo con atributos marca y velocidadMaxima. Crea dos subclases: Coche (añade atributo numeroPuertas) y Moto (añade atributo tieneSidecar). Cada una debe sobreescribir un método describir().
5. Polimorfismo
El polimorfismo permite tratar objetos de distintas clases hijas como si fueran del tipo del padre, y que cada uno responda a los métodos a su manera.
public class Main {
public static void main(String[] args) {
Animal[] animales = {
new Perro("Rex"),
new Gato("Luna"),
new Perro("Max")
};
for (Animal a : animales) {
a.hacerSonido(); // cada objeto responde según su clase real
}
}
}
Salida:
Rex ladra: ¡Guau!
Luna maúlla: ¡Miau!
Max ladra: ¡Guau!
Java decide qué versión de hacerSonido() ejecutar en tiempo de ejecución según el tipo real del objeto, no según el tipo de la referencia. Esto se llama despacho dinámico.
Ejercicio 5
Crea una lista de tipo List<Vehiculo> con instancias de Coche y Moto. Itera la lista y llama a describir() en cada elemento. Agrega una tercera subclase Camion sin modificar el código del bucle.
6. Clases Abstractas e Interfaces
Clases abstractas
Una clase abstracta no puede instanciarse directamente. Define métodos abstractos (sin cuerpo) que las subclases deben implementar obligatoriamente.
public abstract class Figura {
protected String color;
public Figura(String color) {
this.color = color;
}
// Método abstracto: sin cuerpo, obliga a las subclases a implementarlo
public abstract double calcularArea();
// Método concreto: se hereda tal cual
public void mostrarColor() {
System.out.println("Color: " + color);
}
}
public class Circulo extends Figura {
private double radio;
public Circulo(String color, double radio) {
super(color);
this.radio = radio;
}
@Override
public double calcularArea() {
return Math.PI * radio * radio;
}
}
Interfaces
Una interfaz define un contrato de comportamiento. Todas las clases que la implementen deben proveer los métodos declarados.
public interface Pagable {
double calcularPago();
String obtenerTipoPago();
}
public class Empleado implements Pagable {
private String nombre;
private double salarioMensual;
public Empleado(String nombre, double salario) {
this.nombre = nombre;
this.salarioMensual = salario;
}
@Override
public double calcularPago() {
return salarioMensual;
}
@Override
public String obtenerTipoPago() {
return "Salario fijo mensual";
}
}
public class Freelancer implements Pagable {
private String nombre;
private double tarifaHora;
private int horasTrabajadas;
public Freelancer(String nombre, double tarifa, int horas) {
this.nombre = nombre;
this.tarifaHora = tarifa;
this.horasTrabajadas = horas;
}
@Override
public double calcularPago() {
return tarifaHora * horasTrabajadas;
}
@Override
public String obtenerTipoPago() {
return "Por horas";
}
}
¿Cuándo usar cada uno?
| Clase abstracta | Interfaz | |
|---|---|---|
| Herencia | Solo una | Múltiples |
| Estado (atributos) | Sí | No (solo constantes) |
| Métodos concretos | Sí | Sí (desde Java 8, default) |
| Uso ideal | Compartir código base | Definir contrato de comportamiento |
Ejercicio 6
Crea una interfaz Exportable con un método exportarCSV(): String. Implementa esta interfaz en las clases Empleado y Producto. Luego usa una lista de tipo List<Exportable> para exportar ambos sin conocer su tipo concreto.
7. Errores Comunes
Error 1 — NullPointerException al olvidar new
// Incorrecto
Persona p;
p.saludar(); // NullPointerException: p no apunta a ningún objeto
// Correcto
Persona p = new Persona();
p.saludar();
Una variable de tipo objeto en Java contiene una referencia, no el objeto en sí. Sin new, la referencia es null.
Error 2 — Comparar objetos con == en lugar de .equals()
String a = new String("hola");
String b = new String("hola");
System.out.println(a == b); // false — compara referencias (direcciones en memoria)
System.out.println(a.equals(b)); // true — compara el contenido
Usa == solo con primitivos (int, boolean, etc.). Para objetos, siempre .equals().
Error 3 — Olvidar @Override y crear un método nuevo sin querer
public class Gato extends Animal {
// Incorrecto: el parámetro es diferente al del padre
public void hacerSonido(String tono) {
System.out.println("Miau con tono " + tono);
}
// Esto NO sobreescribe el método del padre: crea uno nuevo.
}
public class Gato extends Animal {
// Con @Override el compilador verifica que realmente sobreescribes
@Override
public void hacerSonido() {
System.out.println(nombre + " maúlla.");
}
}
@Override no es obligatorio pero siempre debes usarlo: si el nombre o la firma no coinciden con ningún método del padre, el compilador lanza un error inmediatamente.
Error 4 — Llamar a super() fuera del constructor o no como primera línea
public class Perro extends Animal {
public Perro(String nombre) {
System.out.println("Creando perro..."); // Error de compilación
super(nombre); // super() debe ser la PRIMERA línea del constructor
}
}
Resumen
| Pilar | ¿Qué hace? | Palabra clave en Java |
|---|---|---|
| Abstracción | Modela entidades del mundo real como clases | class |
| Encapsulamiento | Protege el estado interno del objeto | private, getters/setters |
| Herencia | Reutiliza y extiende el comportamiento de otra clase | extends, super |
| Polimorfismo | Trata distintos objetos de forma uniforme | @Override, despacho dinámico |
Próximos pasos
- Colecciones:
ArrayList,HashMapy la API de Streams para trabajar con grupos de objetos - Excepciones: manejo de errores con
try-catch, excepciones personalizadas - Genéricos: clases y métodos que funcionan con cualquier tipo (
List<T>) - Patrones de diseño: Singleton, Factory, Observer — soluciones probadas a problemas comunes