tutorial - ¿Por qué "final" no está permitido en los métodos de interfaz de Java 8?
tutorial java oficial (4)
Una de las características más útiles de Java 8 son los nuevos métodos default
en las interfaces. Hay esencialmente dos razones (puede haber otras) por las que se han introducido:
- Proporcionar implementaciones reales por defecto. Ejemplo:
Iterator.remove()
- Permitiendo la evolución de la API JDK. Ejemplo:
Iterable.forEach()
Desde la perspectiva de un diseñador de API, me hubiera gustado poder usar otros modificadores en los métodos de interfaz, por ejemplo, final
. Esto sería útil al agregar métodos de conveniencia, previniendo anulaciones "accidentales" en la implementación de clases:
interface Sender {
// Convenience method to send an empty message
default final void send() {
send(null);
}
// Implementations should only implement this method
void send(String message);
}
Lo anterior ya es una práctica común si el Sender
fuera una clase:
abstract class Sender {
// Convenience method to send an empty message
final void send() {
send(null);
}
// Implementations should only implement this method
abstract void send(String message);
}
Ahora, las palabras clave default
y final
son obviamente contradictorias, pero la palabra clave predeterminada en sí misma no habría sido estrictamente requerida , por lo que asumo que esta contradicción es deliberada, para reflejar las diferencias sutiles entre "métodos de clase con cuerpo" (solo métodos) y "métodos de interfaz con el cuerpo" (métodos predeterminados), es decir, diferencias que todavía no he entendido.
En algún momento, el soporte para modificadores como static
y final
en los métodos de interfaz aún no se había explorado completamente, citando a Brian Goetz :
La otra parte es hasta qué punto vamos a ir a admitir herramientas de creación de clases en interfaces, como métodos finales, métodos privados, métodos protegidos, métodos estáticos, etc. La respuesta es: todavía no sabemos.
Desde entonces, a finales de 2011, obviamente, se agregó soporte para métodos static
en interfaces. Claramente, esto agregó mucho valor a las propias bibliotecas JDK, como con Comparator.comparing()
.
Pregunta:
¿Cuál es la razón por la que la final
(y también static final
) nunca llegó a las interfaces de Java 8?
En la lista de correo de lambda hay muchas discusiones al respecto . Uno de los que parece contener mucha discusión sobre todo eso es el siguiente: En la visibilidad del método de interfaz variada (era Defensores finales) .
En esta discusión, Talden, el autor de la pregunta original pregunta algo muy similar a tu pregunta:
La decisión de hacer público a todos los miembros de la interfaz fue, de hecho, una decisión desafortunada. Que cualquier uso de la interfaz en el diseño interno expone los detalles privados de la implementación es muy importante.
Es difícil solucionarlo sin agregar algunos matices oscuros o de compatibilidad al lenguaje. Una ruptura de la compatibilidad de esa magnitud y la sutileza potencial se vería impensable, por lo que debe existir una solución que no rompa el código existente.
Podría reintroducir la palabra clave ''paquete'' como un especificador de acceso viable. La ausencia de un especificador en una interfaz implicaría acceso público y la ausencia de un especificador en una clase implica acceso a paquetes. No está claro qué especificadores tienen sentido en una interfaz, especialmente si, para minimizar la carga de conocimiento de los desarrolladores, debemos asegurarnos de que los especificadores de acceso signifiquen lo mismo tanto en clase como en interfaz si están presentes.
En ausencia de métodos predeterminados, habría especulado que el especificador de un miembro en una interfaz debe ser al menos tan visible como la propia interfaz (de modo que la interfaz puede implementarse en todos los contextos visibles), con métodos predeterminados que no son tan cierto
¿Se ha comunicado claramente si esto es incluso una posible discusión dentro del alcance? Si no es así, debe celebrarse en otro lugar.
Finalmente, la respuesta de Brian Goetz fue:
Sí, esto ya está siendo explorado.
Sin embargo, permítame establecer algunas expectativas realistas: las características de lenguaje / VM tienen un largo tiempo de ejecución, incluso las que parecen triviales como esta. El tiempo para proponer nuevas ideas de características de lenguaje para Java SE 8 ya ha pasado.
Entonces, lo más probable es que nunca se implementó porque nunca fue parte del alcance. Nunca fue propuesto a tiempo para ser considerado.
En otra discusión acalorada sobre los métodos del defensor final sobre el tema, Brian dijo nuevamente :
Y has conseguido exactamente lo que deseabas. Eso es exactamente lo que agrega esta característica: herencia múltiple de comportamiento. Por supuesto entendemos que la gente los usará como rasgos. Y hemos trabajado arduamente para asegurarnos de que el modelo de herencia que ofrecen sea lo suficientemente simple y limpio para que las personas puedan obtener buenos resultados al hacerlo en una amplia variedad de situaciones. Al mismo tiempo, hemos optado por no empujarlos más allá del límite de lo que funciona de manera simple y limpia, y eso lleva a reacciones de "w, no fuiste lo suficientemente lejos" en algún caso. Pero en realidad, la mayor parte de este hilo parece estar refunfuñando de que el vidrio está lleno al 98%. ¡Tomaré ese 98% y seguiré adelante!
Así que esto refuerza mi teoría de que simplemente no era parte del alcance o parte de su diseño. Lo que hicieron fue proporcionar suficiente funcionalidad para lidiar con los problemas de la evolución de la API.
Esta pregunta está, hasta cierto punto, relacionada con ¿Cuál es la razón por la que la "sincronización" no está permitida en los métodos de interfaz de Java 8?
La clave para comprender los métodos predeterminados es que el objetivo principal del diseño es la evolución de la interfaz , no "convertir las interfaces en rasgos (mediocres)". Si bien hay una cierta superposición entre los dos, y tratamos de acomodarnos a este último donde no se interpuso en el camino del primero, estas preguntas se entienden mejor cuando se ven desde esta perspectiva. (Tenga en cuenta también que los métodos de clase serán diferentes de los métodos de interfaz, independientemente de su intención, en virtud del hecho de que los métodos de interfaz pueden heredarse de forma múltiple).
La idea básica de un método predeterminado es: es un método de interfaz con una implementación predeterminada, y una clase derivada puede proporcionar una implementación más específica. Y debido a que el centro de diseño fue la evolución de la interfaz, fue un objetivo fundamental del diseño que los métodos predeterminados puedan agregarse a las interfaces después del hecho de una manera compatible con fuente y binario.
La respuesta demasiado simple a "por qué no los métodos predeterminados finales" es que entonces el cuerpo no sería simplemente la implementación predeterminada, sería la única implementación. Si bien esa es una respuesta demasiado simple, nos da una pista de que la pregunta ya va en una dirección cuestionable.
Otra razón por la cual los métodos de interfaz finales son cuestionables es que crean problemas imposibles para los implementadores. Por ejemplo, suponiendo que tienes:
interface A {
default void foo() { ... }
}
interface B {
}
class C implements A, B {
}
Aquí, todo es bueno; C
hereda foo()
de A
Ahora suponiendo que B
se cambia para tener un método foo
, con un valor predeterminado:
interface B {
default void foo() { ... }
}
Ahora, cuando vamos a recompilar C
, el compilador nos dirá que no sabe qué comportamiento heredar para foo()
, por lo que C
tiene que anularlo (y podría elegir delegar a A.super.foo()
si quería conservar el mismo comportamiento.) ¿Pero qué pasaría si B
hubiera hecho su final
defecto y A
no estuviera bajo el control del autor de C
? Ahora C
está irremediablemente roto; no se puede compilar sin anular foo()
, pero no puede anular foo()
si fue final en B
Este es solo un ejemplo, pero el punto es que los métodos finales son realmente una herramienta que tiene más sentido en el mundo de las clases de herencia simple (generalmente qué estado de pareja se relaciona con el comportamiento), que las interfaces que simplemente contribuyen con el comportamiento y pueden heredarse de forma múltiple. . Es demasiado difícil razonar acerca de "qué otras interfaces podrían mezclarse con el implementador eventual", y permitir que un método de interfaz sea definitivo probablemente cause estos problemas (y no explotarán en la persona que escribió la interfaz, sino en la pobre usuario que intenta implementarlo.)
Otra razón para rechazarlos es que no significarían lo que crees que significan. Una implementación predeterminada solo se considera si la clase (o sus superclases) no proporciona una declaración (concreta o abstracta) del método. Si un método predeterminado fuera definitivo, pero una superclase ya implementara el método, el valor predeterminado se ignoraría, lo que probablemente no sea lo que el autor predeterminado esperaba al declararlo definitivo. (Este comportamiento de herencia es un reflejo del centro de diseño de los métodos predeterminados: evolución de la interfaz. Debería ser posible agregar un método predeterminado (o una implementación predeterminada a un método de interfaz existente) a las interfaces existentes que ya tienen implementaciones, sin cambiar el comportamiento de las clases existentes que implementan la interfaz, garantizando que las clases que ya funcionaron antes de agregar los métodos predeterminados funcionarán de la misma manera en presencia de los métodos predeterminados.)
No creo que sea necesario especificar final
en un método de interfaz de conveniencia, aunque estoy de acuerdo en que puede ser útil, pero al parecer los costos han superado a los beneficios.
Lo que se supone que debes hacer, de cualquier manera, es escribir el javadoc apropiado para el método predeterminado, mostrando exactamente lo que el método es y no está permitido hacer. De esa manera, las clases que implementan la interfaz "no están permitidas" para cambiar la implementación, aunque no hay garantías.
Cualquiera puede escribir una Collection
que se adhiera a la interfaz y luego haga cosas en los métodos que son absolutamente intuitivos, no hay forma de protegerse de eso, aparte de escribir pruebas unitarias exhaustivas.
Será difícil encontrar e identificar la respuesta "THE", para los resultados mencionados en los comentarios de @EJP: Hay aproximadamente 2 (+/- 2) personas en el mundo que pueden dar la respuesta definitiva. Y en caso de duda, la respuesta podría ser algo como: "Parecer que los métodos predeterminados finales de soporte no valen la pena el esfuerzo de reestructurar los mecanismos internos de resolución de llamadas". Esto es una especulación, por supuesto, pero al menos está respaldado por evidencias sutiles, como esta Declaración (por una de las dos personas) en la lista de correo de OpenJDK :
"Supongo que si los métodos" predeterminados finales "estuvieran permitidos, es posible que necesiten volver a escribirlos desde invocaciones internas especiales a la interfaz de invocación visible por el usuario".
y los hechos triviales como que un método simplemente no se considera un método (realmente) final cuando es un método default
, tal como se implementa actualmente en el Method::is_final_method en OpenJDK.
Más información realmente "autoritaria" es de hecho difícil de encontrar, incluso con búsquedas web excesivas y leyendo registros de confirmación. Pensé que podría estar relacionado con posibles ambigüedades durante la resolución de las llamadas del método de interfaz con la instrucción invokeinterface
y las llamadas al método de clase, correspondientes a la instrucción invokevirtual
: Para la instrucción invokevirtual
, puede haber una búsqueda simple en vtable , porque el método debe se puede heredar de una superclase o se puede implementar directamente por la clase. En contraste con eso, una llamada a invokeinterface
debe examinar el sitio de llamada respectivo para averiguar a qué interfaz se refiere realmente esta llamada (esto se explica con más detalle en la página de InterfaceCalls de la HotSpot Wiki). Sin embargo, los métodos final
no se insertan en vtable o reemplazan las entradas existentes en vtable (vea klassVtable.cpp. Línea 333 ), y de manera similar, los métodos predeterminados están reemplazando las entradas existentes en vtable (vea klassVtable.cpp, Línea 202 ). Por lo tanto, la razón real (y, por lo tanto, la respuesta) debe estar oculta más profundamente dentro de los mecanismos de resolución de llamadas de métodos (más bien complejos), pero quizás estas referencias, sin embargo, se considerarán útiles, ya sea solo para otros que logran derivar la respuesta real a partir de ese.