Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Sintaxis de Clases y Objetos

De la Burbuja al Código

Universidad Nacional de Rio Negro - Sede Andina

Este capítulo aborda la sintaxis concreta de Java para implementar los conceptos de Programación Orientada a Objetos que viste en los capítulos anteriores:

Aquí aprenderás cómo materializar esos conceptos en código Java funcional.


Bloque 1: De la “Burbuja” al Código

La Estructura de una Clase en Java

En el capítulo anterior definimos que una Clase es una descripción abstracta que funciona como molde o plantilla. En Java, esa descripción se materializa mediante la palabra clave class seguida del nombre de la clase. La estructura general es:

public class NombreClase {
    // Zona de declaración de atributos (estado)
    
    // Zona de constructor(es)
    
    // Zona de métodos (comportamiento)
}

Esta estructura tripartita no es arbitraria: refleja directamente los tres componentes fundamentales de un objeto según el paradigma (Objeto):

  1. Atributos → definen el estado que cada instancia almacenará

  2. Constructor → establece el proceso de construcción (Construcción)

  3. Métodos → implementan el comportamiento del objeto

Anatomía Detallada de una Clase

Consideremos el siguiente ejemplo completo:

public class Persona {
    // ══════════════════════════════════════════════════════════
    // ATRIBUTOS: Variables de instancia que almacenan el estado
    // ══════════════════════════════════════════════════════════
    private String nombre;
    private int edad;
    
    // ══════════════════════════════════════════════════════════
    // CONSTRUCTOR: Método especial de inicialización
    // ══════════════════════════════════════════════════════════
    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
    
    // ══════════════════════════════════════════════════════════
    // MÉTODOS: Operaciones que definen el comportamiento
    // ══════════════════════════════════════════════════════════
    public void saludar() {
        System.out.println("Hola, soy " + nombre);
    }
    
    public void cumplirAnios() {
        edad = edad + 1;
    }
}

Cada zona tiene un propósito específico y reglas sintácticas propias que se detallan a continuación.

Componentes de una Clase

Atributos (Variables de Instancia)

Los atributos, también llamados variables de instancia o campos, son variables declaradas dentro de la clase pero fuera de cualquier método. Estos atributos almacenan el Estado del objeto.

Sintaxis de declaración:

[modificadorDeAcceso] tipo nombreAtributo;

Ejemplos:

private String nombre;      // Atributo de tipo String (texto)
private int edad;           // Atributo de tipo int (entero)
private double salario;     // Atributo de tipo double (decimal)
private boolean activo;     // Atributo de tipo boolean (verdadero/falso)
private Fecha nacimiento;   // Atributo de tipo Fecha (otra clase)

Características fundamentales de los atributos:

  1. Pertenencia a la instancia: Cada objeto creado a partir de la clase tiene su propia copia de los atributos. Modificar el atributo nombre de un objeto juan no afecta al atributo nombre de otro objeto maria. Esto es exactamente lo que describimos en Atributo.

  2. Persistencia durante la vida del objeto: Los valores de los atributos se mantienen mientras el objeto exista. A diferencia de las variables locales (dentro de métodos), que desaparecen cuando el método termina, los atributos persisten entre llamadas a métodos.

  3. Valores por defecto: Si no se inicializa explícitamente, Java asigna valores por defecto según el tipo:

    TipoValor por defecto
    int, long, short, byte0
    float, double0.0
    booleanfalse
    Referencias (objetos)null

El Constructor

El constructor es un método especial que se ejecuta automáticamente cuando se crea una nueva instancia de la clase mediante el operador new. Su propósito fundamental es garantizar que el objeto nazca en un estado válido y consistente, tal como se describió en Construcción.

Sintaxis del constructor:

[modificadorDeAcceso] NombreClase([parámetros]) {
    // Código de inicialización
}

Características distintivas del constructor:

  1. Nombre idéntico a la clase: El constructor debe llamarse exactamente igual que la clase (respetando mayúsculas y minúsculas).

  2. Sin tipo de retorno: A diferencia de los métodos regulares, el constructor no declara tipo de retorno, ni siquiera void. Esto no es un olvido; el constructor implícitamente “retorna” la referencia al objeto recién creado.

  3. Ejecución automática: El constructor se invoca automáticamente cuando se usa new. No se puede llamar directamente como un método ordinario (excepto usando this() desde otro constructor de la misma clase).

  4. Responsabilidad de inicialización: El constructor debe establecer todos los atributos en valores válidos. Un objeto nunca debería existir en un estado inconsistente.

Ejemplo detallado:

public Persona(String nombre, int edad) {
    // Validación previa (opcional pero recomendada)
    if (nombre == null || nombre.isEmpty()) {
        throw new IllegalArgumentException("El nombre no puede estar vacío");
    }
    if (edad < 0) {
        throw new IllegalArgumentException("La edad no puede ser negativa");
    }
    
    // Inicialización de atributos
    this.nombre = nombre;
    this.edad = edad;
}
El Constructor por Defecto

Si no se define ningún constructor explícitamente, Java proporciona automáticamente un constructor por defecto (sin parámetros) que:

public class Punto {
    private int x;
    private int y;
    // Java genera implícitamente:
    // public Punto() { }
}

// Se puede usar así:
Punto origen = new Punto();  // x=0, y=0 (valores por defecto)

Métodos

Los métodos implementan el comportamiento de los objetos, es decir, las operaciones que pueden realizar. Esto corresponde directamente al Método del capítulo anterior.

Sintaxis general de un método:

[modificadorDeAcceso] tipoRetorno nombreMetodo([parámetros]) {
    // Cuerpo del método
    [return valor;]  // Si tipoRetorno no es void
}

Componentes de la firma de un método:

ComponenteDescripciónEjemplo
Modificador de accesoControla quién puede invocar el métodopublic, private
Tipo de retornoTipo del valor que devuelve el métodoint, String, void
Nombre del métodoIdentificador que describe la accióncalcularEdad, saludar
ParámetrosDatos que recibe el método para trabajar(int cantidad, String texto)

Ejemplo con diferentes tipos de métodos:

public class CuentaBancaria {
    private double saldo;
    private String titular;
    
    // Constructor
    public CuentaBancaria(String titular, double saldoInicial) {
        this.titular = titular;
        this.saldo = saldoInicial;
    }
    
    // Método de consulta (getter): retorna información, no modifica estado
    public double getSaldo() {
        return saldo;
    }
    
    // Método de modificación: cambia el estado del objeto
    public void depositar(double monto) {
        if (monto > 0) {
            saldo = saldo + monto;
        }
    }
    
    // Método que retorna un resultado calculado
    public boolean puedeExtraer(double monto) {
        return monto > 0 && monto <= saldo;
    }
    
    // Método que modifica estado y retorna resultado
    public boolean extraer(double monto) {
        if (puedeExtraer(monto)) {
            saldo = saldo - monto;
            return true;
        }
        return false;
    }
}

Clasificación de métodos según su propósito:

Como se mencionó en Método, los métodos pueden clasificarse según lo que hacen:

  1. Métodos de consulta (queries): Retornan información sobre el estado sin modificarlo.

    • Ejemplos: getSaldo(), getNombre(), estaVacio()

  2. Métodos de modificación (commands): Cambian el estado del objeto.

    • Ejemplos: depositar(), cambiarNombre(), agregar()

  3. Métodos de cálculo: Realizan operaciones y retornan resultados.

    • Ejemplos: calcularIntereses(), distanciaA()

La referencia this

La palabra clave this es una referencia especial que todo objeto tiene a sí mismo. Cuando un método se ejecuta, this apunta automáticamente al objeto sobre el cual se invocó el método.

Conexión con el modelo de memoria:

Recordemos de la discusión sobre Objeto que cada objeto tiene una identidad única. En términos de implementación, esa identidad corresponde a la dirección de memoria donde reside el objeto en el Heap. La palabra clave this es precisamente esa dirección: una referencia que el objeto tiene hacia sí mismo.

El puntero this como referencia al propio objeto en el Heap.

Figure 1:El puntero this como referencia al propio objeto en el Heap.

Usos Principales de this

1. Desambiguación entre atributos y parámetros:

Cuando un parámetro tiene el mismo nombre que un atributo, Java da prioridad a la variable más “cercana” (el parámetro). Para referirse al atributo, se usa this:

public class Persona {
    private String nombre;  // Atributo de instancia
    private int edad;       // Atributo de instancia
    
    public Persona(String nombre, int edad) {
        // Sin 'this', 'nombre' se refiere al parámetro
        // this.nombre se refiere al atributo
        this.nombre = nombre;
        this.edad = edad;
    }
    
    public void setEdad(int edad) {
        // 'edad' (parámetro) vs 'this.edad' (atributo)
        if (edad >= 0) {
            this.edad = edad;
        }
    }
}

2. Invocar otros métodos del mismo objeto:

Dentro de un método, se pueden llamar otros métodos del mismo objeto. El uso de this es opcional pero puede mejorar la claridad:

public class Persona {
    private String nombre;
    private int edad;
    
    public void saludar() {
        System.out.println("Hola, soy " + nombre);
    }
    
    public void presentarse() {
        // Ambas formas son equivalentes:
        this.saludar();           // Explícito con this
        saludar();                // Implícito (this se asume)
        
        System.out.println("Tengo " + this.edad + " años");
    }
}

3. Retornar la referencia al objeto actual:

Un método puede retornar this para permitir el encadenamiento de llamadas (method chaining):

public class Constructor {
    private String html;
    
    public Constructor() {
        html = "";
    }
    
    public Constructor agregarParrafo(String texto) {
        html += "<p>" + texto + "</p>";
        return this;  // Retorna el mismo objeto
    }
    
    public Constructor agregarTitulo(String texto) {
        html += "<h1>" + texto + "</h1>";
        return this;
    }
    
    public String construir() {
        return html;
    }
}

// Uso con encadenamiento:
String pagina = new Constructor()
    .agregarTitulo("Bienvenido")
    .agregarParrafo("Este es el contenido")
    .agregarParrafo("Otro párrafo")
    .construir();

4. Invocar otro constructor de la misma clase:

Usando this() (con paréntesis y argumentos), un constructor puede invocar a otro constructor de la misma clase. Esto evita duplicación de código:

public class Rectangulo {
    private int ancho;
    private int alto;
    
    // Constructor principal (el más completo)
    public Rectangulo(int ancho, int alto) {
        this.ancho = ancho;
        this.alto = alto;
    }
    
    // Constructor para cuadrados: delega al constructor principal
    public Rectangulo(int lado) {
        this(lado, lado);  // Invoca al constructor de dos parámetros
    }
    
    // Constructor por defecto: crea un rectángulo 1x1
    public Rectangulo() {
        this(1, 1);  // Invoca al constructor de dos parámetros
    }
}

Instanciación de Objetos

La instanciación es el proceso de crear un objeto concreto a partir de una clase, tal como se describió en Instancia. En Java, este proceso se realiza mediante el operador new:

Sintaxis:

NombreClase nombreVariable = new NombreClase(argumentos);

Desglose del proceso:

Persona juan = new Persona("Juan", 25);
ParteDescripción
PersonaTipo de la variable (la clase)
juanNombre de la variable (referencia)
=Operador de asignación
newOperador que reserva memoria y crea el objeto
Persona("Juan", 25)Invocación del constructor con argumentos

¿Qué sucede en memoria?

El proceso de instanciación involucra varias operaciones internas:

  1. Reserva de memoria en el Heap: Se asigna espacio suficiente para almacenar todos los atributos del objeto.

  2. Inicialización de atributos por defecto: Antes de ejecutar el constructor, todos los atributos reciben valores por defecto.

  3. Ejecución del constructor: Se ejecuta el código del constructor, que típicamente asigna valores iniciales a los atributos.

  4. Retorno de la referencia: El operador new retorna una referencia (dirección de memoria) que se almacena en la variable.

Modelo de memoria Stack/Heap durante la instanciación de múltiples objetos.

Figure 2:Modelo de memoria Stack/Heap durante la instanciación de múltiples objetos.

Ejemplo completo de instanciación y uso:

// Instanciación: se crean dos objetos independientes
Persona juan = new Persona("Juan", 25);
Persona maria = new Persona("María", 30);

// Cada objeto tiene su propio estado
juan.saludar();    // Imprime: Hola, soy Juan
maria.saludar();   // Imprime: Hola, soy María

// Modificar uno no afecta al otro
juan.cumplirAnios();  // juan.edad pasa a 26
// maria.edad sigue siendo 30

Bloque 2: Protegiendo el Estado y Flexibilidad

Encapsulamiento: De Concepto a Implementación

En la sección de encapsulamiento en el capítulo anterior del capítulo anterior definimos el encapsulamiento como el principio de ocultar los detalles internos de un objeto exponiendo solo una interfaz controlada. Ahora veremos cómo implementar este principio en Java.

El encapsulamiento tiene dos aspectos complementarios:

  1. Fusión de datos y comportamiento: Ya lo logramos al definir clases con atributos y métodos juntos (ver La fusión de datos y comportamiento).

  2. Ocultamiento de información: Lo implementamos mediante modificadores de acceso que restringen quién puede acceder a cada miembro de la clase.

¿Por Qué Encapsular?

Recordemos los beneficios del encapsulamiento descritos en los beneficios del encapsulamiento descritos en el capítulo anterior:

  1. Protección del estado: Impedir que código externo corrompa el estado interno del objeto asignando valores inválidos o inconsistentes.

  2. Control de acceso: El objeto decide qué información expone y cómo permite modificarla, pudiendo aplicar validaciones.

  3. Flexibilidad de implementación: La estructura interna puede cambiar sin afectar al código que usa la clase, siempre que la interfaz pública se mantenga.

  4. Documentación implícita: La interfaz pública (métodos public) documenta qué operaciones están disponibles.

Ejemplo del problema sin encapsulamiento:

// Sin encapsulamiento: atributos públicos
public class CuentaBancaria {
    public double saldo;        // Accesible directamente
    public String titular;      // Accesible directamente
}

// Código externo puede hacer cualquier cosa:
CuentaBancaria cuenta = new CuentaBancaria();
cuenta.saldo = -50000;          // ¡Saldo negativo imposible!
cuenta.saldo = cuenta.saldo * 2; // ¡Duplicó su dinero mágicamente!
cuenta.titular = "";            // ¡Titular vacío!

El objeto no tiene control sobre su propio estado. Cualquier código puede asignar valores absurdos.

Modificadores de Acceso en Java

Java proporciona cuatro niveles de acceso para controlar la visibilidad de clases, atributos y métodos:

ModificadorClase propiaMismo paqueteSubclasesCualquier clase
private
(sin modificador)
protected
public

Descripción detallada:

private — Máxima restricción:

Un miembro private solo es accesible desde dentro de la misma clase. Ni siquiera las subclases pueden accederlo directamente.

public class Persona {
    private String nombre;      // Solo accesible dentro de Persona
    private int edad;           // Solo accesible dentro de Persona
    
    public Persona(String nombre, int edad) {
        this.nombre = nombre;   // OK: estamos dentro de Persona
        this.edad = edad;       // OK: estamos dentro de Persona
    }
}

// Desde otra clase:
Persona p = new Persona("Ana", 25);
String n = p.nombre;  // ERROR de compilación: nombre has private access

(sin modificador) — Acceso de paquete:

También llamado “package-private”. El miembro es accesible desde cualquier clase del mismo paquete (carpeta), pero no desde otros paquetes.

// Archivo: modelos/Persona.java
package modelos;

class Persona {          // Sin public: solo visible en el paquete 'modelos'
    String nombre;       // Sin modificador: visible en el paquete 'modelos'
}

protected — Acceso para herencia:

El miembro es accesible desde el mismo paquete y desde subclases (incluso en otros paquetes). Se utiliza principalmente cuando se desea que las subclases accedan a ciertos miembros sin exponerlos públicamente.

public class Animal {
    protected String nombre;    // Accesible en subclases
    
    protected void emitirSonido() {
        // Las subclases pueden llamar o sobrescribir esto
    }
}

public — Sin restricciones:

El miembro es accesible desde cualquier parte del programa. Es la interfaz que el objeto expone al mundo exterior.

