update - Eclipse/javac no está de acuerdo en compilar la firma con el método de colisión predeterminado; quien tiene razon
pom java compiler (4)
Como lo entiendo, la pregunta es sobre pasar un objeto de una clase ya compilada como un parámetro. Dado que no puede llamar al extractName(X)
con una clase abstracta o una interfaz, el objeto argumento debe tener su método getName()
resuelto y no ambiguo. Java usa un enlace tardío para resolver qué método anulado se llama en tiempo de ejecución, por lo que estaría de acuerdo con BonusLord en que el método podría compilarse y ejecutarse correctamente incluso si javac
produce el error.
Aquí hay una clase simple que demuestra el problema:
package com.mimvista.debug;
public class DefaultCollisionTest {
public static interface Interface1 {
public String getName();
}
public static interface Interface2 {
public default String getName() { return "Mr. 2"; };
}
public static <X extends Interface1&Interface2> String extractName(X target) {
return target.getName();
}
}
Eclipse (Neon 2) compila alegremente esta clase mientras que javac (JDK 1.8.0_121) escupe el siguiente error de compilación:
$ javac src/com/mimvista/debug/DefaultCollisionTest.java
src/com/mimvista/debug/DefaultCollisionTest.java:13: error: class INT#1 inherits abstract and default for getName() from types Interface2 and Interface1
public static <X extends Interface1&Interface2> String extractName(X target) {
^
where INT#1 is an intersection type:
INT#1 extends Object,Interface1,Interface2
1 error
Creo que Eclipse es correcto en este caso, pero no estoy totalmente seguro. Según mi comprensión del error "hereda el resumen y el valor predeterminado", creo que solo se debe generar al compilar una clase declarada real que implemente esas dos interfaces. Parece que javac puede estar generando una clase intermedia bajo el capó para lidiar con esa firma genérica y someterla erróneamente a la prueba de colisión del método predeterminado.
Javac es correcto según JLS 9.4.1.3. Interfaces> Métodos hereditarios con firmas equivalentes a sobrescrituras :
Si una interfaz
I
hereda un método por defecto cuya firma es equivalente al reemplazo con otro método heredado porI
, entonces se produce un error en tiempo de compilación. (Este es el caso si el otro método es abstracto o predeterminado.)
La letra pequeña explica:
[...] cuando se hereda un resumen y un método predeterminado con firmas coincidentes, producimos un error. En este caso, sería posible dar prioridad a uno u otro, quizás supongamos que el método predeterminado también proporciona una implementación razonable para el método abstracto. Pero esto es arriesgado, ya que, aparte del nombre y la firma coincidentes, no tenemos ninguna razón para creer que el método predeterminado se comporte de manera coherente con el contrato del método abstracto; es posible que el método predeterminado no haya existido cuando se desarrolló originalmente la subinterfaz. Es más seguro en esta situación pedirle al usuario que afirme activamente que la implementación predeterminada es apropiada (a través de una declaración de anulación).
En contraste, el comportamiento de larga data de los métodos concretos heredados en las clases es que anulan los métodos abstractos declarados en las interfaces (ver §8.4.8 ). El mismo argumento sobre una posible violación de contrato se aplica aquí, pero en este caso hay un desequilibrio inherente entre las clases y las interfaces. Preferimos, para preservar la naturaleza independiente de las jerarquías de clase, minimizar los choques de interfaz de clase simplemente dando prioridad a métodos concretos.
También comparar con 8.4.8.4. Clases> Métodos hereditarios con firmas equivalentes a sobrescrituras :
Es un error en tiempo de compilación si una clase C hereda un método predeterminado cuya firma es equivalente a la de reemplazo con otro método heredado por C, a menos que exista un método abstracto declarado en una superclase de C y heredado por C que sea equivalente de reemplazo con Los dos métodos.
Esta excepción a las reglas estrictas de conflicto por defecto y por defecto se hace cuando se declara un método abstracto en una superclase: la afirmación de lo abstracto proveniente de la jerarquía de superclase esencialmente supera al método predeterminado, haciendo que el método predeterminado actúe como si Fue abstracto. Sin embargo, el método abstracto de una clase no anula los métodos predeterminados, ya que las interfaces todavía tienen permiso para refinar la firma del método abstracto proveniente de la jerarquía de clases.
Incluso en palabras más simples: el supuesto es que las dos interfaces no están relacionadas lógicamente y ambas especifican algún tipo de contrato de comportamiento. Por lo tanto, no es seguro asumir que la implementación predeterminada en Interface2
es un cumplimiento válido del contrato de Interface1
. Es más seguro lanzar un error y dejar que el desarrollador lo resuelva.
No encontré un lugar en el JLS donde abordaría exactamente su caso, pero creo que el error está en la esencia de las especificaciones anteriores: declara que extractName()
debe tomar un objeto que implemente Interface1
e Interface2
. Pero para un objeto de este tipo, solo sería válido si "existe un método abstracto declarado en una superclase de C y heredado por C que es equivalente de anulación con los dos métodos" . Su declaración genérica no especifica nada sobre la superclase de X
, por lo que el compilador la trata como un conflicto "abstracto-predeterminado".
Yo diría que este es un error de Javac, o al menos debería serlo.
Parece que los implementadores de Javac tomaron un atajo y reutilizaron el código para crear una interfaz en la implementación de un límite genérico. IOW, Javac trata <X extends I1&I2>
como si fuera la interface X extends I1, I2
.
En realidad, sin embargo, <X extends I1&I2>
es diferente. Solo significa que X
tiene los métodos de I1
e I2
, pero no dice nada acerca de las implementaciones de los métodos. Por lo tanto, la ausencia o presencia de la implementación por default
debe ser irrelevante.
Desafortunadamente, como dice @slim, el objetivo es pasar el compilador de JDK, por lo que Javac tiene la última palabra. Enviar un informe de error, tal vez?
Eclipse tiene razón .
No he encontrado este error javac en la base de datos de errores de Java y, por lo tanto, lo reporté: JDK-8186643
Mejor explicación por Stephan Herrmann (ver su comentario más abajo ):
A la derecha, reportar un error contra un tipo de intersección solo debería ocurrir cuando el tipo de intersección no está bien formado y por lo tanto la intersección está vacía. Pero como muestra esta respuesta, la intersección no está vacía y, por lo tanto, debería ser legal. En realidad, la
class INT#1 inherits ...
mensaje de errorclass INT#1 inherits ...
no tiene sentido, porque en ese momento nadie mencionó una clase INT # 1, solo tenemos la intersección de dos interfaces , y esa intersección se usa solo como un enlace, no como un tipo.
Una clase que implementa múltiples interfaces del mismo método puede compilarse con ambos compiladores, incluso si el método de una interfaz tiene una implementación predeterminada. Se puede hacer referencia a la clase ya que <T extends I1 & I2>
siempre que ni I1 ni I2 tengan una implementación predeterminada para un método con el mismo nombre. Solo si una de las dos interfaces tiene una implementación predeterminada, javac falla.
En caso de ambigüedad, la implementación debería aplicarse, el error ya debería ocurrir al definir una clase, no cuando se hace referencia a la clase como <T extends ...>
(ver JLS 4.9. Tipos de intersección ).
Vea el siguiente ejemplo que funciona con <T extends I1 & I2>
y <T extends IDefault>
, pero falla con <T extends I1 & IDefault>
y javac :
interface I1 {
String get();
}
interface I2 {
String get();
}
interface IDefault {
default String get() {
return "default";
};
}
public class Foo implements I1, I2, IDefault {
@Override
public String get() {
return "foo";
}
public static void main(String[] args) {
System.out.print(getOf(new Foo()));
}
// static <T extends I1 & IDefault> String getOf(T t) { // fails with javac
static <T extends I1 & I2> String getOf(T t) { // OK
return t.get();
}
}