Problema
Consideremos la siguiente clase Person
y las clases hijas Manager
y Employee
, definidas de la siguiente manera:
public static class Person {
String name;
public Person(String name) { this.name = name; }
}
public static class Manager extends Person {
public Manager(String name) { super(name); }
}
public static class Employee extends Person {
public Employee(String name) { super(name); }
}
Supongamos ahora un método estático genérico que acepta dos parámetros de un tipo genérico T
y devuelve un objeto de tipo T
. Al tipo T
se le aplica la restricción <T extends Person>
, lo que significa que T
debe tener AL MENOS todos los atributos y métodos definidos en la clase Person
.
Supongamos el siguiente método estático definido de la siguiente forma:
public static T foo(T t1, T t2) {
// Hacer algo
return …;
}
Dadas las siguientes instancias p1
y p2
, realizamos la siguiente invocación del método foo()
:
Employee p1 = new Employee("John");
Manager p2 = new Manager("Johanna");
Person out = foo(p1, p2);
El objetivo es imponer que ambos parámetros pasados al método foo()
sean del mismo tipo, de forma que el compilador genere un error si no se cumple esta condición.
Observación
Al ejecutar la invocación del método anterior, NO se genera ningún error de compilación, ya que tanto p1
(de tipo Employee
) como p2
(de tipo Manager
) cumplen con la restricción impuesta al tipo genérico T
. Esto se debe a que ambas clases heredan los atributos de la clase Person
.
Para entender mejor este comportamiento, es necesario analizar los mecanismos de compilación y precompilación del código.
El rol del type erasure
El type erasure es el mecanismo de precompilación que reemplaza todos los tipos genéricos por el tipo de objeto con mayor compatibilidad, respetando las restricciones impuestas.
En este caso, como se establece la restricción <T extends Person>
, el tipo genérico T
será reemplazado por la clase que proporcione la mejor compatibilidad y cumpla con las restricciones impuestas. El código resultante será:
public static Person foo(Person t1, Person t2) {
// Hacer algo
return …;
}
Por lo tanto, se hace evidente que, cuando se realiza la invocación mencionada anteriormente, tanto p1
como p2
son aceptados por el compilador, ya que ambos son «descendientes» de Person
.
El rol del type inference
Cuando se pasan como parámetros de foo()
instancias de objetos del mismo tipo, entra en juego el mecanismo de type inference, que determina qué clase utilizar durante la compilación.
- Si se pasan instancias de dos objetos del mismo tipo, la clase de compilación será la subclase de las instancias pasadas como argumentos.
- Si se pasan instancias de dos objetos de diferentes subclases de
Person
, el compilador seleccionará la clase común superior, que en este caso esPerson
.
Ejemplo:
- Si se pasan dos objetos de tipo
Employee
, la clase de compilación seráEmployee
. - Si se pasan un
Employee
y unManager
, la clase de compilación seráPerson
, ya que es la clase común superior.
Problema identificado
El problema principal de los tipos genéricos es que, cuando se pasan a un método dos instancias de tipo genérico T
limitado con la sintaxis <T extends C>
, si las instancias pasadas como parámetros son subclases de C
, no es posible generar errores de compilación si los parámetros tienen tipos diferentes.
Posibles soluciones al problema
Solución alternativa 1: Clase auxiliar
Si se conoce de antemano el número de parámetros del mismo tipo que se pasarán al método, se puede definir una clase con tipo genérico que acepte los objetos y luego pasar esta clase como un único parámetro al método foo()
.
Implementación
<code>// Clase auxiliar con dos objetos del mismo tipo
public static class Pair<P> {
private P first;
private P second;
public Pair(P first, P second) {
this.first = first;
this.second = second;
}
public P getFirst() {
return first;
}
public P getSecond() {
return second;
}
}
// Nueva definición de foo()
public static <T extends Person> T foo(Pair<T> pair) {
// Hacer algo
return ...;
}</code>
Employee p1 = new Employee("John");
Employee p2 = new Employee("Francis");
Manager p3 = new Manager("Johanna");
// Aceptado
Pair<Employee> pair = new Pair<>(p1, p2);
Person out = foo(pair);
// Error de compilación (p1 es Employee y p3 es Manager)
Pair<Employee> pair2 = new Pair<>(p1, p3);
Person out = foo(pair2);
Lenguaje del código: PHP (php)
Explicación
En esta solución:
- La clase
Pair
garantiza que ambos parámetrosfirst
ysecond
sean del mismo tipo. - Como
foo
acepta un parámetro de tipoPair<T>
, si se intenta crear unPair<Employee>
con unEmployee
y unManager
, se generará un error de compilación.
Solución alternativa 2: Verificación en tiempo de ejecución
Otra posible solución es realizar una verificación en runtime para asegurarse de que los dos parámetros pasados al método foo()
sean del mismo tipo. Para esto, se puede utilizar el método getClass()
para comparar las clases de los parámetros.
Implementación
public static <T extends Person> T my_method(T t1, T t2) {
if (t1.getClass() != t2.getClass()) {
throw new IllegalArgumentException("Los argumentos son de tipos diferentes");
}
// Hacer algo
return ...;
}