public class CuentaBancaria {
    // Métodos públicos: la interfaz del objeto
    public double getSaldo() {
        return saldo;
    }
    
    public void depositar(double monto) {
        // ...
    }
}

La Regla de Oro del Encapsulamiento

Esta regla implementa lo descrito en Ocultamiento de información: los detalles internos quedan ocultos y solo se expone una interfaz controlada.

public class CuentaBancaria {
    // ═══════════════════════════════════════
    // ATRIBUTOS: siempre private
    // ═══════════════════════════════════════
    private double saldo;
    private String titular;
    private String numeroCuenta;
    
    // ═══════════════════════════════════════
    // CONSTRUCTOR: típicamente public
    // ═══════════════════════════════════════
    public CuentaBancaria(String titular, String numeroCuenta) {
        this.titular = titular;
        this.numeroCuenta = numeroCuenta;
        this.saldo = 0;  // Saldo inicial
    }
    
    // ═══════════════════════════════════════
    // INTERFAZ PÚBLICA: métodos public
    // ═══════════════════════════════════════
    public double getSaldo() {
        return saldo;
    }
    
    public void depositar(double monto) {
        if (validarMonto(monto)) {
            saldo += monto;
            registrarMovimiento("Depósito", monto);
        }
    }
    
    public boolean extraer(double monto) {
        if (validarMonto(monto) && saldo >= monto) {
            saldo -= monto;
            registrarMovimiento("Extracción", monto);
            return true;
        }
        return false;
    }
    
    // ═══════════════════════════════════════
    // MÉTODOS AUXILIARES: private
    // ═══════════════════════════════════════
    private boolean validarMonto(double monto) {
        return monto > 0;
    }
    
    private void registrarMovimiento(String tipo, double monto) {
        // Lógica interna de registro
        System.out.println(tipo + ": $" + monto);
    }
}

Getters y Setters: Práctica Común vs. Buen Diseño

En la industria es común encontrar métodos accesores (getters) y mutadores (setters) para acceder y modificar atributos privados. Sin embargo, su uso indiscriminado representa una violación encubierta del encapsulamiento.

El Problema con Getters y Setters

Consideremos una clase CuentaBancaria con el enfoque tradicional de getters/setters:

// ❌ MAL DISEÑO: Getters y setters exponen la estructura interna
public class CuentaBancaria {
    private double saldo;
    
    public double getSaldo() {
        return saldo;
    }
    
    public void setSaldo(double saldo) {
        this.saldo = saldo;
    }
}

// El código cliente hace la lógica:
cuenta.setSaldo(cuenta.getSaldo() + 100);  // Depósito
cuenta.setSaldo(cuenta.getSaldo() - 50);   // Extracción

Problemas de este enfoque:

  1. Lógica dispersa: La operación “depositar” está fuera del objeto

  2. Sin validaciones: Cualquiera puede asignar saldos negativos

  3. Acoplamiento alto: Si cambia la representación interna, todo el código cliente se rompe

  4. Falta de semántica: setSaldo no comunica intención de negocio

Comportamiento con Nombre vs. Accesores

El diseño correcto reemplaza accesores genéricos por métodos con significado en el dominio:

// ✓ BUEN DISEÑO: Comportamiento con nombre significativo
public class CuentaBancaria {
    private double saldo;
    
    public CuentaBancaria(double saldoInicial) {
        if (saldoInicial < 0) {
            throw new IllegalArgumentException("Saldo inicial no puede ser negativo");
        }
        this.saldo = saldoInicial;
    }
    
    // En lugar de getSaldo() + setSaldo(), métodos con COMPORTAMIENTO:
    
    public void depositar(double monto) {
        if (monto <= 0) {
            throw new IllegalArgumentException("Monto debe ser positivo");
        }
        saldo += monto;
    }
    
    public void extraer(double monto) {
        if (monto <= 0) {
            throw new IllegalArgumentException("Monto debe ser positivo");
        }
        if (monto > saldo) {
            throw new IllegalStateException("Saldo insuficiente");
        }
        saldo -= monto;
    }
    
    public void transferirA(CuentaBancaria destino, double monto) {
        this.extraer(monto);
        destino.depositar(monto);
    }
    
    // Consulta con nombre significativo (no "getSaldo")
    public double consultarSaldo() {
        return saldo;
    }
    
    public boolean puedeExtraer(double monto) {
        return monto > 0 && monto <= saldo;
    }
}

// Uso: el código cliente expresa INTENCIÓN
cuenta.depositar(100);
cuenta.extraer(50);
cuenta.transferirA(otraCuenta, 200);

Ventajas de este enfoque:

  1. Lógica encapsulada: Las validaciones están dentro del objeto

  2. Semántica clara: depositar() comunica intención, setSaldo() no

  3. Imposible estados inválidos: No se puede asignar saldo negativo

  4. Bajo acoplamiento: Si cambia la representación interna, el código cliente no cambia

Cuándo Son Aceptables los Getters

Existen casos donde un getter tiene sentido:

  1. Valores inmutables de identidad: Consultar el DNI, número de cuenta, ISBN

  2. Representación textual: Métodos como toString() necesitan acceder a atributos

  3. Valores calculados: Cuando el “getter” realmente calcula algo (como getEdad() a partir de fecha de nacimiento)

public class Libro {
    private final String isbn;      // Inmutable, identificador
    private final String titulo;    // Inmutable
    private boolean disponible;     // Mutable, pero controlado
    
    // ✓ Aceptable: valor inmutable de identidad
    public String getIsbn() {
        return isbn;
    }
    
    // ✓ Aceptable: valor inmutable para mostrar
    public String getTitulo() {
        return titulo;
    }
    
    // ❌ En lugar de isDisponible() y setDisponible()...
    // ✓ Comportamiento con nombre:
    public void prestar() {
        if (!disponible) {
            throw new IllegalStateException("Libro ya prestado");
        }
        disponible = false;
    }
    
    public void devolver() {
        disponible = true;
    }
    
    public boolean estaDisponible() {
        return disponible;
    }
}

Los Setters: Casi Siempre Evitables

Los setters son particularmente problemáticos porque:

  1. Exponen mutabilidad: Permiten cambiar el estado sin control

  2. No expresan intención: setEstado(3) no comunica nada

  3. Violan “Tell, Don’t Ask”: En lugar de pedirle datos al objeto y procesarlos afuera, debemos decirle al objeto qué hacer

Resumen: Criterio de la Cátedra

SituaciónEnfoque esperado
Necesito leer un valor identificadorGetter aceptable (getIsbn(), getDni())
Necesito modificar un atributoMétodo con comportamiento (depositar(), prestar())
Necesito consultar estadoMétodo con nombre descriptivo (estaDisponible(), puedeExtraer())
Necesito cambiar configuraciónMétodo específico (cambiarEmail(), actualizarDireccion())

Atributos Inmutables

Algunos atributos no deberían cambiar después de la construcción del objeto. Para estos casos:

  1. Declarar el atributo como private final

  2. Inicializarlo en el constructor

  3. Proporcionar solo un método de consulta si es necesario (nunca setter)

public class Persona {
    private final String dni;       // Inmutable: no cambia nunca
    private String nombre;          // Mutable: puede cambiar
    
    public Persona(String dni, String nombre) {
        this.dni = dni;
        this.nombre = nombre;
    }
    
    // Consulta de valor inmutable (aceptable)
    public String getDni() {
        return dni;
    }
    
    // NO hay setDni(): el DNI no puede cambiar
    
    // En lugar de setNombre(), un método con semántica:
    public void corregirNombre(String nombreCorregido) {
        if (nombreCorregido == null || nombreCorregido.isEmpty()) {
            throw new IllegalArgumentException("Nombre no puede estar vacío");
        }
        this.nombre = nombreCorregido;
    }
    
    public String obtenerNombre() {
        return nombre;
    }
}

Convenciones de Nomenclatura en Java

Las convenciones de nomenclatura son fundamentales para escribir código legible y mantenible. Java tiene estándares bien establecidos que esta cátedra adopta y exige.

Nombres de Clases

Las clases se nombran en PascalCase (también llamado UpperCamelCase): cada palabra comienza con mayúscula, sin separadores.

