Semana 6 — Polimorfismo y clases abstractas
Objetivos
- Distinguir el tipo estático (el de la variable) del tipo dinámico (el del objeto) y predecir qué método se ejecuta.
- Explicar el dynamic dispatch (la JVM elige el método según el objeto, no según la variable) y por qué permite agregar casos nuevos sin cambiar el código que los usa.
- Usar upcasting, downcasting e
instanceofsabiendo cuándo un casting es señal de mal diseño. - Diseñar con clases y métodos abstractos: la superclase fija el qué y las subclases el cómo.
Apuntes de la semana: notes/week-06.md · Código: code/week-06
Sesión A — Polimorfismo y dynamic dispatch
Lecturas:
- Eck, subsec. 5.5.4 Polymorphism.
- Eck, subsec. 5.5.3 Example: Vehicles (relectura: asignación entre tipos, casting e
instanceof).
Lecturas complementarias:
- dev.java, Polymorphism.
- Wiki de OpenJDK, Virtual Calls: cómo implementa HotSpot el dynamic dispatch con una vtable por clase.
- JLS 25, §15.12.4 Run-Time Evaluation of Method Invocation: la regla precisa de qué método se elige en tiempo de ejecución.
Temas:
- Sustituir: donde se espera un
Notificationse puede pasar unEmailNotification, unSmsNotificationo unPushNotification(ejemplo propio). Upcasting implícito. - Tipo estático y tipo dinámico: el compilador decide qué métodos se pueden llamar (según el tipo de la variable); la JVM decide qué versión se ejecuta (según el objeto). Por eso
n.send()hace cosas distintas con cada objeto de una listaArrayList<Notification>. - Qué deja el compilador en el bytecode (
invokevirtual Notification.send, visto conjavap -c) y cómo la JVM busca el método desde la clase del objeto hacia arriba; la vtable de HotSpot. - El overloading se resuelve en compilación; el override, en ejecución. Ejercicio de traza con ambos mezclados (
GreeteryLoudGreeter). - Downcasting e
instanceof:ClassCastExceptionsi el objeto no es del tipo pedido. Error común: una cadena deif (x instanceof A) ... else if (x instanceof B)que reemplaza al polimorfismo; agregar un tipo nuevo obliga a tocar cada cadena. - Campos y métodos
staticno son polimórficos: se resuelven por el tipo estático. - Llamar desde un constructor a un método del que una subclase puede hacer override: la subclase todavía no terminó de inicializarse. Error común difícil de ver, que
javac -Xlint:allmarca con[this-escape].
Sesión B — Clases y métodos abstractos
Lecturas:
- Eck, subsec. 5.5.5 Abstract Classes.
Lecturas complementarias:
- dev.java, Abstract Methods and Classes.
Temas:
- Una clase que no tiene sentido instanciar:
Employeecon un método abstractomonthlyPay()y subclasesSalariedEmployeeyHourlyEmployee(ejemplo propio).new Employee(...)no compila. - Método abstracto: la superclase promete la operación y obliga a cada subclase concreta a darla. Una subclase que no la implementa también tiene que ser abstracta.
- Una clase abstracta puede tener campos, constructores (llamados con
super) y métodos concretos que usan a los abstractos: el patrón Template Method. Ejemplo:payslip()concreto arma el texto y llama amonthlyPay(). - Diagrama UML con clases y métodos abstractos en cursiva (o con
<<abstract>>). - Error común: poner en la superclase un método concreto que devuelve
0o lanza una excepción “para que compile”, en lugar de declararlo abstracto: el compilador ya no avisa si una subclase lo olvida.
Laboratorio — Polimorfismo en el proyecto
Código inicial en code/week-06/.
- Traza en papel: predecir la salida de
LabTrace.java(overloading, override y casting); luego correrlo conjava -cp build LabTracey comparar. - Reemplazar la cadena de
instanceofdeShippingCalculator.costpor un override decosten cada subclase deShipment(Letter,Parcel,Pallet).ShippingCalculatorTestdebe seguir pasando sin cambios. - Diseñar una jerarquía con una clase abstracta para un caso del proyecto (tipos de producto con reglas de precio distintas) y escribir pruebas que recorran una lista de la superclase.
- Preparar el arranque de P2: diagrama de clases de la jerarquía de productos.
Entregas
- P1 entrega.
- P2 publicado: Catálogo (jerarquía de productos, polimorfismo e interfaces para precios y descuentos). Entrega en semana 9. Ver
projects/README.md.
Preguntas de repaso
Notification n = new SmsNotification();¿Compilan.send()? ¿Yn.phoneNumber(), si ese método solo existe enSmsNotification? ¿Cuálsend()se ejecuta?- Una clase tiene
void f(Object o)yvoid f(String s). ConObject x = "hola"; obj.f(x);, ¿cuál se llama? ¿Por qué no es dynamic dispatch? - ¿Por qué una cadena de
instanceofes más difícil de mantener que un override en cada subclase? ¿Cuándo es razonable de todos modos? - ¿Puede una clase abstracta tener constructor si nadie puede hacer
newde ella? ¿Para qué sirve? - ¿Qué diferencia hay entre un método abstracto y un método concreto que lanza
UnsupportedOperationException?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (24)Pruebas (3)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
/** Upcasting, downcasting e instanceof. */
public class CastDemo {
public static void main(String[] args) {
Notification n = new SmsNotification("987654321", "Código 4821"); // upcasting implícito
// n.phoneNumber() no compila: Notification no tiene ese método.
if (n instanceof SmsNotification sms) {
IO.println("Es un SMS para " + sms.phoneNumber());
}
Notification other = new EmailNotification("alice@example.com", "Hola");
try {
SmsNotification wrong = (SmsNotification) other; // compila, pero...
IO.println(wrong.phoneNumber());
} catch (ClassCastException e) {
IO.println("ClassCastException: " + e.getMessage());
}
}
}/** El override de describe() corre antes de que LabeledTag termine su constructor. */
public class ConstructorTrap {
public static void main(String[] args) {
new LabeledTag("A-7");
}
}/**
* El costo de un aviso calculado con una cadena de instanceof. Funciona, pero
* cada tipo nuevo de aviso obliga a volver aquí (y a cada cadena parecida).
* Notification.cost(), con un override en cada subclase, hace lo mismo sin cadena.
*/
public class CostByInstanceof {
public static long cost(Notification n) {
if (n instanceof EmailNotification) {
return 0;
} else if (n instanceof SmsNotification) {
return 5;
} else {
return 1;
}
}
}/** Un aviso que se envía por correo electrónico. */
public class EmailNotification extends Notification {
public EmailNotification(String address, String text) {
super(address, text);
}
@Override
public String send() {
return "[correo] para " + recipient() + ": " + text();
}
@Override
public long cost() {
return 0;
}
}/**
* Un empleado de la bodega. No tiene sentido crear un "empleado en general":
* cada empleado concreto cobra de una forma, y la clase obliga a decir cuál.
*/
public abstract class Employee {
private final String name;
protected Employee(String name) {
this.name = name;
}
public String name() {
return name;
}
/** Pago del mes, en céntimos. Cada subclase concreta dice cómo se calcula. */
public abstract long monthlyPay();
/** Template Method: el formato es común; el monto lo da la subclase. */
public String payslip() {
return name + ": " + soles(monthlyPay());
}
protected static String soles(long cents) {
return String.format("S/ %d.%02d", cents / 100, cents % 100);
}
}/** Overloading: dos métodos con el mismo nombre y distinta lista de parámetros. */
public class Greeter {
public String greet(Object o) {
return "Greeter.greet(Object)";
}
public String greet(String s) {
return "Greeter.greet(String)";
}
}/** Un empleado que cobra por hora trabajada en el mes. */
public class HourlyEmployee extends Employee {
private final long hourlyRate; // céntimos por hora
private final int hours;
public HourlyEmployee(String name, long hourlyRate, int hours) {
super(name);
this.hourlyRate = hourlyRate;
this.hours = hours;
}
@Override
public long monthlyPay() {
return hourlyRate * hours;
}
@Override
public String payslip() {
return super.payslip() + " (" + hours + " h)";
}
}/** Hace override de describe() usando un campo propio. Es final: nadie la extiende. */
public final class LabeledTag extends Tag {
private final String label;
public LabeledTag(String label) {
super(); // aquí corre Tag(), que llama a describe()
this.label = label;
IO.println("LabeledTag(): " + describe());
}
@Override
public String describe() {
return "etiqueta " + label;
}
}/**
* Laboratorio, paso 1: antes de ejecutar este programa, escriban en papel
* qué imprime cada línea y por qué. Después ejecútenlo con
* java -cp build LabTrace
* y comparen. (No está en make test, para no ver la salida antes.)
*/
public class LabTrace {
static class Animal {
String sound() {
return "...";
}
String meet(Animal other) {
return "Animal saluda a Animal";
}
String meet(Dog other) {
return "Animal saluda a Dog";
}
}
static class Dog extends Animal {
@Override
String sound() {
return "guau";
}
@Override
String meet(Animal other) {
return "Dog saluda a Animal";
}
}
static class Puppy extends Dog {
@Override
String meet(Dog other) {
return "Puppy saluda a Dog";
}
}
public static void main(String[] args) {
Animal a = new Dog();
Animal b = new Puppy();
Dog d = new Puppy();
IO.println("1. " + a.sound());
IO.println("2. " + b.sound());
IO.println("3. " + a.meet(a));
IO.println("4. " + a.meet(d));
IO.println("5. " + b.meet(a));
IO.println("6. " + b.meet(d));
IO.println("7. " + d.meet(b));
IO.println("8. " + d.meet((Dog) b));
Object o = d;
IO.println("9. " + ((Animal) o).meet(new Dog()));
}
}/** Laboratorio: un sobre. Tarifa plana. */
public class Letter extends Shipment {
public Letter(int grams) {
super(grams);
}
}/** Hace override solo de una de las dos versiones de greet. */
public class LoudGreeter extends Greeter {
@Override
public String greet(Object o) {
return "LoudGreeter.greet(Object)";
}
}/** Un aviso para una persona. El envío general es un aviso en pantalla. */
public class Notification {
private final String recipient;
private final String text;
public Notification(String recipient, String text) {
this.recipient = recipient;
this.text = text;
}
public String recipient() {
return recipient;
}
public String text() {
return text;
}
/** Envía el aviso; devuelve lo que se mostró, para poder probarlo. */
public String send() {
return "[aviso] " + recipient + ": " + text;
}
/** Costo del envío, en céntimos. */
public long cost() {
return 1;
}
}import java.util.ArrayList;
/** Polimorfismo: una lista de Notification con objetos de tres clases. */
public class NotificationDemo {
public static void main(String[] args) {
ArrayList<Notification> inbox = new ArrayList<>();
inbox.add(new EmailNotification("alice@example.com", "Tu pedido salió"));
inbox.add(new SmsNotification("987654321", "Código 4821"));
inbox.add(new PushNotification("bob", "Stock bajo: arroz"));
long total = 0;
for (Notification n : inbox) {
IO.println(n.send()); // ¿qué send()? el del objeto
total += n.cost();
}
IO.println("Costo total: " + total + " céntimos");
}
}/** Laboratorio: una paleta. Tarifa base más un recargo por kilo. */
public class Pallet extends Shipment {
public Pallet(int grams) {
super(grams);
}
}/** Laboratorio: un paquete. Paga por kilo o fracción. */
public class Parcel extends Shipment {
public Parcel(int grams) {
super(grams);
}
}import java.util.ArrayList;
/** Una planilla con empleados de dos clases, recorrida como Employee. */
public class PayrollDemo {
public static void main(String[] args) {
ArrayList<Employee> staff = new ArrayList<>();
staff.add(new SalariedEmployee("alice", 350_000));
staff.add(new HourlyEmployee("bob", 1_250, 96));
long total = 0;
for (Employee e : staff) {
IO.println(e.payslip());
total += e.monthlyPay();
}
IO.println("Total de la planilla: " + total + " céntimos");
}
}/**
* Un aviso en la pantalla de la aplicación. No hace override de send() ni de cost():
* usa las versiones que hereda de Notification.
*/
public class PushNotification extends Notification {
public PushNotification(String user, String text) {
super(user, text);
}
}/** Un empleado con sueldo fijo mensual. */
public class SalariedEmployee extends Employee {
private final long monthlySalary; // céntimos
public SalariedEmployee(String name, long monthlySalary) {
super(name);
this.monthlySalary = monthlySalary;
}
@Override
public long monthlyPay() {
return monthlySalary;
}
}/** Laboratorio: un envío de la bodega, con su peso en gramos. */
public class Shipment {
private final int grams;
public Shipment(int grams) {
this.grams = grams;
}
public int grams() {
return grams;
}
}/**
* Laboratorio, paso 2: el costo de cada envío se calcula con una cadena de
* instanceof. Reemplácenla por un override de un método en cada subclase de
* Shipment; ShippingCalculatorTest debe seguir pasando sin cambios.
*/
public class ShippingCalculator {
/** Costo en céntimos. */
public static long cost(Shipment s) {
int kilos = (s.grams() + 999) / 1000; // kilo o fracción
if (s instanceof Letter) {
return 500;
} else if (s instanceof Parcel) {
return 800L * kilos;
} else if (s instanceof Pallet) {
return 20_000 + 150L * kilos;
} else {
throw new IllegalArgumentException("tipo de envío desconocido");
}
}
}/** Un aviso que se envía como mensaje de texto a un celular. */
public class SmsNotification extends Notification {
public SmsNotification(String phoneNumber, String text) {
super(phoneNumber, text);
}
/** Solo existe en SmsNotification, no en Notification. */
public String phoneNumber() {
return recipient();
}
@Override
public String send() {
return "[SMS] al " + phoneNumber() + ": " + text();
}
@Override
public long cost() {
return 5;
}
}/** Los métodos static y los campos no son polimórficos: cuenta el tipo estático. */
public class StaticDemo {
static class Parent {
String label = "campo de Parent";
static String kind() {
return "Parent.kind()";
}
String describe() {
return "Parent.describe()";
}
}
static class Child extends Parent {
String label = "campo de Child"; // oculta (no es override) el de Parent
static String kind() { // oculta (no es override) el de Parent
return "Child.kind()";
}
@Override
String describe() {
return "Child.describe()";
}
}
// Llamamos a kind() a través de una variable a propósito, para mostrar
// que se usa el tipo de la variable; javac avisa con [static].
@SuppressWarnings("static")
public static void main(String[] args) {
Parent p = new Child();
IO.println(p.describe()); // de instancia: decide el objeto
IO.println(p.kind()); // static: decide el tipo de p
IO.println(p.label); // campo: decide el tipo de p
IO.println(((Child) p).label);
}
}/**
* Error común: un constructor que llama a un método del que una subclase puede
* hacer override. Si lo hace, su versión corre antes de que el constructor de la
* subclase haya inicializado sus campos.
*/
public class Tag {
// javac avisa con [this-escape]; se suprime para poder mostrar el efecto.
@SuppressWarnings("this-escape")
public Tag() {
IO.println("Tag(): " + describe());
}
public String describe() {
return "etiqueta";
}
}/**
* Overloading y override mezclados. El overloading se decide al compilar,
* con los tipos estáticos de los argumentos; el override, al ejecutar,
* con la clase del objeto que recibe el mensaje.
*/
public class TraceDemo {
public static void main(String[] args) {
Greeter g = new LoudGreeter();
String s = "hola";
Object x = s; // el mismo String, visto como Object
IO.println("1. " + g.greet(s));
IO.println("2. " + g.greet(x));
IO.println("3. " + g.greet((String) x));
Greeter plain = new Greeter();
IO.println("4. " + plain.greet(x));
}
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
class EmployeeTest {
@Test
void salariedPaysFixedSalary() {
Employee e = new SalariedEmployee("alice", 350_000);
assertEquals(350_000, e.monthlyPay());
}
@Test
void hourlyPaysRateTimesHours() {
Employee e = new HourlyEmployee("bob", 1_250, 96);
assertEquals(120_000, e.monthlyPay());
}
@Test
void payslipUsesTheSubclassPay() {
Employee e = new SalariedEmployee("alice", 350_000);
assertEquals("alice: S/ 3500.00", e.payslip());
}
@Test
void hourlyPayslipAddsHours() {
Employee e = new HourlyEmployee("bob", 1_250, 96);
assertEquals("bob: S/ 1200.00 (96 h)", e.payslip());
}
@Test
void totalOverAListOfEmployees() {
List<Employee> staff = List.of(
new SalariedEmployee("alice", 350_000),
new HourlyEmployee("bob", 1_250, 96));
long total = 0;
for (Employee e : staff) {
total += e.monthlyPay();
}
assertEquals(470_000, total);
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import org.junit.jupiter.api.Test;
class NotificationTest {
@Test
void eachObjectUsesItsOwnSend() {
Notification email = new EmailNotification("alice@example.com", "Hola");
Notification sms = new SmsNotification("987654321", "Hola");
assertEquals("[correo] para alice@example.com: Hola", email.send());
assertEquals("[SMS] al 987654321: Hola", sms.send());
}
@Test
void pushUsesInheritedSend() {
Notification push = new PushNotification("bob", "Hola");
assertEquals("[aviso] bob: Hola", push.send());
}
@Test
void overriddenCostAgreesWithInstanceofChain() {
ArrayList<Notification> all = new ArrayList<>();
all.add(new EmailNotification("alice@example.com", "a"));
all.add(new SmsNotification("987654321", "b"));
all.add(new PushNotification("bob", "c"));
for (Notification n : all) {
assertEquals(CostByInstanceof.cost(n), n.cost());
}
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
// Laboratorio: estas pruebas fijan el comportamiento actual. Deben seguir
// pasando después de quitar la cadena de instanceof.
class ShippingCalculatorTest {
@Test
void letterHasFlatRate() {
assertEquals(500, ShippingCalculator.cost(new Letter(40)));
}
@Test
void parcelPaysPerKiloOrFraction() {
assertEquals(800, ShippingCalculator.cost(new Parcel(1_000)));
assertEquals(1_600, ShippingCalculator.cost(new Parcel(1_001)));
}
@Test
void palletPaysBasePlusPerKilo() {
assertEquals(20_000 + 150 * 120, ShippingCalculator.cost(new Pallet(120_000)));
}
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 6: polimorfismo, dynamic dispatch y clases abstractas.
# make compila todo a build/
# make test corre las demostraciones y las pruebas JUnit
# make clean borra build/
# LabTrace no está en DEMOS: es la traza del laboratorio (java -cp build LabTrace).
JAVAC = javac
JFLAGS = -Xlint:all -Werror
SRC = $(wildcard *.java)
DEMOS = NotificationDemo TraceDemo StaticDemo ConstructorTrap CastDemo PayrollDemo
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
clean:
rm -rf build
.PHONY: all test clean