Semana 8 — Tipos sealed, pattern matching y examen parcial
Objetivos
- Declarar una jerarquía cerrada con
sealedypermits, y explicar qué gana el compilador al conocer todos los subtipos. - Usar pattern matching con
instanceofy enswitch, incluidos los patrones de record. - Comparar dos formas de agregar comportamiento a una jerarquía: el override de un método en cada subclase frente a un
switchexhaustivo sobre un tipo sealed. - Repasar las partes 1 y 2 del curso para el examen parcial.
Apuntes de la semana: notes/week-08.md (incluye una sección de repaso con ejercicios tipo examen) · Código: code/week-08
Sesión A — Tipos sealed y pattern matching · Repaso
Lecturas (Eck cubre Java 17 y no trata switch con patrones):
- Eck, subsec. 3.6.5 Switch Expressions (la forma
case ... ->yswitchcomo expresión, que aquí se extiende a patrones). - Java SE 25, Java Language Updates: Sealed Classes, Pattern Matching y Pattern Matching with switch.
- dev.java, Using Pattern Matching.
Lecturas complementarias:
- JEP 409: Sealed Classes, JEP 440: Record Patterns y JEP 441: Pattern Matching for switch: la motivación de cada característica, contada por quienes la diseñaron.
- JLS 25, §8.1.1.2 sealed, non-sealed, and final Classes y §14.30 Patterns.
Temas:
sealed interface Shape permits Circle, Square, Triangle: la jerarquía queda cerrada (Trianglees el subtipo que se agrega al final, para ver qué marca el compilador). Cada subtipo permitido esfinal,sealedonon-sealed. Con records como subtipos: datos inmutables de la semana 3 más una jerarquía cerrada.- Pattern matching con
instanceof:if (s instanceof Circle c)prueba el tipo y declara la variable en un paso (la forma que ya usamos enequalsen la semana 5). switchcon patrones de tipo y de record:case Circle(double r) -> Math.PI * r * r;. Guards conwhen.case null.- Exhaustividad: si el
switchcubre todos los subtipos permitidos, no hace faltadefault, y al agregar un subtipo nuevo el compilador marca cadaswitchincompleto. Error común: ponerdefault“por seguridad” y perder ese aviso. - Ejemplo propio: un evaluador de expresiones aritméticas con
sealed interface Expry recordsNum,Add,Mul; evaluar e imprimir conswitch. - Dos estilos: override del método en cada clase (fácil agregar tipos, difícil agregar operaciones) frente a
switchsobre un tipo sealed (fácil agregar operaciones, el compilador avisa al agregar tipos). Contraste con la cadena deinstanceofde la semana 6, donde nadie avisa. - Repaso: preguntas de las semanas 1 a 7 resueltas en clase.
Sesión B — Examen parcial
Temas: partes 1 y 2 del curso, semanas 1 a 8 (objetos y clases; herencia, polimorfismo e interfaces). Ver exams/README.md.
Qué entra:
- Clases, constructores,
this,static, overloading. Referencias, alias,null, identidad e igualdad. Diagramas de stack y heap. - Encapsulamiento, invariantes, inmutabilidad y records. Arreglos y
ArrayList. Diagramas de clases UML y reparto de responsabilidades. - Herencia,
super, override,protected,final.toString,equals,hashCodey sus contratos. - Polimorfismo y dynamic dispatch (trazas de qué método se ejecuta), clases abstractas, interfaces, composición frente a herencia, principio de Liskov.
- Pruebas JUnit: leer una prueba y decir qué verifica; escribir pruebas cortas para una clase dada.
- Tipos
sealedy pattern matching: solo lectura de código (decir qué hace unswitchcon patrones o por qué no compila), dado lo cerca que está la sesión A.
Cómo estudiar:
- Las Preguntas de repaso de las semanas 1 a 8.
- Trazas a mano: escribir programas cortos con alias, overloading y override, predecir la salida y verificarla con
javaojshell. - Rehacer el diseño de un dominio pequeño: tarjetas CRC, diagrama de clases, invariantes y pruebas.
- Los cuestionarios (quizzes) de Eck de los capítulos 5 y 7, y los apuntes de cada semana.
Laboratorio — Repaso con ejercicios
Código en code/week-08/: PaymentFees.java y sus pruebas son el punto de partida; make errores muestra los errores de compilación de los ejemplos de broken/.
- Reescribir con un tipo sealed y
switchuna jerarquía pequeña que hoy usa una cadena deinstanceof(PaymentFees, la comisión de cada forma de pago), sin que dejen de pasar sus pruebas; agregar un subtipo y ver qué marca el compilador. - Ejercicios tipo examen en parejas (sección de repaso de los apuntes): trazas de dynamic dispatch, contrato de
equals, diagramas de stack y heap, encontrar la violación de Liskov. - Leer pruebas JUnit ajenas y decir qué comportamiento fijan y qué caso falta.
Entregas
- P2 en curso (Catálogo). Entrega en semana 9.
Preguntas de repaso
- ¿Qué pasa al compilar si se agrega
Triangleapermitsy unswitchsobreShapesindefaultno lo trata? ¿Y si elswitchtienedefault? - Escriban un
switchque calcule el área de cadaShapeusando patrones de record. ¿Por qué no necesita casting? - ¿Cuándo prefieren un método abstracto con un override en cada subclase y cuándo un
switchexhaustivo sobre un tipo sealed? - ¿Por qué cada subtipo permitido de una interfaz sealed tiene que ser
final,sealedonon-sealed? ¿Por qué un record cumple esa regla sin escribir nada?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (18)Pruebas (3)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
/** La suma de dos expresiones. */
public record Add(Expr left, Expr right) implements Expr {
}/** Operaciones sobre expresiones, cada una en un solo switch exhaustivo. */
public final class Calc {
private Calc() {
}
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);
};
}
/** Texto de la expresión; las sumas van entre paréntesis. */
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);
};
}
/** Quita los 1 que multiplican y los 0 que suman. Patrones anidados con guards. */
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;
};
}
}/** Un círculo dado por su radio. Un record es final: cumple la regla de sealed. */
public record Circle(double radius) implements Shape {
public Circle {
if (radius <= 0) {
throw new IllegalArgumentException("radio no positivo: " + radius);
}
}
}/** Una expresión aritmética: un número, una suma o un producto. */
public sealed interface Expr permits Num, Add, Mul {
}/** Evalúa, imprime y simplifica 2 + 3 * 4 y (1 * x) + 0. */
public class ExprDemo {
public static void main(String[] args) {
Expr e = new Add(new Num(2), new Mul(new Num(3), new Num(4)));
IO.println(e);
IO.println(Calc.show(e) + " = " + Calc.eval(e));
Expr f = new Mul(new Add(new Num(2), new Num(3)), new Num(4));
IO.println(Calc.show(f) + " = " + Calc.eval(f));
Expr g = new Add(new Mul(new Num(1), new Num(7)), new Num(0));
IO.println(Calc.show(g) + " se simplifica a " + Calc.show(Calc.simplify(g)));
}
}/**
* El mismo evaluador en el otro estilo: cada tipo hace override de eval y show.
* Agregar un tipo es fácil (una clase nueva); agregar una operación obliga a
* tocar todas las clases.
*/
public class ExprMethods {
interface Expr {
int eval();
String show();
}
record Num(int value) implements Expr {
public int eval() {
return value;
}
public String show() {
return Integer.toString(value);
}
}
record Add(Expr left, Expr right) implements Expr {
public int eval() {
return left.eval() + right.eval();
}
public String show() {
return "(" + left.show() + " + " + right.show() + ")";
}
}
record Mul(Expr left, Expr right) implements Expr {
public int eval() {
return left.eval() * right.eval();
}
public String show() {
return left.show() + " * " + right.show();
}
}
public static void main(String[] args) {
Expr e = new Add(new Num(2), new Mul(new Num(3), new Num(4)));
IO.println(e.show() + " = " + e.eval());
}
}/** El producto de dos expresiones. */
public record Mul(Expr left, Expr right) implements Expr {
}/** Un número entero: la hoja del árbol de una expresión. */
public record Num(int value) implements Expr {
}/**
* Código inicial del laboratorio: la comisión de cada forma de pago, calculada
* con una cadena de instanceof. Reescríbanlo con un tipo sealed y un switch.
*/
public final class PaymentFees {
private PaymentFees() {
}
public interface Payment {
long cents();
}
/** Pago en efectivo: sin comisión. */
public record Cash(long cents) implements Payment {
}
/** Pago con tarjeta: 3 % si es de crédito, 1 % si es de débito. */
public record Card(long cents, boolean credit) implements Payment {
}
/** Transferencia bancaria: S/ 1.50 fijos. */
public record Transfer(long cents, String bank) implements Payment {
}
/** Comisión en céntimos. */
public static long fee(Payment p) {
if (p instanceof Cash) {
return 0;
} else if (p instanceof Card c) {
return c.credit() ? c.cents() * 3 / 100 : c.cents() / 100;
} else if (p instanceof Transfer) {
return 150;
}
throw new IllegalArgumentException("forma de pago desconocida: " + p);
}
}/**
* Una figura del plano. La jerarquía está cerrada: solo estos tres records
* pueden implementar Shape, y el compilador lo sabe.
*/
public sealed interface Shape permits Circle, Square, Triangle {
}import java.util.List;
/** Recorre una lista de figuras y usa las operaciones de Shapes. */
public class ShapeDemo {
public static void main(String[] args) {
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));
// La versión con instanceof olvidó Triangle y nadie avisó.
try {
Shapes.areaWithInstanceof(new Triangle(3, 4));
} catch (IllegalArgumentException e) {
IO.println("instanceof: " + e.getMessage());
}
}
}/** Operaciones sobre figuras, escritas con switch sobre el tipo sealed Shape. */
public final class Shapes {
private Shapes() {
}
/** Área de la figura. Sin default: si falta un subtipo, no compila. */
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;
};
}
/** Una descripción corta. Muestra guards (when) y case null. */
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();
};
}
/** Lo mismo que area, con instanceof: compila aunque falte un caso. */
public static double areaWithInstanceof(Shape s) {
if (s instanceof Circle c) {
return Math.PI * c.radius() * c.radius();
} else if (s instanceof Square sq) {
return sq.side() * sq.side();
}
throw new IllegalArgumentException("figura desconocida: " + s);
}
}/** Un cuadrado dado por su lado. */
public record Square(double side) implements Shape {
public Square {
if (side <= 0) {
throw new IllegalArgumentException("lado no positivo: " + side);
}
}
}/** Un triángulo dado por su base y su altura. Es el subtipo que se agregó al final. */
public record Triangle(double base, double height) implements Shape {
public Triangle {
if (base <= 0 || height <= 0) {
throw new IllegalArgumentException("base o altura no positiva");
}
}
}// No compila: Shape ya permite Triangle y este switch no lo trata.
public class AreaMissingCase {
static double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Square(double side) -> side * side;
};
}
}// Sí compila, y ese es el problema: con default, el compilador no avisa que falta Triangle.
public class ChainNoWarning {
static double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Square(double side) -> side * side;
default -> 0;
};
}
}// No compila: el primer caso ya captura todos los Shape; el segundo nunca se alcanzaría.
public class Dominated {
static String name(Shape s) {
return switch (s) {
case Shape any -> "una figura";
case Circle c -> "un círculo";
};
}
}// No compila: Hexagon no está en la lista permits de Shape.
public record Hexagon(double side) implements Shape {
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class CalcTest {
private final Expr twoPlusThreeTimesFour = new Add(new Num(2), new Mul(new Num(3), new Num(4)));
@Test
void evalRespectsTheTree() {
assertEquals(14, Calc.eval(twoPlusThreeTimesFour));
assertEquals(20, Calc.eval(new Mul(new Add(new Num(2), new Num(3)), new Num(4))));
}
@Test
void showParenthesizesSums() {
assertEquals("(2 + 3 * 4)", Calc.show(twoPlusThreeTimesFour));
}
@Test
void simplifyRemovesNeutralElements() {
Expr e = new Add(new Mul(new Num(1), new Num(7)), new Num(0));
assertEquals(new Num(7), Calc.simplify(e));
}
@Test
void bothStylesAgree() {
ExprMethods.Expr e = new ExprMethods.Add(new ExprMethods.Num(2),
new ExprMethods.Mul(new ExprMethods.Num(3), new ExprMethods.Num(4)));
assertEquals(Calc.eval(twoPlusThreeTimesFour), e.eval());
assertEquals(Calc.show(twoPlusThreeTimesFour), e.show());
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
// Estas pruebas tienen que seguir pasando después de reescribir PaymentFees.
class PaymentFeesTest {
@Test
void cashHasNoFee() {
assertEquals(0, PaymentFees.fee(new PaymentFees.Cash(5000)));
}
@Test
void creditCardPaysThreePercent() {
assertEquals(150, PaymentFees.fee(new PaymentFees.Card(5000, true)));
}
@Test
void debitCardPaysOnePercent() {
assertEquals(50, PaymentFees.fee(new PaymentFees.Card(5000, false)));
}
@Test
void transferHasAFixedFee() {
assertEquals(150, PaymentFees.fee(new PaymentFees.Transfer(5000, "BN")));
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
class ShapesTest {
@Test
void areaOfEachShape() {
assertEquals(Math.PI, Shapes.area(new Circle(1)), 1e-9);
assertEquals(4.0, Shapes.area(new Square(2)), 1e-9);
assertEquals(6.0, Shapes.area(new Triangle(3, 4)), 1e-9);
}
@Test
void guardsChooseTheFirstMatchingCase() {
assertEquals("círculo grande", Shapes.describe(new Circle(10)));
assertEquals("círculo de radio 2.0", Shapes.describe(new Circle(2)));
assertEquals("cuadrado unitario", Shapes.describe(new Square(1)));
assertEquals("cuadrado de lado 3.0", Shapes.describe(new Square(3)));
}
@Test
void nullHasItsOwnCase() {
assertEquals("sin figura", Shapes.describe(null));
}
@Test
void recordsRejectInvalidSizes() {
assertThrows(IllegalArgumentException.class, () -> new Circle(0));
assertThrows(IllegalArgumentException.class, () -> new Triangle(3, -1));
}
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 8: tipos sealed y pattern matching.
# make compila todo a build/
# make test corre las demostraciones y las pruebas JUnit
# make errores compila cada ejemplo de broken/ y muestra qué dice javac
# make clean borra build/
JAVAC = javac
JFLAGS = -Xlint:all -Werror
SRC = $(wildcard *.java)
DEMOS = ShapeDemo ExprDemo ExprMethods
SHAPES = Shape.java Circle.java Square.java Triangle.java
all: build/.stamp
build/.stamp: $(SRC)
mkdir -p build
$(JAVAC) $(JFLAGS) -cp $(JUNIT_JAR) -d build $(SRC)
touch $@
test: all
@for d in $(DEMOS); do echo "== $$d"; java -cp build $$d || exit 1; done
java -jar $(JUNIT_JAR) execute --class-path build --scan-class-path \
--disable-banner --disable-ansi-colors --details=tree
# Los ejemplos de broken/ muestran errores de compilación a propósito (salvo
# ChainNoWarning, que compila y calla); por eso no están en SRC.
errores:
@mkdir -p build/broken
@for f in broken/*.java; do \
echo "== $$f"; \
if $(JAVAC) $(JFLAGS) -d build/broken $(SHAPES) $$f; then echo "(compila sin errores)"; fi; \
done
clean:
rm -rf build
.PHONY: all test errores clean