java inheritance overriding shadowing package-private

Herencia en la visibilidad del paquete en Java



inheritance overriding (3)

Estoy buscando una explicación para el siguiente comportamiento:

  • Tengo 6 clases, {aA, bB, cC, aD, bE, cF}, cada una con un método de paquete visible m () que escribe el nombre de la clase.
  • Tengo una clase a.Main con un método principal que hace algunas pruebas de estas clases.
  • El resultado parece no seguir las reglas de herencia adecuadas.

Aquí están las clases:

package a; public class A { void m() { System.out.println("A"); } } // ------ package b; import a.A; public class B extends A { void m() { System.out.println("B"); } } // ------ package c; import b.B; public class C extends B { void m() { System.out.println("C"); } } // ------ package a; import c.C; public class D extends C { void m() { System.out.println("D"); } } // ------ package b; import a.D; public class E extends D { void m() { System.out.println("E"); } } // ------ package c; import b.E; public class F extends E { void m() { System.out.println("F"); } }

La clase principal está en el package a :

package a; import b.B; import b.E; import c.C; import c.F; public class Main { public static void main(String[] args) { A a = new A(); B b = new B(); C c = new C(); D d = new D(); E e = new E(); F f = new F(); System.out.println("((A)a).m();"); ((A)a).m(); System.out.println("((A)b).m();"); ((A)b).m(); System.out.println("((A)c).m();"); ((A)c).m(); System.out.println("((A)d).m();"); ((A)d).m(); System.out.println("((A)e).m();"); ((A)e).m(); System.out.println("((A)f).m();"); ((A)f).m(); System.out.println("((D)d).m();"); ((D)d).m(); System.out.println("((D)e).m();"); ((D)e).m(); System.out.println("((D)f).m();"); ((D)f).m(); } }

Y aquí está el resultado:

((A)a).m(); A ((A)b).m(); A ((A)c).m(); A ((A)d).m(); D ((A)e).m(); E ((A)f).m(); F ((D)d).m(); D ((D)e).m(); D ((D)f).m(); D

Y aquí están mis preguntas:

1) Entiendo que Dm() esconde Am() , pero un reparto a A debería exponer el método oculto m() , ¿es eso cierto? ¿O es que Dm() anula Am() a pesar de que Bm() y Cm() rompen la cadena de herencia?

((A)d).m(); D

2) Aún peor, el siguiente código muestra una anulación en efecto, ¿por qué?

((A)e).m(); E ((A)f).m(); F

Y por qué no en esta parte:

((A)a).m(); A ((A)b).m(); A ((A)c).m(); A

¿y éste?

((D)d).m(); D ((D)e).m(); D ((D)f).m(); D

Estoy usando OpenJDK javac 11.0.2.

EDITAR: ¿Cómo anular un método con el alcance de visibilidad predeterminado (paquete) responde a la primera pregunta ?

Un método de instancia mD declarado o heredado por la clase D, anula de D otro método mA declarado en la clase A, si todo lo siguiente es cierto:

  • A es una superclase de D.
  • D no hereda mA (porque cruza los límites del paquete)
  • La firma de mD es una sub firma (§8.4.2) de la firma de mA.
  • Uno de los siguientes es cierto: [...]
    • mA se declara con acceso al paquete en el mismo paquete que D (en este caso), y D declara mD o mA es miembro de la superclase directa de D. [...]

PERO: la segunda pregunta sigue sin resolverse.


Entiendo que Dm() oculta Am() , pero un reparto a A debería exponer el método oculto m() , ¿es eso cierto?

