Programación Orientada a Objetos
Clases y objetos
Atributos
- Un atributo (o campo) guarda el estado de un objeto.
- Puede ser:
- De instancia: cada objeto tiene su propio valor.
- Estático (
static): compartido por todos los objetos de la clase.
- Se declaran dentro de la clase, fuera de métodos.
Ejemplo:
public class Cuenta {
private String titular; // atributo de instancia
private double saldo; // atributo de instancia
public static String banco = "Notion Bank"; // atributo estático
}Buenas prácticas:
- Casi siempre declarar atributos como
private. - Evitar exponer atributos públicos (rompe el principio de encapsulamiento).
- Mantener coherencia: si un atributo no debe cambiar, evaluar
final.
Métodos
- Un método define el comportamiento de la clase.
- Puede:
- Leer o modificar atributos.
- Validar reglas.
- Devolver resultados.
- Tipos comunes:
- De instancia: operan sobre el estado del objeto.
- Estáticos (
static): utilitarios o lógica que no depende del estado.
Ejemplo:
public class Cuenta {
private double saldo;
public void depositar(double monto) {
if (monto <= 0) {
throw new IllegalArgumentException("monto debe ser > 0");
}
saldo += monto;
}
public double getSaldo() {
return saldo;
}
}Constructores
- Un constructor inicializa un objeto cuando se invoca
new. - Tiene el mismo nombre que la clase y no tiene tipo de retorno.
- Puede haber:
- Constructor por defecto: si no se define ninguno.
- Constructores personalizados: para exigir datos iniciales.
- Sobrecarga de constructores: varias firmas.
Ejemplo:
public class Cuenta {
private final String titular;
private double saldo;
public Cuenta(String titular) {
this.titular = titular;
this.saldo = 0;
}
public Cuenta(String titular, double saldoInicial) {
this.titular = titular;
this.saldo = saldoInicial;
}
}Tip: usar this(...) para llamar a otro constructor y evitar duplicar lógica.
Modificadores de acceso
Controlan desde dónde se puede acceder a clases, atributos y métodos.
public: accesible desde cualquier lugar.protected: accesible desde el mismo paquete y subclases.- (sin keyword) (package-private): accesible solo dentro del paquete.
private: accesible solo dentro de la clase.
| Modificador | ¿Dónde se puede acceder? |
|---|---|
public | En cualquier parte |
protected | Mismo paquete y subclases |
| package-private | Solo mismo paquete |
private | Solo misma clase |
🔐 Encapsulamiento
El encapsulamiento es uno de los pilares fundamentales de la Programación Orientada a Objetos.
Su objetivo principal es:
Proteger el estado interno de un objeto y controlar cómo se modifica.
En lugar de permitir que cualquier parte del programa modifique los datos directamente, el objeto expone métodos controlados para interactuar con su estado, recuerden que los objetos se componen de estados y comportamientos, sus características fundamentales
Esto permite:
- Evitar modificaciones incorrectas
- Mantener consistencia en los datos
- Facilitar mantenimiento del código
- Reducir errores
private
La palabra reservada private es un modificador de acceso.
Significa que un atributo o método solo puede ser accedido desde la misma clase.
Ejemplo incorrecto (sin encapsulamiento)
public class CuentaBancaria {
public double saldo;
}
CuentaBancaria cuenta = new CuentaBancaria();
cuenta.saldo += -5000;Esto es un error, ya que nada evita que el saldo entre en un estado inválido.
Ejemplo correcto
public class CuentaBancaria{
private double saldo;
}
CuentaBancaria cuenta = new CuentaBancaria();
cuenta.saldo += -5000;Lo anterior lanzaría una excepción, manteniendo el estado interno del objeto protegido, y obligandonos a interactuar con este mediante metodos controlados.
Getters y Setters
Los getters y setters son métodos que permiten leer o modificar atributos privados de forma controlada.
- Getter → permite leer el valor
- Setter → permite modificar el valor
public class Persona {
private String nombre;
private int edad;
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
}Persona persona = new Persona();
persona.setNombre("Alejandro");
System.out.println(persona.getNombre());La salida es Alejandro
Inmutabilidad
Un objeto inmutable es un objeto que no puede cambiar su estado despues de ser creado
Esto significa que sus atributos:
- Se incializan en el constructor (Ya que no cambian, no es posible inicializarlos luego)
- No tiene setters
- Son de tipo
final
Ejemplo
public class Usuario {
private final String nombre;
private final String email;
public Usuario(String nombre, String email) {
this.nombre = nombre;
this.email = email;
}
public String getNombre() {
return nombre;
}
public String getEmail() {
return email;
}
}
Usuario usuario = new Usuario("Alejandro", "alejandro@email.com");
//Despues de creado no puede cambiar
usuario.setNombre("Pedro"); // no existeVentajas
- Más facil de razonar
- Evita errores
- Thread-safe
- Muy usados en programación funcional
- Muy común en diseño de sistemas moderno
Principios de diseño básicos
El encapsulamiento ayuda a cumplir principios importantes de diseño de software
- Ocultamiento de información
Una clase solo expone lo estrictamente necesario
- Alta cohesión
Una clase debe tener una responsabilidad clara
- Bajo acoplamiento
Encapsular estado ayuda a evitar dependencias innecesarias.
- Control de estado interno
Un objeto protege sus reglas internas
🧬 Herencia
La herencia es uno de los pilares de la Programación Orientada a Objetos.
Permite que una clase herede atributos y comportamientos de otra clase.
Esto permite:
- reutilizar código
- evitar duplicación
- modelar relaciones del mundo real
- extender comportamientos existentes
En términos simples:
Una clase puede extender otra clase y reutilizar sus funcionalidades.
Ejemplo básico de herencia
Supongamos que tenemos una clase Animal.
public class Animal {
public void comer() {
System.out.println("El animal está comiendo");
}
}Ahora podemos crear una clase más especifica
public class Perro extends Animal {
public void ladrar() {
System.out.println("Guau!");
}
}
Perro perro = new Perro();
perro.comer();
perro.ladrar();
//Salida:
// El animal está comiendo
/// Guau!Extends
La palabra reservada extends se usa para indicar que una clase hereda de otra, por lo general se asocia al termino “ES UN” para entender este comportamiento
Por ejemplo, un perro “Es un” animal, es decir con extends heredamos dicho comportamiento a perro.
Un ejemplo incorrecto: Motor → Auto, un motor no es un auto, sino que está o es contenido en un auto, por lo que no debemos usar herencia.
Super
La reservada super permite acceder a miembros de la clase padre, se usa principalmente para:
- Llamar constructores del padre
- Acceder a métodos (comportamientos) del padre
- Acceder a atributos (estado) del padre
Ejemplo:
public class Persona {
protected String nombre;
public Persona(String nombre) {
this.nombre = nombre;
}
}
public class Estudiante extends Persona {
private String carrera;
public Estudiante(String nombre, String carrera) {
super(nombre);
this.carrera = carrera;
}
}Super llama al constructor de la clase padre, esto se puede replicar también con los métodos del padre.
Override
El override permite que una clase hija reemplace el comportamiento o implementación de un método de la clase padre, @Override le indica al compilador que estamos sobreescribiendo un método existente
Ejemplo
public class Animal {
public void sonido() {
System.out.println("El animal hace un sonido");
}
}
public class Perro extends Animal {
@Override
public void sonido() {
System.out.println("El perro ladra");
}
}
Perro perro = new Perro();
perro.sonido();
//SALIDA:
// El perro ladraProblemas con la herencia
Aunque es uno de los pilares de POO, puede causar problemas de diseño si se usa incorrectamente
- Alto acoplamiento
- Jerarquias complejas
- Herencias incorrectas
- Problema del metodo roto (Si la clase padre cambia, puede afectar a los hijos)
Polimorfismo
Sobrescritura
Como vimos anteriormente, ocurre cuando una clase hija redefine un método de la clase padre para darle un comportamiento diferente, conocido como polimorfismo dinámico
Cuando se usa, se debe tener en cuenta que:
- El método debe existir en la clase padre
- Debe tener el mismo nombre
- Debe tener los mismos parámetros
- Se usa la anotación @Override para indicar la sobreescritura
Interfaces
Son contratos que definen comportamientos que las clases deben cumplir, una intefaz no implementa un comportamiento completo, solo define qué métodos se deben cumplir, se usa la palabra reservada interface
Ejemplo:
interface Vehiculo{
void acelerar();
void frenar();
}
// Una clase implementa el contrato de la interface
//mediante la palabra reservada "implements"
class Carro implements Vehiculo {
@Override
public void acelerar() {
System.out.println("El carro acelera");
}
@Override
public void frenar() {
System.out.println("El carro frena");
}
}Ventajas
- Permiten desacoplar código
- Definir contratos
- Facilitar pruebas unitarias
- Permitir múltiples comportamientos
Java por defecto no permite la herencia múltiple de clases, por lo que una manera de realizarlo es utilizar interfaces:
interface Volador {
void volar();
}
interface Nadador {
void nadar();
}
class Pato implements Volador, Nadador {
@Override
public void volar() {
System.out.println("El pato vuela");
}
@Override
public void nadar() {
System.out.println("El pato nada");
}
}
// Esto permite modelar comportamientos de forma flexible.Sobrecarga
Permite definir múltiples métodos con el mismo nombre dentro de una misma clase, pero con diferentes parámetros. Esto permite que un mismo nombre de método se utilice para realizar operaciones similares con distintos tipos o cantidades de datos, está asociado al polimorfismo estático, ya que este actúa en tiempo de compilación.
Ejemplo:
public class Calculadora {
public int sumar(int a, int b) {
return a + b;
}
public double sumar(double a, double b) {
return a + b;
}
public int sumar(int a, int b, int c) {
return a + b + c;
}
}Abstracción
La abstracción consiste en mostrar solo la información necesaria y ocultar la complejidad interna.
En otras palabras:
El usuario usa el objeto sin preocuparse por cómo funciona internamente.
Ejemplo del mundo real:
- Un carro
- Sabes usar:
- volante
- freno
- acelerador
- No necesitas saber cómo funciona el motor internamente
Ejemplo:
public void pagar() {
procesarPago();
}El usuario solo llama al método pagar, pero internamente desconoce como ocurren las operaciones.
Clases abstractas
Una clase abstracta es una clase que no puede ser instanciada directamente.
Se usa como base para otras clases.
Se define con la palabra reservada abstract
Ejemplo:
abstract class Animal {
public abstract void hacerSonido();
}Una clase abstracta puede tener:
- Métodos abstractos (sin implementación)
- Métodos normales (con implementación)
- Atributos
Diferencia interface vs clase abstracta
| Característica | Interface | Clase Abstracta |
|---|---|---|
| Herencia múltiple | Sí | No |
| Métodos con implementación | Sí (default) | Sí |
| Atributos | Constantes | Variables normales |
| Constructor | No | Sí |
| Uso principal | Contratos | Base de jerarquía |
Relaciones entre clases
Asociación
La asociación es la relación más básica entre dos clases.
Significa simplemente que una clase usa o conoce a otra clase.
No implica propiedad ni dependencia fuerte.
Ejemplo conceptual
Un Profesor puede enseñar a un Estudiante.
Ambos existen independientemente.
Ejemplo en código:
class Estudiante {
private String nombre;
public Estudiante(String nombre) {
this.nombre = nombre;
}
}
class Profesor {
private String nombre;
private Estudiante estudiante;
public Profesor(String nombre, Estudiante estudiante) {
this.nombre = nombre;
this.estudiante = estudiante;
}
}
public class Main {
public static void main(String[] args) {
Estudiante estudiante = new Estudiante("Carlos");
Profesor profesor = new Profesor("Ana", estudiante);
}
}
// Aquí existe una relación entre Profesor y Estudiante,
// pero ninguno depende completamente del otro.Tipos:
- Uno a uno
- Persona → Documento Identidad
- Uno a muchos
- Profesor → Estudiantes
- Muchos a muchos
- Estudiantes → Cursos
Agregación
La agregación es un tipo de asociación donde una clase contiene a otra, pero la clase contenida puede existir por sí sola.
Es una relación débil de pertenencia.
Ejemplo conceptual
Un Equipo tiene Jugadores.
Pero los jugadores pueden existir sin el equipo.
static class Jugador {
private String nombre;
public Jugador(String nombre) {
this.nombre = nombre;
}
}
import java.util.List;
static class Equipo {
private String nombre;
private List<Jugador> jugadores;
public Equipo(String nombre, List<Jugador> jugadores) {
this.nombre = nombre;
this.jugadores = jugadores;
}
}
public class Main {
public static void main(String[] args) {
Jugador j1 = new Jugador("Messi");
Jugador j2 = new Jugador("Suarez");
Equipo equipo = new Equipo("Barcelona", List.of(j1, j2));
}
}Composición
La composición es una relación más fuerte que la agregación.
En composición:
- Un objeto contiene otro objeto
- El objeto contenido no puede existir sin el contenedor
Si el objeto principal se destruye, los objetos internos también desaparecen.
Ejemplo conceptual
Un Carro tiene un Motor.
El motor forma parte del carro.
class Motor {
public void encender() {
System.out.println("Motor encendido");
}
}
class Carro {
private Motor motor;
public Carro() {
this.motor = new Motor();
}
public void encender() {
motor.encender();
}
}
public class Main {
public static void main(String[] args) {
Carro carro = new Carro();
carro.encender();
}
}Dependencia
La dependencia ocurre cuando una clase usa otra temporalmente, generalmente como:
- parámetro
- variable local
- retorno de método
Es la relación más débil entre clases.
class Impresora {
public void imprimir(String documento) {
System.out.println("Imprimiendo: " + documento);
}
}
class ReporteService {
public void generarReporte(Impresora impresora) {
String reporte = "Reporte financiero";
impresora.imprimir(reporte);
}
}
public class Main {
public static void main(String[] args) {
Impresora impresora = new Impresora();
ReporteService service = new ReporteService();
service.generarReporte(impresora);
}
}Composición vs herencia
Este es uno de los principios más importantes en diseño orientado a objetos.
Existe una regla muy conocida:
Favor composition over inheritance
Es decir:
Prefiere composición antes que herencia
Problemas de la herencia:
- Jerarquías rígidas
- Alto acoplamiento
- Difícil mantenimiento
- Cambios en la clase padre afectan a todas las hijas
Ventajas de la composición:
- Más flexible
- Menos acoplamiento
- Fácil de extender
- Permite cambiar comportamientos dinámicamente
// Herencia:
class Pajaro {
public void volar() {}
}
class Pinguino extends Pajaro {
}
// Problema: Los pinguinos no vuelan
//Composición:
interface Volador {
void volar();
}
class VueloNormal implements Volador {
public void volar() {
System.out.println("Volando");
}
}
class SinVuelo implements Volador {
public void volar() {
System.out.println("No puede volar");
}
}
class Pajaro {
private Volador comportamientoVuelo;
public Pajaro(Volador comportamientoVuelo) {
this.comportamientoVuelo = comportamientoVuelo;
}
public void volar() {
comportamientoVuelo.volar();
}
}
Pajaro aguila = new Pajaro(new VueloNormal());
Pajaro pinguino = new Pajaro(new SinVuelo());Mixins
Los mixins son una forma de reutilizar comportamiento entre clases sin usar herencia tradicional.
En lugar de crear una jerarquía rígida de clases, los mixins permiten agregar funcionalidades específicas a una clase.
En muchos lenguajes (como Ruby o Scala) los mixins son una característica del lenguaje.
En Java se pueden simular usando interfaces con métodos default.
Esto permite compartir comportamiento entre clases sin obligarlas a heredar de una misma clase padre.
Problema que resuelven los mixins
Supongamos que tenemos diferentes clases:
- Usuario
- Producto
- Orden
- Archivo
Todas necesitan registrar logs.
Una solución sería crear una clase base:
class Logger {
public void log(String mensaje) {
System.out.println(mensaje);
}
}
// Pero java no permite herencia multiple, por lo que no podriamos usar:
class Usuario extends Entidad, LoggerDesde Java 8, las interfaces pueden tener métodos con implementación usando default.
Esto permite compartir comportamiento
interface LoggerMixin {
default void log(String mensaje) {
System.out.println("[LOG] " + mensaje);
}
}
class Usuario implements LoggerMixin {
private String nombre;
public Usuario(String nombre) {
this.nombre = nombre;
}
public void crearUsuario() {
log("Usuario creado: " + nombre);
}
}
public class Main {
public static void main(String[] args) {
Usuario usuario = new Usuario("Carlos");
usuario.crearUsuario();
}
}Cuándo usar mixins
Los mixins son útiles cuando:
- Se necesita compartir comportamiento entre clases no relacionadas
- Querer evitar jerarquías de herencia complejas
- Querer agregar funcionalidades modulares
Ejemplo real:
Una clase podría necesitar:
- Logging
- Auditoría
- Validación
- Métricas
En lugar de crear una jerarquía gigante, se combinan mixins.
| Característica | Herencia | Mixins |
|---|---|---|
| Jerarquía | Rígida | Flexible |
| Reutilización | Limitada | Alta |
| Acoplamiento | Alto | Bajo |
| Composición de comportamiento | Difícil | Fácil |
SOLID
Los principios SOLID son cinco reglas de diseño orientado a objetos que ayudan a crear software:
- Más mantenible
- Más escalable
- Más flexible
- Más fácil de probar
Fueron popularizados por Robert C. Martin (Uncle Bob)
| Letra | Principio |
|---|---|
| S | Single Responsibility Principle |
| O | Open/Closed Principle |
| L | Liskov Substitution Principle |
| I | Interface Segregation Principle |
| D | Dependency Inversion Principle |
Single Responsibility Principle (SRP)
Definición
Una clase debe tener una sola razón para cambiar.
Esto significa que una clase debe tener una única responsabilidad.
class UsuarioService {
public void guardarUsuario(String nombre) {
// guardar en base de datos
}
public void enviarEmailBienvenida(String nombre) {
// enviar email
}
public void generarReporteUsuarios() {
// generar reporte
}
}Problemas:
Esta clase tiene tres responsabilidades:
- Persistencia
- Envío de correos
- Generación de reportes
Solución
Separar las responsabilidades
class UsuarioRepository {
public void guardar(String nombre) {
System.out.println("Guardando usuario");
}
}
class EmailService {
public void enviarBienvenida(String nombre) {
System.out.println("Enviando email");
}
}
class ReporteService {
public void generarReporteUsuarios() {
System.out.println("Generando reporte");
}
}Cada clase ahora tiene una única responsabilidad.
Open/Closed Principle (OCP)
Definición
El software debe estar abierto para extensión pero cerrado para modificación.
Esto significa que debemos poder agregar nuevas funcionalidades sin modificar el código existente.
class CalculadoraDescuentos {
public double calcular(String tipoCliente, double precio) {
if (tipoCliente.equals("REGULAR")) {
return precio * 0.9;
}
if (tipoCliente.equals("VIP")) {
return precio * 0.8;
}
return precio;
}
}
// Problema: Cada nuevo tipo de cliente requiere modificar la claseSolución
interface Descuento {
double aplicar(double precio);
}
class DescuentoRegular implements Descuento {
public double aplicar(double precio) {
return precio * 0.9;
}
}
class DescuentoVIP implements Descuento {
public double aplicar(double precio) {
return precio * 0.8;
}
}
class CalculadoraDescuentos {
public double calcular(Descuento descuento, double precio) {
return descuento.aplicar(precio);
}
}
Liskov Substitution Principle (LSP)
Definición
Las clases hijas deben poder reemplazar a su clase padre sin romper el programa.
Esto significa que una subclase debe respetar el comportamiento esperado de la clase base.
Ejemplo:
class Pajaro {
public void volar() {
System.out.println("Volando");
}
}
class Pinguino extends Pajaro {
@Override
public void volar() {
throw new UnsupportedOperationException();
}
}
// Un pingüino no vuela, por lo que rompe el contrato de la clase padre.Solución
- Separar comportamientos
interface Volador {
void volar();
}
class Aguila implements Volador {
public void volar() {
System.out.println("El águila vuela");
}
}
class Pinguino {
public void nadar() {
System.out.println("El pingüino nada");
}
}
Interface Segregation Principle (ISP)
Definición
Ninguna clase debe verse obligada a implementar métodos que no necesita.
Es mejor tener muchas interfaces pequeñas que una interfaz gigante.
Ejemplo
interface Trabajador {
void trabajar();
void comer();
void dormir();
}
class Robot implements Trabajador {
public void trabajar() {}
public void comer() {
throw new UnsupportedOperationException();
}
public void dormir() {
throw new UnsupportedOperationException();
}
}
// El robot no come ni duermeSolución
Separar interfaces.
interface Trabajador {
void trabajar();
}
interface SerVivo {
void comer();
void dormir();
}
class Humano implements Trabajador, SerVivo {
public void trabajar() {}
public void comer() {}
public void dormir() {}
}
class Robot implements Trabajador {
public void trabajar() {}
}
// Ahora cada clase implementa solo lo que necesita.Dependency Inversion Principle (DIP)
Definición
Los módulos de alto nivel no deben depender de módulos de bajo nivel.
Ambos deben depender de abstracciones.
Y además:
Las abstracciones no deben depender de detalles.
Ejemplo:
class MySQLDatabase {
public void guardar(String dato) {
System.out.println("Guardando en MySQL");
}
}
class UsuarioService {
private MySQLDatabase database = new MySQLDatabase();
public void guardarUsuario(String nombre) {
database.guardar(nombre);
}
}
Problema:
UsuarioService depende directamente de MySQL.
Si cambiamos la base de datos, hay que modificar la clase.
Solución
interface Database {
void guardar(String dato);
}
class MongoDatabase implements Database {
public void guardar(String dato) {
System.out.println("Guardando en MongoDB");
}
}
class UsuarioService {
private Database database;
public UsuarioService(Database database) {
this.database = database;
}
public void guardarUsuario(String nombre) {
database.guardar(nombre);
}
}
Database db = new MySQLDatabase();
UsuarioService service = new UsuarioService(db);
service.guardarUsuario("Alejandro");