Parcial de práctica — Programación Orientada a Objetos

Temas: partes 1 y 2 del curso (semanas 1 a 8) · Duración: 2 horas

Este es un examen de práctica con el formato del parcial. Sirve para medir cuánto tiempo les toma cada tipo de pregunta: resuélvanlo en papel, con el reloj corriendo, y recién después comprueben sus respuestas con java o jshell.

Indicaciones

  • En el código cuenta la idea, no los errores menores de sintaxis.
  • En las preguntas de “qué imprime”, justifiquen cada respuesta: una salida correcta sin explicación no basta.
  • Todo el código de los enunciados compila con javac -Xlint:all (Java 25), salvo donde una pregunta propone cambiarlo. IO.println es el de Java 25.
  • Los montos van en céntimos, como enteros, igual que en el curso.
Parte Preguntas
A. Leer código 1–4
B. Escribir código 5–6
C. Diseño 7–8
D. Preguntas cortas 9–12

Parte A — Leer código

Pregunta 1 — Referencias y arreglos de objetos

public class P1Cards {

    static class TransitCard {
        private final String id;
        private int balance;            // céntimos

        TransitCard(String id) {
            this.id = id;
        }

        void recharge(int amount) {
            balance += amount;
        }

        String id() {
            return id;
        }

        int balance() {
            return balance;
        }
    }

    public static void main(String[] args) {
        TransitCard[] cards = { new TransitCard("A-001"), new TransitCard("A-002") };
        TransitCard[] same = cards;
        TransitCard[] copy = new TransitCard[2];
        for (int i = 0; i < 2; i++) {
            copy[i] = cards[i];
        }

        same[0].recharge(500);
        copy[1].recharge(200);
        copy[0] = new TransitCard("A-003");
        copy[0].recharge(100);
        for (TransitCard t : cards) {
            t = new TransitCard("X");
            t.recharge(999);
        }

        IO.println(cards[0].balance() + " " + cards[1].balance() + " " + copy[0].balance());
        IO.println((cards == same) + " " + (cards == copy) + " " + (cards[1] == copy[1]));

        int n = 1;
        String code = "A-00" + n;
        IO.println((code == cards[0].id()) + " " + code.equals(cards[0].id()));
    }
}

a) ¿Qué imprime el programa? Justifiquen cada valor.

b) Dibujen el stack y el heap justo antes del primer IO.println: las variables de main, los arreglos y las tarjetas, con sus saldos. ¿Cuántos objetos TransitCard se crearon en total? ¿Cuántos siguen siendo alcanzables desde main?

c) Expliquen el resultado de la última línea. ¿Con qué deberían comparar dos códigos de tarjeta, y por qué el == “funciona” a veces?

Pregunta 2 — Overloading, override y dynamic dispatch

public class P2Dispatch {

    static class Notification {
        String channel() {
            return "genérico";
        }

        long cost() {
            return 0;
        }
    }

    static class SmsNotification extends Notification {
        @Override
        String channel() {
            return "sms";
        }

        @Override
        long cost() {
            return 5;
        }
    }

    static class PushNotification extends Notification {
        @Override
        String channel() {
            return "push";
        }
    }

    static class Dispatcher {
        String route(Notification n) {
            return "general/" + n.channel();
        }

        String route(SmsNotification s) {
            return "operadora/" + s.channel();
        }
    }

    static class AuditDispatcher extends Dispatcher {
        @Override
        String route(Notification n) {
            return "auditado+" + super.route(n);
        }
    }

    public static void main(String[] args) {
        Notification a = new SmsNotification();
        SmsNotification b = new SmsNotification();
        Notification c = new PushNotification();
        Dispatcher d = new AuditDispatcher();

        IO.println("1. " + d.route(a));
        IO.println("2. " + d.route(b));
        IO.println("3. " + d.route((Notification) b));
        IO.println("4. " + new Dispatcher().route(c));
        IO.println("5. " + d.route(c));
        IO.println("6. " + (a.cost() + c.cost()));
    }
}

a) ¿Qué imprime cada línea, de la 1 a la 6? Para cada una de las líneas 1 a 5 digan qué decidió el compilador (qué firma de route) y qué decidió la JVM al ejecutar (la versión de qué clase).

b) ¿Por qué la línea 2 no pasa por AuditDispatcher, si d apunta a un AuditDispatcher? ¿Qué habría que agregar a AuditDispatcher para que también audite esa llamada?

c) Se agrega, después de la declaración de d, la línea SmsNotification s = a;. ¿Compila? Si en su lugar se agrega SmsNotification s = (SmsNotification) c;, ¿compila? ¿Qué pasa al ejecutarla?

