¿Por qué se agregaron métodos predeterminados y estáticos a las interfaces en Java 8 cuando ya teníamos clases abstractas?

99

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:

  1. Los métodos estáticos no pertenecen a las interfaces. Pertenecen a clases de utilidad.
  2. 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 interfaz Ix, la clase que Cximplementa Ixy tiene un foomé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?

Señor Smith
fuente
30
Pregunta extra: ¿Por qué no introdujeron la herencia múltiple para las clases?
2
Los métodos estáticos no pertenecen a las interfaces. Pertenecen a clases de utilidad. ¡No, pertenecen a la @Deprecatedcategorí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.
11
@JarrodRoberson ¿Puede darnos más pistas (los enlaces serían geniales) sobre "son una pesadilla para la prueba unitaria y refactorizar cuando se da cuenta de por qué son una mala idea"? Nunca pensé eso y me gustaría saber más al respecto.
acuosa
11
@Chris La herencia múltiple de estado causa muchos problemas, especialmente con la asignación de memoria ( el clásico problema del diamante ). Sin embargo, la herencia múltiple del comportamiento solo depende de que la implementación cumpla con el contrato ya establecido por la interfaz (la interfaz puede llamar a otros métodos que declara y requiere). Una distinción sutil pero muy interesante.
ssube
1
desea agregar un método a la interfaz existente, era engorroso o casi imposible antes de java8, ahora puede agregarlo como método predeterminado.
VdeX

Respuestas:

59

Un buen ejemplo motivador para los métodos predeterminados está en la biblioteca estándar de Java, donde ahora tiene

list.sort(ordering);

en lugar de

Collections.sort(list, ordering);

No creo que pudieran haberlo hecho de otra manera sin más de una implementación idéntica de List.sort.

soru
fuente
19
C # supera este problema con los métodos de extensión.
Robert Harvey
55
y permite una lista enlazada de usar un O (1) espacio adicional y O (n log n) tiempo mergesort, porque las listas enlazadas se pueden combinar en su lugar, en java 7 que vuelca a una matriz externa y luego tipo que
monstruo de trinquete
55
Encontré este documento donde Goetz explica el problema. Así que marcaré esta respuesta como la solución por ahora.
Señor Smith
1
@RobertHarvey: Cree una lista de doscientos millones de elementos <Byte>, úsela IEnumerable<Byte>.Appendpara unirse a ellos, y luego llame Count, luego dígame cómo los métodos de extensión resuelven el problema. Si CountIsKnowny Countfueran miembros de IEnumerable<T>, el retorno de Appendpodría anunciarse CountIsKnownsi las colecciones constituyentes lo hicieran, pero sin tales métodos eso no es posible.
supercat
66
@supercat: No tengo la menor idea de lo que estás hablando.
Robert Harvey
48

De hecho, la respuesta correcta se encuentra en la Documentación de Java , que establece:

[d] Los métodos predeterminados le permiten agregar nueva funcionalidad a las interfaces de sus bibliotecas y garantizar la compatibilidad binaria con el código escrito para versiones anteriores de esas interfaces.

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.Collectionsahora (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, UUIDpodría haberse definido como una interfaz en su lugar, permitiendo implementaciones reales de terceros de las piezas que faltan. (Si UUIDse 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 javactravé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 .

ngreen
fuente
1
Para un mayor seguimiento de mi último párrafo (es decir , no hacer cumplir los contratos en las interfaces ), vale la pena señalar que los defaultmétodos no pueden anular equals, hashCodey toString. 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/…
ngreen
Es una lástima que Java solo tenga un único método virtual equalsy uno único hashCode, 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 como hashMapclaves es útil, pero también hay ocasiones en las que sería útil almacenar colecciones en una hashMapque coincida con las cosas en función de la equivalencia en lugar del estado actual [la equivalencia implica un estado coincidente y la inmutabilidad ] .
supercat
Bueno, Java tiene una solución alternativa con el Comparador y las interfaces Comparables. Pero creo que son feos.
ngreen
Esas interfaces sólo son compatibles para ciertos tipos de colección, y plantean sus propios problemas: un comparador puede en sí potencialmente encapsular el estado (por ejemplo, un comparador cadena especializada podría ignorar un número configurable de caracteres al comienzo de cada cadena, en cuyo caso el número de los caracteres a ignorar serían parte del estado del comparador), que a su vez se convertiría en parte del estado de cualquier colección que fue ordenada por él, pero no hay un mecanismo definido para preguntar a dos comparadores si son equivalentes.
supercat
Ah, sí, me duelen los comparadores. Estoy trabajando en una estructura de árbol que debería ser simple pero no porque sea muy difícil obtener el comparador correcto. Probablemente voy a escribir una clase de árbol personalizada solo para que el problema desaparezca.
ngreen
44

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:

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

No hay forma de garantizar que cuando alguien implemente la interfaz, poparroje una excepción si la pila está vacía. Podríamos hacer cumplir esta regla separándonos popen dos métodos: un public finalmétodo que hace cumplir el contrato y un protected abstractmétodo que realiza el estallido real.

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

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 popes 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,

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.

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.unmodifiableListpodría haber sido declarado en la Listinterfaz, y hubiera sido más fácil de encontrar.

Doval
fuente
44
Contraargumento: dado que los métodos estáticos, si se escriben correctamente, son independientes, tienen más sentido en una clase estática separada donde se pueden recopilar y clasificar por nombre de clase, y menos sentido en una interfaz, donde son esencialmente convenientes que invita a abusos como mantener el estado estático en el objeto o causar efectos secundarios, lo que hace que los métodos estáticos no sean verificables.
Robert Harvey
3
@RobertHarvey ¿Qué te impide hacer cosas igualmente tontas si tu método estático está en una clase? Además, el método en la interfaz puede no requerir ningún estado en absoluto. Simplemente podría estar tratando de hacer cumplir un contrato. Supongamos que tiene una Stackinterfaz y quiere asegurarse de que cuando popse llama con una pila vacía, se produce una excepción. Dados los métodos abstractos boolean isEmpty()y protected T pop_impl(), podría implementar final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }Esto aplica el contrato en TODOS los implementadores.
Doval
¿Esperar lo? Los métodos Push y Pop en una pila no van a ser static.
Robert Harvey
@RobertHarvey Hubiera sido más claro si no fuera por el límite de caracteres en los comentarios, pero estaba defendiendo las implementaciones predeterminadas en una interfaz, no los métodos estáticos.
Doval
8
Creo que los métodos de interfaz predeterminados son más bien un truco que se ha introducido para poder ampliar la biblioteca estándar sin la necesidad de adaptar el código existente basado en ella.
Giorgio
2

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.

rae1
fuente
1
Los métodos de extensión no agregan funcionalidad a las interfaces. Los métodos de extensión son simplemente azúcar sintáctica para llamar a métodos estáticos en una clase usando la list.sort(ordering);forma conveniente .
Robert Harvey
Si observa la IEnumerableinterfaz en C #, puede ver cómo la implementación de métodos de extensión a esa interfaz (como lo LINQ to Objectshace) agrega funcionalidad para cada clase que implementa IEnumerable. Eso es lo que quise decir al agregar funcionalidad.
rae1
2
Eso es lo mejor de los métodos de extensión; dan la ilusión de que estás agregando funcionalidad a una clase o interfaz. Simplemente no confunda eso con agregar métodos reales a una clase; los métodos de clase tienen acceso a los miembros privados de un objeto, los métodos de extensión no (ya que en realidad son solo otra forma de llamar a métodos estáticos).
Robert Harvey
2
Exactamente, y es por eso que veo alguna relación con tener métodos estáticos o predeterminados en una interfaz en Java; La implementación se basa en lo que está disponible para la interfaz, no en la clase misma.
rae1
1

Los dos propósitos principales que veo en los defaultmétodos (algunos casos de uso sirven para ambos propósitos):

  1. Sintaxis de azúcar. Una clase de utilidad podría servir para ese propósito, pero los métodos de instancia son mejores.
  2. Extensión de una interfaz existente. La implementación es genérica pero a veces ineficiente.

Si se tratara del segundo propósito, no lo verías en una interfaz completamente nueva como Predicate. @FunctionalInterfaceSe requiere que todas las interfaces anotadas tengan exactamente un método abstracto para que un lambda pueda implementarlo. Añadido defaultmétodos como and, or, negateson 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 Collectioncomo stream, forEach, removeIf-, básicamente, es sólo la utilidad que no es necesario para anular. Y luego hay métodos como spliterator. 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 staticmétodos, supongo que los demás lo cubren bastante bien: permite que la interfaz sea su propia clase de utilidad. ¿Tal vez podríamos deshacernos Collectionsen el futuro de Java? Set.empty()se movería

Vlasec
fuente