El método tiene el mismo borrado que otro método en tipo

381

¿Por qué no es legal tener los siguientes dos métodos en la misma clase?

class Test{
   void add(Set<Integer> ii){}
   void add(Set<String> ss){}
}

Consigo el compilation error

El método add (Set) tiene el mismo add de borrado (Set) que otro método del tipo Test.

Si bien puedo solucionarlo, me preguntaba por qué a javac no le gusta esto.

Puedo ver que en muchos casos, la lógica de esos dos métodos sería muy similar y podría ser reemplazada por una sola

public void add(Set<?> set){}

método, pero este no es siempre el caso.

Esto es muy molesto si quieres tener dos constructorsque tomen esos argumentos porque entonces no puedes simplemente cambiar el nombre de uno de los constructors.

Omry Yadan
fuente
1
¿Qué pasa si te quedas sin estructuras de datos y aún necesitas más versiones?
Omry Yadan
1
Puede crear clases personalizadas que hereden de las versiones base.
willlma
1
OP, ¿se te ocurrió alguna solución al problema del constructor? Necesito aceptar dos tipos de Listy no sé cómo manejarlo.
Tomáš Zato - Restablece a Mónica el
99
Cuando trabajo con Java, realmente extraño C # ...
Svish
1
@ TomášZato, lo resolví agregando parámetros ficticios al constructor: Boolean noopSignatureOverload.
Nthalk

Respuestas:

357

Esta regla está destinada a evitar conflictos en el código heredado que todavía usa tipos sin formato.

Aquí hay una ilustración de por qué esto no estaba permitido, extraído del JLS. Supongamos que antes de que se introdujeran genéricos en Java, escribí un código como este:

class CollectionConverter {
  List toList(Collection c) {...}
}

Extiendes mi clase, así:

class Overrider extends CollectionConverter{
  List toList(Collection c) {...}
}

Después de la introducción de los genéricos, decidí actualizar mi biblioteca.

class CollectionConverter {
  <T> List<T> toList(Collection<T> c) {...}
}

No estás listo para hacer ninguna actualización, así que dejas tu Overriderclase sola. Para anular correctamente el toList()método, los diseñadores del lenguaje decidieron que un tipo sin formato era "equivalente de anulación" a cualquier tipo genérico. Esto significa que aunque la firma de su método ya no es formalmente igual a la firma de mi superclase, su método todavía se anula.

Ahora, pasa el tiempo y decides que estás listo para actualizar tu clase. Pero te equivocas un poco y, en lugar de editar el toList()método en bruto existente, agregas un método nuevo como este:

class Overrider extends CollectionConverter {
  @Override
  List toList(Collection c) {...}
  @Override
  <T> List<T> toList(Collection<T> c) {...}
}

Debido a la equivalencia de anulación de tipos sin formato, ambos métodos tienen una forma válida para anular el toList(Collection<T>)método. Pero, por supuesto, el compilador necesita resolver un único método. Para eliminar esta ambigüedad, no se permite que las clases tengan múltiples métodos que sean equivalentes a la anulación, es decir, múltiples métodos con los mismos tipos de parámetros después del borrado.

La clave es que esta es una regla de idioma diseñada para mantener la compatibilidad con el código antiguo utilizando tipos sin formato. No es una limitación requerida por el borrado de los parámetros de tipo; debido a que la resolución del método se produce en tiempo de compilación, habría sido suficiente agregar tipos genéricos al identificador del método.

erickson
fuente
3
Gran respuesta y ejemplo! Sin embargo, no estoy seguro si entiendo completamente su última oración ("Debido a que la resolución del método ocurre en tiempo de compilación, antes del borrado, no se requiere la verificación de tipo para que esto funcione"). Podrías elaborar un poco?
Jonas Eicher
2
Tiene sentido. Acabo de pasar un tiempo pensando en la reificación de tipos en los métodos de plantilla, pero sí: el compilador se asegura de que se seleccione el método correcto antes de borrar el tipo. Hermoso. Si no estuviera contaminado por los problemas de compatibilidad del código heredado.
Jonas Eicher
1
@daveloyall No, no conozco esa opción para javac.
erickson
13
No es la primera vez que encuentro un error de Java, que no es ningún error y podría compilarse si solo los autores de Java usaran advertencias como todos los demás. Solo ellos piensan que saben todo mejor.
Tomáš Zato - Restablece a Mónica el
1
@ TomášZato No, no conozco ninguna solución limpia para eso. Si desea algo sucio, puede pasar el List<?>y algunos enumque defina para indicar el tipo. La lógica específica del tipo en realidad podría estar en un método en la enumeración. Alternativamente, es posible que desee crear dos clases diferentes, en lugar de una clase con dos constructores diferentes. La lógica común estaría en una superclase o un objeto auxiliar al que delegan ambos tipos.
erickson
118

Los genéricos de Java usan borrado de tipo. El bit en los corchetes angulares ( <Integer>y <String>) se elimina, por lo que terminaría con dos métodos que tienen una firma idéntica (la add(Set)que ve en el error). Eso no está permitido porque el tiempo de ejecución no sabría cuál usar para cada caso.

Si Java alguna vez obtiene genéricos reificados, entonces podría hacer esto, pero probablemente eso sea poco probable ahora.

GaryF
fuente
24
Lo siento, pero su respuesta (y las otras respuestas) no explican por qué hay un error aquí. La resolución de sobrecarga se realiza en tiempo de compilación y el compilador seguramente tiene la información de tipo necesaria para decidir qué método vincular por dirección o por cualquier método al que se haga referencia en el código de bytes que creo que no es firma. Incluso creo que algunos compiladores permitirán que esto se compile.
Stilgar
55
@Stilgar, ¿qué impide que se llame o se inspeccione el método mediante reflexión? La lista de métodos devueltos por Class.getMethods () tendría dos métodos idénticos, lo que no tendría sentido.
Adrian Mouat
55
La información de reflexión puede / debe contener los metadatos necesarios para trabajar con genéricos. Si no, ¿cómo sabe el compilador de Java acerca de los métodos genéricos cuando importa una biblioteca ya compilada?
Stilgar
44
Entonces el método getMethod necesita reparación. Por ejemplo, introduzca una sobrecarga que especifique la sobrecarga genérica y haga que el método original devuelva solo la versión no genérica, sin devolver ningún método anotado como genérico. Por supuesto, esto debería haberse hecho en la versión 1.5. Si lo hacen ahora, romperán la compatibilidad con el método. Mantengo mi afirmación de que el borrado de tipo no dicta este comportamiento. Es la implementación que no obtuvo suficiente trabajo probablemente debido a los recursos limitados.
Stilgar
1
Esta no es una respuesta precisa, pero resume rápidamente el problema en una ficción útil: las firmas del método son demasiado similares, el compilador podría no notar la diferencia y entonces obtendrá "problemas de compilación no resueltos".
worc
46

Esto se debe a que los genéricos de Java se implementan con Type Erasure .

Sus métodos se traducirían, en tiempo de compilación, a algo como:

La resolución del método ocurre en tiempo de compilación y no considera los parámetros de tipo. ( ver la respuesta de erickson )

void add(Set ii);
void add(Set ss);

Ambos métodos tienen la misma firma sin los parámetros de tipo, de ahí el error.

bruno conde
fuente
21

El problema es eso Set<Integer>y en Set<String>realidad son tratados como un SetJVM. Seleccionar un tipo para el Set (String o Integer en su caso) es solo azúcar sintáctico utilizado por el compilador. La JVM no puede distinguir entre Set<String>y Set<Integer>.

kgiannakakis
fuente
77
Es cierto que el tiempo de ejecución de JVM no tiene información para distinguir cada uno Set, pero dado que la resolución del método ocurre en tiempo de compilación, cuando la información necesaria está disponible, esto no es relevante. El problema es que permitir estas sobrecargas entraría en conflicto con la asignación de tipos sin formato, por lo que se hicieron ilegales en la sintaxis de Java.
erickson
@erickson Incluso cuando el compilador sabe a qué método llamar, no puede, como en el código de bytes, ambos se ven exactamente iguales. Tendría que cambiar cómo se especifica una llamada al método, ya (Ljava/util/Collection;)Ljava/util/List;que no funciona. Podrías usarlo (Ljava/util/Collection<String>;)Ljava/util/List<String>;, pero ese es un cambio incompatible y te encontrarás con problemas irresolubles en lugares donde todo lo que tienes es un tipo borrado. Probablemente tendrías que borrar por completo, pero eso es bastante complicado.
maaartinus
@maaartinus Sí, estoy de acuerdo en que tendría que cambiar el especificador del método. Estoy tratando de resolver algunos de los problemas irresolubles que los llevaron a renunciar a ese intento.
erickson
7

Definir un solo método sin tipo como void add(Set ii){}

Puede mencionar el tipo al llamar al método según su elección. Funcionará para cualquier tipo de conjunto.

Idrees Ashraf
fuente
3

Podría ser posible que el compilador traduzca Set (Integer) a Set (Object) en código de bytes java. Si este es el caso, Set (Integer) se usaría solo en la fase de compilación para la verificación de sintaxis.

Rossoft
fuente
55
Técnicamente es solo el tipo crudo Set. Los genéricos no existen en el código de byte, son azúcar sintáctica para la fundición y brindan seguridad en tiempo de compilación.
Andrzej Doyle
1

Me topé con esto cuando intenté escribir algo como: Continuable<T> callAsync(Callable<T> code) {....} y se Continuable<Continuable<T>> callAsync(Callable<Continuable<T>> veryAsyncCode) {...} convirtieron para el compilador en las 2 definiciones de Continuable<> callAsync(Callable<> veryAsyncCode) {...}

La eliminación de tipo literalmente significa borrar la información de los argumentos de tipo de los genéricos. Esto es MUY molesto, pero es una limitación que estará con Java por un tiempo. Para el caso de los constructores, no se puede hacer mucho, por ejemplo, 2 nuevas subclases especializadas con diferentes parámetros en el constructor. O use métodos de inicialización en su lugar ... (¿constructores virtuales?) Con diferentes nombres ...

para métodos de operación similares, el cambio de nombre ayudaría, como

class Test{
   void addIntegers(Set<Integer> ii){}
   void addStrings(Set<String> ss){}
}

O con algunos nombres más descriptivos, autodocumentados para casos de oyu, like addNamesy addIndexesor such.

Kote Isaev
fuente