// ✓ Correcto: PascalCase
public class CuentaBancaria { }
public class RegistroDeVentas { }
public class SistemaDeGestion { }

// ❌ Incorrecto
public class cuentaBancaria { }     // Empieza con minúscula
public class cuenta_bancaria { }    // Usa guion bajo
public class CUENTABANCARIA { }     // Todo mayúsculas

El nombre debe ser un sustantivo que represente el concepto que modela:

// ✓ Sustantivos que representan entidades
public class Persona { }
public class Factura { }
public class Motor { }

// ❌ Verbos o acciones (eso es para métodos)
public class Calcular { }
public class Procesar { }

Nombres de Atributos

Los atributos se nombran en camelCase: comienzan con minúscula, y cada palabra subsiguiente con mayúscula.

public class Persona {
    // ✓ Correcto: camelCase
    private String nombre;
    private String apellido;
    private int edad;
    private LocalDate fechaNacimiento;
    private String direccionPostal;
    
    // ❌ Incorrecto
    private String Nombre;           // Empieza con mayúscula
    private String fecha_nacimiento; // Usa guion bajo
    private int EDAD;                // Todo mayúsculas
}

Nombres en Plural para Colecciones

public class Biblioteca {
    // ✓ Correcto: plural indica que son múltiples
    private List<Libro> libros;
    private List<Socio> socios;
    private List<Prestamo> prestamos;
    
    // ❌ Incorrecto: singular para colecciones
    private List<Libro> libro;      // Confuso: ¿es uno o varios?
    private List<Socio> listaSocio; // Redundante y mal nombrado
}

public class Equipo {
    // ✓ Correcto
    private List<Jugador> jugadores;
    private List<Partido> partidosJugados;
    
    // ❌ Incorrecto
    private List<Jugador> jugador;
    private List<Partido> listaDePartidos;  // "lista" es redundante
}

Contraste con atributos singulares:

public class Auto {
    // Singular: hay exactamente uno
    private Motor motor;
    private Conductor conductor;
    private String patente;
    
    // Plural: hay cero o más
    private List<Pasajero> pasajeros;
    private List<Multa> multas;
}

Nombres de Métodos

Los métodos se nombran en camelCase y generalmente comienzan con un verbo que indica la acción:

public class CuentaBancaria {
    // ✓ Correcto: verbos que indican acción
    public void depositar(double monto) { }
    public void extraer(double monto) { }
    public void transferirA(CuentaBancaria destino, double monto) { }
    
    // ✓ Correcto: consultas con verbos
    public double consultarSaldo() { }
    public boolean puedeExtraer(double monto) { }
    public boolean estaActiva() { }
    
    // ❌ Incorrecto: no comunican acción
    public void saldo() { }          // ¿Consulta o modifica?
    public void monto(double m) { }  // ¿Qué hace con el monto?
}

Patrones comunes de nombres de métodos:

PatrónUsoEjemplos
verboAcción simpledepositar(), guardar(), eliminar()
verboSustantivoAcción sobre algoagregarLibro(), calcularTotal()
verboPreposiciónAcción con destinotransferirA(), enviarPor()
esSustantivo / estáAdjetivoConsulta booleanaesValido(), estaVacio(), estaActivo()
tieneSustantivoConsulta de existenciatieneDeuda(), tieneSocio()
puedeverboConsulta de capacidadpuedeExtraer(), puedePrestar()
obtenerSustantivoConsulta de valorobtenerSaldo(), obtenerNombre()
calcularSustantivoCálculocalcularInteres(), calcularEdad()

Métodos que Operan sobre Colecciones

Cuando un método opera sobre una colección interna, el nombre debe reflejar la operación sobre un elemento de esa colección:

public class Biblioteca {
    private List<Libro> libros;
    private List<Socio> socios;
    
    // ✓ Correcto: operan sobre UN elemento de la colección
    public void agregarLibro(Libro libro) {
        libros.add(libro);
    }
    
    public void quitarLibro(Libro libro) {
        libros.remove(libro);
    }
    
    public void registrarSocio(Socio socio) {
        socios.add(socio);
    }
    
    public void darDeBajaSocio(Socio socio) {
        socios.remove(socio);
    }
    
    // ✓ Consultas sobre la colección
    public int cantidadLibros() {
        return libros.size();
    }
    
    public boolean tieneSocio(Socio socio) {
        return socios.contains(socio);
    }
    
    // ❌ Incorrecto: nombres confusos
    public void libros(Libro l) { }      // ¿Qué hace?
    public void addLibro(Libro l) { }    // Mezcla inglés/español
    public void setLibros(List<Libro> l) { } // Setter genérico (evitar)
}

Nombres de Parámetros

Los parámetros siguen camelCase y deben tener nombres descriptivos:

public class Transferencia {
    // ✓ Correcto: parámetros descriptivos
    public void ejecutar(CuentaBancaria origen, 
                         CuentaBancaria destino, 
                         double monto) {
        origen.extraer(monto);
        destino.depositar(monto);
    }
    
    // ❌ Incorrecto: nombres crípticos
    public void ejecutar(CuentaBancaria c1, 
                         CuentaBancaria c2, 
                         double m) {
        // ¿Cuál es origen y cuál destino?
    }
}

Constantes

Las constantes (atributos static final) se nombran en SCREAMING_SNAKE_CASE: todo mayúsculas con guiones bajos:

public class Configuracion {
    // ✓ Correcto: constantes en mayúsculas
    public static final int MAX_INTENTOS = 3;
    public static final double TASA_INTERES = 0.05;
    public static final String MENSAJE_ERROR = "Operación inválida";
    
    // ❌ Incorrecto
    public static final int maxIntentos = 3;    // Parece variable
    public static final int Max_Intentos = 3;   // Mezcla estilos
}

Resumen de Convenciones

ElementoConvenciónEjemplo
ClasePascalCase (sustantivo)CuentaBancaria, RegistroVentas
AtributocamelCasesaldoActual, fechaCreacion
Atributo coleccióncamelCase plurallibros, prestamos, jugadores
MétodocamelCase (verbo)depositar(), calcularTotal()
ParámetrocamelCase descriptivomontoDeposito, cuentaDestino
ConstanteSCREAMING_SNAKE_CASEMAX_REINTENTOS, TASA_IVA

La sobrecarga (overloading) es la capacidad de definir múltiples métodos con el mismo nombre dentro de la misma clase, siempre que difieran en su lista de parámetros (cantidad, tipos, o ambos).

¿Qué es la Sobrecarga?

Cuando Java encuentra una llamada a un método, determina cuál versión invocar basándose en los argumentos proporcionados. Esto se conoce como resolución de sobrecarga y ocurre en tiempo de compilación.

public class Calculadora {
    // Versión 1: suma de dos enteros
    public int sumar(int a, int b) {
        return a + b;
    }
    
    // Versión 2: suma de tres enteros
    public int sumar(int a, int b, int c) {
        return a + b + c;
    }
    
    // Versión 3: suma de dos decimales
    public double sumar(double a, double b) {
        return a + b;
    }
}

Uso:

Calculadora calc = new Calculadora();

int r1 = calc.sumar(5, 3);          // Invoca versión 1 → 8
int r2 = calc.sumar(5, 3, 2);       // Invoca versión 2 → 10
double r3 = calc.sumar(5.5, 3.2);   // Invoca versión 3 → 8.7

Java selecciona automáticamente la versión correcta según los argumentos.

¿Por Qué es Útil la Sobrecarga?

La sobrecarga proporciona flexibilidad y comodidad al usuario de la clase:

  1. Misma operación, diferentes datos: Permite realizar la “misma” acción con distintos tipos o cantidades de información.

  2. Valores por defecto implícitos: Versiones con menos parámetros pueden asumir valores predeterminados.

  3. Interfaz intuitiva: El usuario no necesita recordar nombres diferentes para variantes de la misma operación.

Reglas de la Sobrecarga

Ejemplos válidos e inválidos:

public class Ejemplo {
    // ✓ VÁLIDO: diferente cantidad de parámetros
    public void metodo(int a) { }
    public void metodo(int a, int b) { }
    
    // ✓ VÁLIDO: diferente tipo de parámetro
    public void metodo(double a) { }
    
