
Les complexes existent nativement en Python, ce n'est pas le cas en Java.
Dès qu'on s'éloigne un peu du domaine du calcul scientifique, on a besoin de manipuler des données structurées.
Par exemple, en première année, pendant le cours de SIP, vous avez vu le modèle entité-relation qui sert à construire les schémas des bases de données relationnelles. Une entité est un objet.
Pour manipuler ces données structurées (qu'on appelle objets), on va isoler un certain nombre de caractéristiques qui semblent pertinentes pour l'application développée.
Il est utile de donner des noms à ces caractéristiques pour leur donner un sens.
On appelle attribut un tel couple (nom, valeur). Dans un langage typé statiquement comme Java, un attribut a aussi un type, et la valeur de l'attribut doit être conforme à ce type (c'est bien sûr vérifié par le compilateur).
Pour stocker les attributs d'un objet, on pourrait utiliser un dictionnaire dont les clefs seraient les noms des attributs (vous connaissez les dictionnaires de Python, ils existent aussi en Java).
Pour utiliser un objet, on a besoin de savoir comment il est représenté (sa liste d'attributs).
Mais il existe toujours plusieurs choix de représentation.
Un dictionnaire ne répond donc pas au besoin car il n'impose pas des noms identiques pour décrire des objets de même sorte.
La classe permet de fixer une représentation (noms et types des attributs) pour une application particulière.
Le mot-clef new
a déjà été vu pour créer un tableau. Ici, on crée un objet (une instance de Complex
). Attention aux parenthèses après le nom de la classe, elles sont obligatoires, on verra leur utilité dans la suite.
On voit qu'une classe est un type (dit utilisateur) tout comme int
est un type (primitif).
Dans cet exemple, nous avons utiliser un type primitif (double
) pour les deux attributs. Puisqu'une classe est un type, il est possible aussi d'avoir des attributs typés par une classe, ce qui permet de créer des liens entre objets. Nous en verrons des exemples plus tard.
Le mot-clef static
ne doit pas être utilisé ici, car les attributs ne sont pas des variables globales : chaque objet a ses propres valeurs d'attributs.
Convention à respecter (rappel) : le nom d'une classe commence par une majuscule alors que celui d'un attribut commence par une minuscule. Les deux utilisent la règle camelCase.
Il est très courant de dire par raccourci que la variable c
est un objet. C'est parfaitement acceptable à partir du moment où la compréhension que c
est en réalité un lien (on parle de référence) vers l'objet est acquise.
On peut considérer que la référence est l'adresse du bloc mémoire qui contient les valeurs des attributs de l'objet.
On a besoin à certains moments qu'une variable de sorte référence ne contienne pas un lien vers un objet existant, et qu'au contraire on puisse dire cette variable ne fait référence à aucun objet : on utilise pour cela la valeur spéciale null
, d'où l'utilisation classique de la formule référence nulle.
c
et d
sont 2 variables différentes, 2 références distinctes vers 2 instances de la classe Complex
.
Maintenant, après l'affectation d = c
, c
et d
restent 2 variables différentes, mais contiennent le même lien, donc sont 2 références vers le même objet.
L'autre instance (celle qui était référencée par d
avant) est devenue inaccessible.
L'environnement d'exécution (la machine virtuelle) se charge de récupérer la mémoire de cette instance devenue inaccessible (sinon, un processus finirait par ne plus avoir de mémoire disponible).
On parle de ramasse-miettes ou de glaneur de cellules (GC : garbage collector en anglais).
Après l'affectation c = null
, c
n'est plus une référence valide, c'est une référence nulle : c
ne permet plus d'accéder à l'instance (mais d
le permet toujours).
Il est important, surtout au départ, de ne pas hésiter à prendre un crayon et un papier pour faire ce type de dessin avec des boîtes et des flèches. C'est simple ici, ce ne sera pas toujours aussi simple par la suite.
Quand on a un peu d'expérience, on continue à faire des dessins, mais on n'utilise plus de papier/crayon, on les fait dans sa tête.
Quand on crée une instance de la classe Complex
, il y a réservation d'un bloc mémoire qui contiendra les valeurs des attributs de cet objet particulier.
Le .
est utilisé pour accéder à la valeur d'un attribut (en lecture ou en écriture) à partir d'une référence et du nom de l'attribut.
c
et d
sont deux références vers le même objet, donc la modification d'un attribut en utilisant l'une de ces références est aussi visible quand on utilise l'autre.
Nous reprenons notre instance de Complex
référencée par la variable c
.
Quand la variable est une référence nulle, l'accès à un attribut provoque une exception à l'exécution : c'est la célèbre NullPointerException
(parfois abrégée en NPE) que vous rencontrerez souvent.
Nous verrons plus tard ce qu'est une exception. Ce qu'il faut par contre savoir, c'est que le programme s'arrête, donc il faut éviter de telles exceptions.
Une variable de sorte référence peut être comparée à une autre variable avec ==
, tout comme on peut comparer deux entiers.
Par contre, ce ne sont pas les objets eux-mêmes (les valeurs de leurs attributs) qui sont comparés, mais les références. C'est pour ça ici que c
et d
sont différents (la comparaison c == d
renvoie false
), ils référencent deux instances distinctes.
Après l'affectation d = c
, puisque maintenant c
et d
sont des liens vers le même objet, la comparaison c == d
renvoie true
.
Les tableaux ont été présentés dans le précédent cours : new
est aussi utilisé pour leur création car la variable qui permet d'y accéder est aussi une référence vers le bloc mémoire qui contient les éléments du tableau.
Dans le cas d'une création par initialisation directe des éléments, un new
est effectué de manière implicite.
L'affectation entre deux variables de type tableau a donc le même effet que celle entre deux variables de type Complex
: il n'y a pas ici recopie des éléments de tab
dans tab2
, et tab
et tab2
font référence maintenant au même tableau (et l'ancien tableau qui était référencé par tab2
est devenu inaccessible).
On a bien sûr la possibilité de créer des tableaux d'objets, par exemple des tableaux de Complex
; de manière exacte, ce sont des tableaux de références vers des Complex
qui n'existent pas quand on crée le tableau (et le tableau contient des références nulles).
Pour créer ces Complex
, il faut utiliser new
, par exemple dans une boucle comme ici.
Pour l'instant, nous avons présenté les objets comme un moyen de grouper les caractéristiques décrivant une entité (on parle de l'état d'un objet). Mais le modèle objet va plus loin en permettant de décrire aussi le comportement des objets, c'est ce qui le distingue du modèle entité-relation.
Pour comprendre en quoi consiste ce comportement, intéressons nous à une opération habituelle sur un complexe, le calcul de son module. En utilisant une approche classique (dite aussi fonctionnelle), on peut écrire une fonction, dans une classe quelconque, qui fait ce calcul en accédant aux attributs de l'objet. Pour écrire cette fonction, il faut connaître le nom et le type des attributs, donc on établit un couplage très fort entre la classe qui contient la fonction modulus()
et la classe Complex
.
Ce type de couplage conduit systématiquement à des difficultés lors des évolutions du logiciel, il faut l'éviter autant que possible.
On peut bien sûr mettre la fonction modulus()
dans la classe Complex
. L'approche objet va encore plus loin en inversant la façon de penser : au lieu de raisonner sous la forme :
- Chère fonction, donne moi le module du complexe que je te fournis en argument (approche fonctionnelle)
on raisonne ainsi :
- Cher complexe, rend moi un service : donne-moi ton module (approche objet)
L'approche objet consiste donc à considérer qu'une entité n'est pas seulement un regroupement de caractéristiques (l'état), mais qu'elle est aussi en mesure de rendre des services aux autres objets (le comportement).
Un tel service, rendu par un objet, s'appelle une méthode ; elle est définie comme une fonction à l'intérieure d'une classe, mais sans utiliser le mot-clef static
.
Pour accéder à un service fourni par un objet, on utilise naturellement le .
.
Une métaphore classique est de parler de message : un objet qui souhaite demander un service à un autre lui envoie un message qui contient le nom du service demandé. L'objet qui reçoit le message (l'objet récepteur) peut alors exécuter la méthode correspondante au service demandé ; en tant qu'objet exécutant une méthode, il est l'objet courant et se connait avec le mot-clef this
(qui est bien sûr une référence).
this
n'ést pas utilisé dans le précédent transparent qui est pour autant correct : en effet, le compilateur, quand il rencontre l'identifiant real
, cherche déjà dans le contexte local (ici la méthode) s'il existe une variable locale ou un paramêtre avec ce nom. Comme il n'en trouve pas, il étend sa recherche au contexte englobant la méthode, donc la classe ; il trouve dans cette classe un attribut de nom real
, il en déduit que real
désigne ici l'attribut de l'objet courant, this
.
De même que pour les fonctions (ou méthodes statiques), une méthode peut avoir des arguments et peut ne pas retourner de valeur.
Deux méthodes peuvent avoir le même nom si elles n'ont pas la même signature.
Un comportement très important dans une classe est le comportement d'initialisation d'un objet : il consiste à lui donner un état (des valeurs à ses attributs) consistant lors de sa création.
Ce comportement a été jugé suffisamment important pour la correction des logiciels qu'un mécanisme spécifique a été créé (Java a repris ici le mécanisme existant en C++) : le constructeur.
Un constructeur est une méthode qui a le même nom que la classe, peut avoir des arguments (donc il peut en exister plusieurs), mais pas de type de retour.
Pour notre classe Complex
, un constructeur peut permettre d'initialiser ses deux attributs avec une autre valeur que celle par défaut 0
.
Nous voyons sur cet exemple que le mécanisme de résolution de nom, exposé plus haut, impose l'utilisation de this
pour accéder à l'attribut real
de l'objet courant, sinon c'est l'argument de même nom de la méthode qui aurait été affecté.
Même si le compilateur signalera par un avertissement cette possible erreur, ne pas oublier qu'il n'y a aucun type de retour spécifié pour un constructeur (si un type de retour est indiqué, ce n'est plus un constructeur, mais une méthode classique) ; et qu'une méthode qui n'est pas un constructeur doit, pour respecter les conventions, commencer par une minuscule.
L'exécution de l'un des constructeurs est obligatoire lors de la création d'un nouvel objet. Les arguments destinés au constructeur sont indiqués entre les parenthèses qui suivent le nom de la classe dont on veut créer une instance.
En fait, s'il n'y a pas de constructeur défini pour une classe, le compilateur va en générer un qui initialisera les attributs de l'objet à 0
, false
ou null
.
Si une valeur est indiquée lors de la définition d'un attribut, cette valeur est utilisée pour initialiser cet attribut d'un objet si le constructeur ne l'initialise pas lui-même.
Nous pouvons maintenant revenir à la signification du mot-clef static
: une méthode qualifiée ainsi peut-être considérée comme une méthode de classe, et non pas une méthode d'instance : il n'y a pas d'objet récepteur. C'est pour cela qu'une telle méthode est équivalente à une fonction d'un langage comme Python.
Concernant les attributs, si l'un d'eux est qualifié par static
, alors c'est un attribut de classe, un attribut donc la valeur est la même pour toutes les instances de cette classe.
Même si une méthode modulus()
est proposée aux utilisateurs de notre classe Complex
, rien n'interdit à ces utilisateurs(*) d'accéder cependant directement aux attributs d'un objet, ce qui ne permet donc pas de supprimer ce couplage très fort mentionné comme inconvénient pour l'évolution d'un logiciel.
La protection permet d'interdire cet accès en déclarant que les attributs sont privés : ils restent accessibles par les méthodes de la classe, mais pas par les méthodes des autres classes. À l'opposé, on va qualifier de publics les services offerts aux autres objets pour qu'ils soient accessibles par tout le monde.
(*) En fait, en l'absence de protection, seuls les utilisateurs appartenant au même paquetage peuvent accéder aux attributs.
Les mots-clefs private
et public
sont utilisés pour indiquer le niveau de protection.
Une bonne pratique de programmation impose de mettre les attributs privés et les méthodes publiques. Il est aussi possible d'avoir des méthodes privées pour modulariser ou factoriser du code. Par contre, même si rien ne l'interdit dans le langage, mettre des attributs publics nécessite une argumentation forte et explicite. Devoir écrire c.getReal()
au lieu de c.real
n'est en aucun cas un argument suffisant.
Voici un exemple qui montre pourquoi garder les attributs privés et proposer des accesseurs facilite l'évolution d'un logiciel.
Nous avons choisi ici de représenter les complexes sous forme algébrique (avec partie réelle et partie imaginaire). Il est aussi possible de les représenter sous forme polaire (avec module et argument). Le choix de l'une ou l'autre de ces représentations sera fait en fonction des calculs à effectuer avec des critères de performance. Quelle que soit la représentation choisie, il sera possible de demander à un complexe sa partie réelle avec la méthode getReal()
: dans un cas, il suffira de retourner la valeur de l'attribut, dans l'autre il faudra la calculer, mais dans tous les cas l'utilisateur de notre classe Complex
ne sera pas impacté dans son code par ce choix de représentation.
Nous avons vu dans le cours précédent que Java sait convertir un nombre en chaîne de caractères pour, par exemple, l'afficher.
Par exemple, une façon habituelle de convertir un entier nb
en chaîne de caractères nbStr
est d'écrire
nbStr = "" + nb;
Java va convertir automatiquement
nb
en chaîne de caractères pour ensuite effectuer la concaténation.
Pour que cela fonctionne avec les types utilisateurs, il est nécessaire de définir une méthode publique toString()
(sans argument et retournant une chaîne de caractères) ; cette méthode sera alors automatiquement appelée par Java en cas de besoin (par exemple ici, si la variable nb
est une instance de Complex
).
Nous reviendrons dans le prochain cours sur cette méthode.

© 2024 CentraleSupélec