Pregunta 3 — Constructores en cadena

public class P3Constructors {

    static class Product {
        private static int created = 0;
        private final String name;
        private final long priceCents;

        Product(String name, long priceCents) {
            created++;
            this.name = name;
            this.priceCents = priceCents;
            IO.println("Product(" + name + ", " + priceCents + ")");
        }

        Product(String name) {
            this(name, 100);
            IO.println("Product(" + name + ")");
        }

        static int created() {
            return created;
        }
    }

    static class PerishableProduct extends Product {
        private final int days;

        PerishableProduct(String name, long priceCents, int days) {
            super(name, priceCents);
            this.days = days;
            IO.println("PerishableProduct(" + name + ", " + days + " días)");
        }

        PerishableProduct(String name) {
            this(name, 250, 7);
            IO.println("PerishableProduct(" + name + ")");
        }
    }

    public static void main(String[] args) {
        new PerishableProduct("leche");
        IO.println("--");
        new Product("arroz");
        IO.println("--");
        Product p = new PerishableProduct("yogur", 400, 10);
        IO.println("creados: " + Product.created());
    }
}

a) ¿Qué imprime el programa? Indiquen qué constructor llama a cuál.

b) En total se ejecutaron siete cuerpos de los constructores de este programa. ¿Por qué created() devuelve 3?

c) Si se borra la línea super(name, priceCents); del primer constructor de PerishableProduct, ¿compila? Expliquen qué agrega el compilador en su lugar y por qué no le sirve.

Pregunta 4 — Leer un switch con patrones

Las tres formas de descuento de una bodega, como interfaz sealed:

public class P4Discounts {

    sealed interface Discount permits Percentage, Fixed, None { }

    record Percentage(int percent) implements Discount { }

    record Fixed(long cents) implements Discount { }

    record None() implements Discount { }

    static long finalPrice(Discount d, long price) {
        return switch (d) {
            case Percentage(int p) when p >= 100 -> 0;
            case Percentage(int p) -> price - price * p / 100;
            case Fixed(long c) when c > price -> 0;
            case Fixed(long c) -> price - c;
            case None() -> price;
        };
    }

    public static void main(String[] args) {
        IO.println(finalPrice(new Percentage(20), 1000));
        IO.println(finalPrice(new Fixed(1500), 1000));
        IO.println(finalPrice(new Percentage(150), 1000));
        IO.println(finalPrice(new Fixed(300), 1000));
        IO.println(finalPrice(new None(), 1000));
    }
}

a) ¿Qué imprime main?

b) Si se borra la línea case None() -> price;, ¿compila? ¿Qué diría el compilador y por qué es una ayuda?

c) Si se intercambian las dos líneas de Percentage (la que no tiene when queda primero), ¿compila? ¿Por qué?

d) ¿Qué pasa con finalPrice(null, 1000)?

Parte B — Escribir código

Pregunta 5 — Una clase con invariantes y sus pruebas

Escriban la clase WaterTank, un tanque de agua de una casa:

  • WaterTank(int capacity): la capacidad en litros. Tiene que ser positiva; si no, se lanza IllegalArgumentException. El tanque empieza vacío.
  • int fill(int liters): agrega agua. Si no cabe todo, llena el tanque hasta su capacidad y devuelve los litros que rebalsaron (0 si cupo todo).
  • int drain(int liters): saca agua. Si se pide más de lo que hay, saca lo que hay. Devuelve los litros que de verdad salieron.
  • int level(), int capacity() y boolean isEmpty().
  • fill y drain rechazan con IllegalArgumentException una cantidad que no sea positiva, sin cambiar el nivel.

a) Enuncien el invariante de la clase. ¿Qué método lo establece y cuáles tienen que preservarlo?

b) Escriban la clase completa, con campos private.

c) Escriban cuatro pruebas JUnit, cada una con un nombre que diga qué se espera, que cubran: el rebalse al llenar; sacar más de lo que hay; un constructor con capacidad inválida; y que una cantidad inválida no cambia el nivel.

Pregunta 6 — Una jerarquía pequeña

Una empresa de buses vende pasajes interprovinciales. Todo pasaje tiene el nombre del pasajero y una tarifa base en céntimos, y se puede pagar: implementa la interfaz de la semana 7.

