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

94

En Java 8, las interfaces pueden contener métodos implementados, métodos estáticos y los llamados métodos "predeterminados" (que las clases implementadoras no necesitan anular).

En mi (probablemente ingenua) vista, 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 se podría utilizar una clase abstracta para este propósito.

En resumen:

Antes de Java 8:

  • Podría 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 por la implementación de clases.
  • No puede agregar un nuevo método en una interfaz sin modificar todas las implementaciones, pero esto es realmente bueno.

Después de Java 8:

  • No hay prácticamente ninguna diferencia entre una interfaz y una clase abstracta (aparte de la herencia múltiple). De hecho, puede emular una clase regular con una interfaz.
  • Al programar las implementaciones, los programadores pueden olvidarse de 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 con esa nueva funcionalidad en mente, y esto puede causar problemas. Por ejemplo, si alguien agrega un nuevo método predeterminado default void foo() a una interfaz Ix , entonces la clase Cx implementa Ix y tiene un método privado foo con la misma firma no se compila.

¿Cuáles son las razones principales de tales cambios importantes y qué nuevos beneficios (si los hay) agregan?

    
pregunta Mister Smith 20.03.2014 - 16:01

5 respuestas

56

Un buen ejemplo de motivación para los métodos predeterminados se encuentra en la biblioteca estándar de Java, donde ahora tiene

list.sort(ordering);

en lugar de

Collections.sort(list, ordering);

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

    
respondido por el soru 20.03.2014 - 16:47
43

La respuesta correcta se encuentra de hecho en la Documentación de Java , que dice:

  Los métodos

[d] efault le permiten agregar nuevas funciones 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 de larga data 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 ha vinculado 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 mediante la extensión de las interfaces de colecciones existentes y el suministro de implementaciones predeterminadas. Romper con 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 interfaces se revela nuevamente en la documentación: [t] su 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, clases de utilidad estática como java.util.Collections ahora (finalmente) puede 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 habría hecho.

En una nota similar, un ejemplo de cómo estas nuevas características pueden ser beneficiosas es considerar una clase que me ha molestado recientemente, java.util.UUID . Realmente no proporciona soporte para tipos de UUID 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 del código para los tipos 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 los métodos estáticos, UUID podría haberse definido como una interfaz en su lugar, permitiendo implementaciones reales de terceros de las piezas faltantes. (Si UUID se definiera originalmente como una interfaz, probablemente tendríamos algún tipo de clase clunky UuidUtil con métodos estáticos, lo que también sería horrible.) Muchas de las API principales de Java se degradan al no basarse en interfaces. , pero a partir de Java 8, la cantidad de excusas para este mal comportamiento ha disminuido afortunadamente.

No es correcto decir que [t] aquí prácticamente no hay diferencia entre una interfaz y una clase abstracta , porque las clases abstractas pueden tener un 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 se supone que haga 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 de javac a través de Procesador de anotaciones y generación de código de bytes en tiempo de ejecución. Idealmente, los contratos se cumplirían en tiempo de compilación y, en el peor de los casos, utilizar un conjunto de pruebas, pero mi entendimiento es que el cumplimiento de los runtime está mal visto. Otra herramienta interesante que podría ayudar a la programación de contratos en Java es el Checker Framework .

    
respondido por el ngreen 03.09.2014 - 20:06
43

Porque solo puedes heredar una clase. Si tiene dos interfaces cuyas implementaciones son lo suficientemente complejas como para que necesite una clase base abstracta, esas dos interfaces se excluyen mutuamente 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 repetitivo en un lenguaje que ya es demasiado detallado.

Como ejemplo motivador de por qué puede ser útil proporcionar implementaciones en interfaces, considere esta interfaz de pila:

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

    T pop() throws EmptyException;
 }

No hay forma de garantizar que cuando alguien implementa la interfaz, pop lanzará una excepción si la pila está vacía. Podríamos aplicar esta regla al separar pop en dos métodos: un método public final que impone el contrato y un método protected abstract 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, también las 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, eso es una gran pérdida de flexibilidad. Hace que sus interfaces sean mutuamente excluyentes. Ser capaz de proporcionar implementaciones que solo se basan en los métodos de 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é 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 implementadora puede anularla.

En cuanto a las implementaciones predeterminadas de los métodos reemplazables, asumo que las implementaciones predeterminadas agregadas a las API de Java solo dependen del 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,

  

No hay prácticamente ninguna diferencia entre una interfaz y una clase abstracta (aparte de la 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 los detalles de la implementación.

Como ejemplo a favor de los métodos estáticos en las interfaces, considere clases de utilidad como Colecciones en la API de Java. Esa clase solo existe porque esos métodos estáticos no pueden ser declarados en sus respectivas interfaces. Collections.unmodifiableList podría haber sido declarado en la interfaz List , y hubiera sido más fácil de encontrar.

    
respondido por el Doval 20.03.2014 - 17:19
2

Quizás la intención fue proporcionar la capacidad de crear mixin sustituyendo la necesidad de inyectar información estática o funcionalidad a través de una dependencia.

Esta idea parece relacionada con la forma en que puede usar los métodos de extensión en C # para agregar la funcionalidad implementada a las interfaces.

    
respondido por el rae1 20.03.2014 - 16:53
1

Los dos propósitos principales que veo en los métodos default (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 de un segundo propósito, no lo verías en una nueva interfaz como Predicate . Se requiere que todas las interfaces anotadas @FunctionalInterface tengan exactamente un método abstracto para que un lambda pueda implementarlo. Los métodos agregados de default como and , or , negate son solo una utilidad, y no se supone que los anules. Sin embargo, los métodos a veces estáticos harían mejor .

En cuanto a la extensión de las interfaces existentes, incluso allí, algunos métodos nuevos son solo la sintaxis de azúcar. Métodos de Collection like stream , forEach , removeIf - básicamente, es solo una utilidad que no necesita 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 se usa ampliamente.

En cuanto a los métodos static , supongo que los otros lo cubren bastante bien: permite que la interfaz sea su propia clase de utilidad. ¿Tal vez podríamos deshacernos de Collections en el futuro de Java? Set.empty() se movería.

    
respondido por el Vlasec 24.12.2016 - 22:57

Lea otras preguntas en las etiquetas