"Te voy a hablar de los « modificadores de acceso ». Ya te hablé de ellos una vez, pero la repetición es un pilar del aprendizaje."

Puedes controlar el acceso (visibilidad) que tienen otras clases a los métodos y variables de tu clase. Un modificador de acceso responde a la pregunta «¿Quién puede acceder a este método/variable?». Puede especificar solo un modificador para cada método o variable.

1) Modificador « público ».

Se puede acceder a una variable, método o clase marcada con el modificador público desde cualquier parte del programa. Este es el mayor grado de apertura: no hay restricciones.

2) Modificador « privado ».

Solo se puede acceder a una variable, método o clase marcada con el modificador privado en la clase donde se declara. El método o la variable marcada se oculta de todas las demás clases. Este es el grado más alto de privacidad: accesible solo por su clase. Dichos métodos no se heredan y no se pueden invalidar. Además, no se puede acceder a ellos en una clase descendiente.

3)  « Modificador por defecto ».

Si una variable o método no está marcado con ningún modificador, se considera que está marcado con el modificador "predeterminado". Las variables y los métodos con este modificador son visibles para todas las clases del paquete donde se declaran, y solo para esas clases. Este modificador también se denomina acceso " paquete " o " paquete privado ", lo que sugiere el hecho de que el acceso a variables y métodos está abierto a todo el paquete que contiene la clase.

4) Modificador « protegido ».

Este nivel de acceso es ligeramente más amplio que el paquete . Se puede acceder a una variable, método o clase marcada con el modificador protected desde su paquete (como "paquete") y desde todas las clases heredadas.

Esta tabla lo explica todo:

Tipo de visibilidad Palabra clave Acceso
Tu clase Tu equipaje descendiente Todas las clases
Privado privado No No No
Paquete (sin modificador) No No
Protegido protegido No
Público público

Hay una forma de recordar fácilmente esta tabla. Imagina que estás escribiendo un testamento. Estás dividiendo todas tus cosas en cuatro categorías. ¿Quién puede usar tus cosas?

quien tiene acceso modificador Ejemplo
solo  yo privado diario personal
Familia (sin modificador) Fotos de familia
Familia y herederos protegido finca familiar
Todos público Memorias

"Es muy parecido a imaginar que las clases en el mismo paquete son parte de una familia".

"También quiero contarles algunos matices interesantes sobre los métodos de anulación".

1) Implementación implícita de un método abstracto.

Digamos que tienes el siguiente código:

Código
class Cat
{
 public String getName()
 {
  return "Oscar";
 }
}

Y decidió crear una clase Tiger que hereda esta clase y agregar una interfaz a la nueva clase

Código
class Cat
{
 public String getName()
 {
   return "Oscar";
 }
}
interface HasName
{
 String getName();
 int getWeight();
}
class Tiger extends Cat implements HasName
{
 public int getWeight()
 {
  return 115;
 }

}

Si solo implementa todos los métodos faltantes que IntelliJ IDEA le indica que implemente, más adelante podría terminar dedicando mucho tiempo a buscar un error.

Resulta que la clase Tiger tiene un método getName heredado de Cat, que se tomará como la implementación del método getName para la interfaz HasName.

"No veo nada terrible en eso".

"No es tan malo, es un lugar probable para que se produzcan errores".

Pero puede ser aún peor:

Código
interface HasWeight
{
 int getValue();
}
interface HasSize
{
 int getValue();
}
class Tiger extends Cat implements HasWeight, HasSize
{
 public int getValue()
 {
  return 115;
 }
}

Resulta que no siempre se puede heredar de múltiples interfaces. Más precisamente, puede heredarlas, pero no puede implementarlas correctamente. Mira el ejemplo. Ambas interfaces requieren que implementes el método getValue(), pero no está claro qué debería devolver: ¿el peso o el tamaño? Es bastante desagradable tener que lidiar con esto.

"Estoy de acuerdo. Quieres implementar un método, pero no puedes. Ya heredaste un método con el mismo nombre de la clase base. Está roto".

"Pero hay buenas noticias".

2) Ampliar la visibilidad. Cuando hereda un tipo, puede expandir la visibilidad de un método. Así es como se ve:

