Métodos predeterminados de Java 8 como rasgos: ¿seguro?

116

¿Es una práctica segura de usar? métodos predeterminados como una versión pobre de los rasgos en Java 8?

Algunos afirman que puede entristecer a los pandas si los usas solo por el simple hecho de hacerlo, porque es genial, pero esa no es mi intención. También se recuerda a menudo que se introdujeron métodos predeterminados para admitir la evolución de la API y la compatibilidad con versiones anteriores, lo cual es cierto, pero esto no significa que sea incorrecto o retorcido usarlos como rasgos per se.

Tengo en mente el siguiente caso de uso práctico :

public interface Loggable {
    default Logger logger() {
        return LoggerFactory.getLogger(this.getClass());
    }
}

O quizás, defina un PeriodTrait:

public interface PeriodeTrait {
    Date getStartDate();
    Date getEndDate();
    default isValid(Date atDate) {
        ...
    }
}

Es cierto que se podría usar la composición (o incluso clases auxiliares) pero parece más detallada y desordenada y no permite beneficiarse del polimorfismo.

Entonces, ¿ está bien / es seguro usar métodos predeterminados como rasgos básicos? , o debería preocuparme por los efectos secundarios imprevistos?

Varias preguntas sobre SO están relacionadas con los rasgos de Java vs Scala; ese no es el punto aquí. Tampoco estoy pidiendo meramente opiniones. En cambio, estoy buscando una respuesta autorizada o al menos una visión de campo: si ha utilizado métodos predeterminados como rasgos en su proyecto corporativo, ¿resultó ser una bomba de tiempo?

youri
fuente
Me parece que podrías obtener los mismos beneficios de heredar una clase abstracta y no tener que preocuparte por hacer llorar a los pandas ... La única razón que veo para usar un método predeterminado en una interfaz es que necesitas la funcionalidad y no puedes modificar un montón de código heredado que se basa en la interfaz.
Deven Phillips
1
Estoy de acuerdo con @ infosec812 sobre la extensión de una clase abstracta que define su propio campo de registro estático. ¿No crearía su método logger () una nueva instancia de registrador cada vez que se llame?
Eidan Spiegel
Para el registro, es posible que desee consultar Projectlombok.org y su anotación @ Slf4j.
Deven Phillips

Respuestas:

120

La respuesta corta es: es seguro si los usa de manera segura :)

La respuesta sarcástica: dime qué quieres decir con rasgos, y tal vez te dé una mejor respuesta :)

Con toda seriedad, el término "rasgo" no está bien definido. Muchos desarrolladores de Java están más familiarizados con los rasgos tal como se expresan en Scala, pero Scala está lejos de ser el primer lenguaje en tener rasgos, ya sea en nombre o en efecto.

Por ejemplo, en Scala, los rasgos tienen estado (pueden tener var variables); en Fortress son pura conducta. Las interfaces de Java con métodos predeterminados no tienen estado; ¿Significa esto que no son rasgos? (Pista: esa fue una pregunta capciosa).

De nuevo, en Scala, los rasgos se componen mediante linealización; si la clase Aextiende rasgos Xy Y, a continuación, el orden en el que Xy Yse mezclan en determina cómo los conflictos entre XyY se resuelven. En Java, este mecanismo de linealización no está presente (fue rechazado, en parte, porque era demasiado "poco parecido a Java").

La razón próxima para agregar métodos predeterminados a las interfaces era apoyar la evolución de la interfaz , pero sabíamos muy bien que íbamos más allá. Si lo considera "evolución de la interfaz ++" o "rasgos--" es una cuestión de interpretación personal. Entonces, para responder a su pregunta sobre seguridad ... siempre que se ciña a lo que el mecanismo realmente admite, en lugar de tratar de estirarlo a algo que no admite, debería estar bien.

