En Java 8, las interfaces pueden contener métodos implementados, métodos estáticos y los llamados métodos "predeterminados" (que las clases de implementación no necesitan anular).
En mi opinión (probablemente ingenua), no había necesidad de violar interfaces como esta. Las interfaces siempre han sido un contrato que debe cumplir, y este es un concepto muy simple y puro. Ahora es una mezcla de varias cosas. En mi opinión:
- Los métodos estáticos no pertenecen a las interfaces. Pertenecen a clases de utilidad.
- Los métodos "predeterminados" no deberían haberse permitido en las interfaces. Siempre puedes usar una clase abstracta para este propósito.
En breve:
Antes de Java 8:
- Puede usar clases abstractas y regulares para proporcionar métodos estáticos y predeterminados. El papel de las interfaces es claro.
- Todos los métodos en una interfaz deben ser anulados implementando clases.
- No puede agregar un nuevo método en una interfaz sin modificar todas las implementaciones, pero esto es realmente algo bueno.
Después de Java 8:
- Prácticamente no hay diferencia entre una interfaz y una clase abstracta (que no sea herencia múltiple). De hecho, puede emular una clase regular con una interfaz.
- Al programar las implementaciones, los programadores pueden olvidar anular los métodos predeterminados.
- Hay un error de compilación si una clase intenta implementar dos o más interfaces que tienen un método predeterminado con la misma firma.
- Al agregar un método predeterminado a una interfaz, cada clase de implementación hereda automáticamente este comportamiento. Es posible que algunas de estas clases no hayan sido diseñadas teniendo en cuenta esa nueva funcionalidad, y esto puede causar problemas. Por ejemplo, si alguien agrega un nuevo método predeterminado
default void foo()
a una interfazIx
, la clase queCx
implementaIx
y tiene unfoo
método privado con la misma firma no se compila.
¿Cuáles son las principales razones de estos cambios importantes y qué nuevos beneficios (si los hay) agregan?
java
programming-languages
interfaces
java8
Señor Smith
fuente
fuente
@Deprecated
categoría! Los métodos estáticos son una de las construcciones más abusadas en Java, debido a la ignorancia y la pereza. Muchos métodos estáticos generalmente significan un programador incompetente, aumentan el acoplamiento en varios órdenes de magnitud y son una pesadilla para la prueba unitaria y la refactorización cuando te das cuenta de por qué son una mala idea.Respuestas:
Un buen ejemplo motivador para los métodos predeterminados está en la biblioteca estándar de Java, donde ahora tiene
en lugar de
No creo que pudieran haberlo hecho de otra manera sin más de una implementación idéntica de
List.sort
.fuente
IEnumerable<Byte>.Append
para unirse a ellos, y luego llameCount
, luego dígame cómo los métodos de extensión resuelven el problema. SiCountIsKnown
yCount
fueran miembros deIEnumerable<T>
, el retorno deAppend
podría anunciarseCountIsKnown
si las colecciones constituyentes lo hicieran, pero sin tales métodos eso no es posible.De hecho, la respuesta correcta se encuentra en la Documentación de Java , que establece:
Esta ha sido una fuente de dolor desde hace mucho tiempo en Java, porque las interfaces tienden a ser imposibles de evolucionar una vez que se hicieron públicas. (El contenido de la documentación está relacionado con el documento al que se vinculó en un comentario: Evolución de la interfaz a través de métodos de extensión virtual ). Además, la rápida adopción de nuevas características (por ejemplo, lambdas y las nuevas API de flujo) solo se puede hacer extendiendo el interfaces de colecciones existentes y proporcionar implementaciones predeterminadas. Romper la compatibilidad binaria o introducir nuevas API significaría que pasarían varios años antes de que las características más importantes de Java 8 fueran de uso común.
La razón para permitir métodos estáticos en las interfaces se revela nuevamente en la documentación: [t] esto le facilita la organización de métodos auxiliares en sus bibliotecas; puede mantener métodos estáticos específicos para una interfaz en la misma interfaz en lugar de en una clase separada. En otras palabras, las clases de utilidad estáticas como
java.util.Collections
ahora (finalmente) pueden considerarse un antipatrón, en general (por supuesto, no siempre ). Supongo que agregar soporte para este comportamiento fue trivial una vez que se implementaron los métodos de extensión virtual, de lo contrario, probablemente no se hubiera hecho.En una nota similar, un ejemplo de cómo estas nuevas características pueden ser de beneficio es considerar una clase que recientemente me ha molestado,
java.util.UUID
. Realmente no proporciona soporte para UUID tipos 1, 2 o 5, y no puede modificarse fácilmente para hacerlo. También está atascado con un generador aleatorio predefinido que no se puede anular. La implementación de código para los tipos de UUID no admitidos requiere una dependencia directa de una API de terceros en lugar de una interfaz, o bien el mantenimiento del código de conversión y el costo de la recolección de basura adicional. Con métodos estáticos,UUID
podría haberse definido como una interfaz en su lugar, permitiendo implementaciones reales de terceros de las piezas que faltan. (SiUUID
se definiera originalmente como una interfaz, probablemente tendríamos algún tipo de torpeUuidUtil
clase con métodos estáticos, lo que también sería horrible.) Muchas API centrales de Java se degradan al no basarse en las interfaces, pero a partir de Java 8, afortunadamente, el número de excusas para este mal comportamiento ha disminuido.No es correcto decir que [t] prácticamente no hay diferencia entre una interfaz y una clase abstracta , porque las clases abstractas pueden tener estado (es decir, declarar campos) mientras que las interfaces no pueden. Por lo tanto, no es equivalente a herencia múltiple o incluso herencia de estilo mixin. Los mixins adecuados (como los rasgos de Groovy 2.3 ) tienen acceso al estado. (Groovy también admite métodos de extensión estática).
Tampoco es una buena idea seguir el ejemplo de Doval , en mi opinión. Se supone que una interfaz define un contrato, pero no debe hacer cumplir el contrato. (No en Java de todos modos). La verificación adecuada de una implementación es responsabilidad de un conjunto de pruebas u otra herramienta. La definición de contratos podría hacerse con anotaciones, y OVal es un buen ejemplo, pero no sé si admite restricciones definidas en las interfaces. Tal sistema es factible, incluso si uno no existe actualmente. (Las estrategias incluyen la personalización en tiempo de compilación a
javac
través del procesador de anotacionesAPI y generación de bytecode en tiempo de ejecución.) Idealmente, los contratos se harían cumplir en tiempo de compilación, y en el peor de los casos, utilizando un conjunto de pruebas, pero entiendo que la ejecución en tiempo de ejecución está mal vista. Otra herramienta interesante que podría ayudar a la programación de contratos en Java es Checker Framework .fuente
default
métodos no pueden anularequals
,hashCode
ytoString
. Un análisis de costo / beneficio muy informativo de por qué esto no está permitido se puede encontrar aquí: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/…equals
y uno únicohashCode
, ya que hay dos tipos diferentes de igualdad que las colecciones pueden necesitar probar, y los elementos que implementarían múltiples interfaces pueden estar atascados con requisitos contractuales conflictivos. Ser útil poder usar listas que no van a cambiar comohashMap
claves es útil, pero también hay ocasiones en las que sería útil almacenar colecciones en unahashMap
que coincida con las cosas en función de la equivalencia en lugar del estado actual [la equivalencia implica un estado coincidente y la inmutabilidad ] .Porque solo puedes heredar una clase. Si tiene dos interfaces cuyas implementaciones son lo suficientemente complejas como para necesitar una clase base abstracta, esas dos interfaces son mutuamente excluyentes en la práctica.
La alternativa es convertir esas clases base abstractas en una colección de métodos estáticos y convertir todos los campos en argumentos. Eso permitiría a cualquier implementador de la interfaz llamar a los métodos estáticos y obtener la funcionalidad, pero es un montón de repeticiones en un lenguaje que ya es demasiado detallado.
Como un ejemplo motivador de por qué puede ser útil proporcionar implementaciones en interfaces, considere esta interfaz Stack:
No hay forma de garantizar que cuando alguien implemente la interfaz,
pop
arroje una excepción si la pila está vacía. Podríamos hacer cumplir esta regla separándonospop
en dos métodos: unpublic final
método que hace cumplir el contrato y unprotected abstract
método que realiza el estallido real.No solo nos aseguramos de que todas las implementaciones respeten el contrato, sino que también los liberamos de tener que verificar si la pila está vacía y lanzar la excepción. ¡Es una gran victoria! ... excepto por el hecho de que tuvimos que cambiar la interfaz a una clase abstracta. En un lenguaje con herencia única, esa es una gran pérdida de flexibilidad. Hace que sus posibles interfaces sean mutuamente excluyentes. Ser capaz de proporcionar implementaciones que solo se basan en los métodos de la interfaz resolvería el problema.
No estoy seguro de si el enfoque de Java 8 para agregar métodos a las interfaces permite agregar métodos finales o métodos abstractos protegidos, pero sé que el lenguaje D lo permite y proporciona soporte nativo para Design by Contract . No hay peligro en esta técnica ya que
pop
es final, por lo que ninguna clase de implementación puede anularla.En cuanto a las implementaciones predeterminadas de métodos reemplazables, supongo que las implementaciones predeterminadas agregadas a las API de Java solo se basan en el contrato de la interfaz a la que se agregaron, por lo que cualquier clase que implemente correctamente la interfaz también se comportará correctamente con las implementaciones predeterminadas.
Además,
Esto no es del todo cierto ya que no puede declarar campos en una interfaz. Cualquier método que escriba en una interfaz no puede confiar en ningún detalle de implementación.
Como ejemplo a favor de los métodos estáticos en las interfaces, considere las clases de utilidad como Colecciones en la API de Java. Esa clase solo existe porque esos métodos estáticos no se pueden declarar en sus respectivas interfaces.
Collections.unmodifiableList
podría haber sido declarado en laList
interfaz, y hubiera sido más fácil de encontrar.fuente
Stack
interfaz y quiere asegurarse de que cuandopop
se llama con una pila vacía, se produce una excepción. Dados los métodos abstractosboolean isEmpty()
yprotected T pop_impl()
, podría implementarfinal T pop() { isEmpty()) throw PopException(); else return pop_impl(); }
Esto aplica el contrato en TODOS los implementadores.static
.Quizás la intención era proporcionar la capacidad de crear clases mixin al reemplazar la necesidad de inyectar información estática o funcionalidad a través de una dependencia.
Esta idea parece estar relacionada con cómo puede usar métodos de extensión en C # para agregar funcionalidad implementada a las interfaces.
fuente
list.sort(ordering);
forma conveniente .IEnumerable
interfaz en C #, puede ver cómo la implementación de métodos de extensión a esa interfaz (como loLINQ to Objects
hace) agrega funcionalidad para cada clase que implementaIEnumerable
. Eso es lo que quise decir al agregar funcionalidad.Los dos propósitos principales que veo en los
default
métodos (algunos casos de uso sirven para ambos propósitos):Si se tratara del segundo propósito, no lo verías en una interfaz completamente nueva como
Predicate
.@FunctionalInterface
Se requiere que todas las interfaces anotadas tengan exactamente un método abstracto para que un lambda pueda implementarlo. Añadidodefault
métodos comoand
,or
,negate
son de utilidad, y no se supone que anularlos. Sin embargo, a veces los métodos estáticos harían mejor .En cuanto a la extensión de las interfaces existentes, incluso allí, algunos métodos nuevos son solo azúcar de sintaxis. Los métodos de
Collection
comostream
,forEach
,removeIf
-, básicamente, es sólo la utilidad que no es necesario para anular. Y luego hay métodos comospliterator
. La implementación predeterminada es subóptima, pero bueno, al menos el código se compila. Solo recurra a esto si su interfaz ya está publicada y ampliamente utilizada.En cuanto a los
static
métodos, supongo que los demás lo cubren bastante bien: permite que la interfaz sea su propia clase de utilidad. ¿Tal vez podríamos deshacernosCollections
en el futuro de Java?Set.empty()
se moveríafuente