Informe 2
Informe 2
Aunque Java no permite la herencia múltiple, sí permite que las clases implementen cualquier
número de interfaces. Una interfaz es un tipo de datos abstracto que define una lista de métodos
públicos abstractos que debe proporcionar cualquier clase que implemente la interfaz. Una
interfaz también puede incluir una lista de variables constantes y métodos predeterminados, que
cubriremos en esta sección. En Java, una interfaz se define con la palabra clave de interfaz,
análoga a la palabra clave de clase utilizada al definir una clase. Una clase invoca la interfaz
utilizando la palabra clave implements en su definición de clase. Consulte las Figuras 5.4 y 5.5 para
conocer el uso correcto de la sintaxis.
Como puede ver en este ejemplo, una interfaz no se declara una clase abstracta, aunque tiene
muchas de las mismas propiedades de la clase abstracta. Observe que se asumen los
modificadores de método en este ejemplo, abstracto y público. En otras palabras, ya sea que los
proporcione o no, el compilador los insertará automáticamente como parte de la definición del
método. Una clase puede implementar múltiples interfaces, cada una separada por una coma,
como en el siguiente ejemplo:
Nota: creando los abstract de WalksOnFourLegs, HasTrunk, Herbivore. Elephant no marca errores.
En el ejemplo, si alguna de las interfaces definiera métodos abstractos, se requeriría la clase
concreta Elephant para implementar esos métodos. Una novedad en Java 8 es la noción de
métodos de interfaz estáticos y predeterminados, que cubriremos al final de esta sección. .
Puede resultar útil pensar en una interfaz como un tipo especializado de clase abstracta, ya que
comparte muchas de las mismas propiedades y reglas que una clase abstracta. La siguiente es una
lista de reglas para crear una interfaz, muchas de las cuales debe reconocer como adaptaciones de
las reglas para definir clases abstractas.
4. Se supone que todas las interfaces de nivel superior tienen acceso público o predeterminado, y
deben incluir el modificador abstracto en su definición. Por lo tanto, marcar una interfaz como
privada, protegida o final desencadenará un error del compilador, ya que es incompatible con
estos supuestos.
5. Se supone que todos los métodos no predeterminados de una interfaz tienen modificadores
abstractos y públicos en su definición. Por lo tanto, marcar un método como privado, protegido o
final desencadenará errores del compilador, ya que son incompatibles con las palabras clave
abstractas y públicas.
La cuarta regla no se aplica a las interfaces internas, aunque las clases y las interfaces internas no
están dentro del alcance del examen OCA. Las tres primeras reglas son idénticas a las tres primeras
reglas para crear una clase abstracta. Imagina que tenemos una interfaz WalksOnTwoLegs,
definida de la siguiente manera:
Se compila sin problemas, ya que no se requieren interfaces para definir ningún método. Ahora
considere los siguientes dos ejemplos, que no se compilan:
La cuarta y quinta regla sobre "palabras clave asumidas" pueden ser nuevas para usted, pero debe
pensar en ellas de la misma manera que el compilador inserta un constructor sin argumentos
predeterminado o una declaración super () en su constructor. Puede proporcionar estos
modificadores usted mismo, aunque el compilador los insertará automáticamente si no lo hace.
Por ejemplo, las siguientes dos definiciones de interfaz son equivalentes, ya que el compilador las
convertirá en el segundo ejemplo:
Nota: los dos códigos funcionan, obviamente no pueden estar los dos.
Asegúrese de revisar el ejemplo anterior y comprender por qué cada una de las líneas no se
compila. Es probable que haya al menos una pregunta en el examen en la que una interfaz o un
método de interfaz utilice un modificador no válido.
Hay dos reglas de herencia que debe tener en cuenta al ampliar una interfaz:
1. Una interfaz que extiende otra interfaz, así como una clase abstracta que implementa una
interfaz, hereda todos los métodos abstractos como sus propios métodos abstractos.
2. La primera clase concreta que implementa una interfaz, o extiende una clase abstracta que
implementa una interfaz, debe proporcionar una implementación para todos los métodos
abstractos heredados.
Como una clase abstracta, una interfaz puede extenderse usando la palabra clave extend. De esta
manera, la nueva interfaz secundaria hereda todos los métodos abstractos de la interfaz principal.
Sin embargo, a diferencia de una clase abstracta, una interfaz puede extender múltiples interfaces.
Considere el siguiente ejemplo:
Cualquier clase que implemente la interfaz Seal debe proporcionar una implementación para
todos los métodos en las interfaces principales, en este caso, getTailLength () y
getNumberOfWhiskers ().
¿Qué pasa con una clase abstracta que implementa una interfaz? En este escenario, la clase
abstracta se trata de la misma manera que una interfaz que extiende otra interfaz. En otras
palabras, la clase abstracta hereda los métodos abstractos de la interfaz pero no es necesaria para
implementarlos. Dicho esto, como una clase abstracta, la primera clase concreta en extender la
clase abstracta debe implementar todos los métodos abstractos heredados de la interfaz.
Ilustramos esto en el siguiente ejemplo:
Nota: para que funciona hay que implementar todos los métodos abstract @Override
(getTailLength(), getNumberOfWhiskers())
En este ejemplo, vemos que HarborSeal es una clase abstracta y se compila sin problemas.
Cualquier clase que amplíe HarborSeal deberá implementar todos los métodos en la interfaz
HasTail y HasWhiskers. Alternativamente, LeopardSeal no es una clase abstracta, por lo que debe
implementar todos los métodos de interfaz dentro de su definición. En este ejemplo, LeopardSeal
no proporciona una implementación para los métodos de la interfaz, por lo que el código no se
compila.
A los creadores de exámenes les gustan las preguntas que combinan la terminología de la clase y
la interfaz. Aunque una clase puede implementar una interfaz, una clase no puede extender una
interfaz. Asimismo, mientras que una interfaz puede extender otra interfaz, una interfaz no puede
implementar otra interfaz. Los siguientes ejemplos ilustran estos principios:
El primer ejemplo muestra una clase
que intenta extender una interfaz que
no se compila. El segundo ejemplo
muestra una interfaz que intenta
extender una clase, que tampoco se
compila.
Tenga cuidado con los ejemplos del examen que combinan definiciones de clase e interfaz.
Asegúrese de que la única conexión entre una clase y una interfaz sea con la sintaxis de la interfaz
de implementación de la clase.
Dado que Java permite la herencia múltiple a través de interfaces, es posible que se pregunte qué
sucederá si define una clase que hereda de dos interfaces que contienen el mismo método
abstracto:
En este escenario, las firmas para los dos métodos de interfaz eatPlants () son compatibles, por lo
que puede definir una clase que cumpla con ambas interfaces simultáneamente:
¿Por qué funciona esto? Recuerde que los métodos de interfaz en este ejemplo son abstractos y
definen el "comportamiento" que debe tener la clase que implementa la interfaz. Si dos métodos
de interfaz abstracta tienen comportamientos idénticos, o en este caso la misma firma de método,
la creación de una clase que implementa uno de los dos métodos implementa automáticamente el
segundo método. De esta manera, los métodos de interfaz se consideran duplicados ya que tienen
la misma firma. ¿Qué sucede si los dos métodos tienen firmas diferentes? Si el nombre del método
es el mismo pero los parámetros de entrada son diferentes, no hay conflicto porque esto se
considera una sobrecarga del método. Demostramos este principio en el siguiente ejemplo:
En este ejemplo, vemos que la clase que implementa ambas interfaces debe proporcionar
implementos de ambas versiones de eatPlants (), ya que se consideran métodos separados. Tenga
en cuenta que no importa si el tipo de retorno de los dos métodos es el mismo o diferente, porque
el compilador trata estos métodos como independientes.
Desafortunadamente, si el nombre del método y los parámetros de entrada son los mismos pero
los tipos de retorno son diferentes entre los dos métodos, la clase o interfaz que intenta heredar
ambas interfaces no se compilará. La razón por la que el código no se compila tiene menos que ver
con las interfaces y más con el diseño de clases, como se discutió en el Capítulo 4. No es posible en
Java definir dos métodos en una clase con el mismo nombre y parámetros de entrada pero
diferentes tipos de retorno. Dadas las siguientes dos definiciones de interfaz para Herbivore y
Omnivore, el siguiente código no se compilará:
El código no se compila, ya que la clase define dos métodos con el mismo nombre y parámetros de
entrada pero diferentes tipos de retorno. Si elimináramos cualquiera de las definiciones de
eatPlants (), el compilador se detendría porque a la definición de Bear le faltaría uno de los
métodos requeridos. En otras palabras, no hay ninguna implementación de la clase Bear que
herede de Herbivore y Omnivore que el compilador acepte.
El compilador también lanzaría una excepción si define una interfaz o clase abstracta que hereda
de dos interfaces en conflicto, como se muestra aquí:
Incluso sin detalles de implementación, el compilador detecta el problema con la definición
abstracta y evita la compilación.
Con esto concluye nuestra discusión sobre los métodos de interfaz abstracta y la herencia
múltiple. Volveremos a esta discusión poco después de que presentemos los métodos de interfaz
predeterminados. Verá que las cosas funcionan de manera un poco diferente con los métodos de
interfaz predeterminados.
Ampliemos nuestro análisis de interfaces para incluir variables de interfaz, que se pueden definir
dentro de una interfaz. Al igual que los métodos de interfaz, se supone que las variables de
interfaz son públicas. Sin embargo, a diferencia de los métodos de interfaz, también se supone que
las variables de interfaz son estáticas y finales.
1. Se supone que las variables de interfaz son públicas, estáticas y finales. Por lo tanto, marcar una
variable como privada o protegida desencadenará un error del compilador, al igual que marcar
cualquier variable como abstracta.
2. El valor de una variable de interfaz debe establecerse cuando se declara, ya que está marcada
como final.
De esta manera, las variables de interfaz son esencialmente variables constantes definidas en el
nivel de interfaz. Debido a que se supone que son estáticos, se puede acceder a ellos incluso sin
una instancia de la interfaz. Al igual que en nuestro ejemplo anterior de CanFly, las siguientes dos
definiciones de interfaz son equivalentes, porque el compilador las convertirá automáticamente
en el segundo ejemplo:
Como vemos en este ejemplo, la compilación insertará automáticamente una final estática pública
en cualquier variable de interfaz constante que encuentre que faltan esos modificadores. También
tenga en cuenta que es una práctica de codificación común usar letras mayúsculas para denotar
valores constantes dentro de una clase. Con base en estas reglas, no debería sorprender que las
siguientes entradas no se compilen:
Con el lanzamiento de Java 8, los autores de Java han introducido un nuevo tipo de método en una
interfaz, denominado método predeterminado. Un método predeterminado es un método
definido dentro de una interfaz con la palabra clave predeterminada en la que se proporciona un
cuerpo de método. Contraste predeterminado
métodos con métodos "regulares" en una interfaz, que se supone que son abstractos y pueden no
tener un cuerpo de método.
Un método predeterminado dentro de una interfaz define un método abstracto con una
implementación predeterminada. De esta manera, las clases tienen la opción de anular el método
predeterminado si es necesario, pero no es necesario que lo hagan. Si la clase no anula el método,
se utilizará la implementación predeterminada. De esta manera, la definición del método es
concreta, no abstracta.
El propósito de agregar métodos predeterminados al lenguaje Java fue en parte ayudar con el
desarrollo de código y la compatibilidad con versiones anteriores. Imagine que tiene una interfaz
compartida entre docenas o incluso cientos de usuarios a los que le gustaría agregar un nuevo
método. Si solo actualiza la interfaz con el nuevo método, la implementación se interrumpiría
entre todos sus suscriptores, quienes luego se verían obligados a actualizar su código. En la
práctica, esto podría incluso disuadirlo de realizar el cambio por completo. Sin embargo, al
proporcionar una implementación predeterminada del método, la interfaz se vuelve
retrocompatible con el código base existente, al mismo tiempo que brinda a las personas que
desean usar el nuevo método la opción de anularlo.
Este ejemplo define dos métodos de interfaz, uno es un método abstracto normal y el otro es un
método predeterminado. Tenga en cuenta que se supone que ambos métodos son públicos, ya
que todos los métodos de una interfaz son públicos. El primer método termina con un punto y
coma y no proporciona un cuerpo, mientras que el segundo método predeterminado proporciona
un cuerpo. Cualquier clase que implemente IsWarmBlooded puede depender de la
implementación predeterminada de getTemperature () o anular el método y crear su propia
versión.
Las siguientes son las reglas de método de interfaz predeterminadas con las que debe estar
familiarizado:
1. Un método predeterminado solo puede declararse dentro de una interfaz y no dentro de una
clase o clase abstracta.
3. No se supone que un método predeterminado sea estático, final o abstracto, ya que puede ser
utilizado o anulado por una clase que implemente la interfaz.
4. Como todos los métodos en una interfaz, se supone que un método predeterminado es público
y no se compilará si está marcado como privado o protegido.
La primera regla debería brindarle cierta comodidad, ya que solo verá los métodos
predeterminados en las interfaces. Si los ve en una clase del examen, asuma que el código no se
compilará. La segunda regla solo denota sintaxis, ya que los métodos predeterminados deben usar
la palabra clave predeterminada. Por ejemplo, los siguientes fragmentos de código no se
compilarán:
En este ejemplo, el primer método, eatMeat (), no se compila porque está marcado como
predeterminado pero no proporciona un cuerpo de método. El segundo método, getRequiredFood
Amount (), tampoco se compila porque proporciona un cuerpo de método pero no está marcado
con la palabra clave predeterminada.
A diferencia de las variables de interfaz, que se asumen como miembros de clase estáticos, los
métodos predeterminados no se pueden marcar como estáticos y requieren una instancia de la
clase que implementa la interfaz para ser invocada. Tampoco se pueden marcar como finales o
abstractos, porque se permite que se anulen en subclases, pero no se requiere que se anulen.
Cuando una interfaz extiende otra interfaz que contiene un método predeterminado, puede optar
por ignorar el método predeterminado, en cuyo caso se utilizará la implementación
predeterminada del método. Alternativamente, la interfaz puede anular el de! nición del método
predeterminado utilizando las reglas estándar para anular el método, como no limitar la
accesibilidad del método y utilizar retornos covariantes. Finalmente, la interfaz puede volver a
declarar el método como abstracto, requiriendo que las clases que implementan la nueva interfaz
proporcionen explícitamente un cuerpo de método. Se aplican opciones análogas para una clase
abstracta que implementa una interfaz.
Por ejemplo, la siguiente clase anula un método de interfaz predeterminado y vuelve a declarar un
segundo método de interfaz como abstracto:
En este ejemplo, la primera interfaz, HasFins, define tres métodos predeterminados:
Debido a que los métodos predeterminados son nuevos en Java 8, probablemente habrá algunas
preguntas en el examen sobre ellos, aunque probablemente no serán más difíciles que en el
ejemplo anterior.
Es posible que se haya dado cuenta de que al permitir métodos predeterminados en las interfaces,
junto con el hecho de que una clase puede implementar múltiples interfaces, Java esencialmente
ha abierto la puerta a múltiples problemas de herencia. Por ejemplo, ¿qué valor tendría el
siguiente código de salida?
En este ejemplo, Cat hereda los dos métodos predeterminados para getSpeed (), entonces, ¿cuál
usa? Dado que Walk and Run se consideran hermanos en términos de cómo se usan en la clase
Cat, no está claro si el código debe generar 5 o 10. La respuesta es que el código no genera ningún
valor, no se compila.
Si una clase implementa dos interfaces que tienen métodos predeterminados con el mismo
nombre y firma, el compilador arrojará un error. Sin embargo, hay una excepción a esta regla: si la
subclase anula los métodos predeterminados duplicados, el código se compilará sin problemas: se
ha eliminado la ambigüedad sobre qué versión del método llamar. Por ejemplo, el siguiente modi!
La implementación ed de Cat compilará y generará 1:
Puede ver que tener una clase que implementa o hereda dos métodos predeterminados
duplicados obliga a la clase a implementar una nueva versión del método, o el código no se
compilará. Esta regla es válida incluso para clases abstractas que implementan múltiples
interfaces, porque el método predeterminado se puede llamar en un método concreto dentro de
la clase abstracta.
Métodos de interfaz estática (Static Interface Methods).
Java 8 ahora también incluye soporte para métodos estáticos dentro de las interfaces. Estos
métodos se definen explícitamente con la palabra clave estática y funcionan de manera casi
idéntica a los métodos estáticos definidos en clases, como se discutió en el Capítulo 4. De hecho,
en realidad solo hay una distinción
entre un método estático en una clase y una interfaz. Un método estático definido en una interfaz
no se hereda en ninguna clase que implemente la interfaz.
Estas son las reglas del método de interfaz estática con las que debe estar familiarizado:
1. Como todos los métodos en una interfaz, se supone que un método estático es público y no se
compilará si está marcado como privado o protegido.
2. Para hacer referencia al método estático, se debe utilizar una referencia al nombre de la
interfaz.
El método getJumpHeight () funciona como un método estático definido en una clase. En otras
palabras, se puede acceder sin una instancia de la clase usando la sintaxis Hop.getJumpHeight ().
Además, tenga en cuenta que el compilador insertará automáticamente el modi de acceso. er
público ya que se supone que todos los métodos en las interfaces son públicos.
Como puede ver, sin una referencia explícita al nombre de la interfaz, el código no se compilará,
aunque Bunny implemente Hop. De esta manera, los métodos de la interfaz estática no son
heredados por una clase que implementa la interfaz. La siguiente versión modificada del código
resuelve el problema con una referencia al nombre de la interfaz Hop:
De ello se deduce, entonces, que una clase que implementa dos interfaces que contienen métodos
estáticos con la misma firma aún se compilará en tiempo de ejecución, porque la subclase no
hereda los métodos estáticos y se debe acceder a ellos con una referencia al nombre de la
interfaz.
Compare esto con el comportamiento que vio para los métodos de interfaz predeterminados en la
sección anterior: el código se compilaría si la subclase anulara los métodos predeterminados y, de
lo contrario, no se compilaría. Puede ver que los métodos de interfaz estática no tienen los
mismos problemas y reglas de herencia múltiple que los métodos de interfaz predeterminados.
Lo más importante a tener en cuenta sobre este ejemplo es que solo se crea y se hace referencia a
un objeto, Lemur. La capacidad de una instancia de Lemur para pasar como una instancia de una
interfaz que implementa, HasTail, así como una instancia de una de sus superclases, Primate, es la
naturaleza del polimorfismo.
Una vez que al objeto se le ha asignado un nuevo tipo de referencia, solo los métodos y variables
disponibles para ese tipo de referencia son invocables en el objeto sin una conversión explícita.
Por ejemplo, los siguientes fragmentos de código no se compilarán:
En este ejemplo, la referencia hasTail tiene acceso directo solo a los métodos definidos con la
interfaz HasTail; por lo tanto, no sabe que la variable edad es parte del objeto. Del mismo modo, el
primate de referencia solo tiene acceso a los métodos definidos en la clase Primate y no tiene
acceso directo al método isTailStriped ().
Objeto frente a referencia (Object vs. Reference):
En Java, se accede a todos los objetos por referencia, por lo que, como desarrollador, nunca tiene
acceso directo al objeto en sí. Sin embargo, conceptualmente, debe considerar el objeto como la
entidad que existe en la memoria, asignada por el entorno de ejecución de Java.
Independientemente del tipo de referencia que tenga para el objeto en la memoria, el objeto en sí
no cambia. Por ejemplo, dado que todos los objetos heredan java.lang.Object, todos pueden
reasignarse a java.lang.Object, como se muestra en el siguiente ejemplo:
Aunque al objeto Lemur se le ha asignado una referencia con un tipo diferente, el objeto en sí no
ha cambiado y todavía existe como un objeto Lemur en la memoria. Lo que ha cambiado,
entonces, es nuestra capacidad para acceder a métodos dentro de la clase Lemur con la referencia
lemurAsObject. Sin un envío explícito a Lemur, como verá en la siguiente sección, ya no tendremos
acceso a las propiedades de Lemur del objeto.
1. El tipo de objeto determina qué propiedades existen dentro del objeto en la memoria.
2. El tipo de referencia al objeto determina qué métodos y variables son accesibles para el
programa Java.
Por lo tanto, se deduce que cambiar con éxito una referencia de un objeto a un nuevo tipo de
referencia puede darle acceso a nuevas propiedades del objeto, pero esas propiedades existían
antes de que ocurriera el cambio de referencia.
Ilustremos esta propiedad usando el ejemplo anterior en la Figura 5.6. Como puede ver en la
figura, el mismo objeto existe en la memoria independientemente de la referencia que lo apunte.
Dependiendo del tipo de referencia, es posible que solo tengamos acceso a ciertos métodos. Para
Por ejemplo, la referencia hasTail tiene acceso al método isTailStriped () pero no tiene acceso a la
variable age definida en la clase Lemur. Como aprenderá en la siguiente sección, es posible
reclamar el acceso a la variable age mediante la conversión explícita de la referencia hasTail a una
referencia de tipo Lemur.
F I GU R A 5.6
En el ejemplo anterior, creamos una instancia única de un objeto Lemur y accedimos a ella a
través de referencias de superclase y de interfaz. Sin embargo, una vez que cambiamos el tipo de
referencia, perdimos el acceso a métodos más específicos definidos en la subclase que aún existen
dentro del objeto. Podemos recuperar esas referencias devolviendo el objeto a la subclase
específica de la que proviene:
En este ejemplo, primero intentamos convertir la referencia de primate de nuevo en una
referencia de lémur, lemur2, sin una conversión explícita. El resultado es que el código no se
compilará. Sin embargo, en el segundo ejemplo, convertimos explícitamente el objeto en una
subclase del objeto Primate y obtenemos acceso a todos los métodos disponibles para la clase
Lemur.
A continuación, se muestran algunas reglas básicas que se deben tener en cuenta al convertir
variables:
2. La conversión de un objeto de una superclase a una subclase requiere una conversión explícita.
4. Incluso cuando el código se compila sin problemas, se puede lanzar una excepción en tiempo de
ejecución si el objeto que se está convirtiendo no es realmente una instancia de esa clase.
La tercera regla es importante; el examen puede intentar engañarte con un elenco que el
compilador no permite. Por ejemplo, pudimos lanzar una referencia de Primate a una referencia
de Lemur, porque Lemur es una subclase de Primate y, por lo tanto, está relacionada.
El casting no está exento de limitaciones. Aunque dos clases comparten una jerarquía relacionada,
eso no significa que una instancia de una pueda convertirse automáticamente en otra. Aquí tienes
un ejemplo:
Este código crea una instancia de Rodent y luego intenta convertirlo en una subclase de Rodent,
Capybara. Aunque este código se compilará sin problemas, lanzará una ClassCastException en
tiempo de ejecución ya que el objeto al que se hace referencia no es una instancia de la clase
Capybara. Lo que hay que tener en cuenta en este ejemplo es que el objeto que se creó no está
relacionado con la clase Capybara de ninguna manera.
La característica más importante del polimorfismo, y una de las principales razones por las que
tenemos una estructura de clases, es admitir métodos virtuales. Un método virtual es un método
en el que la implementación específica no se determina hasta el tiempo de ejecución. De hecho,
todos los métodos Java no finales, no estáticos y no privados se consideran métodos virtuales, ya
que cualquiera de ellos puede anularse en tiempo de ejecución. Lo que hace que un método
virtual sea especial en Java es que si llama a un método en un objeto que anula un método,
obtiene el método anulado, incluso si la llamada al método está en una referencia principal o
dentro de la clase principal.
En otras palabras, aunque la clase padre Bird define su propia versión de getName () y no sabe
nada sobre la clase Peacock durante el tiempo de compilación, en el tiempo de ejecución la
instancia usa la versión anulada del método, como se define en la instancia. del objeto.
Enfatizamos este punto usando una referencia a la clase Bird en el método main (), aunque el
resultado habría sido el mismo si se hubiera usado una referencia a Peacock.
Una de las aplicaciones más útiles del polimorfismo es la capacidad de pasar instancias de una
subclase o interfaz a un método. Por ejemplo, puedes de! ne un método que toma una instancia
de una interfaz como parámetro. De esta manera, cualquier clase que implemente la interfaz se
puede pasar al método. Dado que está transmitiendo de un subtipo a un supertipo, no se requiere
una transmisión explícita. Esta propiedad se conoce como parámetros polimórficos de un método
y la demostramos en el siguiente ejemplo:
Centrémonos en el método de alimentación (reptiles reptiles) en este ejemplo. Como puede ver,
ese método pudo manejar instancias de Alligator y Crocodile sin problemas, porque ambas son
subclases de la clase Reptile. También pudo aceptar un tipo de clase Reptile coincidente. Si
hubiéramos intentado pasar una clase no relacionada, como las clases Rodent o Capybara
previamente definidas, o una superclase como java.lang.Object, al método feed (), el código no se
habría compilado.
Concluyamos este capítulo volviendo a las últimas tres reglas de anulación de métodos para
demostrar cómo el polimorfismo requiere que se incluyan como parte de la especificación de Java.
Verá que sin estas reglas en su lugar, es fácil construir un ejemplo con polimorfismo en Java.
La primera regla es que un método anulado debe ser al menos tan accesible como el método que
está anulando. Supongamos que esta regla no es necesaria y consideremos el siguiente ejemplo: