Módulo 10: Clases y objetos

Objetivos del módulo

  • Entender por qué existe la programación orientada a objetos (POO) y qué problemas resuelve frente a la programación estructurada
  • Distinguir los tres rasgos de un objeto: estado, comportamiento e identidad
  • Escribir clases con atributos y métodos de instancia, y crear objetos con new
  • Comprender qué ocurre en memoria: la pila, el montón y las referencias
  • Manejar null y diagnosticar una NullPointerException
  • Definir constructores (por defecto, con parámetros, sobrecargados y encadenados con this(...))
  • Usar this para referirte al objeto actual
  • Reconocer los alias (varias referencias al mismo objeto) y comparar objetos correctamente
  • Saber cuándo un objeto pasa a ser basura y qué hace el recolector
  • Trabajar con arrays de objetos y dar a tus clases un toString útil
  • Organizar el código en una clase por fichero con una clase de prueba que contiene el main

1. Del paradigma estructurado al orientado a objetos

Hasta ahora has programado de forma estructurada: datos en variables y arrays, y métodos static que reciben esos datos y los procesan. Funciona muy bien para programas pequeños, pero empieza a crujir cuando el programa crece.

1.1. Un problema real: la tienda con arrays paralelos

Imagina que quieres gestionar los productos de una pequeña tienda. Con lo que sabes del módulo 08, lo natural es usar arrays paralelos: un array para los nombres, otro para los precios y otro para el stock, donde la posición i de cada uno describe el mismo producto.

public class TiendaArrays {

    // Tres arrays "paralelos": la posición i de cada uno describe el mismo producto
    static String[] nombres = {"Teclado", "Ratón", "Monitor"};
    static double[] precios = {29.99, 15.50, 189.00};
    static int[] stocks = {10, 25, 4};

    // Cada método recibe la posición y tiene que consultar los tres arrays
    static void mostrar(int i) {
        System.out.printf("%-8s %8.2f € (%d uds.)%n", nombres[i], precios[i], stocks[i]);
    }

    static boolean vender(int i, int cantidad) {
        if (cantidad > stocks[i]) {
            return false;
        }
        stocks[i] -= cantidad;
        return true;
    }

    public static void main(String[] args) {
        vender(1, 3);
        for (int i = 0; i < nombres.length; i++) {
            mostrar(i);
        }
    }
}
Teclado     29.99 € (10 uds.)
Ratón       15.50 € (22 uds.)
Monitor    189.00 € (4 uds.)

Funciona, pero tiene problemas que se agravan con el tamaño:

Problema Por qué ocurre
Datos dispersos Un producto no “existe” como tal: está repartido entre tres arrays. Nada impide que tengan longitudes distintas
Fácil equivocarse de índice precios[i] con el i de otro bucle descuadra los datos sin que el compilador diga nada
Cambios en cascada Si añades el atributo “categoría”, tienes que crear otro array y tocar todos los métodos que reciben i
Nada protege los datos Cualquier parte del programa puede poner stocks[2] = -50
Difícil de reutilizar Para llevar la gestión de productos a otro programa tienes que copiar arrays y métodos sueltos

1.2. La idea de la orientación a objetos

La programación orientada a objetos (POO) propone algo muy intuitivo: en lugar de separar datos y funciones, agrupa en una misma unidad los datos de una entidad y las operaciones que trabajan con ellos. Esa unidad es el objeto.

Comparación: arrays paralelos con funciones sueltas frente a objetos Producto que contienen sus datos y sus métodos

Figura 1. A la izquierda, un producto está repartido en tres arrays; a la derecha, cada objeto Producto contiene sus datos y sabe operar con ellos.

📘 Concepto: La programación orientada a objetos es un paradigma en el que un programa se organiza como un conjunto de objetos que colaboran entre sí enviándose mensajes (llamadas a métodos). Cada objeto agrupa datos (su estado) y operaciones (su comportamiento).