No existe tal cosa como ocultar, por ejemplo, métodos (no estáticos). Aquí, es un ejemplo de shadowing . Un reparto a A en la mayoría de los lugares solo ayuda a resolver la ambigüedad (por ejemplo, cm() ya que puede referirse tanto a A#m C#m [que no es accesible desde a ]) que de lo contrario conduciría a un error de compilación.

¿O es que Dm() anula Am() a pesar de que Bm() y Cm() rompen la cadena de herencia?

bm() es una llamada ambigua porque tanto A#m B#m son aplicables si establece el factor de visibilidad a un lado. Lo mismo vale para cm() . ((A)b).m() y ((A)c).m() se refieren claramente a A#m que es accesible para la persona que llama.

((A)d).m() es más interesante: tanto A como D residen en el mismo paquete (por lo tanto, accesible [que es diferente de los dos casos anteriores]) y D hereda indirectamente A Durante el despacho dinámico, Java podrá llamar a D#m porque D#m realmente anula a A#m no hay razón para no llamarlo (a pesar del desorden en la ruta de herencia [recuerde que ni B#m ni C#m anula A#m debido al problema de visibilidad]).

Peor aún, el siguiente código muestra una anulación en efecto, ¿por qué?

No puedo explicar esto porque no es el comportamiento que esperaba.

Me atrevo a decir que el resultado de

((A)e).m(); ((A)f).m();

debe ser idéntico al resultado de

((D)e).m(); ((D)f).m();

cual es

D D

ya que no hay forma de acceder a los métodos privados del paquete en c desde a .


Este es un desafío para la mente, de hecho.

La siguiente respuesta aún no es completamente concluyente, pero mis resultados de echar un vistazo a esto. Tal vez al menos contribuye a encontrar una respuesta definitiva. Algunas partes de la pregunta ya han sido respondidas, por lo que me estoy centrando en el punto que todavía causa confusión y aún no se explica.

El caso crítico se puede resumir en cuatro clases:

package a; public class A { void m() { System.out.println("A"); } }

package a; import b.B; public class D extends B { @Override void m() { System.out.println("D"); } }

package b; import a.A; public class B extends A { void m() { System.out.println("B"); } }

package b; import a.D; public class E extends D { @Override void m() { System.out.println("E"); } }

(Tenga en cuenta que @Override anotaciones de @Override cuando sea posible; esperaba que esto ya pudiera dar una pista, pero aún no pude sacar conclusiones de eso ...)

Y la clase principal:

package a; import b.E; public class Main { public static void main(String[] args) { D d = new D(); E e = new E(); System.out.print("((A)d).m();"); ((A) d).m(); System.out.print("((A)e).m();"); ((A) e).m(); System.out.print("((D)d).m();"); ((D) d).m(); System.out.print("((D)e).m();"); ((D) e).m(); } }

La salida inesperada aquí es

((A)d).m();D ((A)e).m();E ((D)d).m();D ((D)e).m();D

Entonces

  • cuando se lanza un objeto de tipo D a A , el método del tipo D se llama
  • cuando se lanza un objeto de tipo E a A , el método del tipo E se llama (!)
  • cuando se lanza un objeto de tipo D a D , el método del tipo D se llama
  • cuando se lanza un objeto de tipo E a D , el método del tipo D se llama

Es fácil detectar el extraño aquí: naturalmente, uno esperaría que lanzar una E a A provoque que se llame al método de D , porque ese es el método "más alto" en el mismo paquete. El comportamiento observado no puede explicarse fácilmente desde el JLS, aunque uno tendría que volver a leerlo, cuidadosamente , para asegurarse de que no haya una razón sutil para eso.

Por curiosidad, eché un vistazo al bytecode generado de la clase Main . Esta es la salida completa de javap -c -v Main (las partes relevantes se desarrollarán a continuación):

public class a.Main minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Class #2 // a/Main #2 = Utf8 a/Main #3 = Class #4 // java/lang/Object #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Methodref #3.#9 // java/lang/Object."<init>":()V #9 = NameAndType #5:#6 // "<init>":()V #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 La/Main; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Class #17 // a/D #17 = Utf8 a/D #18 = Methodref #16.#9 // a/D."<init>":()V #19 = Class #20 // b/E #20 = Utf8 b/E #21 = Methodref #19.#9 // b/E."<init>":()V #22 = Fieldref #23.#25 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #24 // java/lang/System #24 = Utf8 java/lang/System #25 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = String #29 // ((A)d).m(); #29 = Utf8 ((A)d).m(); #30 = Methodref #31.#33 // java/io/PrintStream.print:(Ljava/lang/String;)V #31 = Class #32 // java/io/PrintStream #32 = Utf8 java/io/PrintStream #33 = NameAndType #34:#35 // print:(Ljava/lang/String;)V #34 = Utf8 print #35 = Utf8 (Ljava/lang/String;)V #36 = Methodref #37.#39 // a/A.m:()V #37 = Class #38 // a/A #38 = Utf8 a/A #39 = NameAndType #40:#6 // m:()V #40 = Utf8 m #41 = String #42 // ((A)e).m(); #42 = Utf8 ((A)e).m(); #43 = String #44 // ((D)d).m(); #44 = Utf8 ((D)d).m(); #45 = Methodref #16.#39 // a/D.m:()V #46 = String #47 // ((D)e).m(); #47 = Utf8 ((D)e).m(); #48 = Utf8 args #49 = Utf8 [Ljava/lang/String; #50 = Utf8 d #51 = Utf8 La/D; #52 = Utf8 e #53 = Utf8 Lb/E; #54 = Utf8 SourceFile #55 = Utf8 Main.java { public a.Main(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 5: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this La/Main; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #16 // class a/D 3: dup 4: invokespecial #18 // Method a/D."<init>":()V 7: astore_1 8: new #19 // class b/E 11: dup 12: invokespecial #21 // Method b/E."<init>":()V 15: astore_2 16: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 19: ldc #28 // String ((A)d).m(); 21: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 24: aload_1 25: invokevirtual #36 // Method a/A.m:()V 28: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 31: ldc #41 // String ((A)e).m(); 33: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 36: aload_2 37: invokevirtual #36 // Method a/A.m:()V 40: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 43: ldc #43 // String ((D)d).m(); 45: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 48: aload_1 49: invokevirtual #45 // Method a/D.m:()V 52: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 55: ldc #46 // String ((D)e).m(); 57: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 60: aload_2 61: invokevirtual #45 // Method a/D.m:()V 64: return LineNumberTable: line 9: 0 line 10: 8 line 11: 16 line 12: 28 line 14: 40 line 15: 52 line 16: 64 LocalVariableTable: Start Length Slot Name Signature 0 65 0 args [Ljava/lang/String; 8 57 1 d La/D; 16 49 2 e Lb/E; } SourceFile: "Main.java"

Lo interesante es la invocación de los métodos:

16: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 19: ldc #28 // String ((A)d).m(); 21: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 24: aload_1 25: invokevirtual #36 // Method a/A.m:()V 28: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 31: ldc #41 // String ((A)e).m(); 33: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 36: aload_2 37: invokevirtual #36 // Method a/A.m:()V 40: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 43: ldc #43 // String ((D)d).m(); 45: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 48: aload_1 49: invokevirtual #45 // Method a/D.m:()V 52: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 55: ldc #46 // String ((D)e).m(); 57: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V 60: aload_2 61: invokevirtual #45 // Method a/D.m:()V

El código de bytes se refiere explícitamente al método Am en las dos primeras llamadas, y se refiere explícitamente al método Dm en las segundas llamadas.

Una conclusión que extraigo de eso: ¡El culpable no es el compilador, sino el manejo de la instrucción invokevirtual de invokevirtual de la JVM!

La documentación de invokevirtual no contiene sorpresas, citando solo la parte relevante aquí:

Sea C la clase de objectref. El método real a invocar se selecciona mediante el siguiente procedimiento de búsqueda:

  1. Si C contiene una declaración para un método de instancia m que anula (§5.4.5) el método resuelto, entonces m es el método a invocar.

  2. De lo contrario, si C tiene una superclase, se realiza una búsqueda de una declaración de un método de instancia que anule el método resuelto, comenzando con la superclase directa de C y continuando con la superclase directa de esa clase, y así sucesivamente, hasta un método de anulación se encuentra o no existen más superclases. Si se encuentra un método de anulación, es el método a invocar.

  3. De lo contrario, si hay exactamente un método máximo específico (§5.4.3.3) en las superinterfaces de C que coincide con el nombre y descriptor del método resuelto y no es abstracto, entonces es el método a invocar.

Supuestamente solo sube la jerarquía, hasta que encuentra un método que ( es o) anula el método, con las anulaciones (§5.4.5) definidas como uno esperaría naturalmente.

Todavía no hay una razón obvia para el comportamiento observado.

Luego comencé a mirar lo que realmente sucede cuando se encuentra un invokevirtual , y profundizo en la función LinkResolver::resolve_method de OpenJDK, pero en ese momento, no estoy completamente seguro de si este es el lugar correcto para mirar, y Actualmente no puedo invertir más tiempo aquí ...

Quizás otros puedan continuar desde aquí, o encontrar inspiración para sus propias investigaciones. Al menos el hecho de que el compilador hace lo correcto, y la peculiaridad parece estar en el manejo de invokevirtual , podría ser un punto de partida.


Interesante pregunta. Lo comprobé en Oracle JDK 13 y Open JDK 13. Ambos dan el mismo resultado, exactamente como lo escribió. Pero este resultado contradice la especificación del lenguaje Java .

A diferencia de la clase D, que está en el mismo paquete que A, las clases B, C, E, F están en un paquete diferente y debido a la declaración privada del paquete de Am() no puede verlo ni anularlo. Para las clases B y C funciona como se especifica en JLS. Pero para las clases E y F no lo hace. Los casos con ((A)e).m() y ((A)f).m() son errores en la implementación del compilador de Java.

¿Cómo debería funcionar ((A)e).m() y ((A)f).m() ? Dado que Dm() anula Am() , esto debería ser válido también para todas sus subclases. Por lo tanto, tanto ((A)e).m() como ((A)f).m() deben ser lo mismo que ((D)e).m() y ((D)f).m() , significa que todos deberían llamar a Dm() .