Java8: ¿Por qué está prohibido definir un método predeterminado para un método de java.lang.Object?

130

Los métodos predeterminados son una buena herramienta nueva en nuestra caja de herramientas Java. Sin embargo, intenté escribir una interfaz que defina una defaultversión del toStringmétodo. Java me dice que esto está prohibido, ya que los métodos declarados en java.lang.Objectno pueden ser defaulteditados. ¿Por qué es este el caso?

Sé que existe la regla de "la clase base siempre gana", por lo que, de manera predeterminada (juego de palabras;), cualquier defaultimplementación de un Objectmétodo sería sobrescrita por el método de Objecttodos modos. Sin embargo, no veo ninguna razón por la cual no debería haber una excepción para los métodos de Objectla especificación. Especialmente porque toStringpodría ser muy útil tener una implementación predeterminada.

Entonces, ¿cuál es la razón por la cual los diseñadores de Java decidieron no permitir que los defaultmétodos anulen los métodos Object?

gexicida
fuente
1
Me siento muy bien conmigo mismo ahora, votando esto 100 veces y, por lo tanto, una insignia de oro. ¡buena pregunta!
Eugene

Respuestas:

186

Este es otro de esos problemas de diseño de lenguaje que parece "obviamente una buena idea" hasta que comienzas a cavar y te das cuenta de que en realidad es una mala idea.

Este correo tiene mucho sobre el tema (y también sobre otros temas). Hubo varias fuerzas de diseño que convergieron para llevarnos al diseño actual:

  • El deseo de mantener el modelo de herencia simple;
  • El hecho de que una vez que miras más allá de los ejemplos obvios (p. Ej., Convertirte AbstractListen una interfaz), te das cuenta de que heredar equals / hashCode / toString está fuertemente vinculado a la herencia y al estado individuales, y las interfaces se heredan y no tienen estado;
  • Que potencialmente abrió la puerta a algunos comportamientos sorprendentes.

Ya ha tocado el objetivo "mantenerlo simple"; las reglas de herencia y resolución de conflictos están diseñadas para ser muy simples (las clases ganan interfaces, las interfaces derivadas ganan superinterfaces y cualquier otro conflicto es resuelto por la clase implementadora). Por supuesto, estas reglas podrían modificarse para hacer una excepción, pero Creo que descubrirá cuando comience a tirar de esa cuerda, que la complejidad incremental no es tan pequeña como podría pensar.

Por supuesto, hay algún grado de beneficio que justificaría una mayor complejidad, pero en este caso no está allí. Los métodos de los que estamos hablando aquí son equals, hashCode y toString. Todos estos métodos son intrínsecamente sobre el estado del objeto, y es la clase propietaria del estado, no la interfaz, quien está en la mejor posición para determinar qué significa la igualdad para esa clase (especialmente porque el contrato para la igualdad es bastante fuerte; ver Efectivo Java por algunas consecuencias sorprendentes); los escritores de interfaces están demasiado lejos.

Es fácil sacar el AbstractListejemplo; Sería maravilloso si pudiéramos deshacernos de él AbstractListy poner el comportamiento en la Listinterfaz. Pero una vez que va más allá de este ejemplo obvio, no hay muchos otros buenos ejemplos que se puedan encontrar. En la raíz, AbstractListestá diseñado para herencia única. Pero las interfaces deben estar diseñadas para la herencia múltiple.

Además, imagina que estás escribiendo esta clase:

class Foo implements com.libraryA.Bar, com.libraryB.Moo { 
    // Implementation of Foo, that does NOT override equals
}

El Fooescritor mira los supertipos, no ve la implementación de iguales y concluye que para obtener la igualdad de referencia, todo lo que necesita hacer es heredar iguales Object. Luego, la próxima semana, el responsable de mantenimiento de la biblioteca para Bar agrega "útilmente" una equalsimplementación predeterminada . Ooops! Ahora la semántica de Foose ha roto por una interfaz en otro dominio de mantenimiento "útilmente" agregando un valor predeterminado para un método común.

Se supone que los valores predeterminados son valores predeterminados. Agregar un valor predeterminado a una interfaz donde no había ninguno (en cualquier lugar de la jerarquía) no debería afectar la semántica de las clases de implementación concretas. Pero si los valores predeterminados pudieran "anular" los métodos Object, eso no sería cierto.

Entonces, si bien parece una característica inofensiva, de hecho es bastante dañina: agrega mucha complejidad para poca expresividad incremental y hace que sea demasiado fácil para los cambios bien intencionados e inofensivos para interfaces compiladas por separado para socavar la semántica prevista de la implementación de clases.

