Aller au contenu principal

Objets

Définition : l'objet est l'entité de base de l'exécution. À chaque objet est attribuée une zone mémoire différente.

Plusieurs objets mettant en œuvre la même encapsulation, il est nécessaire d'identifier de manière unique un objet.

Définition : l'identité d'un objet est assurée par la zone mémoire attribuée à cet objet. En Java, cette identité est manipulée à travers une référence (l'adresse elle-même n'est pas accessible au programme).

La zone mémoire d'un objet contient la valeur de ses attributs (suivant la description de l'encapsulation). Un objet encapsule les méthodes. En pratique, tous les objets mettant en œuvre la même encapsulation partagent le code des méthodes. Ce code commun est exécuté avec l'identité (la zone mémoire) de l'objet sur lequel est déclenchée une action.

Un langage orienté objet doit fournir des mécanismes pour :

  • créer un objet : instanciation ;
  • déclencher une action sur un objet : envoi de messages ;
  • assurer la séparation utilisation / réalisation : masquage d'information.

Instanciation​

Définition : l'instanciation désigne l'opération de créer un objet à partir de la description de l'encapsulation. Elle attribue une zone mémoire à l'objet et initialise ses attributs.

Définition : dans un langage à classe, un objet est instancié à partir d'une classe. Un objet est souvent désigné par le terme instance.

Pour respecter l'encapsulation, la classe fournit le code d'initialisation par l'intermédiaire d'un traitement appelé constructeur. Comme une méthode, un constructeur admet une liste de paramètres. En Java, l'instanciation est une expression construite à partir de l'opérateur new et d'un constructeur de la classe.

Le constructeur dans la classe PorteCharniere :

class PorteCharniere {
// La définition d'un constructeur
PorteCharniere() {
estFerme = false;
// ...
}

// La définition des variables / attributs
boolean estFerme;

// La définition des méthodes
void fermer() {
// ...
estFerme = true;
}

void ouvrir() {
// ...
estFerme = false;
}

boolean estFerme() {
return estFerme;
}
}

Affectation dans une variable​

Comme habituellement dans le code, les variables / paramètres (de méthode) vont permettre de manipuler les objets / instances. Dans un langage typé, le compilateur vérifie la compatibilité des types entre le type de la variable / paramètre et l'instance affectée à cette variable. Une variable de type PorteCharniere contient une instance de la classe PorteCharniere.

PorteCharniere sesame;
sesame = new PorteCharniere();
PorteCharniere paradis = new PorteCharniere();
sesame = new PorteCharniere();
paradis = sesame;
Telecommande maTelecommande = new Telecommande(3);
maTelecommande = sesame; // erreur : types incompatibles

En Java, les variables / paramètres correspondant à des objets sont des références. La variable paradis contient une référence vers un objet. La libération de la zone mémoire d'un objet est prise en charge par le ramasse-miettes (garbage collector) de la machine virtuelle Java (JVM).

  • Les variables des types de base (comme int, float, boolean, byte, char) contiennent des valeurs. Les comportements entre type de base et type référence sont différents dans l'affectation et le passage de paramètres.
  • Les chaînes de caractères littérales (par exemple "abc") sont des instances de la classe String.
  • Les tableaux Java sont des objets : l'accès aux éléments se fait avec l'opérateur [] et chaque tableau possède un attribut constant length.

Le constructeur dans la classe Telecommande :

class Telecommande {
int nbPortes;
PorteCharniere[] mesPortes;

Telecommande(int capacite) {
nbPortes = 0;
mesPortes = new PorteCharniere[capacite];
}
void positionner(PorteCharniere p) {
mesPortes[nbPortes] = p;
nbPortes++;
/* Todo dépassement */
}
void activer(int numero) {
/* Todo */
}
void desactiver(int numero) {
/* Todo */
}
void desactiverTout() {
/* Todo */
}
}

