« Je vais te parler des 'modificateurs d'accès'. Je t'ai déjà dit un mot à leur sujet une fois avant, mais la répétition est mère d'apprentissage. »

Tu peux contrôler l'accès (la visibilité) que les autres classes ont pour les méthodes et les variables de ta classe. Un modificateur d'accès répond à la question 'Qui peut accéder à cette méthode/variable ?'. Tu peux spécifier un seul modificateur pour chaque méthode ou variable.

1) Le modificateur 'public'.

Une variable, méthode ou classe marquée avec le modificateur public est accessible depuis n'importe où dans le programme. C'est le plus haut degré d'accès : aucune limitation.

2) Le modificateur 'private'.

Une variable, méthode ou classe marquée avec le modificateur private est uniquement accessible depuis la classe dans laquelle elle est déclarée. La méthode ou variable marquée est cachée de toutes les autres classes. C'est le degré de confidentialité le plus élevé : l'accès est uniquement possible depuis sa propre classe. Ces méthodes ne sont pas héritées et ne peuvent pas être remplacées. De plus, elles ne sont pas accessibles dans les classes descendantes.

3) Le 'modificateur par défaut'.

Si une variable ou méthode n'est pas marquée avec un modificateur, elle est considérée comme marquée avec le modificateur « par défaut ». Les variables et méthodes avec ce modificateur sont visibles pour toutes les classes du package où elles sont déclarées, et seulement pour ces classes. Ce modificateur est aussi appelé accès « package » ou « package private », faisant allusion au fait que l'accès aux variables et méthodes est ouvert à l'ensemble du package qui contient la classe.

4) Le modificateur 'protected'.

Ce niveau d'accès est un peu plus large que package. Une variable, méthode ou classe marquée par le modificateur protected est accessible depuis son package (comme pour l'accès « package ») et toutes les classes héritées.

Ce tableau explique tout :

Type de visibilité Mot-clé Accès
Ta classe Ton package Descendants Toutes les classes
Privé private Oui Non Non Non
Package (aucun modificateur) Oui Oui Non Non
Protégé protected Oui Oui Oui Non
Public public Oui Oui Oui Oui

Il y a un moyen de retenir facilement de ce tableau. Imagine que tu écris ton testament. Tu répartis tous tes biens dans quatre catégories. Qui est autorisé à utiliser tes biens ?

Qui a accès Modificateur Exemple
Seulement  moi private Journal intime
Famille (aucun modificateur) Photos de famille
Famille et héritiers protected Biens immobiliers de la famille
Tout le monde public Mémoires

« Ça revient un peu à imaginer que les classes dans le même package font partie d'une famille. »

« Je voudrais aussi t'enseigner quelques nuances intéressantes sur le remplacement de méthodes. »

1) Implémentation implicite d'une méthode abstraite.

Disons que tu as le code suivant :

Code
class Cat
{
 public String getName()
 {
  return "Oscar";
 }
}

Et que tu as décidé de créer une classe Tiger qui hérite de cette classe, et d'ajouter une interface à la nouvelle classe

Code
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 tu te contentes d'implémenter toutes les méthodes manquantes qu'IntelliJ IDEA te demande d'implémenter, plus tard, tu pourrais finir par passer beaucoup de temps à chercher des bogues.

Il se trouve que la classe Tiger a une méthode getName héritée de Cat, qui sera considérée comme l'implémentation de la méthode getName pour l'interface HasName.

« Je ne vois pas de gros problème à cela. »

« Ce n'est pas trop terrible, mais c'est un endroit susceptible d'être pris d'assaut par les erreurs. »

Mais ça peut être encore pire :

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

Il se trouve que tu ne peux pas toujours hériter de plusieurs interfaces. Plus précisément, tu peux en hériter, mais tu ne peux pas les implémenter correctement. Observe cet exemple. Les deux interfaces exigent que tu implémentes la méthode getValue(), mais ce qu'elle doit renvoyer n'est pas clair : le poids ou la taille ? C'est tout à fait désagréable d'avoir à traiter ce genre de problèmes.

« Bien dit. Tu souhaites implémenter une méthode, mais tu ne peux pas. Tu as déjà hérité d'une méthode avec le même nom à partir de la classe de base. Ça ne peut pas marcher. »

Mais il y a une bonne nouvelle. »

2) Extension de visibilité. Lorsque tu hérites d'un type, tu peux étendre la visibilité d'une méthode. Voici à quoi ça ressemble :

