Apuntes · Capítulo 8Herencia e interfaces
Tipos sealed, pattern matching y repaso para el parcial
Tipos sealed, pattern matching y repaso para el parcial
La semana 6 terminó con una advertencia: una cadena de
if (x instanceof A) ... else if (x instanceof B) ... reemplaza al
polimorfismo, y cuando aparece un tipo nuevo hay que encontrar a mano cada
cadena que lo debería tratar. La respuesta de esa semana fue mover el
comportamiento a un método con un override en cada subclase. La semana 7 agregó las
interfaces y la composición, y con el principio de Liskov aprendimos a
desconfiar de las jerarquías que crecen sin control.
Este capítulo presenta la otra respuesta que Java da desde la versión 17. Hay
jerarquías que no deberían crecer: una figura del plano es un círculo, un
cuadrado o un triángulo, y nada más; una expresión aritmética es un número, una
suma o un producto. Para esos casos, Java permite cerrar la jerarquía con
sealed, y entonces el compilador sabe cuáles son todos los subtipos. Con esa
información, un switch que pregunta por el tipo deja de ser frágil: si falta
un caso, no compila. Es la cadena de instanceof de la semana 6, pero con el
compilador de nuestro lado.
La segunda mitad del capítulo es el repaso para el examen parcial de la Sesión B: qué entra, cómo estudiar y una serie de ejercicios tipo examen.
Lectura. Eck cubre Java 17 y no trata el
switchcon patrones. Lean de Eck la subsección 3.6.5 Switch Expressions, que presenta la formacase ... ->y elswitchcomo expresión. Para lo nuevo, la guía oficial de Java 25, Java Language Updates: Sealed Classes, Pattern Matching y Pattern Matching with switch, y en dev.java, Using Pattern Matching. El código del capítulo está encode/week-08.
Ruta de lectura.
- Antes de clase (obligatorio, 15–20 min): Un problema que la herencia no resuelve sola y
sealedypermits.- En clase: en la sesión A,
final,sealedonon-sealed, Pattern matching coninstanceof,switchcon patrones y Exhaustividad: el compilador avisa, y el repaso de Qué hay que saber hacer. La sesión B es el examen parcial.- Después de clase: Ejemplo resuelto: un evaluador de expresiones, Dos estilos: override o
switch, el Resumen y los Ejercicios. Para el parcial, Ejercicios tipo examen y el parcial de práctica deexams/.
Jerarquías cerradas con sealed
Un problema que la herencia no resuelve sola
Una interfaz normal está abierta: cualquier clase, en cualquier parte del
programa, puede escribir implements Shape. Eso es justamente lo que se busca
cuando la interfaz es una capacidad, como el Payable de la semana 7: mañana
alguien escribe otra clase que se puede pagar y todo el código que recibe un
Payable la acepta sin cambios.
Pero a veces la lista de subtipos es parte del diseño. Si un programa de geometría trabaja con círculos y cuadrados, el código que calcula áreas, dibuja o exporta figuras tiene que tratar a cada uno de esos tipos. Si la jerarquía está abierta, el compilador no puede saber si un método trató todos los casos, porque nunca sabe cuáles son todos.
sealed y permits
Una clase o interfaz sealed dice en su declaración qué subtipos
directos tiene, y ningún otro puede extenderla.
Shape.java:
public sealed interface Shape permits Circle, Square, Triangle {
}
Los subtipos permitidos son tres records, uno por archivo
(Circle.java,
Square.java,
Triangle.java):
public record Circle(double radius) implements Shape {
public Circle {
if (radius <= 0) {
throw new IllegalArgumentException("radio no positivo: " + radius);
}
}
}
public record Square(double side) implements Shape { ... }
public record Triangle(double base, double height) implements Shape { ... }
Son los records de la semana 3, con su constructor compacto que valida: datos
inmutables, con equals, hashCode y toString generados. La novedad es que
ahora forman una jerarquía cerrada. Si alguien intenta agregar un subtipo
que no está en la lista, el compilador lo rechaza
(broken/Hexagon.java):
$ make errores
...
== broken/Hexagon.java
broken/Hexagon.java:2: error: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)
public record Hexagon(double side) implements Shape {
^
1 error
final, sealed o non-sealed
Cerrar el primer nivel no sirve de mucho si el segundo queda abierto: si
Circle fuera una clase normal, cualquiera podría escribir
class Ellipse extends Circle y la jerarquía volvería a crecer por abajo. Por
eso Java exige que cada subtipo permitido diga cómo sigue la jerarquía, con
uno de tres modificadores:
| Modificador | Significa |
|---|---|
final |
Nadie puede extenderlo. La jerarquía termina aquí. |
sealed ... permits ... |
Se cierra otra vez, con su propia lista. |
non-sealed |
Se abre a propósito: desde aquí cualquiera puede extenderlo. |
Si no se escribe ninguno, no compila:
public sealed interface Vehicle permits Car, Truck {
}
class Car implements Vehicle { // falta final, sealed o non-sealed
}
final class Truck implements Vehicle {
}
$ javac -Xlint:all Vehicle.java
Vehicle.java:4: error: sealed, non-sealed or final modifiers expected
class Car implements Vehicle {
^
1 error
Un record es final por definición, así que cumple la regla sin escribir
nada. Por eso records y tipos sealed se usan tanto juntos: el tipo sealed
dice “una figura es una de estas tres cosas” y cada record dice “y esta cosa
tiene estos datos”.
En Java. Los subtipos permitidos tienen que estar en el mismo paquete que el tipo sealed (o en el mismo módulo, si el programa usa módulos). Si todos están en el mismo archivo que el tipo sealed, se puede omitir
permitsy el compilador la deduce. Las clases sealed son parte del lenguaje desde Java 17 (JEP 409).
Idea clave.
sealedconvierte la lista de subtipos en algo que el compilador conoce. No cambia lo que el programa hace al ejecutarse; cambia lo que el compilador puede verificar antes.
Pattern matching con instanceof
Ya usaron una forma de pattern matching en la semana 5, al escribir equals:
if (o instanceof Fraction f) {
return num == f.num && den == f.den;
}
o instanceof Fraction f hace dos cosas en un paso: prueba si o es un
Fraction y, si lo es, declara la variable f, ya de tipo Fraction, sin
casting. Fraction f es un patrón de tipo (type pattern). Antes de Java 16
había que escribir la prueba y el casting por separado:
if (o instanceof Fraction) {
Fraction f = (Fraction) o; // el casting repite lo que ya se probó
...
}
La variable del patrón existe solo donde el compilador puede asegurar que la
prueba fue verdadera. Dentro del if, sí; en el else, no. Y funciona también
con la negación y con &&:
if (!(o instanceof String s)) {
return 0;
}
return s.length(); // aquí s existe: si no fuera String, ya habríamos salido
if (o instanceof String s && !s.isEmpty()) { ... } // s existe a la derecha del &&
Con || no funciona: en o instanceof String s || s.isEmpty(), si la primera
parte es falsa, s no tiene valor, y el compilador lo rechaza.
Error común. Usar el patrón para seguir escribiendo cadenas de
instanceof. Es más corto que con casting, pero tiene el mismo problema de la semana 6:Shapes.areaWithInstanceoftrata círculos y cuadrados, olvidó los triángulos, y compila igual. El error aparece recién al ejecutar:instanceof: figura desconocida: Triangle[base=3.0, height=4.0]
switch con patrones
Patrones de tipo en un switch
En el switch clásico de C, cada case compara con una constante. Desde
Java 21, un case puede llevar un patrón: el switch prueba el tipo del
valor y, si coincide, declara la variable. Con la forma case ... -> de Eck
(3.6.5), el switch además es una expresión que produce un valor:
double area = switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square sq -> sq.side() * sq.side();
case Triangle t -> t.base() * t.height() / 2;
};
Patrones de record
Como Circle es un record, se puede ir un paso más allá y desarmarlo en el
mismo case. Un patrón de record (record pattern) nombra el record y, entre
paréntesis, un patrón para cada componente.
Shapes.java:
public static double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Square(double side) -> side * side;
case Triangle(double b, double h) -> b * h / 2;
};
}
case Circle(double r) se lee “si s es un Circle, llama r a su radio”.
No hay casting ni llamadas a métodos de acceso: el patrón los hace. Los
nombres de las variables los eligen ustedes (r en lugar de radius); lo que
importa es el orden y el tipo de los componentes, que tienen que coincidir con la
declaración del record. En lugar del tipo se puede escribir var:
case Triangle(var b, var h).
Los patrones de record se pueden anidar. En el evaluador de expresiones de
más abajo, case Mul(Num(int v), Expr r) coincide solo con un producto cuyo
lado izquierdo es un número, y de paso extrae ese número.
En Java. Si un componente no se usa, se puede escribir
_en su lugar:case Triangle(double b, _) -> .... Son los patrones sin nombre (unnamed patterns) de JEP 456, parte del lenguaje desde Java 22.
Guards con when
A veces el tipo no basta para decidir. Un guard es una condición que se
agrega al patrón con when; el caso se elige solo si el patrón coincide
y la condición es verdadera:
public static String describe(Shape s) {
return switch (s) {
case null -> "sin figura";
case Circle c when c.radius() >= 10 -> "círculo grande";
case Circle c -> "círculo de radio " + c.radius();
case Square(double side) when side == 1 -> "cuadrado unitario";
case Square sq -> "cuadrado de lado " + sq.side();
case Triangle t -> "triángulo de base " + t.base();
};
}
Los casos se prueban en orden, de arriba abajo, y gana el primero que
coincide. Por eso el caso con guard va antes que el caso general del mismo
tipo. Al revés no compila: case Circle c sin guard ya captura todos los
círculos, y el caso con guard nunca se alcanzaría. El compilador detecta ese
caso dominado (dominated);
broken/Dominated.java lo muestra con
case Shape any antes de case Circle c:
== broken/Dominated.java
broken/Dominated.java:6: error: this case label is dominated by a preceding case label
case Circle c -> "un círculo";
^
1 error
case null
Un switch clásico que recibe null lanza NullPointerException, y uno con
patrones también, salvo que tenga un case null. Shapes.area(null) lanza la
excepción; Shapes.describe(null) devuelve "sin figura". Si null es un
valor que tiene sentido recibir, case null lo trata en el mismo lugar que los
demás; si no lo tiene, es mejor dejar que falle (la semana 9 vuelve sobre esto:
fallar pronto).
Ejemplo resuelto: el recorrido completo
ShapeDemo.java recorre una lista de figuras
con las dos operaciones:
List<Shape> shapes = List.of(new Circle(1), new Square(2), new Triangle(3, 4), new Circle(10));
for (Shape s : shapes) {
IO.println(s + " -> área " + "%.2f".formatted(Shapes.area(s)) + ", " + Shapes.describe(s));
}
IO.println(Shapes.describe(null));
$ java -cp build ShapeDemo
Circle[radius=1.0] -> área 3.14, círculo de radio 1.0
Square[side=2.0] -> área 4.00, cuadrado de lado 2.0
Triangle[base=3.0, height=4.0] -> área 6.00, triángulo de base 3.0
Circle[radius=10.0] -> área 314.16, círculo grande
sin figura
instanceof: figura desconocida: Triangle[base=3.0, height=4.0]
Fíjense en la cuarta línea: Circle(10) coincide con el primer caso de
círculo, el del guard, y nunca llega al segundo. La primera parte de cada
línea es el toString que el record generó. La última línea es la versión con
instanceof del error común de la sección anterior.
Exhaustividad: el compilador avisa
Un switch sin default
Shapes.area no tiene default, y no le hace falta: Shape es sealed y el
switch trata a sus tres subtipos permitidos. El compilador puede comprobar
que el switch es exhaustivo (exhaustive): para cualquier Shape que
reciba, algún caso coincide.
Ahora imaginen la historia al revés. La primera versión del programa tenía solo
círculos y cuadrados: permits Circle, Square, y un area con dos casos.
Meses después alguien agrega Triangle a la lista. En ese momento, cada
switch sin default que no trata a Triangle deja de compilar.
broken/AreaMissingCase.java es
ese area viejo, compilado contra la jerarquía nueva:
static double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Square(double side) -> side * side;
};
}
== broken/AreaMissingCase.java
broken/AreaMissingCase.java:4: error: the switch expression does not cover all possible input values
return switch (s) {
^
1 error
El error dice dónde falta el caso. En un programa grande, el compilador
hace la lista de todos los lugares que hay que actualizar, que es justo lo que
la cadena de instanceof nunca podía hacer.


Error común: el default “por seguridad”
Error común. Agregar
defaulta unswitchsobre un tipo sealed “por si acaso”.broken/ChainNoWarning.javalo hace:return switch (s) { case Circle(double r) -> Math.PI * r * r; case Square(double side) -> side * side; default -> 0; };== broken/ChainNoWarning.java (compila sin errores)Compila, y ese es el problema: el área de un triángulo ahora es 0, en silencio. El
defaultle dice al compilador “todo lo demás va aquí”, y el compilador ya no tiene nada que avisar. Sobre un tipo sealed, no escribandefault: dejen que el compilador verifique.
En Java. ¿Y si
Shapese recompila con un subtipo nuevo pero la clase que tiene elswitchno? El compilador no puede avisar de algo que no ve, así que agrega por su cuenta un caso escondido: si en ejecución llega un valor que ningún caso cubre, elswitchlanzaMatchException. El error no pasa en silencio, aunque llegue tarde. Recompilar todo, como hacemake, evita el problema.
Idea clave. Un
switchsindefaultsobre un tipo sealed es una lista de verificación que mantiene el compilador. Cada vez que la jerarquía cambia, el compilador revisa todas las listas.
Ejemplo resuelto: un evaluador de expresiones
Las expresiones aritméticas son el ejemplo clásico de jerarquía cerrada. Una
expresión es un número, una suma de dos expresiones o un producto de dos
expresiones (Expr.java,
Num.java, Add.java,
Mul.java):
public sealed interface Expr permits Num, Add, Mul {
}
public record Num(int value) implements Expr { }
public record Add(Expr left, Expr right) implements Expr { }
public record Mul(Expr left, Expr right) implements Expr { }
La definición es recursiva: los componentes de Add y Mul son a su vez
expresiones. 2 + 3 * 4 es una suma cuyo lado derecho es un producto:
Expr e = new Add(new Num(2), new Mul(new Num(3), new Num(4)));
Cada operación sobre expresiones es un switch exhaustivo, y la recursión
sigue la forma de los datos (Calc.java):
public static int eval(Expr e) {
return switch (e) {
case Num(int v) -> v;
case Add(Expr l, Expr r) -> eval(l) + eval(r);
case Mul(Expr l, Expr r) -> eval(l) * eval(r);
};
}
public static String show(Expr e) {
return switch (e) {
case Num(int v) -> Integer.toString(v);
case Add(Expr l, Expr r) -> "(" + show(l) + " + " + show(r) + ")";
case Mul(Expr l, Expr r) -> show(l) + " * " + show(r);
};
}


Un tercer switch muestra patrones anidados con guards: simplify quita
las multiplicaciones por 1 y las sumas de 0.
public static Expr simplify(Expr e) {
return switch (e) {
case Mul(Num(int v), Expr r) when v == 1 -> simplify(r);
case Mul(Expr l, Num(int v)) when v == 1 -> simplify(l);
case Add(Num(int v), Expr r) when v == 0 -> simplify(r);
case Add(Expr l, Num(int v)) when v == 0 -> simplify(l);
case Add(Expr l, Expr r) -> new Add(simplify(l), simplify(r));
case Mul(Expr l, Expr r) -> new Mul(simplify(l), simplify(r));
case Num n -> n;
};
}
Los cuatro primeros casos tienen guard, así que no cuentan para la
exhaustividad. Los tres últimos, sin guard, cubren los tres tipos: el
switch es exhaustivo. ExprDemo.java usa
las tres operaciones:
$ java -cp build ExprDemo
Add[left=Num[value=2], right=Mul[left=Num[value=3], right=Num[value=4]]]
(2 + 3 * 4) = 14
(2 + 3) * 4 = 20
(1 * 7 + 0) se simplifica a 7
La primera línea es el toString generado por los records: muestra el árbol
tal como está en memoria. Las dos siguientes muestran que el árbol, no el
texto, decide el orden de las operaciones.
Dos estilos: override o switch
El mismo evaluador se puede escribir al estilo de la semana 6, con un método
abstracto del que cada tipo hace override
(ExprMethods.java):
interface Expr {
int eval();
String show();
}
record Add(Expr left, Expr right) implements Expr {
public int eval() {
return left.eval() + right.eval();
}
public String show() {
return "(" + left.show() + " + " + right.show() + ")";
}
}
$ java -cp build ExprMethods
(2 + 3 * 4) = 14
Mismo resultado, organización opuesta. Piensen en una tabla con un tipo por
fila y una operación por columna. Con override, el código de cada
fila está junto (la clase Add tiene su eval y su show); con switch,
el de cada columna (el método eval tiene los tres casos).
| Override en cada tipo | switch sobre un tipo sealed |
|
|---|---|---|
| Agregar un tipo | Fácil: una clase nueva, nada más. | Hay que tocar cada switch, pero el compilador los señala. |
| Agregar una operación | Hay que tocar cada clase. | Fácil: un método nuevo, nada más. |
| ¿Quién puede agregar tipos? | Cualquiera: la jerarquía está abierta. | Solo quien controla la lista permits. |
| Encaja con | Capacidades con muchas implementaciones posibles (Payable, Notification). |
Datos con forma fija (figuras, expresiones, mensajes de un protocolo). |
No hay un estilo ganador. La pregunta de diseño es qué va a cambiar más:
si van a aparecer tipos nuevos, override en cada tipo; si los tipos son fijos y
van a aparecer operaciones nuevas, tipo sealed y switch. Y lo que ya no
tiene sentido es la cadena de instanceof sobre una jerarquía abierta, que
combina lo peor de los dos: hay que tocar cada cadena al agregar un tipo, y
nadie avisa.
Idea clave. Polimorfismo con override y
switchsobre un tipo sealed son dos herramientas para el mismo problema. La primera hace fácil agregar tipos; la segunda hace fácil agregar operaciones y convierte en error de compilación olvidar un tipo.
El código de la semana y make test
$ cd code/week-08
$ make test
...
== ShapeDemo
Circle[radius=1.0] -> área 3.14, círculo de radio 1.0
...
== ExprDemo
...
== ExprMethods
(2 + 3 * 4) = 14
...
│ ├─ PaymentFeesTest ✔
│ ├─ CalcTest ✔
│ └─ ShapesTest ✔
...
[ 12 tests successful ]
[ 0 tests failed ]
make errores compila cada archivo de broken/ junto con la jerarquía de
Shape y muestra lo que dice javac; esos archivos están aparte porque no
compilan a propósito. PaymentFees.java y
sus pruebas son el punto de partida del laboratorio: una cadena de
instanceof que hay que reescribir con un tipo sealed sin que dejen de pasar
las pruebas.
Resumen
- Una clase o interfaz sealed (
sealed ... permits ...) nombra sus subtipos directos; ningún otro puede extenderla. Cada subtipo permitido esfinal,sealedonon-sealed; un record ya esfinal. x instanceof Circle cprueba el tipo y declara la variable en un paso. La variable existe solo donde la prueba es verdadera.- Un
switchpuede tener patrones de tipo (case Circle c) y patrones de record (case Circle(double r)), que desarman el record y se pueden anidar. - Los casos se prueban en orden; un guard
whenagrega una condición. Un caso que nunca se alcanzaría (dominado) no compila.case nulltrata elnullen el mismoswitch. - Sobre un tipo sealed, un
switchque trata todos los subtipos es exhaustivo y no necesitadefault. Al agregar un subtipo, cadaswitchincompleto deja de compilar. Undefaultinnecesario apaga ese aviso. - Override en cada tipo: fácil agregar tipos.
switchsobre un tipo sealed: fácil agregar operaciones, y el compilador avisa al agregar tipos.
Ejercicios
-
Agreguen a la jerarquía de
Exprun recordNeg(Expr inner)que representa el negativo de una expresión. Antes de tocarCalc, ejecutenmakey copien los errores. ¿Cuántos son y en qué líneas? Después complétenlo para queshow(new Neg(new Num(5)))devuelva-(5)y escriban las pruebas. -
En
Shapes.describe, intercambien las dos líneas deCircle(el caso general primero y el del guard después). Antes de compilar, escriban qué esperan que pase. Compilen y expliquen el mensaje. -
Escriban
perimeter(Shape s)con unswitchy patrones de record. Para el triángulo, supongan que es isósceles con la base y la altura dadas. Escriban una prueba por cada figura. -
Reescriban
Shapes.areaWithInstanceofcomo unswitchexhaustivo. ¿Qué pasa con la línea delthrow? ¿Por qué ya no hace falta? -
Digan cuáles de estas líneas compilan, suponiendo
Object o, y por qué:if (o instanceof String s && s.length() > 3) { IO.println(s); } if (o instanceof String s || s.length() > 3) { IO.println(s); } if (!(o instanceof String s)) { IO.println(s); } if (!(o instanceof String s)) { return; } IO.println(s); -
¿Qué pasa con
Shapes.area(null)y conShapes.describe(null)? Escriban antes la respuesta y compruébenla con dos pruebas JUnit (para la primera, conassertThrows, como receta). ¿Cuándo conviene escribircase nully cuándo no? -
Cambien
Trianglepor una clasenon-sealed class Polygon implements Shapecon una subclasePentagon extends Polygon. ¿Sigue siendo exhaustivo unswitchconcase Circle,case Squareycase Polygon? ¿Y uno concase Pentagonen lugar decase Polygon? Compruébenlo. -
Los dos estilos: agreguen a
ExprMethodsla operaciónsimplifyy aCalcel tipoNeg(si no lo hicieron en el ejercicio 1). Cuenten cuántos archivos y métodos tuvieron que tocar en cada caso. -
En el proyecto Bodega, los productos se venden por unidad, por peso o vencen. Si tuvieran que calcular el precio, el texto de la boleta y la etiqueta del estante de cada tipo, ¿qué estilo elegirían: override en cada tipo o un tipo sealed con
switch? Justifiquen pensando en qué va a cambiar más. -
(Desafío) Compilen la jerarquía de
ShapesinTriangleyShapescon solo dos casos. Después agreguenTriangleapermitsy recompilen soloShape.javayTriangle.java(sinmake). Ejecuten un programa que llame aShapes.area(new Triangle(3, 4)). ¿Qué excepción aparece? ¿Por qué el compilador no pudo avisar?
Repaso para el examen parcial
El examen de la Sesión B cubre las partes 1 y 2 del curso, semanas 1 a 8. La
lista de temas está en la guía de la semana
y el formato en exams/README.md. Esta sección ordena
lo que hay que dominar y propone ejercicios del tipo de los que pueden
aparecer. No tienen respuestas a propósito: resuélvanlos en papel, justifiquen
cada respuesta y después, cuando se pueda, compruébenlas con java o jshell.
Qué hay que saber hacer
| Semanas | Tema | Saber hacer |
|---|---|---|
| 1–2 | Clases, objetos, referencias | Dibujar el stack y el heap después de cada línea; distinguir == de equals; predecir qué pasa con alias y con null. |
| 2–3 | Constructores, this, static, overloading |
Decir qué constructor o qué overload de un método se elige; explicar qué es de la clase y qué de cada objeto. |
| 3 | Encapsulamiento, invariantes, records | Enunciar el invariante de una clase; encontrar un método o getter que lo rompe; decidir entre record y clase. |
| 3 | Pruebas JUnit | Leer una prueba y decir qué comportamiento fija; escribir pruebas cortas, incluidos los casos de error. |
| 4 | Arreglos, ArrayList, diseño |
Recorrer y modificar colecciones sin errores; repartir responsabilidades y dibujar un diagrama de clases. |
| 5 | Herencia, super, Object |
Encadenar constructores; hacer override de toString, equals y hashCode cumpliendo sus contratos. |
| 6 | Polimorfismo, clases abstractas | Separar tipo estático de tipo dinámico; trazar qué método se ejecuta con overloading y override mezclados. |
| 7 | Interfaces, composición, Liskov | Elegir entre interfaz, clase abstracta y composición; encontrar y explicar una violación de Liskov. |
| 8 | Tipos sealed, pattern matching | Leer un switch con patrones: qué devuelve o por qué no compila. |
Cómo estudiar. Las Preguntas de repaso de las guías de las semanas 1 a 8
y los ejercicios de cada capítulo de estos apuntes. Antes de ejecutar cualquier
ejemplo de code/week-NN/, escriban qué esperan ver; el error que más cuesta
en el examen es el que nunca se hizo en la práctica. Y los cuestionarios de Eck
de los capítulos 5 y
7.
Ejercicios tipo examen
R1. Referencias y alias. Dibujen el stack y el heap justo antes de cada
IO.println y digan qué se imprime:
class Counter {
int value;
Counter(int v) { value = v; }
}
static void bump(Counter c) { c.value++; c = new Counter(100); c.value++; }
static void bump(int x) { x++; }
Counter p = new Counter(1);
Counter q = p;
Counter r = new Counter(1);
bump(p);
bump(q.value);
IO.println(p.value + " " + q.value + " " + r.value);
IO.println((p == q) + " " + (p == r));
R2. Overloading y override. Con estas clases, digan qué imprime cada línea y, para cada una, qué se decidió al compilar y qué al ejecutar:
class Account {
String kind() { return "cuenta"; }
String transferTo(Account other) { return "Account -> Account"; }
}
class SavingsAccount extends Account {
@Override String kind() { return "ahorros"; }
String transferTo(SavingsAccount other) { return "Savings -> Savings"; }
@Override String transferTo(Account other) { return "Savings -> Account"; }
}
Account a = new SavingsAccount();
SavingsAccount s = new SavingsAccount();
IO.println(a.kind());
IO.println(a.transferTo(s));
IO.println(s.transferTo(s));
IO.println(s.transferTo(a));
IO.println(((Account) s).transferTo(s));
R3. Tipo estático y tipo dinámico. HourlyEmployee y SalariedEmployee
extienden la clase abstracta Employee, que declara monthlyPay(). Solo
HourlyEmployee tiene el método hours(). Para cada línea, digan si compila;
si compila, si puede fallar al ejecutar y con qué excepción:
Employee e = new HourlyEmployee("bob", 160, 2500);
long pay = e.monthlyPay();
int h = e.hours();
HourlyEmployee he = e;
HourlyEmployee he2 = (HourlyEmployee) e;
SalariedEmployee se = (SalariedEmployee) e;
Employee other = new Employee("alice");
R4. El contrato de equals. Una nota de 0 a 20:
public final class Grade {
private final int value;
public Grade(int value) { this.value = value; }
public boolean equals(Grade other) {
return other != null && value == other.value;
}
}
a) ¿Qué devuelve list.contains(new Grade(15)) si list es un
ArrayList<Grade> que ya tiene un new Grade(15)? Expliquen por qué.
b) Corrijan equals. ¿Qué otro método tienen que escribir, y qué contrato se
rompe si no lo hacen? c) ¿Les serviría un record aquí? ¿Qué ganarían?
R5. Un constructor que llama a un método con override. ¿Qué imprime
new Derived("x")? Expliquen paso a paso el orden en que se ejecuta cada
parte:
class Base {
Base() { IO.println("Base: " + describe()); }
String describe() { return "base"; }
}
class Derived extends Base {
private final String name;
Derived(String n) { super(); name = n; }
@Override String describe() { return "derived " + name; }
}
R6. Un invariante que se escapa. El curso nunca tiene más alumnos que su capacidad:
public class Course {
private final ArrayList<String> students = new ArrayList<>();
private final int capacity;
public Course(int capacity) { this.capacity = capacity; }
public void enroll(String name) {
if (students.size() == capacity) {
throw new IllegalStateException("curso lleno");
}
students.add(name);
}
public ArrayList<String> students() { return students; }
}
a) Enuncien el invariante. b) Escriban dos líneas de código, fuera de la clase, que lo rompan sin que se lance ninguna excepción. c) Hay otro dato con el que el constructor no se protege: ¿cuál? d) Corrijan la clase y escriban una prueba JUnit que habría fallado con la versión original.
R7. Liskov. Una lista de reproducción de solo lectura:
class ReadOnlyPlaylist extends Playlist {
@Override
public void add(Song s) {
throw new UnsupportedOperationException("solo lectura");
}
}
a) ¿Qué expectativa de Playlist se rompe? b) Escriban una prueba que pase
con Playlist y falle con ReadOnlyPlaylist, en la que el objeto llegue a la
prueba como un Playlist. c) Propongan un diseño, con interfaz o composición,
que no tenga el problema, y dibujen su diagrama de clases.
R8. Diseño. Un estacionamiento cobra por hora según el vehículo: los autos
pagan una tarifa por hora, las motos la mitad y los camiones una tarifa por
hora más un recargo fijo. Algunos vehículos (autos y motos, no camiones) pueden
ser eléctricos, y esos pueden usar un punto de carga que cobra por kWh.
a) Dibujen el diagrama de clases. b) ¿Qué sería una clase abstracta y qué una
interfaz? Justifiquen. c) Escriban las firmas de los métodos principales y el
método que calcula el total de un vehículo que estuvo h horas.
R9. Leer pruebas. Para cada prueba, escriban en una frase qué
comportamiento de BankAccount fija. Después digan qué casos importantes
faltan y escriban dos pruebas más:
@Test
void withdrawReducesBalance() {
BankAccount acc = new BankAccount("A-1");
acc.deposit(500);
acc.withdraw(200);
assertEquals(300, acc.balance());
}
@Test
void cannotWithdrawMoreThanBalance() {
BankAccount acc = new BankAccount("A-1");
assertThrows(IllegalArgumentException.class, () -> acc.withdraw(1));
}
R10. Leer un switch con patrones.
sealed interface Vehicle permits Car, Bike { }
record Car(String plate, int seats) implements Vehicle { }
record Bike(boolean electric) implements Vehicle { }
static String toll(Vehicle v) {
return switch (v) {
case Car(String p, int seats) when seats > 5 -> "minivan " + p;
case Car c -> "auto " + c.plate();
case Bike(boolean e) when e -> "bici eléctrica";
case Bike b -> "bici";
};
}
a) ¿Qué devuelve toll para new Car("ABC-123", 7), new Car("XYZ-9", 4) y
new Bike(false)? b) Si se borra la línea case Bike b -> "bici";, ¿compila?
¿Qué dice el compilador? c) Si se mueve case Car c a la primera línea,
¿compila? d) Se agrega Truck a permits. ¿Qué pasa con toll? ¿Y si toll
tuviera un default?