Pour respecter l'encapsulation, le code du constructeur doit s'assurer d'initialiser les attributs avec des valeurs cohérentes par rapport à l'encapsulation. Sinon les futures actions déclenchées sur l'objet ne respecteront pas le service proposé par l'encapsulation. Le cas le plus fréquent est une valeur invalide passée en paramètre du constructeur. Les actions d'une instance de télécommande sont-elles cohérentes avec le service de l'encapsulation si la valeur du paramètre d'initialisation est égale à zéro ?

Envoi de messages​

Définition : un message est formé d'un nom et d'une liste de valeurs de paramètres (cette liste peut être vide). À la réception d'un message, l'objet fait la correspondance entre le message et l'action à déclencher.

La notion d'envoi de messages cherche à abstraire la manière dont est choisie l'action. Un même message peut déclencher des actions différentes sur des encapsulations différentes : quel est le code déclenché par l'appel à une méthode ? Est-ce un accès à un attribut ou l'appel à une méthode ? En général, la notation pointée est utilisée pour indiquer l'envoi d'un message à un objet. L'envoi du message s'écrit en utilisant la variable qui référence l'objet. Envoi de messages avec les variables de l'exemple précédent :

// appel d'un traitement
paradis.fermer();
sesame.ouvrir();
sesame.estFerme();
maTelecommande.positionner(sesame);
maTelecommande.activer(1);
maTelecommande.desactiver(2);
maTelecommande.desactiverTout();
// accès à un attribut
sesame.estFerme = true;
maTelecommande.mesPortes[1].estFerme = false;
maTelecommande.mesPortes[1].ouvrir();

En Java, l'abstraction n'est pas complète car la syntaxe est différente entre un attribut et une méthode. C'est au moment de l'exécution que l'envoi de messages est réalisé : le message est transmis à l'objet référencé par la variable. Le compilateur ne peut donc pas vérifier l'existence d'un message sur l'objet lui-même ; la seule vérification possible est faite par rapport au type de la variable.

Construire une encapsulation​

Encapsulation et masquage d'information​

Un objet met en œuvre une encapsulation, il y a donc abstraction sur la réalisation de l'objet. Pour permettre de changer cette réalisation sans modifier le code utilisant cet objet (code client), nous cherchons à masquer l'information sur la réalisation au code client. Les détails de la réalisation d'un objet doivent être visibles le moins possible. Dans un langage à classe, ce masquage d'information est défini au niveau de la classe avec le mécanisme de portée des identificateurs. De base, deux portées sont nécessaires pour séparer utilisation et réalisation :

  • portée publique : l'identificateur est accessible par un code écrit en dehors de la classe ;
  • portée privée : l'identificateur est accessible uniquement par le code contenu dans la classe.

Dans le code de la classe Telecommande, l'instruction mesPortes[i].estFerme ne respecte pas l'encapsulation. La définition de l'attribut risque de changer :

  • le choix de stocker la valeur plutôt que de la calculer ;
  • le choix de son type ou de la signification de sa valeur.

Le choix de définir un attribut est une décision de réalisation.

Définition : principe de masquage des attributs. Afin de respecter l'encapsulation, tous les attributs d'un objet doivent être déclarés privés et seules les méthodes peuvent être déclarées publiques.

class Telecommande {
private int nbPortes;
private PorteCharniere[] mesPortes;

public Telecommande(int capacite) {
nbPortes = 0;
mesPortes = new PorteCharniere[capacite];
}

public boolean estRempli() {
return mesPortes.length <= nbPortes;
}
public void positionner(PorteCharniere p) {
if (mesPortes.length <= nbPortes)
return; /* Todo erreur de dépassement */
mesPortes[nbPortes] = p;
nbPortes++;
}
public void activer(int numero) {
mesPortes[numero].ouvrir();
}
public void desactiver(int numero) {
mesPortes[numero].fermer();
}
public void desactiverTout() {
for (int i = 0; i < nbPortes; i++)
if (!mesPortes[i].estFerme())
mesPortes[i].fermer();
}
}

Objet constant​

Un objet possède un état défini à partir des valeurs contenues dans l'ensemble de ses attributs. Un objet est constant si son état ne peut pas être modifié. En Java, un objet n'est constant que si aucune de ses méthodes ne modifie son état (et que ses attributs ne sont pas accessibles de l'extérieur) : exemple dans l'API standard Java, la différence entre les instances de la classe String et les instances de la classe StringBuffer. Cette encapsulation des chaînes constantes montre l'utilisation habituelle des objets constants en Java : le partage d'objets.

Communication entre objets​

Un objet est passif. Il déclenche des actions uniquement en réponse à des sollicitations d'autres objets. Prenons le cas où un objet t (instance de la classe Petit) doit envoyer le message voler à un objet r (instance de la classe Coeur).

Pour communiquer, l'objet t doit posséder une référence sur l'objet r au moins au moment de l'exécution de l'envoi du message. La solution directe est de stocker cette référence dans les attributs de l'objet t ; l'objet t "a un" objet r. La classe Petit doit déclarer un attribut / une variable d'instance de type Coeur.

Définition : le lien "a-un" entre deux classes. Une classe P possède un lien "a-un" avec une classe R si elle définit un attribut de type R. Une instance de P possède dans ses attributs une instance de R.

Le lien "a-un" ne fixe pas la manière d'obtenir la référence stockée dans l'attribut : instanciation de la classe Coeur dans le constructeur de la classe Petit ou passage de la référence comme paramètre d'un constructeur ou d'une méthode de la classe Petit.

Le lien "a-un" dans la classe Telecommande :

class Telecommande {
int nbPortes;
PorteCharniere[] mesPortes;
Telecommande(int capacite) {
nbPortes = 0;
mesPortes = new PorteCharniere[capacite];
}

void positionner(PorteCharniere p) {
if (nbPortes >= mesPortes.length)
return; /* Todo erreur de dépassement */
mesPortes[nbPortes] = p;
nbPortes++;
}
void activer(int numero) {
mesPortes[numero].ouvrir();
}
void desactiver(int numero) {
mesPortes[numero].fermer();
}
void desactiverTout() {
for (int i = 0; i < nbPortes; i++)
if (!mesPortes[i].estFerme)
mesPortes[i].fermer();
}
}

Des traitements applicables à toutes les encapsulations​

Dans la pratique, il existe un petit nombre de traitements particuliers qui peuvent s'appliquer à tous les objets. Voici les plus fréquents.

Vérifier l'égalité entre deux instances de la même classe. L'opérateur Java == indique l'égalité de références (c'est la même instance). Mais deux instances différentes peuvent être considérées comme égales par rapport à la valeur de leurs attributs. C'est l'objectif de la méthode equals() (qui doit être redéfinie conjointement avec hashCode()).

Copier ou cloner un objet. Java passe toujours les paramètres par valeur ; pour un paramètre objet, la valeur transmise est la référence, si bien que la méthode appelée manipule le même objet que l'appelant. Pour éviter un effet de bord, il est donc parfois nécessaire d'effectuer une copie d'un objet. C'est l'objectif de la méthode clone() (protégée dans Object, elle n'est utilisable que si la classe implémente l'interface Cloneable).

Le langage Java regroupe ces méthodes particulières dans la classe Object. Ces méthodes sont définies avec un traitement par défaut qu'il faut adapter à chaque classe : par exemple, la méthode equals() a pour traitement par défaut l'égalité de références.

Dans la classe Object se trouve aussi la méthode toString(). Elle retourne une chaîne de caractères qui va permettre de tracer, par exemple, la valeur des attributs d'un objet. Son code par défaut retourne le nom de la classe suivi de @ et du code de hachage de l'objet en hexadécimal.

Voici une manière d'écrire son code dans la classe Telecommande pour tracer la valeur de deux attributs :

public String toString() {
return "<nbPortes:" + nbPortes + ", capacite:" + mesPortes.length + '>';
}