Brian Goetz
fuente
13
Me alegra que te hayas tomado el tiempo para explicar esto, y agradezco todos los factores que se consideraron. Estoy de acuerdo en que esto sería peligroso para hashCodey equals, pero creo que sería muy útil toString. Por ejemplo, alguna Displayableinterfaz podría definir un String display()método, y ahorraría una tonelada de repeticiones para poder definir , default String toString() { return display(); }en Displayablelugar de requerir que cada uno Displayableimplemente toString()o extienda una DisplayableToStringclase base.
Brandon
8
@Brandon Tienes razón en que permitir heredar toString () no sería peligroso de la misma manera que lo sería para equals () y hashCode (). Por otro lado, ahora la característica sería aún más irregular, y aún incurriría en la misma complejidad adicional sobre las reglas de herencia, por el bien de este método ... parece mejor trazar la línea limpiamente donde lo hicimos .
Brian Goetz
55
@gexicide Si toString()se basa solo en los métodos de la interfaz, simplemente podría agregar algo como default String toStringImpl()a la interfaz y anular toString()en cada subclase para llamar a la implementación de la interfaz, un poco feo, pero funciona, y mejor que nada. :) Otra forma de hacerlo es hacer algo como Objects.hash(), Arrays.deepEquals()y Arrays.deepToString(). ¡Hice +1 en la respuesta de @ BrianGoetz!
Siu Ching Pong -Asuka Kenji-
3
El comportamiento predeterminado de toString () de un lambda es realmente desagradable. Sé que la fábrica lambda está diseñada para ser muy simple y rápida, pero escupir un nombre de clase derrotado realmente no es útil. Tener una default toString()anulación en una interfaz funcional nos permitiría, al menos, hacer algo como escupir la firma de la función y la clase principal del implementador. Aún mejor, si pudiéramos aplicar algunas estrategias recursivas de String, podríamos caminar a través del cierre para obtener una muy buena descripción de la lambda, y así mejorar drásticamente la curva de aprendizaje de lambda.
Groostav
Cualquier cambio en toString en cualquier clase, subclase o miembro de instancia podría tener ese efecto en la implementación de clases o usuarios de una clase. Además, cualquier cambio en cualquiera de los métodos predeterminados probablemente también afectaría a todas las clases de implementación. Entonces, ¿qué tiene de especial toString, hashCode cuando se trata de alguien que altera el comportamiento de una interfaz? Si una clase extiende a otra clase, también podrían alterarla. O si están utilizando el patrón de delegación. Alguien que use interfaces Java 8 deberá hacerlo actualizando. Se podría haber proporcionado una advertencia / error que se puede suprimir en la subclase.
mmm
30

Está prohibido definir métodos predeterminados en interfaces para métodos en java.lang.Object, ya que los métodos predeterminados nunca serían "accesibles".

Los métodos de interfaz predeterminados se pueden sobrescribir en las clases que implementan la interfaz y la implementación de la clase del método tiene mayor prioridad que la implementación de la interfaz, incluso si el método se implementa en una superclase. Como todas las clases heredan de java.lang.Object, los métodos en java.lang.Objecttendrán prioridad sobre el método predeterminado en la interfaz y se invocarán en su lugar.

Brian Goetz de Oracle proporciona algunos detalles más sobre la decisión de diseño en esta publicación de la lista de correo .

jarnbjo
fuente
3

No veo en la cabeza de los autores del lenguaje Java, por lo que solo podemos adivinar. Pero veo muchas razones y estoy absolutamente de acuerdo con ellas en este tema.

La razón principal para introducir métodos predeterminados es poder agregar nuevos métodos a las interfaces sin romper la compatibilidad con versiones anteriores de implementaciones anteriores. Los métodos predeterminados también pueden usarse para proporcionar métodos de "conveniencia" sin la necesidad de definirlos en cada una de las clases de implementación.

Ninguno de estos se aplica a toString y otros métodos de Object. En pocas palabras, los métodos predeterminados fueron diseñados para proporcionar el comportamiento predeterminado donde no hay otra definición. No proporcionar implementaciones que "compitan" con otras implementaciones existentes.

La regla de "la clase base siempre gana" también tiene razones sólidas. Se supone que las clases definen implementaciones reales , mientras que las interfaces definen implementaciones predeterminadas , que son algo más débiles.

Además, la introducción de CUALQUIER excepción a las reglas generales causa una complejidad innecesaria y genera otras preguntas. El objeto es (más o menos) una clase como cualquier otra, entonces, ¿por qué debería tener un comportamiento diferente?

En general, la solución que propone probablemente traiga más inconvenientes que profesionales.

Marwin
fuente
No noté el segundo párrafo de la respuesta del gexicida al publicar la mía. Contiene un enlace que explica el problema con más detalle.
Marwin
1

El razonamiento es muy simple, es porque Object es la clase base para todas las clases Java. Entonces, incluso si tenemos el método de Object definido como método predeterminado en alguna interfaz, será inútil porque el método de Object siempre se usará. Es por eso que para evitar confusiones, no podemos tener métodos predeterminados que anulen los métodos de la clase Object.

Kumar Abhishek
fuente
1

Para dar una respuesta muy pedante, solo está prohibido definir un defaultmétodo para un método públicojava.lang.Object . Hay 11 métodos a considerar, que se pueden clasificar de tres maneras para responder a esta pregunta.

  1. Seis de los Objectmétodos no puede tener defaultmétodos porque son finaly no puede ser anulado en absoluto: getClass(), notify(), notifyAll(), wait(), wait(long), y wait(long, int).
  2. Tres de los Objectmétodos no puede tener defaultmétodos para las razones dadas anteriormente por Brian Goetz: equals(Object), hashCode(), y toString().
  3. Dos de los Objectmétodos pueden tener defaultmétodos, aunque el valor de estos valores predeterminados es cuestionable en el mejor de los casos: clone()y finalize().

    public class Main {
        public static void main(String... args) {
            new FOO().clone();
            new FOO().finalize();
        }
    
        interface ClonerFinalizer {
            default Object clone() {System.out.println("default clone"); return this;}
            default void finalize() {System.out.println("default finalize");}
        }
    
        static class FOO implements ClonerFinalizer {
            @Override
            public Object clone() {
                return ClonerFinalizer.super.clone();
            }
            @Override
            public void finalize() {
                ClonerFinalizer.super.finalize();
            }
        }
    }
jaco0646
fuente
.¿Cuál es el punto de? Aún no respondió la parte POR QUÉ: "Entonces, ¿cuál es la razón por la cual los diseñadores de Java decidieron no permitir que los métodos predeterminados anulen los métodos de Object?"
pro_cheats