codigo Java Descripción
class Cat
{
 protected String getName()
 {
  return "Oscar";
 }
}
class Tiger extends Cat
{
 public String getName()
 {
  return "Oscar Tiggerman";
 }
}
Hemos ampliado la visibilidad del método de protecteda public.
Código Por qué esto es «legal»
public static void main(String[] args)
{
 Cat cat = new Cat();
 cat.getName();
}
Todo es estupendo. Aquí ni siquiera sabemos que la visibilidad se ha ampliado en una clase descendiente.
public static void main(String[] args)
{
 Tiger tiger = new Tiger();
 tiger.getName();
}
Aquí llamamos al método cuya visibilidad se ha ampliado.

Si esto no fuera posible, siempre podríamos declarar un método en Tiger:
public String getPublicName()
{
super.getName(); // llamar al método protegido
}

En otras palabras, no estamos hablando de ninguna violación de seguridad.

public static void main(String[] args)
{
 Cat catTiger = new Tiger();
 catTiger.getName();
}
Si se cumplen todas las condiciones necesarias para llamar a un método en una clase base ( Cat ), entonces ciertamente se cumplen para llamar al método en el tipo descendiente ( Tiger ). Porque las restricciones en la llamada al método eran débiles, no fuertes.

"No estoy seguro de haberlo entendido completamente, pero recordaré que esto es posible".

3) Restringir el tipo de devolución.

En un método anulado, podemos cambiar el tipo de retorno a un tipo de referencia reducido.

codigo Java Descripción
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Tiger getMyParent()
 {
  return (Tiger) this.parent;
 }
}
Anulamos el método getMyParenty ahora devuelve un Tigerobjeto.
Código Por qué esto es «legal»
public static void main(String[] args)
{
 Cat parent = new Cat();

 Cat me = new Cat();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
Todo es estupendo. Aquí ni siquiera sabemos que el tipo de devolución del método getMyParent se ha ampliado en la clase descendiente.

Cómo funcionaba y funciona el «código antiguo».

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Tiger me = new Tiger();
 me.setMyParent(parent);
 Tiger myParent = me.getMyParent();
}
Aquí llamamos al método cuyo tipo de devolución se ha reducido.

Si esto no fuera posible, siempre podríamos declarar un método en Tiger:
public Tiger getMyTigerParent()
{
return (Tiger) this.parent;
}

En otras palabras, no hay violaciones de seguridad y/o violaciones de conversión de tipos.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
Y todo funciona bien aquí, aunque ampliamos el tipo de variables a la clase base (Cat).

Debido a la anulación, se llama al método setMyParent correcto.

Y no hay nada de qué preocuparse al llamar al método getMyParent , porque el valor de retorno, aunque de la clase Tiger, todavía se puede asignar a la variable myParent de la clase base (Cat) sin ningún problema.

Los objetos Tiger se pueden almacenar de forma segura tanto en variables Tiger como en variables Cat.

"Sí. Lo tengo. Al anular métodos, debe tener en cuenta cómo funciona todo esto si pasamos nuestros objetos a un código que solo puede manejar la clase base y no sabe nada sobre nuestra clase" .

"¡Exactamente! Entonces la gran pregunta es ¿por qué no podemos reducir el tipo de valor devuelto cuando anulamos un método?"

"Es obvio que en este caso el código de la clase base dejaría de funcionar:"

codigo Java Explicación del problema
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Object getMyParent()
 {
  if (this.parent != null)
   return this.parent;
  else
   return "I'm an orphan";
 }
}
Sobrecargamos el método getMyParent y redujimos el tipo de su valor de retorno.

Todo está bien aquí.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 Cat myParent = me.getMyParent();
}
Entonces este código dejará de funcionar.

El método getMyParent puede devolver cualquier instancia de un objeto, porque en realidad se llama en un objeto Tiger.

Y no tenemos un cheque antes de la asignación. Por lo tanto, es muy posible que la variable myParent de tipo Cat almacene una referencia de cadena.

"¡Maravilloso ejemplo, Amigo!"

En Java, antes de llamar a un método, no se verifica si el objeto tiene dicho método. Todas las comprobaciones se realizan en tiempo de ejecución. Y una llamada [hipotética] a un método faltante probablemente haría que el programa intentara ejecutar un código de bytes inexistente. En última instancia, esto conduciría a un error fatal y el sistema operativo cerraría el programa a la fuerza.

"Vaya. Ahora lo sé".