Un objetivo de diseño clave era que, desde la perspectiva del cliente de una interfaz, los métodos predeterminados no deberían distinguirse de los métodos de interfaz "normales". El carácter predeterminado de un método, por lo tanto, solo es interesante para el diseñador e implementador de la interfaz.

A continuación, se muestran algunos casos de uso que están dentro de los objetivos de diseño:

  • Evolución de la interfaz. Aquí, estamos agregando un nuevo método a una interfaz existente, que tiene una implementación predeterminada sensible en términos de métodos existentes en esa interfaz. Un ejemplo sería agregar el forEachmétodo a Collection, donde la implementación predeterminada se escribe en términos del iterator()método.

  • Métodos "opcionales". Aquí, el diseñador de una interfaz está diciendo "Los implementadores no necesitan implementar este método si están dispuestos a vivir con las limitaciones de funcionalidad que ello implica". Por ejemplo, Iterator.removese le dio un valor predeterminado que arroja UnsupportedOperationException; dado que la gran mayoría de las implementaciones de Iteratortienen este comportamiento de todos modos, el valor predeterminado hace que este método sea esencialmente opcional. (Si el comportamiento de AbstractCollectionse expresara como valores predeterminados Collection, podríamos hacer lo mismo con los métodos mutativos).

  • Métodos de conveniencia. Estos son métodos que son estrictamente por conveniencia, nuevamente generalmente implementados en términos de métodos no predeterminados en la clase. El logger()método en su primer ejemplo es una ilustración razonable de esto.

  • Combinadores. Estos son métodos de composición que crean instancias nuevas de la interfaz en función de la instancia actual. Por ejemplo, los métodos Predicate.and()o Comparator.thenComparing()son ejemplos de combinadores.

Si proporciona una implementación predeterminada, también debe proporcionar alguna especificación para la predeterminada (en el JDK, usamos la @implSpecetiqueta javadoc para esto) para ayudar a los implementadores a comprender si quieren anular el método o no. Algunos valores predeterminados, como los métodos de conveniencia y los combinadores, casi nunca se anulan; otros, como los métodos opcionales, a menudo se anulan. Debe proporcionar suficientes especificaciones (no solo documentación) sobre lo que promete hacer el implementador predeterminado, para que el implementador pueda tomar una decisión sensata sobre si necesita anularlo.

Brian Goetz
fuente
9
Gracias Brian por esta completa respuesta. Ahora puedo usar métodos predeterminados con un corazón ligero. Lectores: se puede encontrar más información de Brian Goetz sobre la evolución de la interfaz y los métodos predeterminados en NightHacking Worldwide Lambdas, por ejemplo.
youri
Gracias @ brian-goetz. Por lo que ha dicho, creo que los métodos predeterminados de Java están más cerca de la noción de rasgos tradicionales como se define en el documento de Ducasse et al ( scg.unibe.ch/archive/papers/Duca06bTOPLASTraits.pdf ). Los "rasgos" de Scala no me parecen rasgos en absoluto, ya que tienen estado, usan la linealización en su composición, y parece que también resuelven implícitamente conflictos de métodos, y estas son todas las cosas que los rasgos tradicionales no hacen tener. De hecho, diría que los rasgos de Scala se parecen más a mixins que a rasgos. ¿Qué piensas? PD: Nunca he codificado en Scala.
adino
¿Qué tal usar implementaciones predeterminadas vacías para métodos en una interfaz que actúa como oyente? La implementación del oyente solo puede estar interesada en escuchar unos pocos métodos de interfaz, por lo que al hacer que los métodos sean predeterminados, el implementador solo tiene que implementar los métodos que necesita escuchar.
Lahiru Chandima
1
@LahiruChandima ¿Te gusta MouseListener? En la medida en que este estilo de API tenga sentido, encajará en el grupo de "métodos opcionales". ¡Asegúrese de documentar claramente el carácter opcional!
Brian Goetz
Si. Como en MouseListener. Gracias por la respuesta.
Lahiru Chandima