java - ¿Por qué necesitamos wilcard delimitado<? extiende T> en el método Collections.max()
generics effective-java (3)
He leído impresionante "Java efectiva" por Joshua Bloch. Pero un ejemplo en los libros no me queda claro. Se tomó del capítulo sobre genéricos, el elemento exacto es "Elemento 28: usar comodines delimitados para aumentar la flexibilidad de la API" .
En este elemento se muestra cómo escribir la versión más universal y a prueba de balas (en el punto de vista del sistema de tipos) del algoritmo de selección del elemento máximo de la colección utilizando parámetros de tipo delimitado y tipos de comodines delimitados.
La firma final del método estático escrito se ve así:
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
Y es casi el mismo que el de la función Collections#max
de la biblioteca estándar.
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
Entiendo por qué necesitamos comodines acotados en T extends Comparable<? super T>
T extends Comparable<? super T>
tipo de restricción, pero ¿es realmente necesario en el tipo del argumento? Me parece que será lo mismo si dejamos solo la List<T>
o la Collection<T>
, ¿no es así? Me refiero a algo como esto:
public static <T extends Comparable<? super T>> T wrongMin(Collection<T> xs)
He escrito el siguiente ejemplo tonto de usar ambas firmas y no veo ninguna diferencia:
public class Algorithms {
public static class ColoredPoint extends Point {
public final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public String toString() {
return String.format("ColoredPoint(x=%d, y=%d, color=%s)", x, y, color);
}
}
public static class Point implements Comparable<Point> {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return String.format("Point(x=%d, y=%d)", x, y);
}
@Override
public int compareTo(Point p) {
return x != p.x ? x - p.x : y - p.y;
}
}
public static <T extends Comparable<? super T>> T min(Collection<? extends T> xs) {
Iterator<? extends T> iter = xs.iterator();
if (!iter.hasNext()) {
throw new IllegalArgumentException("Collection is empty");
}
T minElem = iter.next();
while (iter.hasNext()) {
T elem = iter.next();
if (elem.compareTo(minElem) < 0) {
minElem = elem;
}
}
return minElem;
}
public static <T extends Comparable<? super T>> T wrongMin(Collection<T> xs) {
return min(xs);
}
public static void main(String[] args) {
List<ColoredPoint> points = Arrays.asList(
new ColoredPoint(1, 2, Color.BLACK),
new ColoredPoint(0, 2, Color.BLUE),
new ColoredPoint(0, -1, Color.RED)
);
Point p1 = wrongMin(points);
Point p2 = min(points);
System.out.println("Minimum element is " + p1);
}
Entonces, ¿puede sugerir un ejemplo en el que tal firma simplificada sea inaceptable?
PD: ¿Y por qué diablos hay T extends Object
en la implementación oficial?
Responder
Bueno, gracias a @Bohemian he logrado averiguar cuál es la diferencia entre ellos.
Considere los siguientes dos métodos auxiliares
private static void expectsPointOrColoredPoint(Point p) {
System.out.println("Overloaded for Point");
}
private static void expectsPointOrColoredPoint(ColoredPoint p) {
System.out.println("Overloaded for ColoredPoint");
}
Claro, no es muy inteligente sobrecargar el método tanto para superclase como para su subclase, pero nos permite ver qué tipo de valor de retorno realmente se infirió (los points
son la List<ColoredPoint>
como antes).
expectsPointOrColoredPoint(min(points)); // print "Overloaded for ColoredPoint"
expectsPointOrColoredPoint(wrongMin(points)); // print "Overloaded for ColoredPoint"
Para ambos métodos el tipo inferido fue ColoredPoint
.
A veces desea ser explícito sobre el tipo pasado a la función sobrecargada. Puedes hacerlo de dos maneras:
Puedes lanzar:
expectsPointOrColoredPoint((Point) min(points)); // print "Overloaded for Point"
expectsPointOrColoredPoint((Point) wrongMin(points)); // print "Overloaded for Point"
Todavía no hay diferencia ...
O puede decirle al compilador qué tipo debe inferirse usando la class.<type>method
sintaxis class.<type>method
:
expectsPointOrColoredPoint(Algorithms.<Point>min(points)); // print "Overloaded for Point"
expectsPointOrColoredPoint(Algorithms.<Point>wrongMin(points)); // will not compile
Jajaja Aquí está la respuesta. List<ColoredPoint>
no se puede pasar a la función que espera la Collection<Point>
porque los genéricos no son covariantes (a diferencia de los arreglos), pero se pueden pasar a la función que espera la Collection<? extends Point>
Collection<? extends Point>
.
No estoy seguro de dónde o quién puede preferir usar un parámetro de tipo explícito en tal caso, pero al menos muestra dónde puede ser inapropiado el wrongMin
.
Y gracias a @erickson y @ tom-hawtin-tackline por las respuestas sobre el propósito de T extends Object
restricción de T extends Object
.
La diferencia entre
T max(Collection<? extends T> coll)
y
T wrongMax(Collection<T> xs)
es que el tipo de retorno de la segunda versión es exactamente el mismo que el tipo de elemento T
la colección, mientras que en la primera versión T
puede ser un supertipo del tipo de elemento.
La segunda pregunta: el motivo de la T extends Object
se asegura de que T
sea una clase y no una interfaz.
Actualización: una demostración un poco más "natural" de la diferencia: suponga que define estos dos métodos:
static void happy(ColoredPoint p, Point q) {}
static void happy(Point p, ColoredPoint q) {}
Y llamenles al primero así:
happy(coloredPoint, min(points));
happy(coloredPoint, wrongMin(points));
El motor de inferencia de tipos podría deducir que en la primera llamada el tipo de retorno de min
debería ser Point
y el código se compilaría. La segunda llamada no se compilaría, ya que la llamada a happy
es ambigua.
Desafortunadamente, el motor de inferencia de tipos no es lo suficientemente poderoso al menos en Java 7, por lo que en realidad ambas llamadas no se compilan. La diferencia es que la primera llamada se puede arreglar especificando el parámetro de tipo como en Algorithms.<Point>min
, mientras que arreglar la segunda llamada requeriría una conversión explícita.
La diferencia está en el tipo devuelto, especialmente influenciado por la inferencia , por lo que el tipo puede ser un tipo jerárquico entre el tipo Comparable y el tipo de Lista. Déjame dar un ejemplo:
class Top {
}
class Middle extends Top implements Comparable<Top> {
@Override
public int compareTo(Top o) {
//
}
}
class Bottom extends Middle {
}
Usando la firma que has proporcionado:
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
Podemos codificar esto sin errores, advertencias o (lo que es más importante) conversiones:
List<Bottom> list;
Middle max = max(list); // T inferred to be Middle
Y si necesita un resultado Middle
, sin inferencia, puede escribir explícitamente la llamada a Middle
:
Comparable<Top> max = MyClass.<Middle>max(list); // No cast
o pasar a un método que acepte Middle
(donde la inferencia no funcionará)
someGenericMethodThatExpectsGenericBoundedToMiddle(MyClass.<Middle>max(list));
No sé si esto ayuda, pero para ilustrar los tipos del compilador como permitido / inferido, la firma se vería así (no que esto compile, por supuesto):
public static <Middle extends Comparable<Top>> Middle max(List<Bottom> list)
No es fácil, pero intentaré ser lo más específico posible:
en T max(Collection<? extends T> coll)
puede pasar un argumento como este List<Animal> or List<Cat> or List<Dog>
, y en T wrongMax(Collection<T> xs)
donde T es el animal que no se puede pasar como un Argumento a esta List<Dog>, List<Cat>
por supuesto en Runtime, puedes agregar objetos Cat o Dog en la List<Animal>
pero en tiempo de compilación no podrías pasar una subclase de Animal en el Tipo de la lista que se pasa como un argumento en el método wrongMax
, por otro lado, en el método max
que podría. Lo siento por mi inglés, todavía lo estoy aprendiendo :), Saludos.