public interface Payable {
    /** Monto por pagar, en céntimos. */
    long amountDue();
}
  • Ticket es una clase abstracta. Su constructor rechaza un nombre vacío y una tarifa base que no sea positiva. Declara abstract long price(). Su amountDue() devuelve el precio. Su método concreto String receipt() devuelve, por ejemplo, "Luis: S/ 30.25" para un precio de 3025 céntimos. Tiene además static long total(List<Ticket> tickets).
  • AdultTicket cobra la tarifa base.
  • StudentTicket es el medio pasaje: la mitad de la tarifa base, redondeada hacia abajo al céntimo.

a) Escriban las tres clases.

b) Escriban una prueba JUnit que arme una List<Ticket> con pasajes de los dos tipos y verifique total.

c) ¿Por qué conviene que Ticket sea una clase abstracta y Payable una interfaz, y no al revés? ¿Qué pasa si StudentTicket no escribe price()?

Parte C — Diseño

Pregunta 7 — Del enunciado al diagrama de clases

Un taller mecánico registra a sus clientes (nombre y DNI) y sus vehículos (placa y marca). Un cliente puede tener varios vehículos; cada vehículo es de un solo cliente. Cada vez que un vehículo entra al taller se abre una orden de trabajo, con la fecha y una lista de ítems. Un ítem es mano de obra (horas por tarifa por hora) o un repuesto (cantidad por precio unitario). La orden calcula su total sumando sus ítems. Cuando la orden se cierra, ya no acepta ítems nuevos.

a) Dibujen el diagrama de clases UML (a mano o en Mermaid): las clases con sus campos y métodos principales y su visibilidad, las asociaciones con su multiplicidad, y la relación entre la orden y sus ítems.

b) ¿Modelarían “ítem” como interfaz o como clase abstracta? Justifiquen según lo que comparten la mano de obra y el repuesto.

c) Enuncien el invariante de la orden de trabajo y digan qué clase lo protege.

Pregunta 8 — Sustitución de Liskov

public class Thermometer {
    private double current;
    private double max;
    private int readings;

    /** Registra una lectura. Después: current() == celsius, max() >= celsius. */
    public void record(double celsius) {
        current = celsius;
        if (readings == 0 || celsius > max) {
            max = celsius;
        }
        readings++;
    }

    public double current() {
        return current;
    }

    public double max() {
        return max;
    }

    public int readings() {
        return readings;
    }
}

Tres subclases:

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

/** (i) Guarda además todas las lecturas. */
public class HistoryThermometer extends Thermometer {
    private final List<Double> history = new ArrayList<>();

    @Override
    public void record(double celsius) {
        history.add(celsius);
        super.record(celsius);
    }

    public List<Double> history() {
        return List.copyOf(history);
    }
}
/** (ii) Un sensor de exteriores: descarta en silencio las lecturas absurdas. */
public class OutdoorThermometer extends Thermometer {
    @Override
    public void record(double celsius) {
        if (celsius < -20 || celsius > 50) {
            return;
        }
        super.record(celsius);
    }
}
/** (iii) Rechaza con una excepción las lecturas fuera de rango. */
public class StrictThermometer extends Thermometer {
    @Override
    public void record(double celsius) {
        if (celsius < -20 || celsius > 50) {
            throw new IllegalArgumentException("lectura fuera de rango: " + celsius);
        }
        super.record(celsius);
    }
}

a) Para cada subclase, digan si cumple el principio de sustitución de Liskov. Si no lo cumple, digan qué se rompe: ¿una precondición reforzada o una postcondición debilitada?

b) Escriban un método de prueba assertRecordContract(Thermometer t) que verifique lo que cualquier cliente de Thermometer espera de record, que pase con un Thermometer y que falle con cada subclase que no cumple. El objeto tiene que llegar al método como Thermometer.

c) Propongan un diseño para el sensor de exteriores que no viole el principio.

Parte D — Preguntas cortas

Respondan cada una en tres o cuatro líneas.

Pregunta 9

¿Por qué un record es inmutable? ¿Es inmutable record Order(String id, ArrayList<String> items)? Expliquen.

Pregunta 10

Una clase hace override de equals pero no de hashCode. ¿Funciona ArrayList.contains con objetos iguales pero distintos? ¿Y HashSet.contains? ¿A qué se debe la diferencia?

Pregunta 11

¿Cuándo conviene composición en lugar de herencia? Den un ejemplo de la biblioteca estándar de Java en que la herencia fue un error y digan qué permite hacer que no debería.

Pregunta 12

Parent y Child extends Parent declaran cada una un método static String kind(). Con Parent p = new Child();, ¿cuál se ejecuta en p.kind()? ¿Por qué no es dynamic dispatch?