Saltar al contenido
ES EN
Tema: automático

Ejercicio resuelto: clases y objetos con un monedero en Java

Ejercicio resuelto de clases y objetos en Java: encapsulación, validación y un monedero con saldo en céntimos, con tests y errores típicos.

Actualizado el

Código de Examen Java Programación orientada a objetos Nivel básico Código probado

Código de Examen: Ejercicio resuelto: clases y objetos con un monedero en Java
Programación I · Examen parcialEjercicio 1 · 2,5 puntos · 25 min

Muchas universidades usan Java en los primeros cursos para introducir la programación orientada a objetos: obliga a declarar tipos y a escribir todo dentro de clases, y eso ayuda a distinguir entre el estado de un objeto y las operaciones que se le permiten. Este ejercicio pide aplicar justo esa idea con un caso pequeño: un monedero electrónico.

Se debe diseñar la clase Monedero. Cada objeto representa el monedero de un titular y guarda su saldo en céntimos (un entero, nunca un double). El saldo máximo permitido es de 15000 céntimos (150,00 euros) y el saldo nunca puede ser negativo. La clase debe ofrecer, como mínimo, un constructor que reciba el nombre del titular, un método recargar(int centimos), un método pagar(int centimos) y un método para consultar el saldo. Los atributos deben ser privados.

Las reglas de cada operación se comprueban en este orden. Primero, si el importe es menor o igual que cero, la operación es INVALIDO. Después, en una recarga, si el saldo resultante superaría 15000 céntimos, la operación es LIMITE. En un pago, si el importe es mayor que el saldo disponible, la operación es INSUFICIENTE. En cualquier otro caso la operación se acepta (OK) y modifica el saldo. Una operación rechazada no cambia el saldo.

El programa principal lee de la entrada estándar. La primera línea contiene un entero n, el número de operaciones. Las n líneas siguientes tienen el formato TITULAR OPERACION IMPORTE, donde TITULAR es una palabra sin espacios, OPERACION es RECARGA o PAGO e IMPORTE es un entero en céntimos. Un titular que aparece por primera vez empieza con un monedero de saldo 0, y cada titular tiene su propio monedero independiente.

Por cada operación se imprime una línea con el resultado (OK, INVALIDO, LIMITE o INSUFICIENTE), el titular y el saldo del monedero tras la operación, expresado en euros con dos decimales y punto decimal (por ejemplo 12.50). Si n es 0 no se imprime nada. Pista de formato: separe los tres campos con un único espacio.

Entrada de ejemplo

6
ana RECARGA 2000
ana PAGO 750
luis RECARGA 500
ana PAGO 1250
luis PAGO 600
ana RECARGA 3

Salida esperada

OK ana 20.00
OK ana 12.50
OK luis 5.00
OK ana 0.00
INSUFICIENTE luis 5.00
OK ana 0.03
Clase Monedero con atributos privados, constructor y métodos de acceso coherentes
0,75
Método recargar y método pagar con las reglas de validación y límite aplicadas en el orden correcto
0,75
Gestión de varios objetos independientes (uno por titular) con un Map
0,5
Lectura de la entrada y formato exacto de la salida, incluido el saldo con dos decimales
0,5

Pistas

Pista 1 · ¿Qué debe guardar cada objeto y qué no debe poder tocar el exterior?

Cada Monedero necesita su titular y su saldo en céntimos. Declare ambos private y deje que solo recargar y pagar cambien el saldo. Así las reglas (saldo no negativo, máximo de 15000) viven en un único sitio y nadie puede saltárselas.

Pista 2 · ¿Cómo representa que una operación se acepta o se rechaza?

Haga que recargar y pagar devuelvan un String con el resultado (OK, INVALIDO, LIMITE o INSUFICIENTE). Compruebe primero el importe no positivo y después la regla propia de cada operación; solo modifique el saldo en el caso OK.

Pista 3 · ¿Cómo mantiene un monedero por titular y cómo imprime euros sin usar double?

Use un Map<String, Monedero> y cree el monedero la primera vez que aparezca el titular (computeIfAbsent o una comprobación con containsKey). Para imprimir, divida el saldo entre 100 y tome el resto: String.format("%d.%02d", saldo / 100, saldo % 100). Así evita los errores de redondeo de la coma flotante.

Solución

Solución explicada

La solución separa dos responsabilidades. La clase Monedero protege su estado y aplica las reglas del negocio; el método main solo lee líneas, localiza el monedero del titular y escribe el resultado. Esta división es exactamente lo que se pretende ilustrar con un primer ejercicio de clases y objetos.

El saldo se guarda como int en céntimos. Con enteros todas las comparaciones son exactas y el límite de 15000 se puede probar con igualdad sin miedo a errores de redondeo.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

public class Main {

    static class Monedero {
        private static final int MAXIMO = 15000;

        private final String titular;
        private int saldo;

        Monedero(String titular) {
            this.titular = titular;
            this.saldo = 0;
        }

        String getTitular() {
            return titular;
        }

        int getSaldo() {
            return saldo;
        }

        String recargar(int centimos) {
            if (centimos <= 0) {
                return "INVALIDO";
            }
            if (saldo + centimos > MAXIMO) {
                return "LIMITE";
            }
            saldo += centimos;
            return "OK";
        }

        String pagar(int centimos) {
            if (centimos <= 0) {
                return "INVALIDO";
            }
            if (centimos > saldo) {
                return "INSUFICIENTE";
            }
            saldo -= centimos;
            return "OK";
        }

        String saldoEnEuros() {
            return String.format("%d.%02d", saldo / 100, saldo % 100);
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String primera = in.readLine();
        if (primera == null || primera.isBlank()) {
            return;
        }
        int n = Integer.parseInt(primera.trim());
        Map<String, Monedero> monederos = new HashMap<>();
        StringBuilder salida = new StringBuilder();

        for (int i = 0; i < n; i++) {
            String linea = in.readLine();
            if (linea == null) {
                break;
            }
            String[] partes = linea.trim().split("\\s+");
            String titular = partes[0];
            String operacion = partes[1];
            int importe = Integer.parseInt(partes[2]);

            Monedero m = monederos.computeIfAbsent(titular, Monedero::new);
            String resultado;
            if (operacion.equals("RECARGA")) {
                resultado = m.recargar(importe);
            } else {
                resultado = m.pagar(importe);
            }
            salida.append(resultado).append(' ')
                  .append(m.getTitular()).append(' ')
                  .append(m.saldoEnEuros()).append('\n');
        }
        System.out.print(salida);
    }
}

La clase Monedero. Los atributos titular y saldo son privados y la constante MAXIMO vale 15000. El constructor fija el saldo en 0. Fuera de la clase no hay forma de escribir en saldo: solo recargar y pagar lo modifican, y ambos validan antes de tocarlo. Esa es la encapsulación en la práctica: el invariante «0 ≤ saldo ≤ 15000» se mantiene porque una única clase lo vigila.

Orden de las comprobaciones. En recargar se comprueba primero centimos <= 0 y después saldo + centimos > MAXIMO; en pagar, el importe no positivo y luego centimos > saldo. El orden importa: un importe negativo debe dar INVALIDO aunque además fuese incoherente con otra regla. Solo la rama final, la aceptada, ejecuta saldo += centimos o saldo -= centimos, de modo que una operación rechazada deja el saldo intacto.

El main. El HashMap asocia cada nombre con su objeto. computeIfAbsent(titular, Monedero::new) crea el monedero con saldo 0 la primera vez y devuelve el existente las siguientes, así que cada titular tiene un objeto distinto e independiente. Cada línea se separa con split sobre espacios y el resultado se acumula en un StringBuilder para imprimir al final. Si n es 0, el bucle no se ejecuta y no se escribe nada.

Formato del saldo. saldoEnEuros usa saldo / 100 y saldo % 100 con %02d, de modo que 50 céntimos se escriben 0.50 y 15000 se escribe 150.00. Al no pasar nunca por un double, no hay redondeos que expliquen resultados sorprendentes.

Casos de prueba. El primer test, el del enunciado, mezcla recargas y pagos de dos titulares y comprueba que sus saldos no se mezclan. El segundo ejerce las tres razones de rechazo (importe cero, importe negativo y pago superior al saldo). El tercero, «Límites exactos de saldo», recarga hasta llegar exactamente a 150.00, que se acepta, y comprueba que 1 céntimo más se rechaza con LIMITE sin alterar el saldo. El cuarto cubre el caso de entrada con 0 operaciones, que no produce ninguna salida, y el quinto una sola operación de un titular nuevo.

Casos de prueba

CasoEntradaSalida esperadaSalida realResultado
Ejemplo del enunciado6 ana RECARGA 2000 ana PAGO 750 luis RECARGA 500 ana PAGO 1250 luis PAGO 600 ana RECARGA 3OK ana 20.00 OK ana 12.50 OK luis 5.00 OK ana 0.00 INSUFICIENTE luis 5.00 OK ana 0.03OK ana 20.00 OK ana 12.50 OK luis 5.00 OK ana 0.00 INSUFICIENTE luis 5.00 OK ana 0.03OK
Importes inválidos e insuficientes5 maria RECARGA 0 maria RECARGA -300 maria PAGO 100 maria RECARGA 1000 maria PAGO 1001INVALIDO maria 0.00 INVALIDO maria 0.00 INSUFICIENTE maria 0.00 OK maria 10.00 INSUFICIENTE maria 10.00INVALIDO maria 0.00 INVALIDO maria 0.00 INSUFICIENTE maria 0.00 OK maria 10.00 INSUFICIENTE maria 10.00OK
Límites exactos de saldo4 jon RECARGA 14000 jon RECARGA 1000 jon RECARGA 1 jon PAGO 15000OK jon 140.00 OK jon 150.00 LIMITE jon 150.00 OK jon 0.00OK jon 140.00 OK jon 150.00 LIMITE jon 150.00 OK jon 0.00OK
Sin operaciones0(vacía)(vacía)OK
Una sola operación1 zoe RECARGA 99OK zoe 0.99OK zoe 0.99OK

Salidas reales: código compilado con Java 21.0.12.1 (Temurin) y ejecutado en un contenedor aislado el 21 de octubre de 2026.

Complejidad

Cada operación cuesta O(1): una búsqueda en el HashMap (esperada constante), un par de comparaciones y una suma o resta de enteros. Para n operaciones, el coste temporal total es O(n) más el coste de leer y formatear las líneas.

La memoria es O(t), con t el número de titulares distintos, porque se guarda un objeto Monedero por titular. La salida se acumula en un StringBuilder, que ocupa O(n) caracteres; para un examen es perfectamente razonable, y la alternativa de imprimir línea a línea tendría el mismo coste asintótico en tiempo.

Errores típicos

  • Declarar saldo como public o como atributo sin modificador y modificarlo desde main: se rompe la encapsulación y las reglas dejan de estar centralizadas, que es justo lo que el ejercicio quiere evaluar.
  • Guardar el saldo en double en euros: 0.1 + 0.2 no es 0.3 en coma flotante y las comparaciones con el límite de 150 pueden fallar. Con céntimos enteros el problema desaparece.
  • Modificar el saldo antes de validar y revertirlo después, o aceptar la operación y comprobar luego: una operación rechazada debe dejar el saldo exactamente como estaba.
  • Comprobar mal el límite: usar >= en lugar de > rechaza la recarga que deja el saldo en exactamente 15000, que sí debe aceptarse (el test «Límites exactos de saldo» lo detecta).
  • Usar un único objeto Monedero compartido para todos los titulares, o crear uno nuevo en cada línea: en el primer caso los saldos se mezclan y en el segundo se pierde el saldo anterior.
  • Imprimir el saldo con System.out.println(saldo / 100.0): muestra 12.5 en lugar de 12.50 y no respeta el formato pedido.

Variantes

Añadir una transferencia entre dos titulares

Se pide una operación TRANSFERENCIA origen destino importe. La idea es que el monedero origen debe poder pagar y el destino poder recibir, y que la operación sea atómica: si cualquiera de las dos mitades falla, ninguno de los saldos cambia. Una forma sencilla es comprobar primero con las mismas reglas (importe positivo, importe <= saldo del origen y saldo + importe <= MAXIMO del destino) y solo entonces hacer pagar y recargar. Hacer el pago antes de comprobar el destino obligaría a devolver el dinero si este rechaza.

Sustituir los textos de resultado por un enum

En lugar de devolver String, defina enum Resultado { OK, INVALIDO, LIMITE, INSUFICIENTE } y haga que recargar y pagar devuelvan ese tipo. El compilador impide devolver un valor inexistente, y al imprimir basta resultado.name(). Es un ejemplo habitual de cómo el sistema de tipos de Java previene errores que con cadenas solo aparecerían al ejecutar.

En los temarios

El diseño de clases con estado encapsulado y la comunicación mediante métodos forman parte del temario de los cursos de orientación a objetos. El programa de IIC2233 Programación Avanzada de la Pontificia Universidad Católica de Chile incluye aplicar conceptos de orientación a objetos para escribir programas en lenguajes como Java y C#, y el curso ISIS1209 de la Universidad de los Andes (Colombia) estudia mecanismos de reutilización de clases, como la herencia, y de definición de contratos, como las interfaces.

Por su parte, el programa de Algoritmos y Programación II (75.41) de la Universidad de Buenos Aires contempla el manejo de memoria dinámica a través de punteros y referencias, una idea que en Java se traduce en entender que una variable de tipo Monedero guarda una referencia a un objeto y no el objeto mismo.

Historial de cambios

  1. Reescrito con la plantilla de Código de Examen: enunciado de examen, pistas, solución explicada, casos de prueba y código compilado y ejecutado; versión en inglés enlazada.
  2. Publicación original.

Artículo generado mediante AI.larebelion · Revisado por Kernel

Kernel

· Dirección de tecnología · España

«Un objeto que no protege su propio saldo no es un objeto, es una estructura con buenas intenciones.»