    // ✓ VÁLIDO: diferente orden de tipos
    public void metodo(int a, String b) { }
    public void metodo(String a, int b) { }
    
    // ✗ INVÁLIDO: solo cambia el nombre del parámetro
    // public void metodo(int x) { }  // ERROR: ya existe metodo(int)
    
    // ✗ INVÁLIDO: solo cambia el tipo de retorno
    // public String metodo(int a) { }  // ERROR: ya existe metodo(int)
}

Sobrecarga de Constructores

Es extremadamente común sobrecargar constructores para ofrecer diferentes formas de crear objetos:

public class Persona {
    private String nombre;
    private int edad;
    private String email;
    private String telefono;
    
    // Constructor completo: todos los datos
    public Persona(String nombre, int edad, String email, String telefono) {
        this.nombre = nombre;
        this.edad = edad;
        this.email = email;
        this.telefono = telefono;
    }
    
    // Constructor parcial: sin teléfono
    public Persona(String nombre, int edad, String email) {
        this(nombre, edad, email, "No especificado");
    }
    
    // Constructor mínimo: solo datos obligatorios
    public Persona(String nombre, int edad) {
        this(nombre, edad, "sin-email@example.com", "No especificado");
    }
    
    // Constructor con valores por defecto
    public Persona(String nombre) {
        this(nombre, 0, "sin-email@example.com", "No especificado");
    }
}

Uso:

// Diferentes formas de crear el mismo tipo de objeto
Persona p1 = new Persona("Carlos", 35, "carlos@mail.com", "261-555-1234");
Persona p2 = new Persona("Laura", 42, "laura@mail.com");
Persona p3 = new Persona("Pedro", 28);
Persona p4 = new Persona("Ana");

Sobrecarga de Métodos Regulares

Los métodos ordinarios también pueden sobrecargarse:

public class Impresora {
    // Imprimir un mensaje simple
    public void imprimir(String mensaje) {
        System.out.println(mensaje);
    }
    
    // Imprimir un mensaje con formato
    public void imprimir(String mensaje, boolean mayusculas) {
        if (mayusculas) {
            System.out.println(mensaje.toUpperCase());
        } else {
            System.out.println(mensaje);
        }
    }
    
    // Imprimir múltiples veces
    public void imprimir(String mensaje, int veces) {
        for (int i = 0; i < veces; i++) {
            System.out.println(mensaje);
        }
    }
    
    // Imprimir un número
    public void imprimir(int numero) {
        System.out.println("Número: " + numero);
    }
}

Uso:

Impresora imp = new Impresora();

imp.imprimir("Hola");                    // "Hola"
imp.imprimir("Hola", true);              // "HOLA"
imp.imprimir("Hola", 3);                 // "Hola" (3 veces)
imp.imprimir(42);                        // "Número: 42"

Sobrecarga vs Parámetros Opcionales

Algunos lenguajes (como Python o JavaScript) permiten parámetros opcionales con valores por defecto:

# Python (NO es Java)
def saludar(nombre, formal=False):
    if formal:
        print(f"Estimado/a {nombre}")
    else:
        print(f"Hola {nombre}")

Java no soporta parámetros opcionales directamente, por lo que la sobrecarga es la alternativa:

public class Saludador {
    public void saludar(String nombre) {
        saludar(nombre, false);  // Delega con valor por defecto
    }
    
    public void saludar(String nombre, boolean formal) {
        if (formal) {
            System.out.println("Estimado/a " + nombre);
        } else {
            System.out.println("Hola " + nombre);
        }
    }
}

Implementación de Asociaciones en Java

Las asociaciones se implementan mediante atributos que contienen referencias a objetos de la otra clase.

Asociación Uno a Uno (1:1)

Asociación uno a uno (1:1) entre Persona y DNI.

Figure 3:Asociación uno a uno (1:1) entre Persona y DNI.

Implementación:

public class DNI {
    private String numero;
    private LocalDate fechaEmision;
    
    public DNI(String numero, LocalDate fechaEmision) {
        this.numero = numero;
        this.fechaEmision = fechaEmision;
    }
    
    public String getNumero() {
        return numero;
    }
}

public class Persona {
    private String nombre;
    private DNI documento;  // Asociación 1:1
    
    public Persona(String nombre, DNI documento) {
        this.nombre = nombre;
        this.documento = documento;
    }
    
    public String getNumeroDocumento() {
        return documento.getNumero();  // Delega al objeto asociado
    }
}

Asociación Uno a Muchos (1:*)

Asociación uno a muchos (1:*) entre Persona y Libro.

Figure 4:Asociación uno a muchos (1:*) entre Persona y Libro.

Implementación usando colecciones:

import java.util.ArrayList;
import java.util.List;

public class Libro {
    private String titulo;
    private String autor;
    
    public Libro(String titulo, String autor) {
        this.titulo = titulo;
        this.autor = autor;
    }
    
    public String getTitulo() {
        return titulo;
    }
}

public class Persona {
    private String nombre;
    private List<Libro> libros;  // Asociación 1:* usando ArrayList
    
    public Persona(String nombre) {
        this.nombre = nombre;
        this.libros = new ArrayList<>();  // Inicializar la colección
    }
    
    public void agregarLibro(Libro libro) {
        libros.add(libro);
    }
    
    public void quitarLibro(Libro libro) {
        libros.remove(libro);
    }
    
    public int cantidadLibros() {
        return libros.size();
    }
    
    public List<Libro> getLibros() {
        // Retornar copia para proteger la colección interna
        return new ArrayList<>(libros);
    }
}

Uso:

Persona juan = new Persona("Juan");
Libro libro1 = new Libro("1984", "George Orwell");
Libro libro2 = new Libro("El Principito", "Saint-Exupéry");

juan.agregarLibro(libro1);
juan.agregarLibro(libro2);

System.out.println(juan.cantidadLibros());  // 2

Asociación Bidireccional

A veces, la navegación debe ser posible en ambas direcciones:

Asociación bidireccional entre Persona y Libro. Ambos lados mantienen la referencia al otro.

Figure 5:Asociación bidireccional entre Persona y Libro. Ambos lados mantienen la referencia al otro.

Implementación:

public class Libro {
    private String titulo;
    private Persona propietario;  // Navegación inversa
    
    public Libro(String titulo) {
        this.titulo = titulo;
        this.propietario = null;
    }
    
    public void setPropietario(Persona propietario) {
        this.propietario = propietario;
    }
    
    public Persona getPropietario() {
        return propietario;
    }
}

public class Persona {
    private String nombre;
    private List<Libro> libros;
    
    public Persona(String nombre) {
        this.nombre = nombre;
        this.libros = new ArrayList<>();
    }
    
    public void agregarLibro(Libro libro) {
        libros.add(libro);
        libro.setPropietario(this);  // Mantener consistencia bidireccional
    }
    
    public void quitarLibro(Libro libro) {
        if (libros.remove(libro)) {
            libro.setPropietario(null);  // Mantener consistencia
        }
    }
}

Composición

La composición es un tipo especial de asociación que representa una relación fuerte de “todo/parte” donde:

Esta relación corresponde conceptualmente a lo que se describió en Identificación de Relaciones como una relación de pertenencia estricta.

Representación UML

La composición se representa con un rombo relleno (♦) del lado del “todo”:

Diagrama de clases UML que representa la composición entre Auto y Motor.

Figure 6:Diagrama de clases UML que representa la composición entre Auto y Motor.

Implementación de Composición

La característica distintiva en código: el todo crea la parte dentro de sí mismo, típicamente en el constructor.

public class Motor {
    private int cilindros;
    private int potenciaHP;
    private boolean encendido;
    
    public Motor(int cilindros, int potenciaHP) {
        this.cilindros = cilindros;
        this.potenciaHP = potenciaHP;
        this.encendido = false;
    }
    
    public void encender() {
        encendido = true;
        System.out.println("Motor encendido: " + potenciaHP + " HP");
    }
    
    public void apagar() {
        encendido = false;
        System.out.println("Motor apagado");
    }
    
    public boolean estaEncendido() {
        return encendido;
    }
}

public class Auto {
    private String modelo;
    private int anio;
    private Motor motor;  // Composición: el auto "tiene un" motor
    