Code Java Description
class Cat
{
 protected String getName()
 {
  return "Oscar";
 }
}
class Tiger extends Cat
{
 public String getName()
 {
  return "Oscar Tiggerman";
 }
}
Nous avons élargi la visibilité de la méthode de protected à public.
Code Pourquoi c'est « légal »
public static void main(String[] args)
{
 Cat cat = new Cat();
 cat.getName();
}
Tout se passe très bien. Ici, nous ne savons même pas que la visibilité a été étendue dans une classe descendante.
public static void main(String[] args)
{
 Tiger tiger = new Tiger();
 tiger.getName();
}
Ici, nous appelons la méthode dont la visibilité a été étendue.

Si ce n'était pas possible, nous pourrions toujours déclarer une méthode dans Tiger :
public String getPublicName()
{
super.getName(); //appel à la méthode protégée
}

En d'autres termes, nous ne parlons pas de violation de sécurité.

public static void main(String[] args)
{
 Cat catTiger = new Tiger();
 catTiger.getName();
}
Si toutes les conditions nécessaires pour appeler une méthode dans une classe de base (Cat) sont satisfaites, elles sont forcément satisfaites pour appeler la méthode sur le type descendant (Tiger). Et ce car les restrictions sur l'appel de la méthode étaient faibles, pas fortes.

« Je ne suis pas sûr d'avoir complètement compris, mais je retiendrai que c'est possible. »

3) Réduction du type de retour.

Dans une méthode remplacée, nous pouvons changer le type de retour sur un type de référence plus étroit.

Code Java Description
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;
 }
}
Nous avons remplacé la méthode getMyParent, et maintenant elle renvoie un objet Tiger.
Code Pourquoi c'est « légal »
public static void main(String[] args)
{
 Cat parent = new Cat();

 Cat me = new Cat();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
Tout se passe très bien. Ici, nous ne savons même pas que le type de retour de la méthode getMyParent a été élargi dans la classe descendante.

Comment l'ancien code fonctionnait et fonctionne encore.

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

 Tiger me = new Tiger();
 me.setMyParent(parent);
 Tiger myParent = me.getMyParent();
}
Ici, nous appelons la méthode dont le type de retour a été élargi.

Si ce n'était pas possible, nous pourrions toujours déclarer une méthode dans Tiger :
public Tiger getMyTigerParent()
{
return (Tiger) this.parent;
}

En d'autres termes, il n'y a pas de violations de sécurité et/ou de conversion de types.

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

 Cat me = new Tiger();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
Et tout fonctionne bien ici, même si nous avons réduit le type des variables sur la classe de base (Cat).

Grâce au remplacement, la bonne méthode setMyParent est appelée.

Et il n'y a rien à craindre lorsque nous appelons la méthode getMyParent, car la valeur de retour, bien que de la classe Tiger, peut toujours être affectée à la variable myParent de la classe de base (Cat) sans aucun problème.

Les objets Tiger peuvent être stockés en toute sécurité aussi bien dans des variables Tiger que dans des variables Cat.

« Oui. Compris. Quand tu remplaces des méthodes, tu dois avoir conscience de la façon dont tout cela fonctionne si nous passons nos objets à du code qui peut seulement gérer la classe de base et ne sait rien au sujet de notre classe. »

« Exactement ! Alors la grande question est : pourquoi ne pouvons-nous pas réduire le type de la valeur de retour lors du remplacement d'une méthode ? »

« Il est évident que dans ce cas le code de la classe de base cesserait de fonctionner : »

Code Java Explication du problème
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";
 }
}
Nous avons surchargé la méthode getMyParent et réduit le type de sa valeur de retour.

Tout va bien ici.

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

 Cat me = new Tiger();
 Cat myParent = me.getMyParent();
}
Alors ce code cessera de fonctionner.

La méthode getMyParent peut renvoyer une instance d'un Object, car elle est en fait appelée sur un objet Tiger.

Et nous n'avons pas de vérification avant l'affectation. Ainsi, il est tout à fait possible que la variable de type myParent de type Cat enregistre une référence String.

« Quel bel exemple, Amigo ! »

En Java, avant qu'une méthode soit appelée, il n'y a pas de contrôle pour voir si l'objet a une telle méthode. Tous les contrôles ont lieu lors de l'exécution. Et un appel à une méthode [potentiellement] manquante conduira très probablement le programme à tenter d'exécuter du bytecode inexistant. Cela conduirait au final à une erreur fatale, et le système d'exploitation forcerait alors la fermeture du programme.

« Ouah. Je suis bien content de le savoir. »