Límite superior del tipo de retorno genérico - interfaz vs. clase - código sorprendentemente válido

171

Este es un ejemplo del mundo real de una API de biblioteca de terceros, pero simplificado.

Compilado con Oracle JDK 8u72

Considere estos dos métodos:

<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

<X extends String> X getString() {
    return (X) "hello";
}

Ambos informan una advertencia de "lanzamiento no verificado". Entiendo por qué. Lo que me desconcierta es por qué puedo llamar

Integer x = getCharSequence();

y se compila? El compilador debe saber que Integerno se implementa CharSequence. La llamada a

Integer y = getString();

da un error (como se esperaba)

incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String

¿Alguien puede explicar por qué este comportamiento se consideraría válido? ¿Cómo sería útil?

El cliente no sabe que esta llamada no es segura: el código del cliente se compila sin previo aviso. ¿Por qué la compilación no advierte sobre eso / emite un error?

Además, ¿en qué se diferencia de este ejemplo?

<X extends CharSequence> void doCharSequence(List<X> l) {
}

List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error

Intentar pasar List<Integer>da un error, como se esperaba:

method doCharSequence in class generic.GenericTest cannot be applied to given types;
  required: java.util.List<X>
  found: java.util.List<java.lang.Integer>
  reason: inference variable X has incompatible bounds
    equality constraints: java.lang.Integer
    upper bounds: java.lang.CharSequence

Si eso se informa como un error, ¿por qué Integer x = getCharSequence();no?

Adam Michalik
fuente
15
¡interesante! el lanzamiento en el LHS Integer x = getCharSequence();se compilará, pero el lanzamiento en el RHS Integer x = (Integer) getCharSequence();falla la compilación
copos
¿Qué versión del compilador de Java estás usando? Por favor, especifique esta información en la pregunta.
Federico Peralta Schaffner
@FedericoPeraltaSchaffner no puede ver por qué eso importa: esta es una pregunta directa sobre el JLS.
Boris the Spider
@BoristheSpider Porque el mecanismo de inferencia de tipos ha cambiado para java8
Federico Peralta Schaffner
1
@FedericoPeraltaSchaffner: ya etiqueté la pregunta con [java-8], pero ahora agregué la versión del compilador en la publicación.
Adam Michalik

Respuestas:

184

CharSequencees un interface. Por lo tanto, incluso si SomeClassno se implementa CharSequence, sería perfectamente posible crear una clase

class SubClass extends SomeClass implements CharSequence

Por lo tanto puedes escribir

SomeClass c = getCharSequence();

porque el tipo inferido Xes el tipo de intersección SomeClass & CharSequence.

Esto es un poco extraño en el caso de Integerporque Integeres final, pero finalno juega ningún papel en estas reglas. Por ejemplo puedes escribir

<T extends Integer & CharSequence>

Por otro lado, Stringno es un interface, por lo que sería imposible ampliar SomeClasspara obtener un subtipo de String, porque Java no admite la herencia múltiple para las clases.

Con el Listejemplo, debe recordar que los genéricos no son covariantes ni contravariantes. Esto significa que si Xes un subtipo de Y, List<X>no es ni un subtipo ni un supertipo de List<Y>. Como Integerno se implementa CharSequence, no puede usarlo List<Integer>en su doCharSequencemétodo.

Sin embargo, puede hacer que esto se compile

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

Si tiene un método que devuelve algo List<T>como esto:

static <T extends CharSequence> List<T> foo() 

tu puedes hacer

List<? extends Integer> list = foo();

Nuevamente, esto se debe a que el tipo inferido es Integer & CharSequencey este es un subtipo de Integer.

Los tipos de intersección ocurren implícitamente cuando especifica múltiples límites (por ejemplo <T extends SomeClass & CharSequence>).

Para obtener más información, aquí está la parte de JLS donde explica cómo funcionan los límites de tipo. Puede incluir múltiples interfaces, p. Ej.

<T extends String & CharSequence & List & Comparator>

pero solo el primer límite puede ser una no interfaz.

Paul Boddington
fuente
62
No tenía idea de que podría poner un &en la definición genérica. +1
escamas el
13
@flkes Puede poner más de uno, pero solo el primer argumento puede ser una no interfaz. <T extends String & List & Comparator>está bien pero <T extends String & Integer>no lo está, porque Integerno es una interfaz.
Paul Boddington el
77
@PaulBoddington Hay un uso práctico para estos métodos. Por ejemplo, si el tipo no se usa realmente para los datos almacenados. Ejemplos de esto son Collections.emptyList()así como Optional.empty(). Estas implementaciones devuelven una interfaz genérica, pero no almacenan nada.
Stefan Dollase
66
Y nadie dice que una clase finalen tiempo de compilación estará finalen tiempo de ejecución.
Holger
77
@Federico Peralta Schaffner: el punto aquí es que el método getCharSequence()promete devolver lo que Xnecesite la persona que llama, que incluye devolver un tipo que se extiende Integere implementa CharSequencesi la persona que llama lo necesita y, según esta promesa, es correcto permitir asignar el resultado Integer. Es el método getCharSequence()que está roto ya que no cumple su promesa, pero eso no es culpa del compilador.
Holger
59

El tipo inferido por su compilador antes de la asignación para Xes Integer & CharSequence. Este tipo se siente extraño, porque Integeres final, pero es un tipo perfectamente válido en Java. Luego se lanza a Integer, lo cual está perfectamente bien.

Hay exactamente un posible valor para el Integer & CharSequencetipo: null. Con la siguiente implementación:

<X extends CharSequence> X getCharSequence() {
    return null;
}

La siguiente tarea funcionará:

Integer x = getCharSequence();

Debido a este posible valor, no hay razón para que la asignación sea incorrecta, incluso si obviamente es inútil. Una advertencia sería útil.

El verdadero problema es la API, no el sitio de la llamada.

De hecho, recientemente escribí en un blog sobre este anti patrón de diseño de API . Nunca debería (casi) diseñar un método genérico para devolver tipos arbitrarios porque (casi) nunca puede garantizar que se entregará el tipo inferido. Una excepción son métodos como Collections.emptyList(), en el caso de que el vacío de la lista (y el borrado de tipo genérico) sea la razón por la cual funcionará cualquier inferencia <T>:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
Lukas Eder
fuente