    public Auto(String modelo, int anio, int cilindros, int potencia) {
        this.modelo = modelo;
        this.anio = anio;
        
        // ═══════════════════════════════════════════════════════════
        // CLAVE DE LA COMPOSICIÓN: el Auto CREA su propio Motor
        // El Motor no existe antes del Auto, ni existirá después
        // ═══════════════════════════════════════════════════════════
        this.motor = new Motor(cilindros, potencia);
    }
    
    public void arrancar() {
        System.out.println("Arrancando " + modelo + " (" + anio + ")");
        motor.encender();  // Delega al componente
    }
    
    public void detener() {
        motor.apagar();
        System.out.println(modelo + " detenido");
    }
    
    public boolean estaEnMarcha() {
        return motor.estaEncendido();
    }
}

Uso:

// Se crea el auto, y automáticamente se crea su motor interno
Auto miAuto = new Auto("Fiat 147", 1985, 4, 65);

miAuto.arrancar();
// Salida:
// Arrancando Fiat 147 (1985)
// Motor encendido: 65 HP

System.out.println(miAuto.estaEnMarcha());  // true

// Cuando miAuto deje de existir (garbage collected),
// su motor interno también dejará de existir.

Composición con Múltiples Partes

Un objeto puede estar compuesto por múltiples partes:

public class Computadora {
    private Procesador cpu;
    private MemoriaRAM ram;
    private DiscoDuro disco;
    
    public Computadora(String modeloCPU, int velocidadGHz,
                       int gbRAM, int gbDisco) {
        // La computadora crea todos sus componentes
        this.cpu = new Procesador(modeloCPU, velocidadGHz);
        this.ram = new MemoriaRAM(gbRAM);
        this.disco = new DiscoDuro(gbDisco);
    }
    
    public void encender() {
        cpu.iniciar();
        ram.inicializar();
        disco.montar();
        System.out.println("Computadora lista");
    }
}

Agregación

La agregación es una relación de “todo/parte” más débil que la composición:

Representación UML

La agregación se representa con un rombo vacío (◇) del lado del contenedor:

Diagrama de clases UML que representa la agregación entre Auto y Conductor.

Figure 7:Diagrama de clases UML que representa la agregación entre Auto y Conductor.

Implementación de Agregación

La característica distintiva: el todo recibe la parte como parámetro (no la crea).

public class Conductor {
    private String nombre;
    private String licencia;
    
    public Conductor(String nombre, String licencia) {
        this.nombre = nombre;
        this.licencia = licencia;
    }
    
    public void conducir() {
        System.out.println(nombre + " está conduciendo");
    }
    
    public String getNombre() {
        return nombre;
    }
}

public class Auto {
    private String modelo;
    private int anio;
    private Conductor conductor;  // Agregación: el auto "tiene un" conductor
    
    public Auto(String modelo, int anio) {
        this.modelo = modelo;
        this.anio = anio;
        this.conductor = null;  // El auto puede existir SIN conductor
    }
    
    // ═══════════════════════════════════════════════════════════
    // CLAVE DE LA AGREGACIÓN: el Auto RECIBE un Conductor externo
    // El Conductor existe antes y después de la asociación
    // ═══════════════════════════════════════════════════════════
    public void asignarConductor(Conductor conductor) {
        this.conductor = conductor;
    }
    
    public void liberarConductor() {
        this.conductor = null;
    }
    
    public void iniciarViaje() {
        if (conductor != null) {
            System.out.println("Viaje en " + modelo);
            conductor.conducir();
        } else {
            System.out.println("No hay conductor asignado al " + modelo);
        }
    }
    
    public boolean tieneConductor() {
        return conductor != null;
    }
}

Uso:

// El conductor existe ANTES de cualquier auto
Conductor pedro = new Conductor("Pedro García", "B-12345678");

// Los autos se crean SIN conductor
Auto auto1 = new Auto("Ford Ka", 2020);
Auto auto2 = new Auto("Chevrolet Corsa", 2018);

// Se asigna el mismo conductor a un auto
auto1.asignarConductor(pedro);
auto1.iniciarViaje();
// Salida: Viaje en Ford Ka
//         Pedro García está conduciendo

// El conductor puede ser reasignado a otro auto
auto1.liberarConductor();
auto2.asignarConductor(pedro);
auto2.iniciarViaje();
// Salida: Viaje en Chevrolet Corsa
//         Pedro García está conduciendo

// Si auto1 se destruye, pedro sigue existiendo
auto1 = null;  // auto1 será garbage collected
// pedro sigue siendo un objeto válido
System.out.println(pedro.getNombre());  // "Pedro García"

Comparación: Composición vs Agregación

AspectoComposición ♦Agregación ◇
Fuerza de relaciónFuerte (“es parte esencial de”)Débil (“está asociado con”)
Ciclo de vidaLa parte depende del todoLa parte es independiente
Creación de la parteEl todo crea la parteLa parte existe externamente
ExclusividadLa parte es exclusiva del todoLa parte puede compartirse
En códigonew Parte() en constructorParte recibida como parámetro
Existencia sin relaciónLa parte no existe solaLa parte puede existir sola
EjemploAuto-Motor, Casa-HabitaciónAuto-Conductor, Equipo-Jugador

¿Cuándo Usar Cada Una?

Usá Composición cuando:

Ejemplos de composición:

Usá Agregación cuando:

Ejemplos de agregación:

Ejemplo Mixto: Auto con Composición y Agregación

Un diseño realista suele combinar ambos tipos de relaciones:

public class Auto {
    private String patente;
    
    // COMPOSICIÓN: el auto CREA sus propias partes internas
    private Motor motor;           // ♦ No existe sin el auto
    private Chasis chasis;         // ♦ No existe sin el auto
    private SistemaElectrico electrica;  // ♦ No existe sin el auto
    
    // AGREGACIÓN: el auto RECIBE elementos externos
    private Conductor conductor;    // ◇ Existe independientemente
    private Seguro seguro;         // ◇ Puede cambiarse
    private GPS gps;               // ◇ Puede quitarse y ponerse
    
    public Auto(String patente, int cilindros, int potencia) {
        this.patente = patente;
        
        // Composición: crear las partes esenciales
        this.motor = new Motor(cilindros, potencia);
        this.chasis = new Chasis(patente);
        this.electrica = new SistemaElectrico();
        
        // Agregación: inicialmente sin elementos opcionales
        this.conductor = null;
        this.seguro = null;
        this.gps = null;
    }
    
    // Métodos para agregaciones
    public void asignarConductor(Conductor c) { this.conductor = c; }
    public void contratarSeguro(Seguro s) { this.seguro = s; }
    public void instalarGPS(GPS g) { this.gps = g; }
    public void desinstalarGPS() { this.gps = null; }
}

---

(bloque4-integracion-practica)=
## Bloque 4: Integración y Práctica

En esta sección integramos todos los conceptos vistos en ejemplos completos que demuestran cómo trabajar con clases, encapsulamiento, sobrecarga, y relaciones entre objetos.

(ejemplo-completo-biblioteca)=
### Ejemplo Completo: Sistema de Biblioteca

Desarrollemos un sistema simplificado de biblioteca que integre todos los conceptos:

```java
import java.util.ArrayList;
import java.util.List;

// ═══════════════════════════════════════════════════════════════════════════
// CLASE LIBRO
// Representa un libro físico en la biblioteca.
// Demuestra: encapsulamiento, getters, métodos de comportamiento
// ═══════════════════════════════════════════════════════════════════════════
public class Libro {
    // Atributos privados (encapsulamiento)
    private final String isbn;        // Inmutable: identificador único
    private final String titulo;      // Inmutable: no cambia
    private final String autor;       // Inmutable: no cambia
    private boolean disponible;       // Mutable: cambia con préstamos
    
    // Constructor: garantiza estado inicial válido
    public Libro(String isbn, String titulo, String autor) {
        // Validaciones (ver {ref}`construccion-de-objetos`)
        if (isbn == null || isbn.isEmpty()) {
            throw new IllegalArgumentException("ISBN requerido");
        }
        if (titulo == null || titulo.isEmpty()) {
            throw new IllegalArgumentException("Título requerido");
        }
        
        this.isbn = isbn;
        this.titulo = titulo;
        this.autor = autor;
        this.disponible = true;  // Los libros nacen disponibles
    }
    
    // Constructor sobrecargado: autor desconocido
    public Libro(String isbn, String titulo) {
        this(isbn, titulo, "Autor desconocido");
    }
    
    // Getters para atributos
    public String getIsbn() {
        return isbn;
    }
    
    public String getTitulo() {
        return titulo;
    }
    
    public String getAutor() {
        return autor;
    }
    
    public boolean isDisponible() {
        return disponible;
    }
    
    // Métodos de comportamiento (no simples setters)
    public boolean prestar() {
        if (disponible) {
            disponible = false;
            return true;
        }
        return false;  // Ya estaba prestado
    }
    
    public void devolver() {
        disponible = true;
    }
    
    @Override
    public String toString() {
        String estado = disponible ? "Disponible" : "Prestado";
        return titulo + " (" + autor + ") [" + estado + "]";
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// CLASE SOCIO
// Representa un socio de la biblioteca.
// Demuestra: agregación (tiene libros prestados), validaciones
// ═══════════════════════════════════════════════════════════════════════════
public class Socio {
    private final int numeroSocio;
    private String nombre;
    private String email;
    private List<Libro> librosPrestados;  // AGREGACIÓN con Libro
    private static final int MAX_LIBROS = 5;
    
    public Socio(int numeroSocio, String nombre, String email) {
        if (nombre == null || nombre.isEmpty()) {
            throw new IllegalArgumentException("Nombre requerido");
        }
        
        this.numeroSocio = numeroSocio;
        this.nombre = nombre;
        this.email = email;
        this.librosPrestados = new ArrayList<>();
    }
    
    // Constructor sobrecargado: sin email
    public Socio(int numeroSocio, String nombre) {
        this(numeroSocio, nombre, null);
    }
    
    // Getters
    public int getNumeroSocio() {
        return numeroSocio;
    }
    
    public String getNombre() {
        return nombre;
    }
    
    public int cantidadLibrosPrestados() {
        return librosPrestados.size();
    }
    
    public boolean puedeTomarPrestado() {
        return librosPrestados.size() < MAX_LIBROS;
    }
    
    // Comportamiento: tomar un libro prestado
    // El libro existe EXTERNAMENTE (agregación)
    public boolean tomarPrestado(Libro libro) {
        if (!puedeTomarPrestado()) {
            System.out.println(nombre + " ya tiene " + MAX_LIBROS + " libros");
            return false;
        }
        
        if (libro.prestar()) {
            librosPrestados.add(libro);
            System.out.println(nombre + " tomó prestado: " + libro.getTitulo());
            return true;
        } else {
            System.out.println("El libro no está disponible");
            return false;
        }
    }
    
    // Comportamiento: devolver un libro
    public boolean devolverLibro(Libro libro) {
        if (librosPrestados.remove(libro)) {
            libro.devolver();
            System.out.println(nombre + " devolvió: " + libro.getTitulo());
            return true;
        }
        return false;  // No tenía ese libro
    }
    
    // Devolver todos los libros
    public void devolverTodos() {
        for (Libro libro : librosPrestados) {
            libro.devolver();
        }
        librosPrestados.clear();
        System.out.println(nombre + " devolvió todos los libros");
    }
    
    public List<Libro> getLibrosPrestados() {
        // Retornar copia defensiva
        return new ArrayList<>(librosPrestados);
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// CLASE ESTANTERIA
// Representa una estantería física que contiene libros.
// Demuestra: composición (crea sus propios libros)
// ═══════════════════════════════════════════════════════════════════════════
public class Estanteria {
    private final int numero;
    private final String seccion;
    private List<Libro> libros;  // COMPOSICIÓN: la estantería crea sus libros
    
    public Estanteria(int numero, String seccion) {
        this.numero = numero;
        this.seccion = seccion;
        this.libros = new ArrayList<>();
    }
    
    public int getNumero() {
        return numero;
    }
    
    public String getSeccion() {
        return seccion;
    }
    
    // COMPOSICIÓN: la estantería CREA el libro internamente
    public Libro agregarLibro(String isbn, String titulo, String autor) {
        Libro nuevoLibro = new Libro(isbn, titulo, autor);
        libros.add(nuevoLibro);
        return nuevoLibro;  // Retorna referencia para poder prestarlo
    }
    
    // Sobrecarga: agregar sin autor
    public Libro agregarLibro(String isbn, String titulo) {
        return agregarLibro(isbn, titulo, "Autor desconocido");
    }
    
    public int cantidadLibros() {
        return libros.size();
    }
    
    public int cantidadDisponibles() {
        int count = 0;
        for (Libro libro : libros) {
            if (libro.isDisponible()) {
                count++;
            }
        }
        return count;
    }
    
    public void listarLibros() {
        System.out.println("═══ Estantería " + numero + " (" + seccion + ") ═══");
        for (Libro libro : libros) {
            System.out.println("  • " + libro);
        }
    }
    
    public Libro buscarPorTitulo(String titulo) {
        for (Libro libro : libros) {
            if (libro.getTitulo().equalsIgnoreCase(titulo)) {
                return libro;
            }
        }
        return null;
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// PROGRAMA PRINCIPAL: DEMOSTRACIÓN
// ═══════════════════════════════════════════════════════════════════════════
public class SistemaBiblioteca {
    public static void main(String[] args) {
        // Crear estanterías (las estanterías CREAN sus libros: composición)
        Estanteria ficcion = new Estanteria(1, "Ficción");
        Estanteria ciencia = new Estanteria(2, "Ciencia");
        
        // Agregar libros (composición: la estantería los crea)
        Libro libro1 = ficcion.agregarLibro("978-1", "Cien años de soledad", "García Márquez");
        Libro libro2 = ficcion.agregarLibro("978-2", "1984", "George Orwell");
        Libro libro3 = ciencia.agregarLibro("978-3", "Cosmos", "Carl Sagan");
        
        // Crear socios (existen independientemente de los libros)
        Socio maria = new Socio(1001, "María López", "maria@email.com");
        Socio juan = new Socio(1002, "Juan Pérez");  // Constructor sobrecargado
        
        // Mostrar estado inicial
        ficcion.listarLibros();
        ciencia.listarLibros();
        
        System.out.println("\n─── Préstamos ───");
        
        // María toma prestados libros (agregación: recibe libros externos)
        maria.tomarPrestado(libro1);  // OK
        maria.tomarPrestado(libro3);  // OK
        
        // Juan intenta el mismo libro
        juan.tomarPrestado(libro1);   // Falla: ya prestado
        juan.tomarPrestado(libro2);   // OK
        
        System.out.println("\n─── Estado después de préstamos ───");
        ficcion.listarLibros();
        ciencia.listarLibros();
        
        System.out.println("\nLibros de María: " + maria.cantidadLibrosPrestados());
        System.out.println("Libros de Juan: " + juan.cantidadLibrosPrestados());
        
        System.out.println("\n─── Devoluciones ───");
        maria.devolverLibro(libro1);
        
        System.out.println("\n─── Estado final ───");
        ficcion.listarLibros();
    }
}

Salida del programa:

═══ Estantería 1 (Ficción) ═══
  • Cien años de soledad (García Márquez) [Disponible]
  • 1984 (George Orwell) [Disponible]
═══ Estantería 2 (Ciencia) ═══
  • Cosmos (Carl Sagan) [Disponible]

─── Préstamos ───
María López tomó prestado: Cien años de soledad
María López tomó prestado: Cosmos
El libro no está disponible
Juan Pérez tomó prestado: 1984

─── Estado después de préstamos ───
═══ Estantería 1 (Ficción) ═══
  • Cien años de soledad (García Márquez) [Prestado]
  • 1984 (George Orwell) [Prestado]
═══ Estantería 2 (Ciencia) ═══
  • Cosmos (Carl Sagan) [Prestado]

Libros de María: 2
Libros de Juan: 1

─── Devoluciones ───
María López devolvió: Cien años de soledad

─── Estado final ───
═══ Estantería 1 (Ficción) ═══
  • Cien años de soledad (García Márquez) [Disponible]
  • 1984 (George Orwell) [Prestado]

Del Diagrama al Código: Proceso Sistemático

El proceso completo de transformación desde el análisis hasta el código funcional sigue estos pasos:

Paso 1: Análisis (Capítulo Anterior)

Usando las La Heurística Lingüística, identificamos:

Paso 2: Diseño del Diagrama de Clases

Dibujamos el diagrama UML identificando:

Diagrama de clases completo para el sistema de biblioteca, integrando composición y agregación.

Figure 8:Diagrama de clases completo para el sistema de biblioteca, integrando composición y agregación.

Paso 3: Implementación en Java

Para cada clase del diagrama:

  1. Declarar la clase: public class NombreClase { }

  2. Definir atributos privados:

    private tipo nombreAtributo;
  3. Crear constructor(es):

    • Validar parámetros

    • Inicializar todos los atributos

    • Considerar sobrecarga para diferentes formas de construcción

  4. Implementar métodos públicos:

    • Getters para atributos que se necesiten leer

    • Métodos de comportamiento (no solo setters)

    • Usar this para referencias internas

  5. Establecer relaciones:

    • Composición: Crear objetos internos en el constructor

    • Agregación: Recibir objetos como parámetros


Resumen

Este capítulo cubrió la transición desde el diseño conceptual (las “burbujas” del diagrama de clases) hacia código funcional en Java. Los conceptos clave son:

Sintaxis de Clases en Java

Encapsulamiento en Práctica

Convenciones de Nomenclatura

ElementoConvenciónEjemplo
ClasePascalCaseCuentaBancaria
Atributo singularcamelCasesaldoActual
Atributo coleccióncamelCase plurallibros, socios
MétodocamelCase (verbo)depositar(), agregarLibro()
ConstanteSCREAMING_SNAKE_CASEMAX_INTENTOS

Sobrecarga de Métodos

Relaciones entre Clases

TipoSímbolo UMLCiclo de vidaImplementación
Asociación───IndependienteAtributo referencia
ComposiciónDependiente (parte del todo)new en constructor
AgregaciónIndependienteRecibido como parámetro

Cardinalidad

Conexión con Conceptos Previos

Este capítulo materializa los conceptos abstractos del capítulo Fundamentos de la Programación Orientada a Objetos:

Concepto abstractoImplementación Java
Clasepublic class NombreClase { }
Objetonew NombreClase(args)
Atributoprivate tipo nombre;
Métodopublic tipoRetorno nombre(params) { }
ConstrucciónConstructor
la sección de encapsulamiento en el capítulo anteriorModificadores de acceso
MensajeInvocación de métodos
Identificación de RelacionesComposición y agregación

Próximos Pasos

Con los fundamentos de sintaxis, encapsulamiento y asociaciones establecidos, la siguiente unidad abordará conceptos más avanzados del paradigma:

Estos conceptos permitirán crear diseños más flexibles y aprovechar al máximo la reutilización de código que ofrece el paradigma orientado a objetos.


Ejercicios

Solution to Exercise 1

Errores identificados y correcciones:

  1. Classclass (minúscula)

  2. String nombreprivate String nombre; (falta modificador y punto y coma)

  3. private precio double;private double precio; (orden incorrecto)

  4. (nombre, double precio)(String nombre, double precio) (falta tipo)

  5. Falta { después del constructor

  6. nombre = nombre;this.nombre = nombre; (ambigüedad)

  7. Falta ; después de this.precio = precio

  8. void String getNombre()public String getNombre() (conflicto de tipos)

  9. Falta ; después de return nombre

Código corregido:

public class Producto {
    private String nombre;
    private double precio;
    
    public Producto(String nombre, double precio) {
        this.nombre = nombre;
        this.precio = precio;
    }
    
    public String getNombre() {
        return nombre;
    }
}
Solution to Exercise 2
public class Empleado {
    private String nombre;
    private double salarioPorHora;
    private int horasTrabajadas;
    
    public Empleado(String nombre, double salarioPorHora) {
        if (nombre == null || nombre.isEmpty()) {
            throw new IllegalArgumentException("Nombre requerido");
        }
        if (salarioPorHora <= 0) {
            throw new IllegalArgumentException("Salario debe ser positivo");
        }
        
        this.nombre = nombre;
        this.salarioPorHora = salarioPorHora;
        this.horasTrabajadas = 0;
    }
    
    public String getNombre() {
        return nombre;
    }
    
    public double getSalarioPorHora() {
        return salarioPorHora;
    }
    
    public int getHorasTrabajadas() {
        return horasTrabajadas;
    }
    
    public void setHorasTrabajadas(int horas) {
        if (horas >= 0 && horas <= 300) {
            this.horasTrabajadas = horas;
        } else {
            throw new IllegalArgumentException(
                "Horas deben estar entre 0 y 300"
            );
        }
    }
    
    public double calcularSueldo() {
        return salarioPorHora * horasTrabajadas;
    }
}
Solution to Exercise 3

a) Factura - Items: COMPOSICIÓN ♦

  • Los items son parte esencial de la factura

  • No tienen sentido sin ella

  • Si se elimina la factura, los items también

  • La factura crea sus propios items

b) Equipo - Jugadores: AGREGACIÓN ◇

  • Un jugador existe independientemente del equipo

  • Puede cambiar de equipo (transferencias)

  • Si el equipo desaparece, los jugadores siguen existiendo

  • El equipo recibe jugadores que ya existen

c) Pedido - DireccionEntrega: COMPOSICIÓN ♦

  • La dirección es creada específicamente para ese pedido

  • Aunque físicamente la dirección exista, la instancia del objeto es del pedido

  • Cambiar la dirección original no afecta el pedido histórico

  • El pedido crea su copia de la dirección

d) Universidad - Profesores: AGREGACIÓN ◇

  • Los profesores existen como personas antes de pertenecer a la universidad

  • Pueden trabajar en múltiples universidades

  • Si la universidad cierra, los profesores siguen existiendo

  • La universidad recibe profesores existentes

e) Cuerpo - Órganos: COMPOSICIÓN ♦

  • Los órganos son partes esenciales del cuerpo

  • No funcionan independientemente del cuerpo

  • Si el cuerpo muere, los órganos dejan de funcionar

  • El cuerpo “crea” sus órganos (biológicamente)

Solution to Exercise 4
public class Mensaje {
    private String texto;
    private String destinatario;
    private String asunto;
    private String prioridad;
    
    // Constructor principal (más completo)
    public Mensaje(String texto, String destinatario, 
                   String asunto, String prioridad) {
        if (texto == null || texto.isEmpty()) {
            throw new IllegalArgumentException("Texto requerido");
        }
        
        this.texto = texto;
        this.destinatario = destinatario;
        this.asunto = asunto;
        
        // Validar prioridad
        if (prioridad.equals("alta") || 
            prioridad.equals("media") || 
            prioridad.equals("baja")) {
            this.prioridad = prioridad;
        } else {
            this.prioridad = "media";  // valor por defecto
        }
    }
    
    // Con texto, destinatario y asunto (prioridad media por defecto)
    public Mensaje(String texto, String destinatario, String asunto) {
        this(texto, destinatario, asunto, "media");
    }
    
    // Con texto y destinatario (sin asunto)
    public Mensaje(String texto, String destinatario) {
        this(texto, destinatario, "(Sin asunto)", "media");
    }
    
    // Solo con texto
    public Mensaje(String texto) {
        this(texto, "Sin destinatario", "(Sin asunto)", "media");
    }
    
    // Getters
    public String getTexto() { return texto; }
    public String getDestinatario() { return destinatario; }
    public String getAsunto() { return asunto; }
    public String getPrioridad() { return prioridad; }
}

// Uso:
Mensaje m1 = new Mensaje("Hola");
Mensaje m2 = new Mensaje("Hola", "juan@mail.com");
Mensaje m3 = new Mensaje("Hola", "juan@mail.com", "Saludo");
Mensaje m4 = new Mensaje("URGENTE", "juan@mail.com", "Alerta", "alta");

Próximo paso

Para seguir, conviene pasar a el material siguiente, donde el recorrido continúa sobre esta base.