Final de práctica — Programación Orientada a Objetos
Temas: todo el curso, con énfasis en las partes 3 y 4 (semanas 9 a 16) · Duración: 3 horas
Este es un examen de práctica con el formato del final. 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 las líneas que una pregunta presenta para decidir si compilan. - Los montos van en céntimos, como enteros, igual que en el curso.
| Parte | Preguntas |
|---|---|
| A. Leer código | 1–6 |
| B. Escribir código | 7–9 |
| C. Diseño | 10–11 |
| D. Preguntas cortas | 12–15 |
Parte A — Leer código
Pregunta 1 — Excepciones que se propagan
public class F1Sales {
static class InsufficientFundsException extends Exception {
private static final long serialVersionUID = 1L;
InsufficientFundsException(String message) {
super(message);
}
}
static class SaleException extends Exception {
private static final long serialVersionUID = 1L;
SaleException(String message, Throwable cause) {
super(message, cause);
}
}
static long parseCents(String text) {
IO.println("parse " + text);
return Long.parseLong(text);
}
static void charge(long balance, long amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException("saldo " + balance + ", pide " + amount);
}
IO.println("cobrado " + amount);
}
static void sell(long balance, String text) throws SaleException {
try {
charge(balance, parseCents(text));
IO.println("venta ok");
} catch (InsufficientFundsException e) {
throw new SaleException("venta rechazada", e);
} finally {
IO.println("fin de venta " + text);
}
}
public static void main(String[] args) {
String[] inputs = { "300", "900", "abc", "200" };
for (String in : inputs) {
try {
sell(500, in);
} catch (SaleException e) {
IO.println(e.getMessage() + ": " + e.getCause().getMessage());
}
}
IO.println("fin");
}
}
a) ¿Qué imprime el programa y cómo termina? ¿Se procesa la
entrada "200"? ¿Se imprime "fin"? Justifiquen siguiendo cada excepción
desde donde se lanza hasta donde se captura (o hasta que nadie la captura).
b) ¿Por qué sell tiene que declarar throws SaleException,
y main compila sin capturar ni declarar la excepción que lanza
Long.parseLong?
c) En la segunda venta, ¿por qué "fin de venta 900" sale antes
que "venta rechazada: ..."? Si sell lanzara
new SaleException("venta rechazada", null), ¿qué cambiaría en la salida?
Pregunta 2 — try-with-resources
public class F2Resources {
static class Resource implements AutoCloseable {
private final String name;
private final boolean failOnClose;
Resource(String name, boolean failOnOpen, boolean failOnClose) {
if (failOnOpen) {
throw new IllegalStateException("no se pudo abrir " + name);
}
this.name = name;
this.failOnClose = failOnClose;
IO.println("abre " + name);
}
void use() {
IO.println("usa " + name);
}
@Override
public void close() {
IO.println("cierra " + name);
if (failOnClose) {
throw new IllegalStateException("falla al cerrar " + name);
}
}
}
public static void main(String[] args) {
IO.println("-- caso 1");
try (Resource log = new Resource("log", false, false);
Resource printer = new Resource("impresora", false, true)) {
log.use();
printer.use();
IO.println("venta guardada");
} catch (IllegalStateException e) {
IO.println("capturada: " + e.getMessage()
+ " (suprimidas: " + e.getSuppressed().length + ")");
}
IO.println("-- caso 2");
try (Resource log = new Resource("log", false, false);
Resource net = new Resource("red", true, false)) {
log.use();
net.use();
} catch (IllegalStateException e) {
IO.println("capturada: " + e.getMessage());
} finally {
IO.println("finally");
}
IO.println("-- caso 3");
try (Resource log = new Resource("log", false, true)) {
log.use();
throw new IllegalArgumentException("línea 3 mal formada");
} catch (RuntimeException e) {
IO.println("capturada: " + e.getClass().getSimpleName() + ": " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
IO.println("suprimida: " + s.getMessage());
}
}
}
}
a) ¿Qué imprime cada uno de los tres casos?
b) En el caso 2, ¿por qué no aparece "cierra red"? En el caso
3, ¿por qué la excepción que llega al catch es la del bloque y no la de
close(), y dónde queda esta última?
Pregunta 3 — Claves, equals y hashCode
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public class F3Keys {
record Song(String title, String artist, int seconds) { }
/** Un código de producto con equals... y sin hashCode. */
@SuppressWarnings("overrides")
static final class Sku {
private final String code;
Sku(String code) {
this.code = code;
}
@Override
public boolean equals(Object o) {
return o instanceof Sku other && code.equals(other.code);
}
}
public static void main(String[] args) {
Map<Song, Integer> plays = new HashMap<>();
Song garua = new Song("Garúa", "Costa Verde", 220);
plays.put(garua, 1);
Song again = new Song("Garúa", "Costa Verde", 220);
plays.put(again, plays.getOrDefault(again, 0) + 1);
IO.println("1. " + plays.size() + " " + plays.get(new Song("Garúa", "Costa Verde", 220)));
Map<Sku, Integer> stock = new HashMap<>();
stock.put(new Sku("A-1"), 10);
stock.put(new Sku("A-1"), 20);
IO.println("2. " + stock.size() + " " + stock.get(new Sku("A-1"))
+ " " + new Sku("A-1").equals(new Sku("A-1")));
List<Song> songs = List.of(
new Song("Malecón", "Costa Verde", 190),
new Song("Cumbre", "Los Andinos", 190),
new Song("Arena", "Duna", 185));
Set<Song> byLength = new TreeSet<>(Comparator.comparingInt(Song::seconds));
byLength.addAll(songs);
Set<Song> hashed = new HashSet<>(songs);
IO.println("3. " + byLength.size() + " " + hashed.size());
IO.println("4. " + byLength.contains(new Song("Otra", "Nadie", 185)));
}
}
a) ¿Qué imprime cada una de las cuatro líneas? Justifiquen.
b) En la línea 2, equals dice que las dos claves son iguales.
¿Por qué el mapa tiene dos entradas y get devuelve null? ¿Qué advierte
javac -Xlint:all si se borra el @SuppressWarnings? Corrijan Sku.
c) En las líneas 3 y 4, ¿por qué el TreeSet y el HashSet
tienen tamaños distintos con los mismos elementos? ¿Qué propiedad le falta al
comparador? Corríjanlo para que dos canciones distintas no se pierdan.
Pregunta 4 — Un stream paso a paso
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class F4Streams {
record Song(String title, String artist, int seconds) { }
public static void main(String[] args) {
List<Song> songs = List.of(
new Song("Garúa", "Costa Verde", 220),
new Song("Puente de piedra", "Los Andinos", 245),
new Song("Arena", "Duna", 185),
new Song("Brisa", "Duna", 210),
new Song("Cumbre", "Los Andinos", 190));
List<String> shortest = songs.stream()
.filter(s -> {
IO.println("filter " + s.title());
return s.seconds() < 230;
})
.sorted(Comparator.comparingInt(Song::seconds))
.map(s -> {
IO.println("map " + s.title());
return s.title();
})
.limit(2)
.toList();
IO.println(shortest);
Map<String, Long> perArtist = songs.stream()
.collect(Collectors.groupingBy(Song::artist, TreeMap::new, Collectors.counting()));
IO.println(perArtist);
}
}
a) ¿Qué imprime el programa? Expliquen por qué todos los
filter salen antes del primer map, y por qué hay solo dos map.
b) ¿Qué cambia en la última línea si se quita TreeMap::new del
groupingBy?
Pregunta 5 — Genéricos y wildcards
Con estos dos métodos y estas variables:
static double sum(List<? extends Number> xs) { ... }
static void addZeros(List<? super Integer> xs, int n) { ... }
List<Integer> ints = new ArrayList<>(List.of(1, 2));
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();
List<? extends Number> view = ints;
Object o = ints;
a) Digan cuáles de estas líneas compilan, cada una por separado. Para las que no, expliquen por qué.
sum(ints); // (a)
addZeros(nums, 2); // (b)
addZeros(objs, 2); // (c)
sum(objs); // (d)
view.add(3); // (e)
Number first = view.get(0); // (f)
boolean b = o instanceof List<Integer>; // (g)
b) ¿Por qué no compila una clase con estos dos métodos?
static void print(List<String> names) { }
static void print(List<Integer> numbers) { }
Pregunta 6 — Eventos y estados
La caja de la bodega reacciona a eventos de texto según su estado:
import java.util.List;
public class F6Register {
enum State { CLOSED, OPEN, SELLING }
static class Register {
private State state = State.CLOSED;
private int sales = 0;
String handle(String event) {
return switch (state) {
case CLOSED -> event.equals("abrir") ? go(State.OPEN, "caja abierta") : "caja cerrada";
case OPEN -> switch (event) {
case "vender" -> go(State.SELLING, "nueva venta");
case "cerrar" -> go(State.CLOSED, "cierre, ventas: " + sales);
default -> "no aplica";
};
case SELLING -> switch (event) {
case "confirmar" -> confirm();
case "cancelar" -> go(State.OPEN, "venta cancelada");
default -> "termine la venta primero";
};
};
}
private String confirm() {
sales++;
return go(State.OPEN, "venta " + sales + " registrada");
}
private String go(State next, String message) {
state = next;
return message;
}
}
public static void main(String[] args) {
Register r = new Register();
for (String e : List.of("vender", "abrir", "vender", "cerrar", "confirmar",
"vender", "cancelar", "abrir", "cerrar")) {
IO.println(e + " -> " + r.handle(e));
}
}
}
a) ¿Qué imprime el programa?
b) Se agrega SUSPENDED al enum State. ¿Qué pasa al
compilar? ¿Pasaría lo mismo si handle usara una sentencia switch (con
case ...: y return en cada rama) en lugar de una expresión?
Parte B — Escribir código
Pregunta 7 — Una clase genérica
Escriban Tally<T>, que cuenta cuántas veces aparece cada elemento:
void add(T item): cuenta una aparición. RechazanullconNullPointerException.void addAll(Collection<...> items): cuenta todos. Tiene que aceptar una colección deTo de cualquier subtipo deT; por ejemplo, unTally<Number>acepta unaList<Integer>.int count(T item): cuántas veces apareció (0 si nunca).int distinct(): cuántos elementos distintos hay.Optional<T> mostCommon(): el más frecuente; si empatan, el que apareció primero; vacío si no se agregó nada.
a) Escriban la clase. Elijan la colección interna y justifiquen la elección en una línea.
b) Escriban cuatro pruebas JUnit: repeticiones y un elemento que
no está; addAll con una lista de un subtipo; mostCommon vacío; y el
empate.
Pregunta 8 — Consultas con streams
Los viajes de una app de taxis:
/** Un viaje de una app de taxis: tarifa en céntimos. */
public record Trip(String driver, String district, int minutes, long fareCents) {
public Trip {
if (driver == null || driver.isBlank() || district == null || district.isBlank()) {
throw new IllegalArgumentException("conductor y distrito son obligatorios");
}
if (minutes <= 0 || fareCents < 0) {
throw new IllegalArgumentException("minutos o tarifa inválidos");
}
}
}
Datos de ejemplo:
List<Trip> trips = List.of(
new Trip("rosa", "Miraflores", 25, 1850),
new Trip("luis", "Surco", 40, 2600),
new Trip("rosa", "Surco", 15, 1100),
new Trip("ana", "Miraflores", 40, 2900),
new Trip("luis", "Lince", 10, 800));
Escriban con streams, en una clase TripReports:
a) long totalFare(List<Trip> trips). Con los datos: 9250.
b) Map<String, Long> fareByDriver(List<Trip> trips), ordenado
por conductor. Con los datos: {ana=2900, luis=3400, rosa=2950}.
c) Optional<String> busiestDistrict(List<Trip> trips): el
distrito con más viajes; si empatan, el primero en orden alfabético; vacío si
no hay viajes. Con los datos: Miraflores.
d) List<Trip> longestFirst(List<Trip> trips): de mayor a menor
duración y, a igual duración, por conductor. Con los datos, los conductores
quedan en el orden ana, luis, rosa, rosa, luis.
e) Un compañero ordena por tarifa con
(x, y) -> (int) (x.fareCents() - y.fareCents()). ¿Qué puede salir mal?
Escriban la versión correcta.
Pregunta 9 — Leer un CSV
El archivo tiene un encabezado y una línea por viaje:
conductor,distrito,minutos,tarifa_centimos
rosa,Miraflores,25,1850
luis,Surco,40,2600
Ya existe esta excepción del dominio:
/** Una línea del CSV que no se puede convertir en un viaje. */
public class MalformedLineException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final int lineNumber;
public MalformedLineException(int lineNumber, String message, Throwable cause) {
super("línea " + lineNumber + ": " + message, cause);
this.lineNumber = lineNumber;
}
public int lineNumber() {
return lineNumber;
}
}
a) Escriban, en una clase TripCsv,
static Trip parse(String line, int lineNumber), que lance
MalformedLineException si la línea no tiene cuatro campos o si un campo
numérico no es válido (sin perder la excepción original), y
static List<Trip> load(Path path) throws IOException, que salte el
encabezado y cierre el archivo aunque haya un error.
b) ¿Por qué load declara throws IOException en lugar de
capturarla? ¿Dónde la capturarían?
Parte C — Diseño
Pregunta 10 — Principios y refactoring
import java.util.List;
public class LowStockReport {
public void send(List<Product> products, String email) {
StringBuilder sb = new StringBuilder();
for (Product p : products) {
if (p.stock() < 5) {
switch (p.kind()) {
case "unidad" -> sb.append(p.name()).append(": ").append(p.stock()).append(" u\n");
case "peso" -> sb.append(p.name()).append(": ").append(p.stock()).append(" kg\n");
default -> sb.append(p.name()).append(": ?\n");
}
}
}
if (!sb.isEmpty()) {
new SmtpMailer("smtp.bodega.pe").send(email, "Stock bajo", sb.toString());
}
}
}
Product es un record (String name, String kind, int stock); SmtpMailer
envía correos de verdad a través de un servidor.
a) Nombren tres problemas de diseño de esta clase. Para cada uno, digan qué principio SOLID (o qué code smell) corresponde y qué cambio concreto de requisitos lo haría doler.
b) La clase no tiene pruebas. Describan, en orden, los primeros pasos pequeños de un refactoring seguro. ¿Qué hacen antes de mover nada?
c) Escriban la versión final de LowStockReport, en la que el
envío entra por el constructor como una interfaz Mailer, y una prueba JUnit
que verifique el correo enviado sin usar ningún servidor.
Pregunta 11 — Patrones: cuándo sí y cuándo no
Para cada situación, digan qué patrón usarían (Strategy, Observer, Factory, Composite o Decorator) o si no hace falta ninguno, y justifiquen en una o dos líneas.
- Cuando el stock de un producto baja de un umbral, la bodega quiere avisar al celular de la dueña y anotarlo en un registro. Mañana podría agregarse un correo.
- El precio de una venta se calcula con una promoción que elige el cajero: ninguna, 10 % los martes o “lleve 3 pague 2”.
- Una “canasta familiar” contiene productos y otras canastas, y su precio es la suma de lo que contiene menos un descuento propio.
- Al leer el CSV, la columna
tipodecide si se crea un producto por unidad o por peso. Hoy esa decisión está copiada en el importador y en el menú. - Se quiere medir cuánto demora cada llamada a
savede un repositorio, sin tocar la clase del repositorio. - El IGV es 18 % y se calcula en un solo lugar del programa. Alguien propone una
interfaz
TaxStrategy“por si cambia”.
Parte D — Preguntas cortas
Respondan cada una en tres o cuatro líneas.
Pregunta 12
¿Qué pierde el programa con el type erasure? Den un ejemplo de algo que no
se puede escribir dentro de una clase genérica Box<T> por esa razón.
Pregunta 13
max de un stream devuelve Optional<T> en lugar de T o null. ¿Qué gana
quien lo llama? ¿Por qué llamar a get() sin verificar no mejora nada?
Pregunta 14
¿Cuántos campos devuelve line.split(",") si line es
"Arroz Costeño, 5 kg",1850,10 (con las comillas dentro del texto)?
Propongan una regla para el formato CSV de la bodega que evite el problema.
Pregunta 15
Un ReadOnlySongRepository implements SongRepository (la interfaz de la
semana 15, con load y save) lanza UnsupportedOperationException en
save. ¿Qué principios SOLID se violan? ¿Cómo cambiarían las interfaces?
Lista de verificación: resultados de aprendizaje
Después de resolver el examen y comparar sus respuestas, marquen lo que ya pueden hacer. Cada fila es un resultado de aprendizaje del programa del curso, con las semanas donde se trabaja y las preguntas de este examen que lo ejercitan. Si una fila no la pueden marcar todavía, vuelvan a esas semanas antes del final.
| Puedo… | Semanas | Preguntas |
|---|---|---|
| … modelar un problema con clases y objetos: identificar responsabilidades, estado e invariantes, y protegerlos con encapsulamiento. | 1–4 | 7a, 10c |
| … explicar cómo se representan los objetos en memoria (referencias, heap, garbage collector) y distinguir identidad de igualdad. | 2, 5, 11 | 3 |
| … usar herencia, clases abstractas e interfaces, explicar el dynamic dispatch y elegir entre herencia y composición con el principio de sustitución de Liskov. | 5–8 | 10c, 11, 15 |
| … escribir código robusto con excepciones: lanzarlas, propagarlas, liberar recursos y elegir entre checked y unchecked, también al leer archivos. | 9, 15 | 1, 2, 9, 14 |
| … escribir código reutilizable con genéricos y el Collections Framework. | 10, 11 | 3, 5, 7a, 12 |
… escribir consultas con lambdas y streams, y usar Optional. |
12 | 4, 8, 13 |
| … aplicar principios de diseño (SOLID, bajo acoplamiento, alta cohesión) y patrones de diseño clásicos, y reconocer cuándo no hacen falta. | 13–16 | 6, 10a, 11, 15 |
| … escribir unit tests con JUnit y refactorizar con ellos como red de seguridad. | 3, 7, 13 | 7b, 10b, 10c |