Hola! Tengo una consulta respecto a un concepto que se desarrolla en el libro de Fontela sobre el principio de sustitución y la genericidad.
La duda recae específicamente en la página 180 donde se desarrolla una problemática al esperar por parámetro una instancia de la clase List<Cuenta> recibiendo un objeto List<CajaAhorro>
[El fragmento lo adjunto abajo]
Me generó dudas la última afirmación "Nunca es cierto que los tipos Clase<Derivada> se puedan usar en vez de Clase<Base>" Es decir, violan el principio de sustitución.
No comprendo muy bien por qué. No se me ocurren contraejemplos para los que tengas problemas (en la filosofía de objetos) recibiendo una lista de clases derivadas en vez de una lista de clases base. Conceptualmente no veo dónde es que se rompe el principio de sustitución.
Hago esta pregunta específicamente ya que en el parrafo aclara que "En realidad, nunca es cierto..."
¿Es realmente un problema conceptual de POO? ¿O es en realidad un "problema" de la implementación de POO por parte de Java'?
Gracias
Me parece que quiere decir que
"Una clase padre puede substituir a una clase hija pero no el reciproco"
Esto es porque la clase padre tiene una interfaz definida la cual hereda la clase hija. Por ende sabemos de entrada que la clase hija responde a esa interfaz porque la heredo completa.
El problema esta en que la clase hija va a implementar mas cosas, es decir, hereda lo del padre y le vamos a agregar cosas. Entonces el padre no conoce esas cosas que le implementa la clase hija.
Por eso no puedes usar una clase hija para un tipo de dato de clase padre porque habran mensajes (los nuevos implementados) que no va a entender la clase padre.
Es una cuestión de los lenguajes de tipado estático y, en particular, de cómo funcionan los Generics en Java.
Imaginate que tengamos la siguiente implementación del método sumarNumeros
:
public Double sumarNumeros(List<Number> numeros) { Double sum = 0d; for (Number numero : numeros) { sum += numero.doubleValue(); } return sum; // Lo anterior se puede escribir más piola así: // return numeros.stream().mapToDouble(Number::doubleValue).sum(); }
La idea es que reciba una lista de cualquier tipo de números y devuelva su suma, independientemente de qué tipo de números sean. En teoría esto debería funcionar ya que Number es una clase abstracta de la cual heredan Integer y Double, entre otras.
Sin embargo, la siguiente prueba falla en tiempo de compilación en la anteúltima línea:
@Test public void test01SumaDeNumeros() { Sumador sumador = new Sumador(); List<Double> numeros = new ArrayList<>(); numeros.add(5.5); numeros.add(8.3); Number suma = sumador.sumarNumeros(numeros); assertEquals(13.8, suma); }
Esto es porque no reconoce a la lista de doubles como un subtipo de la lista de numbers. Es decir, por más que un double es un number, la relación no se extrapola automáticamente para los generics.
Una forma de solucionarlo sería declarar a la lista de la siguiente forma en la prueba:
@Test public void test02SumaDeNumeros() { Sumador sumador = new Sumador(); List<Number> numeros = new ArrayList<>(); numeros.add(5.5); numeros.add(8.3); Number suma = sumador.sumarNumeros(numeros); assertEquals(13.8, suma); }
Lo único que cambió es que la lista usada en la prueba acepta cualquier tipo de número y no solo doubles. La desventaja de esto es que estamos obligando al usuario de mi método a que se adecue a los tipos de datos que necesito y que no respete el principio de sustitución de Liskov.
Por lo tanto, la solución ideal es la que propone Fontela al final de esa misma página:
public Double sumarNumeros(List<? extends Number> numeros) { return numeros.stream().mapToDouble(Number::doubleValue).sum(); }
En este caso simplemente se utiliza el comodín ?
para definir la generalización en los generics y que el comportamiento sea el esperado.
En Smalltalk esto no sucede y es mucho más fácil.