Un poco de historia para situarte: las ideas de la POO nacieron con Simula 67 (Noruega, años 60), se popularizaron con Smalltalk (años 70-80) y C++ (1985), y llegaron a las masas con Java (1995), que se diseñó orientado a objetos desde el principio. Hoy la mayoría de lenguajes de uso profesional (Java, C#, Python, Kotlin, JavaScript, PHP, Swift…) son orientados a objetos o lo admiten.

1.3. Ventajas que vas a ir descubriendo

Ventaja Qué significa Módulo
Modelado natural El código se parece al problema: hay Producto, Cliente, Pedido… 10
Encapsulación Cada objeto protege sus datos y solo deja tocarlos a través de sus métodos 11
Reutilización Una clase bien hecha se usa en muchos programas; la herencia permite ampliarla 11, 12
Polimorfismo Un mismo código funciona con objetos de tipos distintos 13
Mantenimiento Los cambios quedan localizados dentro de la clase afectada Todos

💡 Consejo: La POO no sustituye a lo que ya sabes: dentro de los métodos seguirás usando variables, if, bucles y arrays. Lo que cambia es cómo organizas el programa.


2. Objetos: estado, comportamiento e identidad

Mira a tu alrededor: tu móvil, una cuenta bancaria, un libro de la biblioteca. Todos son objetos y todos tienen tres rasgos:

📘 Concepto: Todo objeto tiene:

  • Estado: los datos que lo describen en un momento dado (los valores de sus atributos). Un producto tiene nombre “Teclado”, precio 29,99 € y 10 unidades.
  • Comportamiento: lo que sabe hacer o lo que se le puede pedir (sus métodos). Un producto puede venderse, mostrarse, calcular el valor de su stock.
  • Identidad: cada objeto es único aunque tenga el mismo estado que otro. Dos teclados idénticos en la estantería siguen siendo dos teclados distintos.
Objeto Estado (atributos) Comportamiento (métodos)
Producto nombre, precio, stock vender, reponer, mostrar
Cuenta bancaria IBAN, titular, saldo ingresar, retirar, transferir
Coche matrícula, combustible, km arrancar, conducir, repostar
Libro ISBN, título, autor, prestado prestar, devolver
Alumno nombre, notas ponerNota, calcularMedia

El comportamiento suele consultar o modificar el estado: vender 3 unidades de un producto cambia su stock; ingresar dinero cambia el saldo. Por eso tiene sentido que datos y operaciones vivan juntos.


3. Clases: la plantilla de los objetos

Si en la tienda hay 500 productos, no vas a describir 500 veces qué es un producto. Describes una vez cómo es cualquier producto y luego creas tantos como necesites. Esa descripción es la clase.

📘 Concepto: Una clase es una plantilla que define qué atributos y qué métodos tendrán los objetos de ese tipo. Un objeto (o instancia) es un ejemplar concreto creado a partir de la clase, con sus propios valores.

La analogía clásica es el molde de galletas: el molde (la clase) define la forma; con él haces muchas galletas (los objetos), cada una con su propia decoración (su estado). El molde no se come; las galletas sí. De la misma forma, la clase no guarda datos de ningún producto concreto: los guardan los objetos.

Una clase Producto en notación UML de la que salen, mediante new, tres objetos con valores distintos

Figura 2. Una única clase Producto y tres objetos creados a partir de ella. El rectángulo de la izquierda es un diagrama de clases UML simplificado (lo estudiarás a fondo en el módulo 18).

💡 Consejo: Lee el diagrama UML de arriba abajo: nombre de la clase, atributos (nombre: tipo) y métodos (nombre(parámetros): tipo de retorno). Es la forma estándar de dibujar clases en cualquier empresa o examen.

3.1. Anatomía de una clase en Java

Esta es la clase Producto de la figura, y a continuación un programa que la usa:

// Producto.java: la clase (la plantilla)
public class Producto {

    // Atributos: el ESTADO que tendrá cada producto
    String nombre;
    double precio;
    int stock;

    // Métodos de instancia: el COMPORTAMIENTO de cada producto
    void mostrar() {
        System.out.printf("%-8s %8.2f € (%d uds.)%n", nombre, precio, stock);
    }

    boolean vender(int cantidad) {
        if (cantidad > stock) {
            return false;          // no hay suficientes unidades
        }
        stock -= cantidad;         // modifica el estado de ESTE producto
        return true;
    }

    double valorStock() {
        return precio * stock;
    }
}
// PruebaProducto.java: la clase con main que usa la clase Producto
public class PruebaProducto {
    public static void main(String[] args) {
        Producto teclado = new Producto();   // se crea un objeto
        teclado.nombre = "Teclado";
        teclado.precio = 29.99;
        teclado.stock = 10;

        Producto raton = new Producto();     // otro objeto distinto
        raton.nombre = "Ratón";
        raton.precio = 15.50;
        raton.stock = 25;

        raton.vender(3);                     // solo cambia el stock del ratón

        teclado.mostrar();
        raton.mostrar();
        System.out.println("Valor del stock de ratones: " + raton.valorStock() + " €");
    }
}
Teclado     29.99 € (10 uds.)
Ratón       15.50 € (22 uds.)
Valor del stock de ratones: 341.0 €

Fíjate en lo que ha cambiado respecto a la versión con arrays:

  • Los datos de cada producto están juntos dentro de su objeto (teclado, raton).
  • vender ya no recibe un índice: se llama sobre un objeto (raton.vender(3)) y trabaja con sus datos.
  • Los métodos no son static: pertenecen a cada objeto. Es la gran diferencia con el módulo 07.
Elemento Ejemplo Qué es
Declaración de la clase public class Producto { ... } Define un nuevo tipo de dato llamado Producto
Atributos (campos, variables de instancia) String nombre; Variables declaradas dentro de la clase y fuera de los métodos. Cada objeto tiene su propia copia
Métodos de instancia boolean vender(int cantidad) Métodos sin static que operan sobre el objeto que los recibe
Constructores (sección 6) Bloques especiales que inicializan el objeto al crearlo

⚠️ Importante: Convenciones de nombres que debes respetar: las clases en UpperCamelCase y en singular (Producto, CuentaBancaria, no productos ni cuenta_bancaria); atributos y métodos en lowerCamelCase (precioUnitario, calcularTotal()). Los métodos suelen ser verbos (vender, mostrar) y los atributos, sustantivos (stock, saldo).

3.2. Atributos y sus valores por defecto

A diferencia de las variables locales, los atributos se inicializan solos al crear el objeto, con un valor por defecto que depende de su tipo. Por ejemplo, si haces new Producto() y muestras sus atributos sin haberles dado valor, verás null, 0.0 y 0: | Tipo del atributo | Valor por defecto | |——————-|——————-| | byte, short, int, long | 0 | | float, double | 0.0 | | boolean | false | | char | '\u0000' (carácter de código 0) | | Cualquier tipo referencia (String, arrays, otras clases) | null |

Las variables locales (las declaradas dentro de un método) no tienen valor por defecto. Si intentas usarlas sin asignarles nada, el compilador protesta:

public class VariableLocal {
    public static void main(String[] args) {
        int contador;                  // variable LOCAL: no tiene valor por defecto
        System.out.println(contador);  // error de compilación
    }
}
VariableLocal.java:4: error: variable contador might not have been initialized
        System.out.println(contador);  // error de compilación
                           ^
1 error

💡 Consejo: Aunque los atributos tengan valor por defecto, no confíes en él “por casualidad”: inicializa el estado de forma explícita en el constructor (sección 6). Así cualquiera que lea la clase sabe con qué valores nace un objeto.

3.3. Métodos de instancia frente a métodos static

En el módulo 07 todos los métodos llevaban static. Ahora aparecen métodos sin static. La diferencia es fundamental:

  Método static (módulo 07) Método de instancia (este módulo)
Pertenece a… La clase Cada objeto
Se llama con… NombreClase.metodo(...) o metodo(...) objeto.metodo(...)
Accede a los atributos de un objeto… Solo si se le pasa el objeto como parámetro Directamente (stock, precio)
Ejemplo Math.sqrt(16) raton.vender(3)

Cuando escribes raton.vender(3), Java ejecuta el código de vender usando los atributos de raton. Si escribes teclado.vender(3), el mismo código trabaja con los atributos de teclado. En el módulo 11 estudiarás static a fondo y verás cuándo conviene cada uno.

✏️ Ejercicio: Añade a Producto un método void reponer(int cantidad) que sume unidades al stock y pruébalo en PruebaProducto con el teclado. ¿Cambia el stock del ratón?


4. Crear objetos con new

4.1. Declarar, crear e inicializar

Crear un objeto implica tres cosas, que normalmente escribes en una sola línea:

Producto teclado = new Producto();
//  (1)      (2)    (3)    (4)
  1. Tipo: la clase actúa como tipo de la variable, igual que int o String.
  2. Variable de referencia: teclado no es el objeto; es una variable que guardará la dirección del objeto.
  3. new: reserva memoria para un objeto nuevo en el montón, pone sus atributos a su valor por defecto y ejecuta el constructor.
  4. Llamada al constructor: inicializa el objeto (sección 6). new devuelve la referencia al objeto creado, que se guarda en teclado.

También puedes separar la declaración de la creación:

Producto teclado;            // declaración: la variable existe, pero no hay objeto
teclado = new Producto();    // creación: ahora sí apunta a un objeto

Una vez creado, accedes a sus atributos y métodos con el operador punto: teclado.precio = 29.99;, teclado.mostrar();.

4.2. Qué ocurre en memoria: pila y montón

Para entender de verdad los objetos tienes que saber dónde vive cada cosa. La JVM organiza la memoria de tu programa principalmente en dos zonas:

📘 Concepto:

  • La pila (stack) guarda las variables locales y los parámetros de cada método que se está ejecutando, en un bloque llamado marco (frame). Cuando el método termina, su marco desaparece.
  • El montón (heap) guarda todos los objetos creados con new (también los arrays y los String). Los objetos no desaparecen al terminar el método: siguen ahí mientras alguien los referencie.

Analiza este programa y después la figura, que muestra la memoria justo antes de terminar main:

public class Memoria {
    public static void main(String[] args) {
        int unidades = 3;                    // primitivo: el valor está en la pila
        Producto p1 = new Producto();        // p1 guarda una referencia al objeto
        p1.nombre = "Teclado";
        p1.precio = 29.99;
        p1.stock = 10;

        Producto p2 = new Producto();
        p2.nombre = "Ratón";
        p2.precio = 15.5;
        p2.stock = 25;

        Producto p3 = null;                  // p3 no apunta a ningún objeto

        System.out.println(unidades + " " + p1.nombre + " " + p2.nombre + " " + p3);
    }
}
3 Teclado Ratón null

Diagrama de memoria: en la pila, el marco de main con unidades=3, p1 y p2 con flechas a objetos Producto en el montón y p3 a null; cada Producto apunta a su vez a un objeto String

Figura 3. La variable primitiva unidades contiene directamente el valor 3. Las variables p1 y p2 contienen referencias (flechas) a objetos del montón. p3 vale null. Observa que el atributo nombre también es una referencia, en este caso a un objeto String.

  Tipos primitivos Tipos referencia
Ejemplos int, double, boolean, char String, arrays, Producto, Scanner…
La variable contiene… El valor Una referencia (dirección) al objeto
Dónde está el dato En la propia variable En el montón
Valor “vacío” No existe (0, false…) null
Asignación a = b Copia el valor Copia la referencia (¡los dos apuntan al mismo objeto!)
Comparación a == b Compara valores Compara si son el mismo objeto

💡 Consejo: Cuando un programa con objetos no haga lo que esperas, dibuja la pila y el montón como en la figura 3: cajitas para las variables y flechas hacia los objetos. Es la técnica que usan los profesores para corregir y la que te salvará en los exámenes de trazas.

4.3. Pasar objetos a métodos

En el módulo 07 aprendiste que Java pasa los argumentos por valor: el método recibe una copia. Con objetos, lo que se copia es la referencia. Esto tiene una consecuencia importantísima:

public class PasoDeObjetos {

    // Recibe una COPIA de la referencia: apunta al mismo objeto que la del llamador
    static void subirPrecio(Producto p, double porcentaje) {
        p.precio += p.precio * porcentaje / 100;   // modifica el objeto compartido
    }

    // Reasignar el parámetro solo cambia la copia local de la referencia
    static void reemplazar(Producto p) {
        p = new Producto();
        p.nombre = "Otro";
        p.precio = 0;
    }

    // Con un primitivo se copia el valor: el original nunca cambia
    static void duplicar(int n) {
        n = n * 2;
    }

    public static void main(String[] args) {
        Producto monitor = new Producto();
        monitor.nombre = "Monitor";
        monitor.precio = 200;

        subirPrecio(monitor, 10);
        System.out.println("Tras subirPrecio: " + monitor.nombre + " " + monitor.precio);

        reemplazar(monitor);
        System.out.println("Tras reemplazar:  " + monitor.nombre + " " + monitor.precio);

        int cantidad = 5;
        duplicar(cantidad);
        System.out.println("Tras duplicar:    " + cantidad);
    }
}
Tras subirPrecio: Monitor 220.0
Tras reemplazar:  Monitor 220.0
Tras duplicar:    5

Dos escenarios: en subirPrecio el parámetro p apunta al mismo objeto que monitor; en reemplazar, p pasa a apuntar a un objeto nuevo y monitor no cambia

Figura 4. (a) La copia de la referencia apunta al mismo objeto: los cambios en sus atributos se ven fuera. (b) Reasignar el parámetro solo cambia la copia local.

⚠️ Importante: En Java todo se pasa por valor. Con primitivos se copia el valor; con objetos se copia la referencia. Por eso un método puede modificar el objeto que recibe (cambiando sus atributos), pero no puede hacer que la variable del llamador apunte a otro objeto.


5. Referencias y null

5.1. El valor null

null es un literal especial que significa “esta referencia no apunta a ningún objeto”. Se puede asignar a cualquier variable de tipo referencia, y es el valor por defecto de los atributos de tipo referencia.

Producto oferta = null;     // de momento no hay ninguna oferta
String apodo = null;        // ningún String
int n = null;               // ERROR de compilación: un primitivo no puede ser null

5.2. La temida NullPointerException

Si usas el operador punto sobre una referencia que vale null, no hay objeto al que acceder y la JVM lanza una NullPointerException (NPE), que detiene el programa:

public class PruebaNull {
    public static void main(String[] args) {
        Producto oferta = null;             // todavía no hay oferta
        System.out.println("Antes del error");
        System.out.println(oferta.nombre);  // ¡no hay objeto al que acceder!
        System.out.println("Esto no se ejecuta");
    }
}
Antes del error
Exception in thread "main" java.lang.NullPointerException: Cannot read field "nombre" because "oferta" is null
    at PruebaNull.main(PruebaNull.java:5)

Desde Java 14 los mensajes de la NPE son descriptivos (helpful NullPointerExceptions): te dicen qué operación falló (Cannot read field "nombre") y qué variable era null ("oferta"). Lee siempre la primera línea del error y la línea del código que aparece en at ....

Un caso muy frecuente: un atributo de tipo referencia que nadie ha inicializado.

// ===== Cliente.java =====
public class Cliente {
    String nombre;
    Producto favorito;      // atributo de tipo referencia: por defecto vale null

    void mostrarFavorito() {
        System.out.println(nombre + " prefiere: " + favorito.nombre);
    }
}

// ===== PruebaCliente.java =====
public class PruebaCliente {
    public static void main(String[] args) {
        Cliente ana = new Cliente();
        ana.nombre = "Ana";
        ana.mostrarFavorito();      // favorito es null
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot read field "nombre" because "this.favorito" is null
    at Cliente.mostrarFavorito(Cliente.java:6)
    at PruebaCliente.main(PruebaCliente.java:5)

La traza se lee de arriba abajo: el error ocurrió en Cliente.java, línea 6 (dentro de mostrarFavorito), que fue llamado desde PruebaCliente.java, línea 5. El mensaje indica que this.favorito (el atributo favorito del objeto actual) era null.

5.3. Cómo evitarla

La solución depende del significado de null en tu programa:

  1. Si null es un valor válido (“el cliente no tiene favorito”), compruébalo antes de usar la referencia.
  2. Si null no debería ocurrir nunca, inicializa el atributo en el constructor para que el objeto nazca en un estado correcto.
void mostrarFavorito() {
    if (favorito == null) {                       // comprobamos ANTES de usarlo
        System.out.println(nombre + " no tiene producto favorito");
    } else {
        System.out.println(nombre + " prefiere: " + favorito.nombre);
    }
}
Ana no tiene producto favorito
Ana prefiere: Cascos

⚠️ Importante: La comparación con null se hace siempre con == o != (favorito == null), nunca con favorito.equals(null), porque si favorito es null, llamar a equals provoca… una NullPointerException.


6. Constructores

Hasta ahora creábamos el objeto y luego rellenábamos sus atributos uno a uno. Es incómodo y peligroso: si olvidas una línea, el objeto queda a medio construir. Para eso existen los constructores.

📘 Concepto: Un constructor es un bloque especial de la clase que se ejecuta automáticamente al crear un objeto con new y cuya misión es inicializar su estado. Se parece a un método, pero:

  • Se llama exactamente igual que la clase.
  • No tiene tipo de retorno, ni siquiera void.
  • Solo se invoca con new (o desde otro constructor con this(...)).

6.1. El constructor por defecto

Si no escribes ningún constructor, el compilador añade uno invisible, sin parámetros y vacío, llamado constructor por defecto. Por eso funcionaba new Producto() en los ejemplos anteriores.

// Lo que el compilador añade si no escribes ningún constructor
Producto() {
}

⚠️ Importante: En cuanto escribes cualquier constructor, el compilador deja de añadir el constructor por defecto. Si sigues necesitando uno sin parámetros, tienes que escribirlo tú.

// ===== Producto.java =====
public class Producto {
    String nombre;
    double precio;

    Producto(String nombre, double precio) {
        this.nombre = nombre;
        this.precio = precio;
    }
}

// ===== Prueba.java =====
public class Prueba {
    public static void main(String[] args) {
        Producto p = new Producto();   // ya no existe el constructor sin parámetros
    }
}
Prueba.java:3: error: constructor Producto in class Producto cannot be applied to given types;
        Producto p = new Producto();   // ya no existe el constructor sin parámetros
                     ^
  required: String,double
  found:    no arguments
  reason: actual and formal argument lists differ in length
1 error

El mensaje dice exactamente lo que pasa: el único constructor disponible requiere String,double y tú no has pasado argumentos (found: no arguments).

6.2. Constructores con parámetros y sobrecarga

Lo habitual es que el constructor reciba los datos necesarios para que el objeto nazca completo. Y como con los métodos (módulo 07), puedes sobrecargar constructores: varios con el mismo nombre y distinta lista de parámetros. Java elige cuál ejecutar según los argumentos de new.

6.3. Encadenar constructores con this(...)

Para no repetir código, un constructor puede llamar a otro de la misma clase con this(...). Así toda la inicialización real queda en un único constructor “principal”:

// ===== Producto.java =====
public class Producto {
    String nombre;
    double precio;
    int stock;

    // Constructor con todos los datos
    Producto(String nombre, double precio, int stock) {
        this.nombre = nombre;
        this.precio = precio;
        this.stock = stock;
        System.out.println("  [constructor de 3 parámetros: " + nombre + "]");
    }

    // Constructor sobrecargado: sin stock inicial
    Producto(String nombre, double precio) {
        this(nombre, precio, 0);          // delega en el constructor de 3 parámetros
        System.out.println("  [constructor de 2 parámetros]");
    }

    // Constructor sobrecargado: solo el nombre
    Producto(String nombre) {
        this(nombre, 0.0);                // delega en el de 2, que delega en el de 3
        System.out.println("  [constructor de 1 parámetro]");
    }

    void mostrar() {
        System.out.printf("%s: %.2f € (%d uds.)%n", nombre, precio, stock);
    }
}

// ===== PruebaConstructores.java =====
public class PruebaConstructores {
    public static void main(String[] args) {
        System.out.println("Creando a:");
        Producto a = new Producto("Teclado", 29.99, 10);
        System.out.println("Creando b:");
        Producto b = new Producto("Ratón", 15.50);
        System.out.println("Creando c:");
        Producto c = new Producto("Alfombrilla");

        a.mostrar();
        b.mostrar();
        c.mostrar();
    }
}
Creando a:
  [constructor de 3 parámetros: Teclado]
Creando b:
  [constructor de 3 parámetros: Ratón]
  [constructor de 2 parámetros]
Creando c:
  [constructor de 3 parámetros: Alfombrilla]
  [constructor de 2 parámetros]
  [constructor de 1 parámetro]
Teclado: 29.99 € (10 uds.)
Ratón: 15.50 € (0 uds.)
Alfombrilla: 0.00 € (0 uds.)

Observa en la salida el orden: al crear c, el constructor de 1 parámetro llama al de 2, que llama al de 3. El de 3 termina primero, luego el de 2 completa su parte y por último el de 1.

flowchart LR
    A["new Producto('Alfombrilla')"] --> B["Producto(String)"]
    B -->|"this(nombre, 0.0)"| C["Producto(String, double)"]
    C -->|"this(nombre, precio, 0)"| D["Producto(String, double, int)<br/>asigna los atributos"]

⚠️ Importante: La llamada this(...) debe ser la primera sentencia del constructor y solo puede aparecer una vez. Tampoco puede haber ciclos (que A llame a B y B a A): el compilador lo detecta.

6.4. El error de poner void a un constructor

Es uno de los errores más frecuentes de principiante, y es traicionero porque compila:

// ===== Producto.java =====
public class Producto {
    String nombre;

    // ¡ERROR típico! Al poner void, esto ya NO es un constructor: es un método
    public void Producto(String nombre) {
        this.nombre = nombre;
    }
}

// ===== Prueba.java =====
public class Prueba {
    public static void main(String[] args) {
        Producto p = new Producto("Teclado");
    }
}
Prueba.java:3: error: constructor Producto in class Producto cannot be applied to given types;
        Producto p = new Producto("Teclado");
                     ^
  required: no arguments
  found:    String
  reason: actual and formal argument lists differ in length
1 error

Al poner void, Producto(String) se convierte en un método que casualmente se llama como la clase. Como la clase ya no tiene constructores escritos, el compilador añade el constructor por defecto sin parámetros, y por eso dice required: no arguments.

Constructor Método
Mismo nombre que la clase Cualquier nombre (en lowerCamelCase)
Sin tipo de retorno Siempre con tipo de retorno (o void)
Se ejecuta con new Se ejecuta con objeto.metodo()
Una vez por objeto, al crearlo Tantas veces como se llame
Si no escribes ninguno, existe uno por defecto Nunca se crea automáticamente

7. La palabra clave this

📘 Concepto: Dentro de un método de instancia o de un constructor, this es una referencia al objeto actual: el objeto sobre el que se ha llamado el método o el que se está construyendo.

Durante la llamada ana.ingresar(100), la variable this del marco de ingresar apunta al mismo objeto que ana

Figura 5. Cuando ejecutas ana.ingresar(100), Java pone en el marco del método una referencia oculta, this, que apunta al objeto ana. Con luis.ingresar(...), this apuntaría al otro objeto.

7.1. Distinguir el atributo del parámetro

Cuando un parámetro se llama igual que un atributo, el parámetro oculta (shadowing) al atributo dentro del método. Sin this, el resultado es un error silencioso:

public class Producto {
    String nombre;
    double precio;

    Producto(String nombre, double precio) {
        nombre = nombre;      // ¡se asigna el parámetro a sí mismo!
        precio = precio;      // el atributo se queda con su valor por defecto
    }
}
null 0.0

nombre = nombre; asigna el parámetro a sí mismo; el atributo nunca se toca y conserva su valor por defecto (null y 0.0). La solución es la que has visto en todos los constructores anteriores:

Producto(String nombre, double precio) {
    this.nombre = nombre;     // atributo del objeto = parámetro
    this.precio = precio;
}

💡 Consejo: IntelliJ IDEA marca en gris la asignación nombre = nombre; y avisa de que “la variable se asigna a sí misma”. Haz caso a los avisos del IDE: muchos son errores reales.

7.2. Otros usos de this

this también sirve para devolver el propio objeto (lo que permite encadenar llamadas) y para pasar el objeto actual como argumento a otro método:

// ===== CuentaBancaria.java =====
public class CuentaBancaria {
    String titular;
    double saldo;

    CuentaBancaria(String titular) {
        this.titular = titular;      // this.titular = atributo; titular = parámetro
    }

    // Devuelve el propio objeto para poder encadenar llamadas
    CuentaBancaria ingresar(double cantidad) {
        this.saldo += cantidad;
        return this;
    }

    CuentaBancaria retirar(double cantidad) {
        this.saldo -= cantidad;
        return this;
    }

    // Pasa el objeto actual (this) a otro método
    void transferirTodoA(CuentaBancaria destino) {
        destino.ingresar(this.saldo);
        this.saldo = 0;
        System.out.println(this.titular + " ha transferido todo a " + destino.titular);
    }
}

// ===== PruebaThis.java =====
public class PruebaThis {
    public static void main(String[] args) {
        CuentaBancaria ana = new CuentaBancaria("Ana");
        CuentaBancaria luis = new CuentaBancaria("Luis");

        ana.ingresar(100).ingresar(50).retirar(30);   // encadenamiento
        System.out.println("Saldo de Ana: " + ana.saldo);

        ana.transferirTodoA(luis);
        System.out.println("Ana: " + ana.saldo + " | Luis: " + luis.saldo);
    }
}
Saldo de Ana: 120.0
Ana ha transferido todo a Luis
Ana: 0.0 | Luis: 120.0
Uso Ejemplo Para qué
this.atributo this.saldo = saldo; Distinguir atributo de parámetro o variable local
this(...) this(nombre, 0.0); Llamar a otro constructor de la misma clase
return this; ingresar(...) devuelve la cuenta Encadenar llamadas (estilo “fluido”)
this como argumento new Pedido(numero, this) Pasar el objeto actual a otro objeto o método

⚠️ Importante: this solo existe en métodos de instancia y constructores. En un método static (como main) no hay “objeto actual”, así que usar this da error de compilación. Lo verás con detalle en el módulo 11.


8. Métodos que consultan y métodos que modifican el estado

Los métodos de una clase suelen ser de dos tipos:

  • Consultores: devuelven información sobre el estado sin cambiarlo (getSaldo(), valorStock(), esCuadrado()).
  • Modificadores: cambian el estado (ingresar, retirar, vender). Un buen modificador valida antes de cambiar nada, para que el objeto nunca quede en un estado imposible (saldo negativo, stock negativo…).
// ===== CuentaBancaria.java =====
public class CuentaBancaria {
    String iban;
    String titular;
    double saldo;

    CuentaBancaria(String iban, String titular, double saldoInicial) {
        this.iban = iban;
        this.titular = titular;
        this.saldo = saldoInicial;
    }

    // Método CONSULTOR: solo devuelve información, no cambia nada
    double getSaldo() {
        return saldo;
    }

    // Métodos MODIFICADORES: cambian el estado, pero validando antes
    boolean ingresar(double cantidad) {
        if (cantidad <= 0) {
            return false;
        }
        saldo += cantidad;
        return true;
    }

    boolean retirar(double cantidad) {
        if (cantidad <= 0 || cantidad > saldo) {
            return false;            // el estado no cambia si la operación no es válida
        }
        saldo -= cantidad;
        return true;
    }
}

// ===== PruebaCuenta.java =====
public class PruebaCuenta {
    public static void main(String[] args) {
        CuentaBancaria c = new CuentaBancaria("ES12 3456", "Marta", 100);

        System.out.println("Ingresar 50:   " + c.ingresar(50));
        System.out.println("Ingresar -20:  " + c.ingresar(-20));
        System.out.println("Retirar 500:   " + c.retirar(500));
        System.out.println("Retirar 120:   " + c.retirar(120));
        System.out.println("Saldo final:   " + c.getSaldo());
    }
}
Ingresar 50:   true
Ingresar -20:  false
Retirar 500:   false
Retirar 120:   true
Saldo final:   30.0

Los modificadores devuelven boolean para informar de si la operación se hizo. Es una técnica sencilla; en el módulo 15 aprenderás a señalar los errores con excepciones, que es lo habitual en código profesional.

💡 Consejo: Pregúntate siempre “¿qué valores no deberían tener nunca los atributos?” (precio negativo, nota mayor que 10, fecha 31 de febrero…) y haz que constructores y modificadores lo impidan. En el módulo 11 llamaremos a esas reglas invariantes de clase y las blindaremos con private.


9. Varias referencias al mismo objeto: alias

Como una variable de objeto guarda una referencia, al asignarla a otra variable no se copia el objeto: se copia la flecha. Las dos variables pasan a ser alias del mismo objeto.

public class PruebaAlias {
    public static void main(String[] args) {
        Producto a = new Producto("Teclado", 30);
        Producto b = a;              // NO se copia el objeto: se copia la referencia

        b.precio = 25;               // cambiamos el objeto a través de b...
        System.out.println("a.precio = " + a.precio);   // ...y a lo ve cambiado

        System.out.println("a == b: " + (a == b));      // misma referencia

        b = new Producto("Ratón", 15);                   // b apunta ahora a otro objeto
        System.out.println("a: " + a.nombre + ", b: " + b.nombre);
    }
}
a.precio = 25.0
a == b: true
a: Teclado, b: Ratón

Primer panel: a y b apuntan al mismo Producto Teclado. Segundo panel: b apunta a un nuevo Producto Ratón y a sigue apuntando a Teclado

Figura 6. Tras b = a, cualquier cambio hecho a través de b se ve a través de a. Al hacer b = new Producto(...), b apunta a otro objeto y a no se ve afectada.

Si lo que quieres es un objeto independiente con los mismos valores, tienes que crearlo. Una forma clásica es el constructor de copia:

// ===== Producto.java (fragmento) =====
// Constructor de copia: crea un objeto NUEVO con los mismos valores
Producto(Producto otro) {
    this(otro.nombre, otro.precio);
}

// ===== PruebaCopia.java =====
public class PruebaCopia {
    public static void main(String[] args) {
        Producto original = new Producto("Teclado", 30);
        Producto copia = new Producto(original);   // objeto independiente

        copia.precio = 25;
        System.out.println("original: " + original.precio);
        System.out.println("copia:    " + copia.precio);
        System.out.println("original == copia: " + (original == copia));
    }
}
original: 30.0
copia:    25.0
original == copia: false

⚠️ Importante: Los alias no son un error en sí mismos: a menudo son justo lo que quieres (varias líneas de pedido que apuntan al mismo producto del almacén). El error es no ser consciente de ellos y modificar un objeto creyendo que es una copia.


10. Comparar objetos: == frente al contenido

Ya lo viste con los String en el módulo 09: == aplicado a referencias no compara el contenido, compara si las dos referencias apuntan al mismo objeto (identidad).

// ===== Producto.java (fragmento) =====
// Comparación "a mano" del contenido (en el módulo 12 haremos esto con equals)
boolean mismoContenido(Producto otro) {
    return otro != null
            && this.nombre.equals(otro.nombre)
            && this.precio == otro.precio;
}

// ===== PruebaComparar.java =====
public class PruebaComparar {
    public static void main(String[] args) {
        Producto p1 = new Producto("Teclado", 30);
        Producto p2 = new Producto("Teclado", 30);   // mismo contenido, otro objeto
        Producto p3 = p1;                              // alias de p1

        System.out.println("p1 == p2:              " + (p1 == p2));
        System.out.println("p1 == p3:              " + (p1 == p3));
        System.out.println("p1.mismoContenido(p2): " + p1.mismoContenido(p2));
        System.out.println("p1.equals(p2):         " + p1.equals(p2));
    }
}
p1 == p2:              false
p1 == p3:              true
p1.mismoContenido(p2): true
p1.equals(p2):         false
Expresión Qué compara Resultado aquí
p1 == p2 Identidad: ¿es el mismo objeto? false (son dos objetos)
p1 == p3 Identidad true (alias)
p1.mismoContenido(p2) El contenido, según nuestro criterio true
p1.equals(p2) Por defecto, lo mismo que == false

¿Por qué equals devuelve false si los dos productos son iguales? Porque toda clase tiene un equals heredado que, mientras no lo redefinas, se comporta como ==. En el módulo 12 aprenderás a sobrescribir equals (y su inseparable hashCode) para que compare el contenido, como hace String.

💡 Consejo: Regla práctica: == para primitivos y para comprobar null; equals para comparar el contenido de objetos (cuando la clase lo tenga bien implementado).


11. El recolector de basura

En lenguajes como C tienes que liberar a mano la memoria que reservas; si te olvidas, el programa “pierde” memoria, y si la liberas dos veces, se cuelga. En Java no existe una instrucción para destruir objetos: de eso se encarga el recolector de basura (garbage collector, GC).

📘 Concepto: Un objeto pasa a ser candidato a la recolección cuando ya no es alcanzable: ninguna variable activa ni ningún otro objeto alcanzable guarda una referencia a él. El recolector, que forma parte de la JVM, localiza esos objetos y libera su memoria automáticamente cuando lo considera oportuno.

Un objeto se queda sin referencias cuando:

  1. La variable que lo referenciaba se reasigna a otro objeto.
  2. La variable se pone a null.
  3. La variable era local y el método termina (su marco desaparece de la pila).
public class PruebaBasura {

    static void crearTemporal() {
        Producto temporal = new Producto("Temporal");
        System.out.println("Dentro del método: " + temporal.nombre);
    }   // al terminar el método, 'temporal' desaparece de la pila: el objeto queda sin referencias

    public static void main(String[] args) {
        Producto a = new Producto("Teclado");
        Producto b = new Producto("Ratón");

        a = b;          // (1) el objeto "Teclado" ya no es accesible: candidato a basura
        b = null;       // (2) "Ratón" sigue vivo: todavía lo referencia a
        crearTemporal(); // (3) "Temporal" es candidato en cuanto el método termina

        System.out.println("a apunta a: " + a.nombre);

        // Tras crear muchos objetos que nadie referencia, el recolector libera memoria solo
        for (int i = 0; i < 1_000_000; i++) {
            new Producto("Basura " + i);   // se crea y se abandona en la misma línea
        }
        System.out.println("Programa terminado sin quedarse sin memoria");
    }
}
Dentro del método: Temporal
a apunta a: Ratón
Programa terminado sin quedarse sin memoria

Tras a = b y b = null, el objeto Ratón sigue alcanzable desde a y el objeto Teclado, sin referencias, aparece tachado como candidato al recolector

Figura 7. El GC parte de las referencias de la pila y sigue las flechas. El objeto “Teclado” no se puede alcanzar desde ninguna variable: será liberado.

El bucle final crea un millón de objetos que nadie referencia. Si la memoria no se liberase, el programa acabaría con un OutOfMemoryError; gracias al recolector, termina sin problemas.

⚠️ Importante:

  • No sabes cuándo se ejecutará el recolector. System.gc() solo es una sugerencia a la JVM; no lo uses para “arreglar” problemas de memoria.
  • El método finalize() que verás en libros antiguos está obsoleto (marcado para eliminación desde Java 18) y no debe usarse.
  • Que haya recolector no significa que no puedas tener fugas: si guardas objetos que ya no necesitas en un array o una lista que sigue viva, siguen siendo alcanzables y no se liberan.

12. Arrays de objetos

Un array puede contener objetos, pero hay un matiz esencial: un array de objetos es en realidad un array de referencias. Al crearlo, todas las casillas valen null; los objetos hay que crearlos aparte.

public class Catalogo {
    public static void main(String[] args) {
        // 1. Se crea el array: 4 casillas de tipo Producto, todas a null
        Producto[] catalogo = new Producto[4];
        System.out.println("Casilla 0 recién creada: " + catalogo[0]);

        // 2. Se crean los objetos y se guardan sus referencias en el array
        catalogo[0] = new Producto("Teclado", 29.99, 10);
        catalogo[1] = new Producto("Ratón", 15.50, 25);
        catalogo[2] = new Producto("Monitor", 189.00, 4);
        // catalogo[3] se queda a null a propósito

        // 3. Recorrer con for-each saltando los huecos vacíos
        double total = 0;
        Producto masCaro = null;
        for (Producto p : catalogo) {
            if (p == null) {
                continue;                       // sin esta comprobación: NullPointerException
            }
            total += p.valorStock();
            if (masCaro == null || p.precio > masCaro.precio) {
                masCaro = p;
            }
        }
        System.out.printf("Valor total del almacén: %.2f €%n", total);
        System.out.println("Producto más caro: " + masCaro.nombre);

        // 4. También se puede inicializar con llaves
        Producto[] ofertas = {
            new Producto("Cascos", 45.00, 7),
            catalogo[1]                          // la misma referencia: ¡es un alias!
        };
        ofertas[1].precio = 12.00;
        System.out.println("Precio del ratón en el catálogo: " + catalogo[1].precio);
    }
}
Casilla 0 recién creada: null
Valor total del almacén: 1443.40 €
Producto más caro: Monitor
Precio del ratón en el catálogo: 12.0

El array catalogo de 4 casillas con referencias a tres objetos Producto y la última casilla a null; el array ofertas apunta a un producto Cascos y al mismo objeto Ratón del catálogo

Figura 8. new Producto[4] crea un array con 4 referencias a null; no crea ningún producto. ofertas[1] y catalogo[1] son alias del mismo objeto: por eso cambiar el precio en las ofertas cambia el catálogo.

12.1. La trampa de las casillas vacías

Si recorres el array completo y alguna casilla es null, obtendrás una NPE:

public class ArrayConHuecos {
    public static void main(String[] args) {
        Producto[] carrito = new Producto[3];
        carrito[0] = new Producto("Teclado", 29.99);

        double total = 0;
        for (int i = 0; i < carrito.length; i++) {
            total += carrito[i].precio;     // falla en i = 1: carrito[1] es null
        }
        System.out.println(total);
    }
}
Exception in thread "main" java.lang.NullPointerException: Cannot read field "precio" because "carrito[i]" is null
    at ArrayConHuecos.main(ArrayConHuecos.java:8)

12.2. El patrón “array + contador”

Como los arrays tienen tamaño fijo, una técnica muy habitual (hasta que estudies las listas en el módulo 16) es guardar junto al array un contador con el número de casillas ocupadas, y llenar siempre desde el principio. Así solo se recorren las posiciones 0..contador-1, que nunca son null:

// ===== Carrito.java =====
// Patrón "array + contador": el array tiene capacidad fija y 'numProductos' indica cuántas casillas se usan
public class Carrito {
    Producto[] productos = new Producto[5];
    int numProductos = 0;

    boolean agregar(Producto p) {
        if (numProductos == productos.length) {
            return false;                    // carrito lleno
        }
        productos[numProductos] = p;
        numProductos++;
        return true;
    }

    double total() {
        double suma = 0;
        for (int i = 0; i < numProductos; i++) {   // solo las casillas ocupadas
            suma += productos[i].precio;
        }
        return suma;
    }
}

// ===== PruebaCarrito.java =====
public class PruebaCarrito {
    public static void main(String[] args) {
        Carrito carrito = new Carrito();
        carrito.agregar(new Producto("Teclado", 29.99));
        carrito.agregar(new Producto("Ratón", 15.50));
        System.out.println("Productos: " + carrito.numProductos);
        System.out.printf("Total: %.2f €%n", carrito.total());
    }
}
Productos: 2
Total: 45.49 €

💡 Consejo: Encapsular el array y el contador dentro de una clase (Carrito, Biblioteca, Pedido) es mucho mejor que tenerlos sueltos en main: la clase se asegura de que el contador siempre sea coherente con el array.


13. toString: cómo se imprime un objeto

¿Qué pasa si haces System.out.println(p) con un producto?

public class SinToString {
    public static void main(String[] args) {
        Producto p = new Producto("Teclado", 29.99);
        System.out.println(p);          // ¿qué se imprime?
    }
}
Producto@1dbd16a6

Se imprime el nombre de la clase, una arroba y un número en hexadecimal (derivado del código hash del objeto, que cambiará en cada ejecución). No es muy útil. La solución es escribir en la clase un método toString() que devuelva un texto descriptivo del objeto:

// ===== Producto.java (fragmento) =====
@Override
public String toString() {
    return "Producto[nombre=" + nombre + ", precio=" + precio + "]";
}

// ===== ConToString.java =====
public class ConToString {
    public static void main(String[] args) {
        Producto p = new Producto("Teclado", 29.99);
        System.out.println(p);                       // llama a p.toString() automáticamente
        String texto = "En oferta: " + p;            // también al concatenar
        System.out.println(texto);
        Producto[] lista = {p, new Producto("Ratón", 15.5)};
        for (Producto x : lista) {
            System.out.println(" - " + x);
        }
    }
}
Producto[nombre=Teclado, precio=29.99]
En oferta: Producto[nombre=Teclado, precio=29.99]
 - Producto[nombre=Teclado, precio=29.99]
 - Producto[nombre=Ratón, precio=15.5]

Java llama a toString() automáticamente cuando imprimes un objeto o lo concatenas con un String.

📘 Concepto: La firma debe ser exactamente public String toString(). La anotación @Override indica que estamos sobrescribiendo un método que todas las clases ya tienen (heredado de Object). De momento basta con que la escribas así; en el módulo 12 entenderás su significado completo.

💡 Consejo: toString debe devolver el texto, no imprimirlo. Un método toString que hace System.out.println es un error típico: entonces no podrías usarlo para concatenar ni para escribir en un fichero.


14. Organización del código: una clase por fichero

14.1. Las reglas

  • Cada clase public debe estar en un fichero con su mismo nombre y extensión .java: public class Producto va en Producto.java. Si no coinciden, el compilador da error.
  • Un fichero puede contener otras clases no public, pero la práctica profesional es una clase por fichero.
  • Separa las clases del modelo (Producto, Cliente, Pedido) de la clase de prueba que tiene el main (Tienda, PruebaProducto). Las clases del modelo no deberían tener main ni leer de teclado: solo representan el problema.
tienda/
├── Producto.java        ← clase del modelo
├── Cliente.java         ← clase del modelo
├── LineaPedido.java     ← clase del modelo
├── Pedido.java          ← clase del modelo
└── Tienda.java          ← clase de prueba con main

14.2. Compilar y ejecutar varias clases

Desde la terminal tienes dos opciones. Con javac compilas todos los ficheros y luego ejecutas la clase que tiene el main. Y desde Java 22 el lanzador de código fuente admite programas de varios ficheros: java PruebaProducto.java compila en memoria la clase indicada y busca en la misma carpeta las demás clases que necesite.

public class PruebaProducto {
    public static void main(String[] args) {
        Producto p = new Producto("Teclado", 29.99);
        System.out.println(p.nombre + " cuesta " + p.precio + " €");
    }
}
$ java PruebaProducto.java
Teclado cuesta 29.99 €
$ javac *.java
$ java PruebaProducto
Teclado cuesta 29.99 €

En IntelliJ IDEA el proceso es más cómodo: clic derecho en la carpeta src → New → Java Class, escribes el nombre (Producto) y el IDE crea el fichero con la cabecera de la clase. Para ejecutar, abre la clase con main y pulsa el triángulo verde junto a su declaración.

💡 Consejo: En el módulo 01 viste los ficheros compactos de Java 25 (void main() { ... }). Son perfectos para pruebas rápidas, pero en cuanto tu programa tiene varias clases, usa la forma clásica con una clase pública por fichero, que es la que encontrarás en cualquier proyecto real y en el resto de este curso.


15. Ejemplos completos

15.1. Cuenta bancaria

Partimos de la CuentaBancaria de la sección 8 y le añadimos un segundo constructor (CuentaBancaria(iban, titular), que delega con this(iban, titular, 0)), un contador numMovimientos que incrementan ingresar y retirar, y estos dos métodos nuevos: una transferencia entre cuentas (un objeto que recibe otro objeto de su misma clase) y toString:

// ===== CuentaBancaria.java (fragmento) =====
boolean transferir(CuentaBancaria destino, double cantidad) {
    if (destino == null || destino == this) {
        return false;                       // no se transfiere a null ni a uno mismo
    }
    if (!this.retirar(cantidad)) {
        return false;
    }
    destino.ingresar(cantidad);
    return true;
}

@Override
public String toString() {
    return String.format("%s (%s): %.2f € [%d movimientos]", iban, titular, saldo, numMovimientos);
}

// ===== Banco.java =====
public class Banco {
    public static void main(String[] args) {
        CuentaBancaria c1 = new CuentaBancaria("ES01-0001", "Marta", 500);
        CuentaBancaria c2 = new CuentaBancaria("ES01-0002", "Javier");

        c1.ingresar(250);
        c1.retirar(100);
        System.out.println("Transferencia de 300: " + c1.transferir(c2, 300));
        System.out.println("Transferencia de 900: " + c1.transferir(c2, 900));
        System.out.println("A sí misma:           " + c1.transferir(c1, 10));

        System.out.println(c1);
        System.out.println(c2);
    }
}
Transferencia de 300: true
Transferencia de 900: false
A sí misma:           false
ES01-0001 (Marta): 350.00 € [3 movimientos]
ES01-0002 (Javier): 300.00 € [1 movimientos]

Detalles a observar: destino == this usa la identidad para impedir transferirse a uno mismo, y transferir reutiliza retirar e ingresar en lugar de tocar saldo directamente, de forma que las validaciones están en un solo sitio.

15.2. Rectángulo

Un ejemplo geométrico con un constructor que delega (Rectangulo(double lado) crea un cuadrado) y un método que compara con otro objeto:

// ===== Rectangulo.java =====
public class Rectangulo {
    double base;
    double altura;

    Rectangulo(double base, double altura) {
        this.base = base;
        this.altura = altura;
    }

    // Un cuadrado es un rectángulo con base = altura
    Rectangulo(double lado) {
        this(lado, lado);
    }

    double area() {
        return base * altura;
    }

    double perimetro() {
        return 2 * (base + altura);
    }

    boolean esCuadrado() {
        return base == altura;
    }

    void escalar(double factor) {
        base *= factor;
        altura *= factor;
    }

    // Recibe OTRO objeto de la misma clase
    boolean esMayorQue(Rectangulo otro) {
        return this.area() > otro.area();
    }

    @Override
    public String toString() {
        return "Rectángulo " + base + " x " + altura;
    }
}

// ===== PruebaRectangulo.java =====
public class PruebaRectangulo {
    public static void main(String[] args) {
        Rectangulo r1 = new Rectangulo(4, 2.5);
        Rectangulo r2 = new Rectangulo(3);

        System.out.println(r1 + " -> área " + r1.area() + ", perímetro " + r1.perimetro());
        System.out.println(r2 + " -> ¿cuadrado? " + r2.esCuadrado());
        System.out.println("¿r1 mayor que r2? " + r1.esMayorQue(r2));

        r2.escalar(2);
        System.out.println("Tras escalar: " + r2 + " -> área " + r2.area());
        System.out.println("¿r1 mayor que r2? " + r1.esMayorQue(r2));
    }
}
Rectángulo 4.0 x 2.5 -> área 10.0, perímetro 13.0
Rectángulo 3.0 x 3.0 -> ¿cuadrado? true
¿r1 mayor que r2? true
Tras escalar: Rectángulo 6.0 x 6.0 -> área 36.0
¿r1 mayor que r2? false

15.3. Alumno con notas

Un atributo puede ser un array. Cada Alumno crea su propio array de notas en el constructor:

// ===== Alumno.java =====
public class Alumno {
    String nombre;
    double[] notas;        // un atributo puede ser un array
    int numNotas;          // cuántas notas se han puesto ya

    Alumno(String nombre, int maxNotas) {
        this.nombre = nombre;
        this.notas = new double[maxNotas];   // cada alumno tiene SU propio array
    }

    boolean ponerNota(double nota) {
        if (nota < 0 || nota > 10 || numNotas == notas.length) {
            return false;
        }
        notas[numNotas++] = nota;
        return true;
    }

    double media() {
        if (numNotas == 0) {
            return 0;
        }
        double suma = 0;
        for (int i = 0; i < numNotas; i++) {
            suma += notas[i];
        }
        return suma / numNotas;
    }

    double notaMaxima() {
        double max = 0;
        for (int i = 0; i < numNotas; i++) {
            max = Math.max(max, notas[i]);
        }
        return max;
    }

    boolean aprobado() {
        return media() >= 5;
    }

    @Override
    public String toString() {
        return String.format("%-6s media %.2f (máx. %.1f) %s",
                nombre, media(), notaMaxima(), aprobado() ? "APROBADO" : "SUSPENSO");
    }
}

// ===== Clase.java =====
public class Clase {
    public static void main(String[] args) {
        Alumno lucia = new Alumno("Lucía", 3);
        Alumno pablo = new Alumno("Pablo", 3);
        lucia.ponerNota(7.5);
        lucia.ponerNota(9);
        pablo.ponerNota(4);
        pablo.ponerNota(5.5);
        System.out.println("¿Nota 12 aceptada? " + pablo.ponerNota(12));

        Alumno[] grupo = {lucia, pablo};
        Alumno mejor = grupo[0];
        for (Alumno a : grupo) {
            System.out.println(a);
            if (a.media() > mejor.media()) {
                mejor = a;
            }
        }
        System.out.println("Mejor media: " + mejor.nombre);
    }
}
¿Nota 12 aceptada? false
Lucía  media 8.25 (máx. 9.0) APROBADO
Pablo  media 4.75 (máx. 5.5) SUSPENSO
Mejor media: Lucía

⚠️ Importante: Fíjate en mejor = a; dentro del bucle: no se copia ningún alumno, solo se guarda una referencia al que tiene mejor media. Es el mismo patrón “máximo” del módulo 06, pero con referencias.

15.4. Coche

Un objeto con un estado más rico, donde los métodos interactúan entre sí (no se puede conducir sin arrancar, ni arrancar sin combustible, y el coche se para si se queda sin gasolina):

// ===== Coche.java =====
public class Coche {
    String matricula;
    double capacidad;      // litros que caben en el depósito
    double combustible;    // litros actuales
    double consumo;        // litros cada 100 km
    double kilometros;
    boolean arrancado;

    Coche(String matricula, double capacidad, double consumo) {
        this.matricula = matricula;
        this.capacidad = capacidad;
        this.consumo = consumo;
    }

    void arrancar() {
        arrancado = combustible > 0;      // sin combustible no arranca
    }

    // Devuelve los litros que realmente caben
    double repostar(double litros) {
        double echados = Math.min(litros, capacidad - combustible);
        combustible += echados;
        return echados;
    }

    // Devuelve los km que realmente se recorren
    double conducir(double km) {
        if (!arrancado) {
            return 0;
        }
        double recorridos = Math.min(km, combustible / consumo * 100);
        combustible -= recorridos * consumo / 100;
        kilometros += recorridos;
        if (combustible < 0.001) {        // se queda sin combustible y se para
            combustible = 0;
            arrancado = false;
        }
        return recorridos;
    }

    @Override
    public String toString() {
        return String.format("%s | %.1f L | %.1f km | %s", matricula, combustible,
                kilometros, arrancado ? "en marcha" : "parado");
    }
}

// ===== PruebaCoche.java =====
public class PruebaCoche {
    public static void main(String[] args) {
        Coche c = new Coche("1234-KLM", 45, 5.5);
        c.arrancar();
        System.out.println("Sin combustible: " + c);
        System.out.println("Litros echados: " + c.repostar(60));   // solo caben 45
        c.arrancar();
        System.out.println("Km recorridos: " + c.conducir(300));
        System.out.println(c);
        System.out.printf("Km recorridos: %.1f%n", c.conducir(800)); // no llega
        System.out.println(c);
    }
}
Sin combustible: 1234-KLM | 0.0 L | 0.0 km | parado
Litros echados: 45.0
Km recorridos: 300.0
1234-KLM | 28.5 L | 300.0 km | en marcha
Km recorridos: 518.2
1234-KLM | 0.0 L | 818.2 km | parado

15.5. El hilo conductor: la tienda

Este ejemplo reúne todo el módulo y será la base de los siguientes: varias clases que colaboran. Un Pedido pertenece a un Cliente y contiene un array de LineaPedido, cada una con una referencia a un Producto del almacén.

Diagrama de clases de la tienda: Pedido referencia a un Cliente y a un array de LineaPedido; cada LineaPedido referencia a un Producto

Figura 9. Las clases de la tienda y cómo se referencian entre sí (las flechas indican “tiene una referencia a”).

// ===== Producto.java =====
public class Producto {
    String codigo;
    String nombre;
    double precio;
    int stock;

    Producto(String codigo, String nombre, double precio, int stock) {
        this.codigo = codigo;
        this.nombre = nombre;
        this.precio = precio;
        this.stock = stock;
    }

    boolean hayStock(int cantidad) {
        return cantidad <= stock;
    }

    void retirarStock(int cantidad) {
        stock -= cantidad;
    }

    @Override
    public String toString() {
        return String.format("%s %-8s %7.2f € (%d uds.)", codigo, nombre, precio, stock);
    }
}

// ===== Cliente.java =====
public class Cliente {
    String dni;
    String nombre;
    String email;

    Cliente(String dni, String nombre, String email) {
        this.dni = dni;
        this.nombre = nombre;
        this.email = email;
    }

    // El cliente crea un pedido y se pasa a sí mismo (this) como dueño
    Pedido nuevoPedido(int numero) {
        return new Pedido(numero, this);
    }

    @Override
    public String toString() {
        return nombre + " <" + email + ">";
    }
}

// ===== LineaPedido.java =====
public class LineaPedido {
    Producto producto;     // referencia a un objeto Producto ya existente
    int cantidad;

    LineaPedido(Producto producto, int cantidad) {
        this.producto = producto;
        this.cantidad = cantidad;
    }

    double subtotal() {
        return producto.precio * cantidad;
    }

    @Override
    public String toString() {
        return String.format("%2d x %-8s %8.2f €", cantidad, producto.nombre, subtotal());
    }
}
// ===== Pedido.java =====
public class Pedido {
    int numero;
    Cliente cliente;
    LineaPedido[] lineas = new LineaPedido[10];
    int numLineas;

    Pedido(int numero, Cliente cliente) {
        this.numero = numero;
        this.cliente = cliente;
    }

    boolean agregar(Producto p, int cantidad) {
        if (numLineas == lineas.length || !p.hayStock(cantidad)) {
            return false;
        }
        p.retirarStock(cantidad);
        lineas[numLineas++] = new LineaPedido(p, cantidad);
        return true;
    }

    double total() {
        double total = 0;
        for (int i = 0; i < numLineas; i++) {
            total += lineas[i].subtotal();
        }
        return total;
    }

    void imprimir() {
        System.out.println("Pedido nº " + numero + " de " + cliente);
        for (int i = 0; i < numLineas; i++) {
            System.out.println("  " + lineas[i]);
        }
        System.out.printf("  TOTAL: %.2f €%n", total());
    }
}

// ===== Tienda.java =====
public class Tienda {
    public static void main(String[] args) {
        Producto[] almacen = {
            new Producto("P01", "Teclado", 29.99, 10),
            new Producto("P02", "Ratón", 15.50, 25),
            new Producto("P03", "Monitor", 189.00, 2)
        };
        Cliente ana = new Cliente("12345678Z", "Ana López", "ana@correo.es");

        Pedido pedido = ana.nuevoPedido(1001);
        pedido.agregar(almacen[0], 2);
        pedido.agregar(almacen[1], 1);
        boolean ok = pedido.agregar(almacen[2], 5);       // solo quedan 2 monitores
        System.out.println("¿Se añadieron 5 monitores? " + ok);
        pedido.imprimir();

        System.out.println("Stock tras el pedido:");
        for (Producto p : almacen) {
            System.out.println("  " + p);
        }
    }
}
¿Se añadieron 5 monitores? false
Pedido nº 1001 de Ana López <ana@correo.es>
   2 x Teclado     59.98 €
   1 x Ratón       15.50 €
  TOTAL: 75.48 €
Stock tras el pedido:
  P01 Teclado    29.99 € (8 uds.)
  P02 Ratón      15.50 € (24 uds.)
  P03 Monitor   189.00 € (2 uds.)

Observa cómo cada clase hace solo lo suyo: LineaPedido sabe calcular su subtotal, Pedido suma las líneas sin saber cómo se calcula cada una, y Producto controla su stock. El stock del almacén ha bajado porque las líneas del pedido apuntan a los mismos objetos del array almacen (alias otra vez).


16. Errores comunes

Error Síntoma Solución
Usar un objeto sin crearlo (Producto p; p.mostrar();) Error de compilación: variable p might not have been initialized Crear el objeto con new
Acceder a una referencia null NullPointerException en ejecución Inicializar en el constructor o comprobar != null
Olvidar this en el constructor (nombre = nombre;) Los atributos quedan a null/0 this.nombre = nombre;
Poner void a un constructor constructor … cannot be applied to given types Quitar el tipo de retorno
Definir un constructor con parámetros y seguir usando new X() constructor X in class X cannot be applied… Escribir también el constructor sin parámetros
Pensar que b = a copia el objeto Cambios “misteriosos” en otro objeto Crear una copia con new (constructor de copia)
Comparar objetos con == false aunque el contenido sea igual Comparar el contenido (equals, módulo 12)
Crear un array de objetos y usar sus casillas sin rellenarlas NullPointerException Crear cada objeto o usar el patrón array + contador
Llamar a un método de instancia desde main sin objeto non-static method … cannot be referenced from a static context Crear un objeto y llamar objeto.metodo()
toString que imprime en lugar de devolver Imprime en momentos inesperados y devuelve basura return del texto
Nombre del fichero distinto de la clase pública class X is public, should be declared in a file named X.java Renombrar el fichero o la clase

17. Ejercicios

Ejercicios resueltos

Ejercicio 1: Punto en el plano

Crea una clase Punto con coordenadas x e y (double), un constructor con parámetros y otro sin parámetros que cree el origen (0, 0) reutilizando el primero. Añade los métodos distancia(Punto otro), desplazar(double dx, double dy), puntoMedio(Punto otro) (que devuelve un punto nuevo) y toString(). Pruébalo desde una clase PruebaPunto.

Ver solución
// ===== Punto.java =====
public class Punto {
    double x;
    double y;

    Punto(double x, double y) {
        this.x = x;
        this.y = y;
    }

    Punto() {
        this(0, 0);          // el origen de coordenadas
    }

    double distancia(Punto otro) {
        double dx = this.x - otro.x;
        double dy = this.y - otro.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    void desplazar(double dx, double dy) {
        x += dx;
        y += dy;
    }

    // Devuelve un punto NUEVO; no modifica ninguno de los dos
    Punto puntoMedio(Punto otro) {
        return new Punto((x + otro.x) / 2, (y + otro.y) / 2);
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}

// ===== PruebaPunto.java =====
public class PruebaPunto {
    public static void main(String[] args) {
        Punto origen = new Punto();
        Punto p = new Punto(3, 4);

        System.out.println("Distancia de " + p + " al origen: " + p.distancia(origen));
        System.out.println("Punto medio: " + p.puntoMedio(origen));

        p.desplazar(-1, 2);
        System.out.println("Tras desplazar: " + p);
        System.out.println("Origen sin cambios: " + origen);
    }
}
Distancia de (3.0, 4.0) al origen: 5.0
Punto medio: (1.5, 2.0)
Tras desplazar: (2.0, 6.0)
Origen sin cambios: (0.0, 0.0)

El constructor sin parámetros delega con this(0, 0). distancia usa los atributos de this y los del parámetro otro: un método de instancia puede acceder a los atributos de otro objeto de su misma clase. puntoMedio no modifica ninguno de los dos puntos, sino que crea y devuelve uno nuevo, por eso origen sigue en (0.0, 0.0).

Ejercicio 2: Libro de la biblioteca

Crea una clase Libro con ISBN, título, autor, número de páginas, si está prestado y cuántas veces se ha prestado. Los métodos prestar() y devolver() devuelven true si la operación es posible y false si no (no se puede prestar un libro ya prestado ni devolver uno que no lo está).

Ver solución
// ===== Libro.java =====
public class Libro {
    String isbn;
    String titulo;
    String autor;
    int paginas;
    boolean prestado;
    int vecesPrestado;

    Libro(String isbn, String titulo, String autor, int paginas) {
        this.isbn = isbn;
        this.titulo = titulo;
        this.autor = autor;
        this.paginas = paginas;
    }

    boolean prestar() {
        if (prestado) {
            return false;          // ya lo tiene otra persona
        }
        prestado = true;
        vecesPrestado++;
        return true;
    }

    boolean devolver() {
        if (!prestado) {
            return false;          // no se puede devolver lo que no se ha prestado
        }
        prestado = false;
        return true;
    }

    @Override
    public String toString() {
        return "\"" + titulo + "\" de " + autor + (prestado ? " [PRESTADO]" : " [disponible]");
    }
}

// ===== PruebaLibro.java =====
public class PruebaLibro {
    public static void main(String[] args) {
        Libro l = new Libro("978-8437604947", "Cien años de soledad", "Gabriel García Márquez", 471);
        System.out.println(l);
        System.out.println("Prestar:  " + l.prestar());
        System.out.println("Prestar:  " + l.prestar());
        System.out.println(l);
        System.out.println("Devolver: " + l.devolver());
        System.out.println("Devolver: " + l.devolver());
        l.prestar();
        System.out.println(l + " - prestado " + l.vecesPrestado + " veces");
    }
}
"Cien años de soledad" de Gabriel García Márquez [disponible]
Prestar:  true
Prestar:  false
"Cien años de soledad" de Gabriel García Márquez [PRESTADO]
Devolver: true
Devolver: false
"Cien años de soledad" de Gabriel García Márquez [PRESTADO] - prestado 2 veces

Los métodos modificadores comprueban el estado antes de cambiarlo, de modo que un libro nunca puede quedar en una situación imposible. El contador vecesPrestado solo aumenta cuando el préstamo se realiza de verdad.

Ejercicio 3: Hora del día

Crea una clase Hora (horas, minutos, segundos) cuyo constructor normalice los valores (por ejemplo, new Hora(25, 70, 0) debe quedar como 02:10:00). Añade avanzar(int segundos) (admite negativos), esAnteriorA(Hora otra) y un toString() con formato HH:MM:SS.

Ver solución
// ===== Hora.java =====
public class Hora {
    int horas;
    int minutos;
    int segundos;

    Hora(int horas, int minutos, int segundos) {
        // Guardamos todo en segundos y normalizamos: 25:70:00 -> 02:10:00
        fijarDesdeSegundos(horas * 3600 + minutos * 60 + segundos);
    }

    Hora() {
        this(0, 0, 0);
    }

    // Método auxiliar reutilizado por el constructor y por avanzar
    void fijarDesdeSegundos(int total) {
        total = ((total % 86400) + 86400) % 86400;   // siempre entre 0 y 86399
        horas = total / 3600;
        minutos = (total % 3600) / 60;
        segundos = total % 60;
    }

    int enSegundos() {
        return horas * 3600 + minutos * 60 + segundos;
    }

    void avanzar(int seg) {
        fijarDesdeSegundos(enSegundos() + seg);
    }

    boolean esAnteriorA(Hora otra) {
        return this.enSegundos() < otra.enSegundos();
    }

    @Override
    public String toString() {
        return String.format("%02d:%02d:%02d", horas, minutos, segundos);
    }
}

// ===== PruebaHora.java =====
public class PruebaHora {
    public static void main(String[] args) {
        Hora h = new Hora(23, 59, 30);
        System.out.println("Hora inicial: " + h);
        h.avanzar(45);
        System.out.println("+45 s:        " + h);
        h.avanzar(-3600);
        System.out.println("-1 h:         " + h);
        System.out.println("Normalizada:  " + new Hora(25, 70, 0));
        System.out.println("¿" + h + " antes que 12:00:00? " + h.esAnteriorA(new Hora(12, 0, 0)));
    }
}
Hora inicial: 23:59:30
+45 s:        00:00:15
-1 h:         23:00:15
Normalizada:  02:10:00
¿23:00:15 antes que 12:00:00? false

El truco está en trabajar internamente con segundos desde medianoche: fijarDesdeSegundos convierte cualquier cantidad (incluso negativa) al rango 0-86399 con ((total % 86400) + 86400) % 86400 y luego la descompone. Tanto el constructor como avanzar reutilizan ese método, así que la normalización está escrita una sola vez.

Ejercicio 4: Fracciones

Crea una clase Fraccion con numerador y denominador enteros. El constructor debe guardar la fracción simplificada y con el signo en el numerador. Implementa sumar y multiplicar, que devuelven una fracción nueva sin modificar las originales, valor() (decimal) y toString() (si el denominador es 1, solo el numerador).

Ver solución
// ===== Fraccion.java =====
public class Fraccion {
    int numerador;
    int denominador;

    Fraccion(int numerador, int denominador) {
        if (denominador == 0) {
            denominador = 1;                  // en el módulo 15 lanzaremos una excepción
        }
        if (denominador < 0) {                // el signo siempre en el numerador
            numerador = -numerador;
            denominador = -denominador;
        }
        this.numerador = numerador;
        this.denominador = denominador;
        simplificar();
    }

    Fraccion(int entero) {
        this(entero, 1);
    }

    private int mcd(int a, int b) {
        return b == 0 ? Math.abs(a) : mcd(b, a % b);   // algoritmo de Euclides (módulo 07)
    }

    void simplificar() {
        int m = mcd(numerador, denominador);
        if (m != 0) {
            numerador /= m;
            denominador /= m;
        }
    }

    Fraccion sumar(Fraccion otra) {
        return new Fraccion(numerador * otra.denominador + otra.numerador * denominador,
                denominador * otra.denominador);
    }

    Fraccion multiplicar(Fraccion otra) {
        return new Fraccion(numerador * otra.numerador, denominador * otra.denominador);
    }

    double valor() {
        return (double) numerador / denominador;
    }

    @Override
    public String toString() {
        return denominador == 1 ? String.valueOf(numerador) : numerador + "/" + denominador;
    }
}

// ===== PruebaFraccion.java =====
public class PruebaFraccion {
    public static void main(String[] args) {
        Fraccion a = new Fraccion(6, 8);
        Fraccion b = new Fraccion(1, -4);
        System.out.println("a = " + a + ", b = " + b);
        System.out.println("a + b = " + a.sumar(b));
        System.out.println("a * b = " + a.multiplicar(b));
        System.out.println("a + 2 = " + a.sumar(new Fraccion(2)));
        System.out.println("4/2   = " + new Fraccion(4, 2));
        System.out.println("valor de a = " + a.valor());
    }
}
a = 3/4, b = -1/4
a + b = 1/2
a * b = -3/16
a + 2 = 11/4
4/2   = 2
valor de a = 0.75

mcd es un método privado de ayuda (la palabra private se explica en el módulo 11: indica que solo se usa dentro de la clase) que reutiliza el algoritmo de Euclides recursivo del módulo 07. Como sumar y multiplicar crean el resultado con new Fraccion(...), el constructor lo simplifica automáticamente. Los objetos Fraccion nunca cambian una vez creados: es un primer ejemplo de diseño inmutable, que estudiarás en el módulo 11.

Ejercicio 5: Biblioteca con un array de libros

Crea una clase Biblioteca que guarde hasta capacidad libros usando el patrón array + contador. Debe permitir agregar(Libro), buscarPorTitulo(String) (sin distinguir mayúsculas; devuelve null si no lo encuentra), listarDeAutor(String) y contarDisponibles().

Ver solución

Se reutiliza la clase Libro del ejercicio 2 sin cambios.

// ===== Biblioteca.java =====
public class Biblioteca {
    String nombre;
    Libro[] libros;
    int numLibros;

    Biblioteca(String nombre, int capacidad) {
        this.nombre = nombre;
        this.libros = new Libro[capacidad];
    }

    boolean agregar(Libro libro) {
        if (libro == null || numLibros == libros.length) {
            return false;
        }
        libros[numLibros++] = libro;
        return true;
    }

    // Devuelve el primer libro con ese título o null si no existe
    Libro buscarPorTitulo(String titulo) {
        for (int i = 0; i < numLibros; i++) {
            if (libros[i].titulo.equalsIgnoreCase(titulo)) {
                return libros[i];
            }
        }
        return null;
    }

    void listarDeAutor(String autor) {
        System.out.println("Libros de " + autor + ":");
        for (int i = 0; i < numLibros; i++) {
            if (libros[i].autor.equals(autor)) {
                System.out.println("  - " + libros[i]);
            }
        }
    }

    int contarDisponibles() {
        int n = 0;
        for (int i = 0; i < numLibros; i++) {
            if (!libros[i].prestado) {
                n++;
            }
        }
        return n;
    }
}

// ===== PruebaBiblioteca.java =====
public class PruebaBiblioteca {
    public static void main(String[] args) {
        Biblioteca bib = new Biblioteca("Biblioteca del barrio", 3);
        bib.agregar(new Libro("978-84-376-0494-7", "Niebla", "Miguel de Unamuno", 288));
        bib.agregar(new Libro("978-84-206-6040-3", "San Manuel Bueno, mártir", "Miguel de Unamuno", 176));
        bib.agregar(new Libro("978-84-376-0237-0", "La Regenta", "Leopoldo Alas Clarín", 912));
        System.out.println("¿Cabe un cuarto? " + bib.agregar(new Libro("1", "Marianela", "Galdós", 300)));

        Libro l = bib.buscarPorTitulo("niebla");
        if (l != null) {
            l.prestar();                       // se presta el objeto guardado en la biblioteca
        }
        System.out.println("Buscar 'Marianela': " + bib.buscarPorTitulo("Marianela"));
        bib.listarDeAutor("Miguel de Unamuno");
        System.out.println("Disponibles: " + bib.contarDisponibles() + " de " + bib.numLibros);
    }
}
¿Cabe un cuarto? false
Buscar 'Marianela': null
Libros de Miguel de Unamuno:
  - "Niebla" de Miguel de Unamuno [PRESTADO]
  - "San Manuel Bueno, mártir" de Miguel de Unamuno [disponible]
Disponibles: 2 de 3

Todos los recorridos llegan solo hasta numLibros, por lo que nunca se accede a casillas null. buscarPorTitulo devuelve una referencia al libro guardado: al hacer l.prestar() en main se modifica el mismo objeto que está dentro de la biblioteca (por eso aparece como prestado en el listado). Quien llama debe comprobar si el resultado es null antes de usarlo.

Ejercicio 6: Traza de memoria

Sin ejecutarlo, dibuja la pila y el montón y escribe qué imprime este programa. Después compruébalo.

// ===== Caja.java =====
public class Caja {
    int valor;

    Caja(int valor) {
        this.valor = valor;
    }
}

// ===== Traza.java =====
public class Traza {

    static void cambiar(Caja c, int n) {
        c.valor = c.valor + n;
        n = 0;
        c = new Caja(99);
        c.valor++;
    }

    public static void main(String[] args) {
        Caja a = new Caja(1);
        Caja b = a;
        Caja c = new Caja(1);
        int n = 5;

        b.valor = 10;
        cambiar(a, n);

        System.out.println(a.valor + " " + b.valor + " " + c.valor + " " + n);
        System.out.println((a == b) + " " + (a == c));
        b = c;
        c.valor = 7;
        System.out.println(a.valor + " " + b.valor + " " + c.valor);
    }
}
Ver solución
15 15 1 5
true false
15 7 7

Paso a paso:

  1. a y b apuntan al mismo objeto (alias); c apunta a otro objeto con valor 1.
  2. b.valor = 10 cambia el objeto compartido: a.valor también es 10.
  3. En cambiar(a, n), el parámetro c es una copia de la referencia a: c.valor + n deja el objeto compartido en 15. Después n = 0 solo cambia la copia local del entero, y c = new Caja(99) hace que la copia local apunte a un objeto nuevo (que se incrementa a 100 y se convierte en basura al terminar el método). Nada de esto afecta a main.
  4. Primera línea: 15 15 1 5. Segunda: a == b es true (alias) y a == c es false.
  5. b = c hace que b deje de ser alias de a y pase a serlo de c; c.valor = 7 se ve por b. Tercera línea: 15 7 7.

Ejercicios propuestos

Ejercicio 7: Bombilla

Crea una clase Bombilla con potencia (vatios), si está encendida y las horas de uso acumuladas. Métodos: encender(), apagar(), usar(double horas) (solo suma horas si está encendida) y consumoKWh(). Crea un array de 4 bombillas y calcula el consumo total de la casa.

Ejercicio 8: Dado

Crea una clase Dado con un número de caras configurable en el constructor (6 por defecto, usando this(...)) y un método lanzar() que devuelva un valor aleatorio entre 1 y el número de caras (usa Math.random()) y lo guarde como último valor. Lanza un dado de 6 caras 6000 veces y muestra cuántas veces sale cada número.

Ejercicio 9: Ascensor

Modela un Ascensor con planta mínima, planta máxima y planta actual. El método irA(int planta) debe rechazar plantas fuera de rango y devolver el número de plantas recorridas. Lleva la cuenta de las plantas recorridas en total y muéstralas en toString().

Ejercicio 10: Carrito de la tienda

Amplía el Carrito de la sección 12.2 para que guarde también la cantidad de cada producto (usa una clase LineaCarrito). Si se añade un producto que ya está en el carrito (mismo objeto), debe sumarse la cantidad en lugar de crear otra línea. Añade un método eliminar(Producto p) que compacte el array.

Ejercicio 11: Agenda de contactos

Crea las clases Contacto (nombre, teléfono, email) y Agenda (array + contador). La agenda debe permitir añadir sin repetir nombre, buscar por nombre, borrar y listar. Escribe una clase PruebaAgenda con un menú por consola usando Scanner.

Ejercicio 12: Préstamos en la biblioteca

Combina los ejercicios 2 y 5: crea una clase Socio (número, nombre, array de libros prestados con máximo 3) y añade a Biblioteca un método prestar(Socio s, String titulo) que solo funcione si el libro existe, está disponible y el socio no ha llegado a su límite. Dibuja el diagrama de memoria tras dos préstamos.


Resumen

Concepto Sintaxis / ejemplo Idea clave
Clase public class Producto { ... } Plantilla que define atributos y métodos
Objeto / instancia new Producto("Teclado", 29.99, 10) Ejemplar concreto con estado propio
Atributo double precio; Variable de cada objeto; tiene valor por defecto
Método de instancia boolean vender(int c) { ... } Sin static; trabaja con los atributos del objeto
Referencia Producto p = ...; La variable guarda una flecha al objeto del montón
Pila / montón marcos de métodos / objetos Variables locales en la pila, objetos en el montón
null Producto p = null; Referencia que no apunta a nada; usarla lanza NPE
Constructor Producto(String n, double p) { ... } Mismo nombre que la clase, sin tipo de retorno
Constructor por defecto (implícito) Solo existe si no escribes ningún constructor
this(...) this(nombre, 0.0); Llama a otro constructor; primera sentencia
this this.nombre = nombre; Referencia al objeto actual
Alias Producto b = a; Dos referencias al mismo objeto
== con objetos p1 == p2 Compara identidad, no contenido
Recolector de basura automático Libera objetos inalcanzables
Array de objetos new Producto[4] Array de referencias, inicialmente null
toString public String toString() Texto del objeto al imprimirlo o concatenarlo
Organización Producto.java + Tienda.java Una clase pública por fichero; main aparte